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

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

Как писать директивы?


Директивы в Angularjs задаются вместе с другими конфигурациями модуля следующим образом:

angular.module('moduleName', [])
    .directive('directiveName', function () {
        /*Метод-фабрика для директивы*/
    })
    .directive('anotherDirectiveName', function () {
        /*Метод-фабрика для директивы*/
    });

При этом есть два варианта их объявления. Простой и более мощный длинный варианты.

Простой вариант создания директивы


Для того, чтобы написать директиву, которая будет вызываться при указании в HTML разметке, в простейшем случае вам нужно задать некую функцию (она называется Связующей Linking, но об этом чуть позже), возвращаемую фабрикой:

angular.module('moduleName', [])
    .directive('directiveName', function () {
        return function(scope,element,attrs){

        }
    });

Эта функция принимает следующие параметры:
  • scope — область видимости, в которой вызывается директива
  • element — элемент DOM, которому принадлежит директива, обернутый в jQuery Lite
  • attrs — объект со списком всех атрибутов тэга, в котором вызывается директива

Давайте на более развернутом примере. Напишем такую директиву (назовем habra-habr), которая будет складывать две строчки и выводить внутри элемента верстки, в котором вызывается. При этом одну строчку мы будем задавать в качестве переменной контролера(forExampleController), а вторую передавать атрибутом(habra) в этом же тэге. А также оставим за собой возможность определять имя переменной контролера при вызове директивы:

[jsFiddle]
<div ng-app="helloHabrahabr">
    <div ng-controller="forExampleController">
        <input ng-model="word">
        <span habra-habr="word" habra="Nehabra"></span>
    </div>
</div>

function forExampleController($scope) {
    $scope.word="Habrahabra"
}

angular.module('helloHabrahabr', [])
  .directive('habraHabr', function() {
    return function($scope, element, attrs) {
        /*Задаем функцию, которая будет вызываться при изменении переменной word, ее имя находится в attrs.habraHabr*/
        $scope.$watch(attrs.habraHabr,function(value){
            element.text(value+attrs.habra);
        });
    }
  });

Всё. Директива в примитивном виде у нас готова. Можно переходить к более развернутой форме.

Развернутый вариант


В своей полноценной форме задание директивы выглядит следующим образом:

angular.module('moduleName', [])
    .directive('directiveName', function () {
        return {
             compile: function compile(temaplateElement, templateAttrs) {
                return {
                    pre: function (scope, element, attrs) {
                    },
                    post: function(scope, element, attrs) { 
                    }
                }
            },
            link: function (scope, element, attrs) {

            },
            priority: 0,
            terminal:false,
            template: '<div></div>',
            templateUrl: 'template.html',
            replace: false,
            transclude: false,
            restrict: 'A',
            scope: false,
            controller: function ($scope, $element, $attrs, $transclude, otherInjectables) {
            }           
        }
    });

Все эти свойства довольно тесно друг с другом связаны и переплетены. И для того, чтобы было проще в этом разобраться, их лучше рассматривать некими смысловыми группами.

Link и Compile

Метод Link это та самая функция, которую возвращала фабрика директивы в короткой версии. Здесь надо понять, что в Angularjs процесс компиляции разбит на два этапа:

  • compile — анализ всех директив используемых в данном элементе DOM ( в том числе и в его потомках child)
  • linking — связывание переменных используемых в шаблоне и переменных в scope


И при этом как в простейшей версии, так и в расширенной метод Link правильно будет называть postLink, поскольку он выполняется после того, как переменные уже сопоставлены. Рассмотрим примеры.

Сперва, я предлагаю переписать пример простой директивы на манер расширенной.

[jsFiddle]
angular.module('helloHabrahabr', [])
    .directive('habraHabr', function() {
        return {
            link:function($scope, element, attrs) {
                /*Задаем функцию, которая будет вызываться при изменении переменной word*/
                $scope.$watch(attrs.habraHabr,function(value){
                        element.text(value+attrs.habra);
                    }
                );
            }
        }
    });

То есть всё, действительно, работает по-прежнему. Теперь можно усложнить задачу и сделать так, чтобы наша фраза выводилась не посредством прямого взаимодействия с DOM element.text(...), а внутри директивы interpolate "{{}}":

