Пользователь
0,0
рейтинг
15 мая 2013 в 17:49

Разработка → Директивы в 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';
                    }
                }
            }
        }
    });


На этом предлагаю сделать паузу. Если тема интересная, в ближайшие дни постараюсь написать продолжение про области видимости и шаблоны.
@durovchpoknet
карма
21,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (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

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