AngularJS: как я отказался от ng-include и связал состояния двух контроллеров

в 11:05, , рубрики: AngularJS, javascript

В прошлой статье я рассказывал про свое первое знакомство с 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js