[jsFiddle]
angular.module('helloHabrahabr', [])
    .directive('habraHabrNotwork', function() {
        return {
            link:function($scope, element, attrs) {
               element.html("<div>{{"+attrs.habraHabrWork+"}}"+attrs.habra+"</div>");
            }
        }
    })
    .directive('habraHabrWork', function() {
        return {
            compile: function compile(templateElement, templateAttrs) {
                templateElement.html("<div>{{"+templateAttrs.habraHabrWork+"}}"+templateAttrs.habra+"</div>");                
            },
            link: function (scope, element, attrs) {

            }
        }
    });

Пример обновлен после комментария tamtakoe

В примере выше директива habraHabrNotwork не будет работать корректно, поскольку мы вставляем директиву "{{}}" с переменными в postLink, то есть, когда уже выполнены компиляця и линкование. Иными словами Angularjs даже не знает, что "{{}}" это директива, которая подлежит исполнению.

Другое дело, вторая директива. Там всё на своем месте, мы вставляем шаблон "{{"+attrs.habraHabrNotwork+"+"+attrs.habra+"}}" до компиляции, и он успешно проходит рендеринг.

Остановимся немного на методе compile. Он может возвращать, как функцию postLink, так и объект с двумя параметрами: pre и post. Где pre и post это методы preLink и postLink соответственно. Из названия методов может показаться, что речь идет о методах до и после Linkа. Но это не совсем так, эти функции выполняются до и после Link а детей директивы в DOM. На примере:

[jsFiddle]
<div ng-app="helloHabrahabr">
  <div ng-controller="forExampleController">
    <input ng-model="word">
    <span habra-habr-work="word" habra="NehabraParent">
        <span habra-habr-work="word" habra="NehabraChild"></span>
    </span>
    <pre>{{log}}</pre>
  </div>
</div>


function forExampleController($scope) {
    $scope.word="Habrahabra";
    $scope.log="";
}

angular.module('helloHabrahabr', [])
    .directive('habraHabrWork', function() {        
        return {
            compile: function compile(templateElement, templateAttrs) {
                templateElement.prepend("<div>{{"+templateAttrs.habraHabrWork+"}}"+templateAttrs.habra+"</div>");
                return {
                    pre: function ($scope, element, attrs, controller) {
                        $scope.log+=templateAttrs.habra +' preLink \n';
                    },
                    post: function ($scope, element, attrs, controller) {
                        $scope.log+=templateAttrs.habra +' postLink \n';
                    }
                }
            }
        }
    });


