23 мая 2013 в 11:13

Директивы в Angularjs для начинающих. Часть 2 tutorial

На мой взгляд, директивы являются основной изюминкой декларативного стиля Angularjs. Однако, если открыть комментарии пользователей в разделе официальной документации Angularjs, посвященной директивам, то вы увидите, что самый популярный из них: «Пожалуйста, перепишите документацию, сделайте ее более доступной и структурированной. Начинающему разработчику на Angularjs сложно в ней разобраться» («Please rewrite a clearer well structured documentation of directives., this is not friendly to first time angular developers»). С этим трудно не согласится, документация пока еще сыровата и в некоторых моментах приходится прилагать большие усилия, чтобы разобраться в логике и сути функционала. Поэтому я предлагаю вам свой вольный пересказ данной главы в надежде, что кому-то это позволит сэкономить время, а так же рассчитываю на вашу поддержку и участие в комментариях. Итак, поехали!
Часть 1

  • Как писать директивы?
  • Простой вариант создания директивы
  • Развернутый вариант
  • Link и Compile


Template и TemplateUrl

Продолжая разговор о директивах, надо отметить, что директивы по сути являются модулями, если абстрагироваться от терминологии Angularjs. То есть, в идеале, они должны быть самостоятельными элементами интерфейса со своими функционалом и разметкой. Разметка при этом может задаваться напрямую в параметре Template или храниться в отдельном файле, URL которого указывается в TemplateUrl:

[jsFiddle]
angular.module('helloHabrahabr', [])
    .directive('habraHabr', function() {        
        return {
            template:"<span>Hello Habr!</span>"
            /* или */
            templateUrl:"helloHabr.html"
        }
    });

helloHabr.html
<span>Hello Habr!</span>

При этом в случае, если шаблон подгружается, функции Compile и Link выполняются после загрузки.

Полезное дополнение в комментариях

Scope

Параметр Scope определяет область видимости внутри директивы. Возможно несколько вариантов:

Не указывать scope вовсе. Тогда директива, грубо говоря, работает напрямую в области видимости контроллера. То есть все переменные контроллера равны переменным директивы.

[jsFiddle]
<div ng-app="helloHabrahabr">
    <div ng-controller="forExampleController">
        {{hello}}        
        <span habra-habr></span> 
    </div>    
</div>

function forExampleController($scope){
    $scope.hello="Hello Habr!";
}


angular.module('helloHabrahabr', [])
    .directive('habraHabr', function() {        
        return {
            template:"<input ng-model='hello'>{{hello}}"
        }
    });

Другой вариант. Scope = true. В этом случае, scope будет наследоваться. То есть поля, заданные в родительском scope будут отображаться и в scope директивы, но при этом все изменения будут локальны:

[jsFiddle]
function forExampleController($scope){
    $scope.hello="Hello Habr!";
}


angular.module('helloHabrahabr', [])
    .directive('habraHabr', function() {        
        return {
            template:"<input ng-model='hello'>{{hello}}",
            scope:true
        }
    });


И наконец, самый интересный вариант. Задать изолированный scope. То есть scope, который по умолчанию абсолютно независим от контекста вызова директивы. Для этого нужно просто указать в качестве scope пустой объект {}:

[jsFiddle]


angular.module('helloHabrahabr', [])
    .directive('habraHabr', function() {        
        return {
            template:"<input ng-model='hello'>{{hello}}",
            scope:{

            }
        }
    });


Дальше есть несколько вариантов работы с таким изолированным scope. Но все они сводятся к одному принципу. В объекте, который мы объявили для scope, в качестве имени свойства слева указывается некая переменная директивы, а справа название атрибута DOM c одним из трех символов в начале: @/=/&. То есть вот так:

scope:{
      localVar1:"@attrName1",
      localVar2:"=attrName2",
      localVar3:"&attrName3"
}


Либо еще одни вариант. Не указывать имя атрибута, тогда оно будет равно имени переменной:

scope:{
      localVar1:"@", /*localVar1:"@localVar1" */
      localVar2:"=",  /*localVar2:"@localVar2" */
      localVar3:"&"  /*localVar3:"@localVar3" */
}


Теперь по порядку. Префикс "@" означает, что локальной переменной будет присвоено значение атрибута:

[jsFiddle]
<div ng-app="helloHabrahabr">
        <span habra-habr="hello" some-attr="Hello Habr!"></span>    
</div>

angular.module('helloHabrahabr', [])
    .directive('habraHabr', function() {        
        return {
            template:"{{hello}}",
            scope:{
                hello:'@someAttr'
            }
        }
    });


Префикс "=" означает, что в атрибуте передается уже не строчка, а имя некоторой переменной в текущем Scope. И локальная переменная будет напрямую с ней связана. То есть изменения переменной как внутри директивы, так и вне отразятся и там, и там:

[jsFiddle]
<div ng-app="helloHabrahabr">
    <div ng-controller="forExampleController">
        {{hello}}        
        <span habra-habr some-attr="hello"></span> 
    </div>    
</div>

function forExampleController($scope){
    $scope.hello="Hello Habr!";

}

angular.module('helloHabrahabr', [])
    .directive('habraHabr', function() {        
        return {
            template:"<input ng-model='hello'>{{hello}}",
            scope:{
                hello:'=someAttr'
            }
        }
    });


И наконец, последний вариант "&" предполагает, что атрибут содержит некое выражение. К примеру, «c= a+b» или проще «a+b». И теперь ваша локальная переменная становится функцией, в которую можно передавать параметры. Параметры передаются в объекте, ключами которого выступают имена переменных в функции. В конкретном случае, localVar({a:1,b:2}) вернет три.

[jsFiddle]
angular.module('helloHabrahabr', [])
    .directive('habraHabr', function() {        
        return {
            template:"{{helloFn({a:1,b:2})}}",
            scope:{
                helloFn:'&someAttr'
            }
        }
    });

При этом интересно, что по умолчанию, если не передавать в локальную функцию никаких параметров, переменным будут присвоены значения соответствующих переменных в родительском scope. А если указать переменную -результат, то и она также будет доступна из вне:

[jsFiddle]
<div ng-app="helloHabrahabr">
    <div ng-controller="forExampleController">
        a={{a}}
        b={{b}}
        parent's hello={{hello}}        
        <span habra-habr some-attr="hello= a+b"></span> 
    </div>    
</div>

function forExampleController($scope){
    $scope.a="Hello";
    $scope.b=" Habr!";
}


angular.module('helloHabrahabr', [])
    .directive('habraHabr', function() {        
        return {
            template:"default helloFn={{helloFn()}}\
            custom hello={{helloFn({a:'Bye',b:'Habr'})}}",
            scope:{
                helloFn:'&someAttr'
            }
        }
    });



Всем спасибо, продолжение следует.
@durovchpoknet
карма
25,0
рейтинг 0,0
Самое читаемое Разработка

