AngularJS: Миграция с 1.2 на 1.4, ч.2

от автора

В первой части мы перебрали все основные проблемы перехода на новую версию, а в этой мы коснёмся того, ради чего мы это делали.

image

Как говорилось ранее, главной причиной перехода может служить существенное увеличение в скорости работы приложения: в 4.3 раза более быстрые манипуляции с DOM и в 3.5 раза более быстрые циклы $digest (по сравнению с 1.2), как заявили Джеф Кросс и Бриан Форд на конференции ngEurope.

Однако скорость эта по большей части приобретается не за счёт внутренних оптимизаций и магии, а за счёт предоставления инструментов, которые позволяют писать код более эффективно.

Давайте же рассмотрим эти инструменты!

Debug Info

Не новость, что ангуляр тратит значительную часть ресурсов на информацию, облегчающую дебаг, такую как добавление классов
элементам DOM (например, классов ng-binding и ng-isolated-scope) или прикрепление к ним различных методов для доступа к scope (например, .scope() и .isolateScope()).

Всё это полезно и необходимо для работы таких инструментов, как Protractor и Batarang, однако нужны ли эти данные на продакшене?

Начиная с версии 1.3, debug info можно отключить:

app.config(['$compileProvider', function ($compileProvider) {     $compileProvider.debugInfoEnabled(false); }]);

Но что делать, если нам необходимо продебажить продакшн, а дебаг отключён?

Здесь нас спасёт метод .reloadWithDebugInfo() объекта angular, а поскольку объект angular глобальный, то мы можем выполнить этот код просто из консоли:

angular.reloadWithDebugInfo();

$applyAsync

С версией 1.3 пришёл сервис $applyAsync, во многом схожий по своей механике с уже существующим сервисом $evalAsync:

Упрощённо говоря, он добавляет выражение в очередь, затем ожидает (выставляет setTimeout(…, 0), в современных браузерах это около 10 милисекунд), и если за прошедшее время в очередь не добавлено ещё одно выражение – запускает
$rootScope.$digest().

Это позволяет запускать множество параллельных выражений, влияющих на DOM, будучи уверенным в том, что они запустятся в один цикл $digest и не беспокоясь о слишком частом вызове $apply.

В чём отличие от $evalAsync?

Главное отличие в том, что $applyAsync сам выполняет всю очередь выражений в начале цикла $digest, перед dirty checking, что позволяет выполнить его только один раз, в то время как очередь $evalAsync выполняется во время грязной проверки (если точнее, то в самом начале цикла грязной проверки), и любое добавленное в очередь выражение (вне $watch) запустит цикл $digest ещё раз, что приведёт к повторному выполнению выражения в этом же $digest чуть позже.

[подробнее]

Однако истинную пользу от использования этого инструмента можно увидеть во внутренних сервисах ангуляра, например, в $httpProvider.

$http

Главное отличие сервиса $http от иных способов совершить XHR запрос – это вызов $apply по его завершению.

Проблема заключается в множестве паралельных запросов, каждый из которых вызывает по своему завершению $apply, что приводит к тормозам.

Проблема решена с приходом $applyAsync в версии 1.3:

app.config(function ($httpProvider) {     $httpProvider.useApplyAsync(true); });

Данный код включает использование очереди $applyAsync внутри $httpProvider.

Это позволяет запускать $apply лишь один раз, когда все промисы одновременных запросов будут resolved, что даёт значительный прирост производительности.

Bind Once

Одна из главных проблем с производительностью в AngularJS заключается в огромном количестве $watch‘еров из-за того, что ко всем выражениям применяется двустороннее связывание, но не все данные этого требуют.

Для статических данных достаточно одностороннего связывания, без навешивания вотчера, и раньше это решалось кастомными директивами, но всё изменилось в версии 1.3.

Начиная с версии 1.3, доступен новый синтаксис в виде :: в начале выражения.

Любое выражение, начинающиеся с ::, будет воспринято как одностороннее связывание и перестанет отслеживаться (unwatch), как только данные в выражении станут стабильными и пройдёт первый цикл $digest.

Например:

{{:: foo }}  <button ng-bind=":: foo"></button>  <ul>     <li ng-repeat=":: foo in bar"></li> </ul>  <custom-directive two-way-bind-property=":: foo"><custom-directive>

Стабильность данных:

Данные считаются нестабильными до тех пор, пока они равны undefined. Любые другие данные, будь то NaN, false, '', [] или null, считаются стабильными и приведут к unwatch выражения.

Это необходимо для статичных данных, которые недоступны во время первого цикла $digest.

Например, если данные приходят с сервера, то можно просто держать в переменной значение undefined во время ожидания запроса. Всё это время вотчер этого выражения будет жить и только после установки данных, отличных от undefined, отдаст концы.

Относитесь к выражениям с :: как к константе: как только она установлена, её уже невозможно изменить.

Обновить необновляемое:

Допустим, у нас есть выражение, которое не обновляется в 99 случаев из 100 (т.е. нам нафиг не впал на неё вотчер), но иногда это требуется. Как быть? Я задался вопросом, можно ли силой обновить bind-once выражение.

Нет, нельзя 🙂 Однако можно написать свою директиву-атрибут, которая заставит сделать re-compile всю директиву в ответ на некие события. Пример доступен здесь.


На этом часть о производительности заканчивается, и можно поговорить о просто приятных плюшках:

ngModel Options

Версия 1.3 подарила в дополнение к ng-model вспомогательную директиву ng-model-options, которая отвечает за то, когда модель будет обновлена.

Время обновления зависит от двух факторов:

1) updateOn – специальные события (events), по которым происходит обновление модели, например, это может быть blur, click или какой-то кастомный ивент. По умолчанию всегда стоит default, означающий, что каждый контрол будет использовать своё собственное событие. Если вы хотите расширить стандартное событие, добавив своё, не забудьте добавить в список default, например: {event: "default customEvent"}.

2) debounce – задержка при обновлении модели в ожидании новых данных (по умолчанию 0, т.е. мгновенно). Если указать инпуту {debounce: 300} и ввести 3 символа с промежутком менее 300 миллисекунд, ваша модель (а значит и различные модификаторы/валидаторы) обновится лишь один раз. Кроме того, debounceможно комбинировать с событиями, указывая свою задержку для каждого из них, например: {event: "default customEvent", debounce: {default: 0, customEvent: 400}}.

Это позволяет нам избавиться от множества велосипедов (прощайте, setTimeout/clearTimeout) и существенно повышает производительность (ведь мы избавляемся от бесполезного перезапуска $digest, а соответственно и всех $watchers), а также уменьшает количество ложных срабатываний для асинхронной валидации (впрочем, $http сервис достаточно умный, чтобы не спамить запросами, а подождать стабильных данных).

Но есть ещё три полезные опции

Флаг allowInvalid позволит установить $modelValue, даже если значение является невалидным для него (по умолчанию пока значение является невалидным, в модель записывается undefined, что не позволяет, например, узнать промежуточное значение)

Флаг setterGetter позволит установить в качестве ngModel свою собственную функцию, своеобразного посредника между ngModel.modelValue и ngModel.viewValue выполняющего роль сеттера и геттера. Живой пример на plunker.

timezone позволяет установить часовой пояс для контролов, связанных со временем (date или time), например '+0430' будет означать ‘4 часа 30 минут GTM’. По умолчанию берётся часовой пояс браузера.

Игнорируя updateOn и debounce

Иногда при ручной записи модели необходимо проигнорировать установленную в ивентах задержку и выполнить обновление мгновенно. Для этого существует метод ngModelCtrl.$commitViewValue().

Отмена изменения

Если необходимо отменить все изменения и висящие в процессе debounce, существует метод $rollbackViewValue() (бывший $cancelUpdate()), который подгоняет вьюху к актуальному состоянию модели.

В качестве примера использования такой возможности официальная документация предлагает инпат, изменения в котором можно откатить по нажатию ESC.

Подробная документация

Валидация

Одно из главных улучшений в плане удобства работы коснулось валидации форм.

Ранее приходилось реализовывать механизм валидации через ndModel.$formatters и ndModel.$parsers, влияя на результат валидации напрямую, через ndModel.$setValidity(), а реализация асинхронных проверок доставляла отдельную радость.

Нововведение отразилось и на производительности:

Ведь ранее валидирующие функции запускались при каждом обновлении в DOM ($parsers) или модели ($formatters), часто влияя на значения друг друга, тем самым перезапуская цикл проверок заново.