На этом предлагаю сделать паузу. Если тема интересная, в ближайшие дни постараюсь написать продолжение про области видимости и шаблоны.
Метки:
Поделиться публикацией
Комментарии 45
  • +5
    Спасибо за статью, тема действительно интересна!
    Кстати, angular очень хорошо подходит для написания сложных браузерных плагинов.
    Документация, в самом деле, никуда не годна. Приходится собирать информацию по крупицам.
    • +1
      Задолбал всех, наверное, со своей рекламой, но, пожалуйста, перевод документации: angular.ru Скоро добью API. Переводите учебник, допиливайте существующие переводы, присылайте свои примеры: angular.ru/cookbook/. Я, когда разберусь с какой-нибудь штукой трачу 30 мин. и пишу пример. Полнота документации зависит в том числе от вас.
    • +2
      Обязательно пишите еще.
      По документации на самом деле быстро вникнуть и проникнуться сложновато (особенно после нескольких лет работы с jQuery и ей подобными).
      • +1
        На документацию по AngularJS грех жаловаться. На официальном сайте её много. От туториалов, до продвинутых сценариев с примерами. Одни юнит тесты чего стоят! Если нужно что-то большее — есть поисковики и исходники.
        Вот чего действительно не хватает, так это альтернативы директивам. Простенькие или многостраничные сайты на директивах нормально пишутся, но если заходит речь о динамических single-page приложениях, то с директивами тут трешак начинается. Очень неудобно с ними работать, особенно после jquery виджетов.

        • 0
          Почему с директивами неудобно получается? Я сейчас в свободное время изучаю потихоньку ангуляр, и именно директивы кажутся мне очень удобными.
          • 0
            Мне очень нравится модульность Angular'a. В рамках же single-page приложения очень хочется избежать постоянной подгрузки темплейтов с сервера. А ввиду большого количества темплейтов, крайне не хочется хранить все темплейты в одной куче в рутовом index.html, а перенести их в конкретные модули. С простыми темплейтами нет проблем, но как всегда хочется большего. Нужна интернализация, автоматическая подстановка значений из разных scope'ов, быстрая динамическая перестройка html. Первого в angular'е нет, но можно самому написать через директивы; подстановка работает с некоторыми ограничениями; а вот динамически менять dom достаточно муторно. По сути, нужно каждый раз компилировать темплейты, чтобы работали подстановки и выполнялись директивы. Наверно тут нужно определенное мастерство, т.к. то же самое на jquery виджетах делается в 10 раз быстрее и занимает меньше кода. В идеале, возможно было бы здорово совместить jquery виджеты (UI) с angular'овскими модулями (logic), но что-то по-простому это не сращивается.

            Не знаю что бы такого в качестве примера привести… За полгода уже подзабылось малость. Ниже идет простенькая директива с кнопками OpenID провайдеров. По сути, приходится темплейты описывать конкатенацией строк, т.е. все описывается декларативно. Если нужно динамически менять какой-нибудь css аттрибут — в темплейт добавляется директива. Если нужен action — добавляется директива. Нужна интернализация — добаляется директива. Форматирование? — директива итд. Так код очень быстро сильно захламляется ненужным синтаксисом. Если же нужно вставить новый блок с другим виджетом динамически… Тут уже траблы. В общем, чем больше виджеты, тем сложнее с ними работать ;)

            Если кто подскажет как правильно и эффективно работать с директивами — буду только рад.

            angular.module('sipSsoLogin.directive', [ 'sipCommonI18n', 'sipSsoLogin.controller' ]).directive('sipSsoLogin', function () {
            
                var buttonTemplate = '<button id="sipSsoProvider_%PROVIDER%" ng-click="onProviderSelected(\'%PROVIDER%\')"><div class="sipSsoProvider sipSsoProvider_%PROVIDER%"></div></button>';
            
                function getProviderButtonTemplate(provider) {
                    return buttonTemplate.split("%PROVIDER%").join(provider);
                };
            
                return {
                    restrict: 'E',
                    template:
                        '<div ng-controller="sipSsoLogin">' +
            
                            '<div class="sipSsoPanel_text" sip-common-i18n="sso.login.directive" />' +
                            '<div>' +
                            getProviderButtonTemplate('google') +
                            getProviderButtonTemplate('yahoo') +
                            getProviderButtonTemplate('yandex') +
                            getProviderButtonTemplate('twitter') +
                            '</div><div>' +
                            getProviderButtonTemplate('openid') +
                            getProviderButtonTemplate('myopenid') +
                            getProviderButtonTemplate('facebook') +
                            getProviderButtonTemplate('vkontakte') +
                            '</div><div class="sipSsoPanel_inputContainer">' +
            
                            '<button style="float: right; margin: 0px 10px 0px 10px;" sip-common-i18n="sso.button.login"></button>' +
                            '<div style="overflow: hidden;" class="sipSsoPanel_inputField">' +
                            '<span style="float: left; padding-right: 5px; vertical-align: middle;">{{prefix}}</span>' +
                            '<div style="overflow: hidden">' +
                            '<input style="display: inline-block; width: 100%;" type="text" value="{{user}}">' +
                            '</div>' +
                            '</div>' +
            
                            '</div>',
                    replace: true
                };
            
            });
            
            
            • +2
              Жесть. Надеюсь, что эта куча говно-кода этот пример является просто примером:
              1. Необходимо всю разметку перенести в отдельный файл-шаблон и заменить опцию template на templateUrl.
              2. Удалить функцию getProviderButtonTemplate и воспользоваться директивой ангуляра ng-repeat
              3. Убрать inline-стили. Но это так — общий совет.

              Получится что-то вроде такого:
              код:
              angular.module('sipSsoLogin.directive', [ 'sipCommonI18n', 'sipSsoLogin.controller' ]).directive('sipSsoLogin', function () {
                  return {
                      restrict: 'E',
                      templateUrl: 'loginPanel.html',
                      replace: true
                  };
              });
              


              loginPanel.html
              <div ng-controller="sipSsoLogin">
              	<div class="sipSsoPanel_text" sip-common-i18n="sso.login.directive" />
              	<div ng-repeat="prov in providers">
              		<button id="{{prov.sipSsoProviderId" ng-click="onProviderSelected(prov)">
              			<div ng-class="prov.sipSsoProvider"></div>
              		</button>
              	</div>
              	<div class="sipSsoPanel_inputContainer">
              		<button class="..." sip-common-i18n="sso.button.login"></button>
              		<div class="..." class="sipSsoPanel_inputField">
              			<span class="...">{{prefix}}</span>
              			<div class="...">
              				<input type="text" value="{{user}}">
              			</div>
              		</div>
              	</div>
              </div>
              


              С этим уже намного приятнее работать и можно двигаться дальше:
              4. Директива 'sipSsoLogin.directive' — не нужна вообще (у неё нет функционала кроме замены шаблона). Удалить её и перейти на директиву ng-include которая реализует подобный функционал.

              <div ng-include="'loginPanel.html'"></div>
              

              5.… у нас остаётся только разметка, в которой кстати я вижу кастомную интернациализацию. В Ангуляре есть собственные средства интернационализации. Необходимо ими воспользоваться и, подозреваю, что уйдёт ещё куча говно кода
              • 0
                Не беспокойся, это в качестве примера. Inline стили тоже для дебага были. Потом все стили плавно переползают в less.

                Вот чего хочется избежать, так это:
                1. Необходимо всю разметку перенести в отдельный файл-шаблон и заменить опцию template на templateUrl.


                Темплейтов куча. Делать кучу запросов к серверу за темплейтами не хочется. Держать темплейты в одном месте тоже. Как быть?

                Интернационализации в angular'е нет.
                • +1
                  Все шаблоны должны быть в отдельных файлах. Не хочу даже обсуждать это. Откуда-то с Хабра: (с) Мы пишем код для людей, а не машин.

                  По поводу требования о загрузке шаблонов. Оно разумно и мне вполне понятно. Смею предложить следующий алгоритм:
                  1. При сборке проекта все файлы шаблонов объединяются в один (и минимизируются для оптимизации).
                  2. Файл с шаблонами загружается в главном контроллёре, скажем так:
                  <body ng-controller="mainController">
                      ...
                  </body>
                  


                  Контроллёр загружает файл с шаблонами, парсит его (разбивает на составные части) и «накачивает» ими Ангуляровский $templateCache, который использует директива ng-include (необходимо только выяснить в коде ангуляра, код кэша).

                  Таким образом и код будет выглядеть стандартно и подгрузки шаблонов не будет.
                  • 0
                    Спасибо, что то похожее думалось сделать, но времени не хватило. Попробую еще раз взяться за angular при первой возможности.
                    • +1
                      можно использовать grunt-angular-templates для сборки темплейтов в один пекедж — и не надо ничего подгружать на ходу
                    • 0
                      Вероятно я Вас не правильно понимаю насчёт интернационализации, но разве это не оно? I18n and L10n in AngularJS
                      • 0
                        Базового функционала явно не хватает. Поэтому народ жалуется на i18n/l10n и пишет свои сервисы, модули итд.
                        • +1
                          Это да. Недавно натолкнулся на хороший проект angular-translate, рекомендую.
                          • 0
                            Воспользуюсь случаем пропиарить свой angular-l10n
                            • 0
                              Не копался еще в исходниках. Интересует смена языков. Есть возможность подгружать переводы аяксом, а так же при необходимости не заменять старые переводы? Т.е. чтобы сразу оригинал и перевод показывались?
                              • 0
                                загрузку ajax-ом не делал — придерживаюсь подхода, что приложение должно быть упаковано целиком, для доставки на клиент. В целом ничего не мешает сделать загрузку через $http — модуль к этому готов.
                                Отображать одновременно строки из нескольких переводов? Технически возможно, но зачем?
                                • 0
                                  Для админки. Обычные пользователи всегда получают только данные для одного языка, админы и переводчики сразу все, чтобы в редакторе можно было сопоставлять оригинальный текст переводу.

                                  Не очень понятен подход про упаковку. Т.е. для обычных пользователей должны подгружаться интерфейсы админки?
                                  • 0
                                    Такого функционала нет и он не нужен в составе основной библиотеки, потому что это уже не локализация.
                                    Можно написать сервис l10n-manage который позволит получить доступ ко всем переводам одновременно и работать с ними как с данными.
                                    • 0
                                      Возможно вы и правы, поковыряю вашу библиотечку на досуге, посмотрю как это сделать
                                      • 0
                                        Пушнул метод l10n.getAllLocales — получить все загруженные сообщения одним объектом
                                        • 0
                                          О! Спасибо! Скоро буду делать многоязычность на сайте, надеюсь, не против, если буду обращаться за советом
                                          • 0
                                            Конечно, только лучше через github — чтобы оно потом и другим пригодилось
                                      • 0
                                        черт. продублировалось
                    • 0
                      У рельсового sprockets есть такая удобная фича как JST, смысл в том что все темплейты собираются в глобальной переменной и их удобно вытаскивать в любом месте, например
                      template: JST[«test/template»]().
                      Я для этого написал пару директив аля jst-include, в итоге все нужные темплейты загружаются 1 раз, и при этом не представляют собой одностраничной мешанины.
                      Посмотрите, может в Вашем случае тоже есть подобное решение.
                      • 0
                        А как мне сделать в роутере для JST, например?
                        • +1
                          Легко:
                          when('/main', {template: JST['main'], controller: MainCtrl}).
                          • 0
                            Спасибо, разобрался. Только window.JST['template'].
                        • +1
                          Ангулар кеширует вьюшки, причем можно собрать все вьюшки в один js файл и сразу закинуть их в кэш (для автоматизации этого процесса есть плагин grunt-angular-templates).
                          • 0
                            Для рельсы можешь пример написать?
                    • 0
                      Мне кажется что директивы как раз таки снимают проблему масштабирования приложений. У меня во всех проектах все разделено на модули, большую часть которых можно реюзать на других проектах. Модули включают в себя сервисы, директивы… От их количества мне ни холодно ни жарко, ибо они никак друг с другом не связаны и используются только там где надо.
                      • 0
                        А какие проблемы с директивами, какой трешак? Ну, работы чуть больше выходит, да, но вот серьезных проблем при полностью правильном подходе не обнаруживал. Ну и да, ангуляр-то предназначен как раз для сингл-пейдж приоложений, не для простеньких сайтов.
                        • 0
                          Наверное имеются ввиду проблемы совмещения директив и jquery-плагинов.
                          Т.к. сами по себе директивы — это просто сказка
                          • 0
                            Кстати, разработчики честно пишут, что AngularJS хорош именно для CRUD-приложений. Игры или насыщенные DOM-манипуляциями приложения предлагают писать на jQuery. А преимущество директив мне видится в том, что они в разы повышают модульность, поэтому подключать сторонние плагины не сложнее чем в jQuery: $(elem).jqplugin, <div angular-plugin></div>
                          • +1
                            по поводу такой записи:

                            function forExampleController($scope) { $scope.word="Habrahabra"; $scope.log=""; }

                            Такие конструкции любят съедать минификаторы (уже набил шишки:)). Лучше писать так:

                            angular.module('app').controller('SomeCtrl', ['$scope', function ($scope) { ...

                            или прописывать зависимости через inject:

                            SomeCtrl.$inject = ['$scope'];
                            • 0
                              Согласен, со временем я пришел к такого вида конструкции (видимо Java аннотации перед объявлением классов и методов оказали на меня влияние):

                              (function () {
                                angular.module('myModule').directive('myDirective', MyDirective)
                              
                                MyDirective.$inject = ['$scope', '$http']
                                function MyDirective ($scope, $http) {
                                  // bla bla bla
                                }
                              })()
                              
                            • 0
                              Хороший материал, однако считаю, что в любой туториал по AngularJS хорошо бы добавлять ссылку на замечательные скринкасты от John Lindquist egghead.io
                            • +1
                              Очень много хабрабры, рябит в глазах. Может лучше более осмысленные имена придумать? «Hello world», например. А лучше «Привет, мир», чтобы данные лучше выделялись среди кода на латинице :-)

                              Так же стоит упомянуть вначале, что attrs.habraHabr преобразуется соответственно из habra-habr, habra:habr и т.д.

                              Написать строчку комментария про $watch. Для новичков же статья

                              Непонятно, зачем писать «внутри директивы interpolate "{{}}"», если проще написать: внутри выражения "{{}}"

                              Почему второй пример записывается так:
                              compile: function compile(templateElement, templateAttrs) { templateElement.html("<div>{{"+templateAttrs.habraHabrWork+"}}"+templateAttrs.habra+"</div>"); return function (scope, element, attrs) { } }
                              а не так, в соответствии с приведенным выше расширенным описанием директивы
                              compile: function compile(temaplateElement, templateAttrs) { templateElement.html("<div>{{"+templateAttrs.habraHabrWork+"}}"+templateAttrs.habra+"</div>"); }, link: function (scope, element, attrs) { }
                              Не говорю, что это не правильно, просто без объяснений поменяли логику.

                              Не очень понято с pre и post. Можно постараться доходчивее объяснить.

                              Сам в сомнениях, но стоит ли переводить link fn как связующую функцию? Пока перевожу как функция линковки, и сам процесс link как линковку. Не совсем по-русски, но связывание уже употребляется при описании дата-биндинга и могут быть разночтения.

                              За статью, конечно, спасибо. Пишите еще обязательно!
                              • 0
                                Спасибо за замечания и предложения
                                преобразуется соответственно из habra-habr, habra:habr и т.д.

                                Обязательно расскажу про нормализацию атрибутов и форматы задания директив в заключительной части. Опустил это вначале из тех соображений, что это вопросы скорее относятся к стилю оформления кода, а не к самой концепции директив, и в самом начале особой пользы для понимания в них я не увидел.
                                Написать строчку комментария про $watch. Для новичков же статья

                                Да, тоже хотелось уйти от этого, но лучшей альтернативы для того, чтобы показать, как можно при инициализации директивы передавать в нее имя переменной в scope контролера и использовать ее, я не придумал.
                                Непонятно, зачем писать «внутри директивы interpolate "{{}}"», если проще написать: внутри выражения "{{}}"

                                Хотел акцентировать внимание на том, что все «выражения», которые пишутся в разметке есть директивы. Возможно слишком назойливо.
                                Почему второй пример записывается так:
                                compile: function compile(templateElement, templateAttrs) { templateElement.html("{{"+templateAttrs.habraHabrWork+"}}"+templateAttrs.habra+""); return function (scope, element, attrs) { } }
                                а не так, в соответствии с приведенным выше расширенным описанием директивы
                                compile: function compile(temaplateElement, templateAttrs) { templateElement.html("{{"+templateAttrs.habraHabrWork+"}}"+templateAttrs.habra+""); }, link: function (scope, element, attrs) { }

                                Согласен, внес изменения.

                                • 0
                                  Некоторые вещи лучше вначале не опускать, потому что у человека затык будет, если он сразу не поймет откуда что взялось. Можно хотя бы в скобочках кратко (habraHabr эквивалентен habra-habr из атрибута). Пусть упрощенно, главное, чтобы человек понял что это не ошибка/опечатка/выдумка автора, а так и должно быть и пошел бы дальше.

                                  Про $watch то же самое. Незнакомая непонятная функция, опять затык. А так ($watch следит за изменениями переменных). Можно и подробнее расписать. Хоть целый абзац ввести. Если он будет помогать понять логику, то чтение пойдет быстрее, т.к. не придется задумываться над непонятными вещами.

                                  С "{{}}" тогда уж лучше дописать строчку где отчетливо акцентировать внимание на этом. Например, «… внутри выражения "{{}}" (или внутри любой другой, встроенной в наш элемент директивы)», а то получается ни рыба ни мясо :)
                                  • 0
                                    После прочтения статьи, искал ваш комментарий :). Дейсвительно абсолютно не ясно было откуда ангуляр узнал о habra-habr если везде ему указывали habraHabr.
                              • 0
                                не туда написал, см. выше
                                • –1
                                  компиляця!
                                  • 0

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