AngularJS: как я отказался от ng-include и связал состояния двух контроллеров
В прошлой статье я рассказывал про свое первое знакомство с AngularJS. С тех пор прошел уже год, сейчас у меня новые проекты и другие, часто менее тривиальные задачи. Здесь я опишу несколько нюансов, с которыми мне пришлось столкнуться в процессе работы над одной из систем. Надеюсь, читатели смогут извлечь пользу из моих практик.Поиск и якорьПредположим, что нам поступила задача разработать клиентскую часть для нашего нового проекта. Это каталог, в котором будут храниться сотни тысяч документов. Поскольку он довольно большой, в API предусмотрена возможность загружать элементы постранично (с указанием начального индекса), а также фильтровать по отдельным полям в документе.А для того, чтобы пользователи не терялись в системе и могли делиться между собой информацией, клиент должен сохранять свое состояние в адресной строке.Что ж, задание понятно. Приступаем.
Чтобы хранить значения фильтров, создадим сервис $query. У него будет два метода:
push (query) — получает объект с набором фильтров и добавляет их в адресную строку (поле search). parse () — преобразует search обратно в набор фильтров. Здесь следует сделать отступление. Поскольку на странице используется несколько шаблонов (например, для пагинации), в адресную строку автоматически добавляется решетка (#). Это происходит из-за того, что ng-include использует сервис $location, при наличии которого angular начинает считать, что мы делаем одностраничное приложение.
Соответственно, объект вида { index: 0, size: 20 } превратится в http://localhost:1337/catalog#? index=0&size=20
Но постойте. Пользователи хотят не только получать состояние страницы, но и отмечать на ней отдельный документ.Официальная документация в таком случае советует использовать $anchorScroll или scrollTo.Т.е. теперь мы получим следующее:
http://localhost:1337/catalog#? index=0&size=20&scrollTo=5
В этот момент мое эстетическое чувство воззвало к поиску другого решения.Первой мыслью было отказаться от ng-include, чтобы адресная строка больше не подвергалась насилию со стороны ангуляра. Но что тогда делать с шаблонами? Выход был только один: написать собственную директиву для работы с шаблонами.
С блекджеком и шаблонами С директивой проблем не возникло. Для работы с шаблонами angular использует сервис $templateCache. В него можно положить кусок html-кода с помощью text/ng-template или метода put (). Также, по аналогии с ng-include, предусмотрим выполнение кода из атрубита onload.Код директивы:
app.directive ('template', ['$templateCache', '$compile', function ($templateCache, $compile) { return { scope: false, link: function (scope, element, attrs) { var tpl = $compile ($templateCache.get (attrs.orgnTemplate))(scope);
tpl.scope ().$eval (attrs.onload || ''); element.after (tpl); element.remove (); } } }]); Теперь мы сможем использовать шаблоны следующим образом:
Так что теперь адресная строка получилась более понятной и приятной на вид: http://localhost:1337/catalog? index=0&size=20#5
И перемещение по якорям больше не требует дополнительного кода.Легкость общения Разбив страницу на отдельные шаблоны и контроллеры, я неожиданно столкнулся с другой проблемой: контроллеры должны взаимодействовать между собой. Даже если не состоят в родительских отношениях. А в отдельных случаях (опять же, пагинация), контроллеры должны синхронизировать свое состояние.Первый вариант заключался во взаимодействии между контроллерами через события. В таком случае на каждое действие контроллеры рассылают друг-другу события. К сожалению, в моем случае кол-во и разнообразие событий на квадратный сантиметр кода стало переходить все разумные рамки. Я решил отказаться от оптимизации и сделать отдельный механизм для обмена информацией вне зависимости от текущего scope.
Так появился сервис $store. В первом варианте у него был один метод:
value (key, value) — сохраняет или извлекает значение по ключу. В контроллеры был добавлен следующий код: $scope.$watch (function () { return $store.value ('foo'); }, function (data) { doSomething (data); }, true); Теперь, когда мне требовалось синхронизировать состояние двух и более контроллеров, я лишь перезаписывал значение в ключе: $store.value ('stream', data); Не забывайте, что все сервисы являются синглтонами, поэтому при добавлении сервиса одновременно в несколько контроллеров, мы получаем доступ к одному и тому же объекту.
Позже, когда я немного автоматизировал передачу данных между двумя шаблонами (например, список элементов теперь автоматически привязывался к пагинации с помощью своего $id), в сервис был добавлен метод alias (): alias (key, values…) — добавляет или возвращает список синонимов для указанного ключа. Таким образом, у меня появилась возможность указывать alias в атрибуте onload директивы шаблона. Грубо говоря, если в контроллере вдруг появляется потребность запросить состояние, это можно сделать не по оригинальному ключу, который может быть недоступен, а по заранее заданному значению.Вместо послесловия Так вышло, что, казалось бы, тривиальная задача переросла в полноценный рефакторинг. Впрочем, по его окончании, по крайней мере, на мой взгляд, код стал гораздо проще для чтения и более предсказуем в работе. Я больше не теряюсь в бесконечных событиях, питаюсь только здоровой пищей и занимаюсь спортом. Надеюсь, эта статья поможет другим обрести душевный покой и научиться чему-то новому. Удачи и приятного дня!