Директивы в 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'
            }
        }
    });



Всем спасибо, продолжение следует.
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 21
  • +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
                  • 0

                    Интересно, что бы вы сказали о React)

                  • 0

                    http://jsfiddle.net/dXLLz/


                    В этом примере имена директивы в html и в js не согласованы между собой (habra-habr-template и habraHabr). Пример не работает. Опечатка?

                    • 0

                      Ссылка к примеру для префикса "=" ведет на пример для префикса "@".

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