В прошлой статье я рассказывал про свое первое знакомство с 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();
}
}
}]);
Теперь мы сможем использовать шаблоны следующим образом:
<div data-template="paging" data-onload="foo = 'bar'">
Решив проблему с $location, я немного переписал сервис $query, чтобы теперь он работал исключительно с history API.
К слову, не пытайтесь использовать их вместе. Это приведет к бесконечному циклу.
Так что теперь адресная строка получилась более понятной и приятной на вид:
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 директивы шаблона. Грубо говоря, если в контроллере вдруг появляется потребность запросить состояние, это можно сделать не по оригинальному ключу, который может быть недоступен, а по заранее заданному значению.
Вместо послесловия
Так вышло, что, казалось бы, тривиальная задача переросла в полноценный рефакторинг. Впрочем, по его окончании, по крайней мере, на мой взгляд, код стал гораздо проще для чтения и более предсказуем в работе. Я больше не теряюсь в бесконечных событиях, питаюсь только здоровой пищей и занимаюсь спортом. Надеюсь, эта статья поможет другим обрести душевный покой и научиться чему-то новому. Удачи и приятного дня!
Автор: 5angel