Комментарии (18)

  • +4
    Я бы обратил еще особое внимание на то, что в изолированном скоупе мы задаем переменные camelCase'ом, но в DOM они должны быть написаны через дефис, т.е:

    scope:{
          localVar1:"@", /*localVar1:"@localVar1" */
          localVar2:"=",  /*localVar2:"@localVar2" */
          localVar3:"&"  /*localVar3:"@localVar3" */
    }
    

    <div ng-app="helloHabrahabr">
            <span habra-habr="hello" local-var1="Hello Habr!"  local-var2="Foo" ></span>    
    </div>
    

    А так работать НЕ будет:
    <div ng-app="helloHabrahabr">
            <span habra-habr="hello" localVar1="Hello Habr!"  localVar2="Foo" ></span>    
    </div>
    
  • +1
    Полагаю, стоит упомянуть и о том, что в templateUrl может быть id уже существующего шаблона на странице, заданного в виде

    <script id="SomeTemplate" type="text/ng-template">
    ...
    </script>
    

    т.е.

    templateUrl: 'SomeTemplate'
    
    • 0
      Или даже ссылка на отдельный файл:

      templateUrl: 'partials/SomeTemplate.html'
      
    • 0
      Добавил в статью ссылку на комментарий
  • 0
    Есть досадное ограничение на темплейты директив — только один рутовый тег на темплейт. То есть вот такой темплейт работать будет

         <div ng-app="helloHabrahabr">
            <span habra-habr="hello" localVar1="Hello Habr!"  localVar2="Foo" ></span>    
         </div>
    


    А вот такой нет

         <div ng-app="helloHabrahabr">
            <span habra-habr="hello" localVar1="Hello Habr!"  localVar2="Foo" ></span>    
         </div>
         <div ng-app="helloHabrahabr">
            <span habra-habr="hello" localVar1="Hello Habr!"  localVar2="Foo" ></span>    
         </div>
    


    Это ограничение можно обойти если использовать компиляцию шаблона в link вручную.

    components.directive('myDir', function ($compile) {
        return {
            link: function ( scope,element, attrs) {
               var tmpl = '<div ng-app="helloHabrahabr">'+
            '<span habra-habr="hello" localVar1="Hello Habr!"  localVar2="Foo" ></span>'+    
         '</div>'+
         '<div ng-app="helloHabrahabr">'+
            '<span habra-habr="hello" localVar1="Hello Habr!"  localVar2="Foo" ></span>'+    
         '</div>'
         
               var newElement = angular.element(tmpl)
               $compile(newElement)(scope)
               element.replaceWith(newElement)   
           } 
    
        })
    
    • 0
      А зачем внутри директивы вообще создавать приложение (ng-app)?
      • 0
        Плохой пример выбрал, скопировал разметку из предыдущего комментария. Приложения там не нужно, а вот любые другие диррективы будут работать, так как шаблон скомпилирован.

        Смысл в том что можно получить несколько корневых тэгов.
  • 0
    Какой страшный этот ваш Angularjs.

    HTML-шаблоны внутри js-кода?
    template:"<input ng-model='hello'>{{hello}}"
    


    Функции внутри разметки?
    <span habra-habr some-attr="hello= a+b"></span> 
    


    Мне лично не нравится.
    • 0
      На деле всё не так страшно — темплейты в коде писать никто не заставляет — куда удобнее держать их в отедельных файлах и собирать в один JS файл на этапе сборки проекта. А для разработки даже можно ничего не собирать — ангуляр сам подтянет темплейты из templateUrl через AJAX
      Касательно логики в темплейте — она используется для изменения состояния контроллера или вывода. Так специально ограничили, чтобы народ не пихал бизнес-логику в код.
      До сегодняшнего релиза там даже conditional expressions не было — сегодня добавили тернарный if — реально удобно.
      Логика в темплейтах должна использоваться примерно так:
      <form ng-hide="isLoading" ng-submit="isLoading = true">
        ...
        <input type="submit">
      </form>
      <div ng-show="isLoading">Sending data on server...</div>
      

      Плашка isLoading будет показана, когда форма будет отправлена на сервер. Когда данные будут получены — контроллер сделает scope.isLoading = false и плашка спрячется, а форма вернётся.
      • 0
        Касательно логики в темплейте...

        Спасибо за объяснения. Действительно, с такими ограничениями всё кажется не так страшно.

        Но мне всё равно не понятно
        … куда удобнее держать их в отедельных файлах и собирать в один JS файл на этапе сборки проекта. А для разработки даже можно ничего не собирать...

        Т.е. при разработки в js-коде будет написано `templateUrl: 'some.html'` а при сборке нужно это както заменить? Вручную? Или Ангулар самостоятельно пройдётся по js-файлам и заменит вхождение этой строки? Сомнительно.
        • +1
          Я уже давал ссылку в первой части — npmjs.org/package/grunt-angular-templates
          При сборке он пройдётся по всем темплейтам и сгенерирует из них строки, которыми заполнит $templateCache
          В исходниках ничего менять не надо будет.
    • 0
      Интересно было бы узнать, что по вашему не страшно и нравится лично вам.
      • +1
        habrahabr.ru/post/179359/#comment_6248559
        Тут примерно вид шаблона, модели и контроллера. Это то, к чему я стремлюсь. На текущий же момент, можно написать такой код:
        jsfiddle.net/termi/4cF97/ — Этот пример максимально упрощен, из него вырезаны все вызовы фреймворка и оставлено только DOM API. Как вы можете видеть, никаких data-* или других кастомных атрибутов не используется, только стандартные.
        Это только демонстрация возможностей, готового фреймворка пока нету.

        Не в коем случае не говорю, что Angularjs плохая библиотека, просто, по моему, простые вещи в ней сделаны слишком сложно, а сложные ещё сложнее. Да и производительность страдает — я тестировал на большом количестве компонентов.
        • НЛО прилетело и опубликовало эту надпись здесь
          • +1
            Где, где у меня в коде классы или идентификаторы? Простите, но Вы код вообще смотрели? В демо jsfiddle.net/termi/4cF97/ всё обращение с DOM идёт через специальное (нативное для браузера) абстрактное API. Код максимально абстрагирован от DOM и может быть использован и с другим бэкендом, например с WebGL.

            Вот ещё один пример: h123.ru/-/tests/KeyboardEvent/ — тут с шаблонизацией, но тоже проходной вариант, я просто игрался с DOM API.

            В комментарии же, ссылку на который я дал, следующий уровень абстрагируемости от DOM.
            • НЛО прилетело и опубликовало эту надпись здесь
    • 0
      Просто вы начали знакомство с Angular «не с той стороны»

      Начните знакомство отсюда angularjs.org/ (с главы The Basics, знакомство займет не больше 10 минут).
      И возможно вы вдохновитесь декларативным подходом, так же как и я в свое время.
      • 0
        Посмотрите пожалуйста мои примеры приведённые в комментариях выше. В обоих примерах используется декларативный стиль. Селекторы, классы, идентификаторы, кастомные аттрибуты и т.д. не используются. И всё это сделано с использованием встроенных в браузер API, а при желании я могу сменить бэкенд на WebGL

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.