В новой версии валидация запускается, только если изменена модель и только если в этой модели нет ошибки ({parse: true). Влияние на саму модель или представление контрола внутри валидатора тоже исключается, что положительно влияет на скорость работы приложения.

Что такое $formatters и $parsers, для чего созданы? Их удалили?

Нет, их не удалили, это другие инструменты для других вещей, и они по-прежнему необходимы.

И $formatters, и $parsers – это массивы, содержащие функциии-обработчики, которые принимают значение и передают его по цепочке следующей функции-обработчику. Каждое звено цепочки может модифицировать значение перед тем, как передать его далее.

$formatters
Каждый раз, когда изменяется модель, $formatters перебирает обработчики в массиве в обратном порядке, от конца к началу. Последнее переданное значение отвечает за то, как будет представлена модель в DOM. Иными словами, $formatters отвечает за то, как будет конвертирован ngModelCtrl.$modelValue в ngModelCtrl.$viewValue.

$parsers
Каждый раз, когда контрол читает значение из DOM, $parsers перебирает массив обработчиков в нормальном порядке, от начала к концу, передавая значение по цепочке. Последнее переданное значение отвечает за то, как будет представлено значение в модели. Иными словами, $parsers отвечает за то, как будет конвертирован ngModelCtrl.$viewValue в ngModelCtrl.$modelValue.

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

Пример выше – это использование обработчиков для предварительной обработки (sanitize) значения, если мы хотим быть уверены, что оно попадёт в модель только в строго заданном виде.

Другие два популярных способа использования это двусторонняя фильтрация значений (например, когда пользователь вводит в инпут "10, 000", а в модели хранится "10000" и наоборот) и создание масок (например, когда пользователь должен заполнить маску телефона "+7 (000) 000-00-00", а в модели мы будем хранить "70000000000").

Как видно из примеров – инструмент незаменимый.

Как работала валидация раньше?

Исчерпывающую информацию о старых методах валидации можно получить по данным статьям на Хабре:

Почему не стоит использовать старые методы валидации и дальше?

Дело в том, что возращение undefined из ndModel.$parsers теперь приводит к выставлению ngModelCtrl.$modelValue в undefined и добавлению в ndModel.$errors значения {parse: false}, т.е. инвалидации поля.

В этом случае валидаторы ($validators и $asyncValidators) даже не начинают своей работы.

Это поведение можно отключить в ngModelOptions, выставив флаг allowInvalid в true.

Начиная с версии 1.3 у нас появились инструменты для удобной синхронной и асинхронной проверки.

Также стоит упомянуть, что раз работа новых валидаторов завязана на обновлении модели, то она зависит и от ngModelOptions, о которых говорилось выше.

Синхронная

Для синхронной валидации новая версия предлагает нам коллекцию ndModel.$validators, расширяя её функцией-валидатором.

Функция-валидатор должна возращать true или false, для валидных и невалидных значений соответственно.

Пример:

    ngModel.$validators.integer = function(modelValue, viewValue) {         // Определяем, что пустая модель является валидной          if (ctrl.$isEmpty(modelValue)) {             return true;         }          if (INTEGER_REGEXP.test(viewValue)) {             return true; // Поле валидно         }          return false; // Поле не валидно     };

Асинхронная

Для асинхронной валидации используется коллекция ngModelCtrl.$asyncValidators, с той же логикой расширяя её функцией-валидатором.

Основные отличия работы асинхронной версии:

  • Асинхронная валидация запускается только если все синхронные валидации оказались валидными
  • Функция-валидатор должна возвращать только промис, осуществляя его resolve() или reject(), для валидных и невалидных значений соответственно.

Промисы в AngularJS порождаются специальным сервисом $q, а также сервисами вроде $timeout и $http, которые в своей основе тоже используют $q.

В коллекции хранится не более одного промиса возращённого одним валидатором. Это значит, что если вызвать валидацию несколько раз, то будет учитываться только промис из последнего вызова валидатора, независимо от результата работы предыдущих промисов.

С момента передачи функцией-валидатором промиса, и до его разрешения (resolve() или reject()) поле ngModelCtrl.$pending хранит имя валидатора, а ngModelCtrl.$valid и ngModelCtrl.$invalid равны undefined

Будьте осторожны с данной особенностью: до тех пор пока идёт валидация форма будет являться не валидной, при этом вы не увидите никаких ошибок в FormCtrl.$errors, но можете увидеть "ожидающие" валидаторы в FormCtrl.$pending.

Примеры:

  ngModelCtrl.$asyncValidators.username = function(modelValue, viewValue) {         // Определяем, что пустая модель является валидной        if (ctrl.$isEmpty(modelValue)) {           return $q.when();       }        var def = $q.defer();        // Эмулируем асинхронный запрос       $timeout(function() {           if (usernames.indexOf(modelValue) === -1) {               def.resolve(); // Имя доступно, поле валидно           } else {             def.reject(); // Имя занято, поле не валидно           }       }, 2000);        return def.promise;   };    ngModelCtrl.$asyncValidators.uniqueUsername = function(modelValue, viewValue) {     var value = modelValue || viewValue;      // Проверяем, существует ли имя пользователя     return $http.get('/api/users/' + value).        then(function resolved() {           // Пользователь найден, значит имя занято, а поле не валидно          return $q.reject('exists');        }, function rejected() {           // Пользователь не найден, имя доступно, а поле валидно          return true;        });   };

Стоит аккуратно использовать асинхронную валидацию с полями, которые используют модификаторы значения (через $formatters и $parsers): это может вызывать множественные срабатывания, а также ложную валидацию или инвалидацию поля.

Пример валидации для таких случаев

var pendingPromise;  ngModelCtrl.$asyncValidators.checkPhoneUnique = function (modelValue) {     if (pendingPromise) {         return pendingPromise;     }      var deferred = $q.defer();      if (modelValue) {         pendingPromise = deferred.promise;          $http.post('/запрос', {value: modelValue})             .success(function (response) {                 if (response.Result === 'Хитрое условие с сервера') {                     deferred.resolve();                 } else {                     deferred.reject();                 }             }).error(function () {                 deferred.reject();             }).finally(function () {                 pendingPromise = null;             });     } else {         deferred.resolve();     }      return deferred.promise; };

ngMessages

Модуль ngMessages призван облегчить показ сообщений на странице.

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

Для примера сравнения будем использовать данную форму добавления комментария:

<form name="commentForm">     <ul class="warnings"         ng-if="commentForm.$error && commentForm.$dirty">         ...     </ul>      <label>Имя:</label>     <input type="text"             name="username"            ng-model="comment.username" required>      <label>Комментарий:</label>     <textarea name="message"               ng-model="comment.message"                minlength="5" maxlength="500"></textarea> </form>

Оставим за бортом реализацию непосредственно валидации и рассмотрим только отображение сообщений о необходимых условиях:

  • Поле "Имя" не должно быть пустым
  • Поле "Сообщение" не должно быть менее 5 символов
  • Поле "Сообщение" не должно быть более 500 символов
    Ошибки будем отображать в блоке .errors.

Старые методы показа ошибок

Пример вышел длинный, поэтому я скрыл его под спойлер.

Теперь представим, как нам отобразить ошибки для этих полей:

<form name="commentForm">     <ul class="warnings"         ng-if="commentForm.$error && commentForm.$dirty">         <span ng-if="commentForm.message.$error.minlength">             Сообщение не может быть менее 5 символов         </span>         <span ng-if="commentForm.username.$error.maxlength">             Сообщение не может быть более 500 символов         </span>         <span ng-if="commentForm.username.$error.required">             Это обязательное поле         </span>     </ul>      <label>Имя:</label>     ...      <label>Комментарий:</label>     ... </form>

Что ж, пока всё выглядит не так уж и плохо, да?
Но почему сообщения о никнейме и комментарии показываются все разом? Давайте исправим это, добавив последовательное отображение:

<form name="commentForm">     <ul class="warnings"         ng-if="commentForm.$error && commentForm.$dirty">         <span ng-if="commentForm.message.$error.minlength &&                      commentForm.username.$valid">             Сообщение не может быть менее 5 символов         </span>         ...     </ul>      <label>Имя:</label>     ...      <label>Комментарий:</label>     ... </form>

Вы всё ещё думаете: «это не так плохо»? Представьте, что полей на странице не 2, а 20, и у каждого минимум 5 сообщений. В подобном стиле наша страница быстро превратиться в мусорку из условий.

Конечно, есть лучшие практики для реализации данной задачи, специальные директивы и костыли, расширяющие поведение FormController (например, в нашем проекте все контролы расширялись свойством showError, которое хранило текущую ошибку), но все они проигрывают в удобстве методам, о которых мы будем говорить далее.

Новые методы показа ошибок

Подключение

ngMesssages поставляется отдельным модулем, и перед тем как начать работать с ним, нам надо его подключить. Скачайте или установите модуль через пакетный менеджер и подключите к проекту:

<script src="path/to/angular-messages.js"></script>

И добавим в зависимости:

angular.module('myApp', ['ngMessages']);

Базовая работа с данным модулем сводится к работе с двумя директивами:

  1. ng-messages – контейнер, содержащий наши сообщения
  2. ng-message – непосредственно сообщение

ng-messages принимает в качестве аргумента коллекцию, по ключам которой будут сверяться и показываться уже ng-message, которые принимают в качестве аргумента для сравнения строку или выражение (начиная с 1.4).

Давайте повторим пример всё той же формы добавления комментария, но уже с помощью ngMessages:

<div ng-messages="commentForm.message.$error" class="warnings">     <p ng-message="minlength">         Сообщение не может быть менее 5 символов     </p>                 <p ng-message="maxlength">         Сообщение не может быть более 500 символов     </p>     <p ng-message="required">         Это обязательное поле     </p> </div>

ngMessages можно использовать и в качестве элемента:

<ng-messages for="commentForm.message.$error" class="warnings">     <ng-message when="minlength">         Сообщение не может быть менее 5 символов     </ng>                <ng-message when="maxlength">         Сообщение не может быть более 500 символов     </ng>     <ng-message when="required">         Это обязательное поле     </ng> </ng-messages>

И так, сходу ngMessages решили для нас две проблемы:

  • Лапша из условий превратилась в удобочитаемый switchподобный список
  • Проблема с выводом сообщений по одному решилась сама собой

Кроме того, решается и проблема приоритезации вывода сообщений. Здесь всё просто: сообщения показываются согласно их расположению в DOM.

Вывод множественных сообщений можно включить добавлением атрибута ng-messages-multiple к директиве ng-messages:

<ng-messages ng-messages-multiple for="commentForm.message.$error">     ... </ng-messages>

Повторное использование сообщений – это ещё одна важная особенность ngMessages, которая позволяет подключать наши сообщения, подобно шаблонам, в любом необходимом месте:

Внимание, далее пойдёт речь о версии 1.4, в версии 1.3 имеется отличие

Версия 1.3 использует ng-messages-include в качестве дополнительного атрибута ng-messages, в то время как в более поздних версиях ng-messages-include это самодостаточная дочерняя директива наряду с ng-message:

1.3

<ng-messages ng-messages-include="length-message"               for="commentForm.message.$error"> </ng-messages>

1.4+

<ng-messages for="commentForm.message.$error">     <ng-messages-include="length-message"></ng-messages-include> </ng-messages>

Это позволяет нам вынести часто повторяющиеся сообщения (например, сообщения о длине поля) в отдельный шаблон, который мы будем включать в нужных местах без необходимости копипасты:

<script type="script/ng-template" id="length-message">     <ng-message when="minlength">         Значение поля слишком короткое     </ng-message> </script>  ...  <ng-messages for="commentForm.message.$error">     <ng-messages-include="length-message"></ng-messages-include> </ng-messages>  <ng-messages for="anotherForm.someField.$error">     <ng-messages-include="length-message"></ng-messages-include> </ng-messages>

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

Мы не ограничены статическим вызовом шаблонов, и с версии 1.4 нам доступны динамические сообщения, которые возможно создавать через директиву ng-message-exp:

// error = {type: required, message: 'Поле не должно быть пустым'}; <ng-messages for="commentForm.message.$error">     <ng-message-exp="error.type">         {{ error.message }}     </ng-message-exp> </ng-messages>

ng-message-exp, в отличие от ng-message, принимает в качестве аргумента выражение (expression). Это позволяет нам, например, показывать сообщения, которые генерирует нам сервер в AJAX запросе.

Динамический вывод сообщений – это мощнейший инструмент. Ведь он позволяет нам генерировать любые типы ошибок с любым содержанием текста без привязки к статическому представлению или привязанной к ng-messagesколлекции!

Что мы имеем в итоге:

  • Сформированный список без лапши из условий
  • Приоритезированный вывод сообщений по одному прямо из коробки
  • Повторное использование сообщений на проекте
  • Удобный инструмент создания динамических сообщений
  • Срочную необходимость бросать всё и переносить свой старый механизм показа ошибок на ng-messages

Контроллеры

bindToController

[пример]

Каждый, кто любит использовать controller as синтаксис, знает боль использования его для директив с изолированным scope.

Проблема заключается в двустороннем связывании свойства, указанного через this.something внутри контроллера. Любые изменения значения извне ни к чему не приведут.

Например:

app.directive('someDirective', function () {     return {         scope: {             name: '='         },         controller: function () {             this.name = 'Foo'         },         controllerAs: 'ctrl'         ...     }; });

Любые изменения name из контроллеров выше ни к чему не приведут.

Это можно решить через жопу вотчер:

  $scope.$watch('name', function (newValue) {         this.name = newValue;   }.bind(this));

Но это неудобно, костыльно, и что делать, если в скоупе не одно свойство, а пятьдесят?

Решение пришло с версией 1.3:

Встречайте свойство bindToController.

app.directive('someDirective', function () {     return {         scope: {             name: '='         },         controller: function () {             this.name = 'Foo'         },         bindToController: true,         ...     }; });

Теперь ctrl.name связано с $scope.name и будет изменяться вместе с ним.

С версией 1.4 был добавлен ещё более удобный синтаксис:

app.directive('someDirective', function () {     return {         scope: true,         bindToController: {             name: '='         },         controller: function () {             this.name = 'Foo'         },         ...     }; });

Теперь можно передавать объект определения scope прямо в bindToController.

Всё, что передано в bindToController, будет привязано к контроллеру, а всё, что передано в scope, будет привязано к scope соответственно.

При этом указывать что-то для scope вовсе не обязательно, хватит true. В этом случае всё указанное в bindToController привяжется и для scope.

Фильтры

{{ expression | filter }}

Динамические фильтры

Здесь речь пойдёт о фильтрах, преимущественно завязанных на данных, приходящих «извне» и не зависящих от выражения (expression), к которому привязан фильтр (filter). Это может быть, например, фильтр, который использует внутри себя некий сервис.

В версии до 1.3 фильтры были довольно глупыми: навешивая на выражение вотчер, они постоянно заново вычисляли результат этого фильтра. Это одна из причин, почему не стоит использовать множество фильтров в одном месте, нагружая лишней работой цикл $digest, и одна из причин, почему новые фильтры работают быстрее. В новой версии фильтры ведут себя куда умнее и не вычисляют значение фильтра до тех пор, пока не изменится выражение, к которому привязан данный фильтр.

Живой пример на plunker

Однако возникает проблема: как обновить значение фильтра, если оно зависит не только от выражения, но и от каких-то внешних факторов? Иными словами, как вернуть фильтру своё старое, «глупое» поведение, когда нам это нужно?

Для этого в 1.3 были введены понятия статичного (stateless) и динамического (stateful) фильтров. По умолчанию фильтр ведёт себя как stateless. Сменить его поведение можно, выставив нашему фильтру флаг $stateful в true.

Пример:

angular.module('myApp', [])     .filter('customFilter', ['someService', function (someService) {         function customFilter(input) {             // манипуляция данными сторонним сервисом someService             input += someService.getData();             return input;         }          customFilter.$stateful = true;          return customFilter;     }]);

Breaking change:
Внимательный читатель мог заметить, что такое изменение по сути может сломать поведение старых динамических фильтров. К сожалению, я забыл описать эту особенность в первой части, но такую возможность стоит учесть.

Фильтр dateFilter

В данный фильтр теперь добавлена поддержка недель как формата weeks

Заключение

На этом всё. Если я забыл какие-то важные особенности новых версий или знаете ресурсы с более полным описанием оных, поправляйте меня в комментариях.

От себя добавлю данный блог, с достаточно полным описанием всех основных фич новых версий ангуляра.

Спасибо хабрахабру

За то, что наконец сделали markdown. Изначально статья была написана именно в этой разметке и провалялась почти пол года из-за моей лени и не желания переводить всё в html.

А это котик, для тех, кто дочитал до конца 🙂

image

Какую версию используете вы?

Проголосовал 1 человек. Воздержавшихся нет.

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

ссылка на оригинал статьи https://habrahabr.ru/post/281721/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *