Многие впервые увидев синтаксис шаблонов Angular2 начинают причитать, мол ужас какой сделали, неужто нельзя было как в Angular1 хотя-бы. Зачем нужно было вводить это разнообразие скобочек, звездочек и прочей ерунды! Однако при ближайшем рассмотрении все становится куда проще, главное не пугаться.
Так как шаблоны в AngularJS являются неотъемлемой его частью, важно разобраться с ними в самом начале знакомства с новой версии этого фреймворка. Заодно обсудим, какие преимущества дает нам данный синтаксис по сравнению с angular 1.x. Причем лучше всего будет рассматривать это на небольших примерах.
Данная статья во многом основана на материалах этих двух статей:
Для того, что бы упростить подачу материала, давайте разберемся. Под AngularJS я буду подразумевать всю ветку Angular 1.x, в то время как под Angular2 — ветку 2.x.
Так же спасибо господину Bronx за ценное дополнение, которое я включил в текст статьи.
Примечание: вечер выходного дня, потому о опечатках и т.д. сообщайте в личку. Премного благодарен и приятного чтения.
Биндинг свойств элементов
В случае простого вывода данных разницы вы не почувствуете, однако, если вы решите передать часть состояния в качестве значения какого-либо атрибутов элементов, можно уже наблюдать интересные решения.
Давайте освежим в памяти как работают биндинги на атрибуты в AngularJS. Для этого мы обычно напрямую пробрасывали выражения (интерполируемые при компиляции шаблона) прямо в атрибут элемента, либо пользовались одной из множества директив. Например
ngValue
для установки значения свойства value
у инпутов.Приведем пример как это работает в AngularJS:
<input ng-value="expression" />
Так же не забываем что мы можем просто интерполировать результат выражения напрямую в качестве значения аргумента:
<input value="{{expression}}" />
Заметим интересную особенность. Второй вариант многие его избегают, так как можно увидеть промежуточное состояние до того, как angular интерполирует значения. Однако первый вариант использует директивы. То есть для того что бы у нас все было хорошо, красиво и удобно, нам надо сделать по директиве на каждое свойство всех элементов. Согласитесь, не слишком то удобно. Почему бы не добавить какое-то обозначение для атрибута, которое бы говорило ангуляру замэпить значение на него. Причем было бы неплохо что бы синтаксис был валидным. И они добавили, теперь для этого надо всего-лишь обернуть интересующий нас атрибут (любой атрибут) в квадратные скобки —
[]
.<input [value]="valueExpression" [placeholder]="placeholderExpression" />
По сути синтаксис
[]
это ни что иное как сокращенная запись для bind-prop
. Если убрать сахар то пример выше будет записан как:<input bind-value="valueExpression" bind-placeholder="placeholderExpression" />
Однако вспомним как можно поменять атрибуты в Javascript:
element[prop] = 'value';
именно отсюда и берутся эти квадратные скобочки. Мы поясняем что значения будут мэпится прямо на свойства элементов. То есть вместо директивы
ng-bind-html
мы можем просто биндить результат выражения на [inner-html]
. Или вместо ng-hide
мы можем использовать свойство [hidden]
самого элемента. Это значительно уменьшает количество специфичных для angular вещей которые нужно знать и приближает нас к DOM, при этом у нас все еще сохраняется абстракция от оного.Ну и закрыть этот вопрос стоит указав, что у нас так же есть возможность мэпить интерполируемое значение, так же как это было в AngularJS:
<input value="{{ valueExpression }}" placeholder="{{ placeholderExpression }}" />
События
В AngularJS мы могли подписаться на события элементов используя специальные директивы. Так же, как и в случае со свойствами, нам приходится иметь дело с целой кучей возможных событий. И на каждое событие приходилось делать директиву. Пожалуй, самой популярной из таких директив является
ngClick
:<button ng-click="doSomething($event)">
Учитывая что мы уже решили такую проблему для свойств элементов, то наверное так же стоит решать и эту проблему? Именно это и сделали! Для того, что бы подписаться на событие, достаточно прописать атрибут используюя следующий синтаксис:
(eventName)
. Таким образом у нас есть возможность подписаться на любое событие генерируемое нашим элементом, при этом без надобности писать отдельную директиву:<button (click)="doSomething($event)">
Так же как и в случае с биндингом свойств, это так же сахар для
on-
записи. То есть пример выше можно было зы баписать как:<button on-click="doSomething($event)">
А вид скобочек получается из ассоциаций с установлением обработчиков событий в javascript:
element.addEventListener('click', ($event) => doSomething($event));
Стоит так же отметить важное отличие в поведении, если сравнивать с биндингом событий в AngularJS. Теперь Angular2 будет выбрасывать нам ошибки в случае обращения к несуществующему методу. Как если бы мы вызывали код прямо в javascript. Я думаю многие из тех у кого бывали глупые баги из-за опечаток в этом контексте будут рады. В некоторых случаях это добавляет определенные неудобства и потому был добавлен так же Elvis-оператор, о котором мы поговорим чуть позже.
Двусторонний биндинг
Существует распространенное мнение, что двусторонний биндинг это плохо, и что это основной грех ангуляра. Это не совсем так. Проблема с двусторонним биндингом в AnugularJS была в том, что он используется повсеместно, не давая разработчикам альтернативы (возможно ситуация с этим в скором времени изменится).
Все же иногда возникают случаи когда двусторонний биндинг здорово упрощает разработку, особенно в случае форм. Так как же реализован оный в Angular2? Давайте подумаем, как организовать двусторонний биндинг имея односторонний биндинг свойст элементов и биндинг событий:
<input type="text" [value]="firstName" (input)="firstName=$event.target.value" />
Опять же, не очень то удобно. Посему в Angular2 так же есть синтаксический сахар с использованием
ngModel
. Результат будет идентичен тому, что мы привели выше:<input type="text" [(ngModel)]="firstName" />
Локальные переменные
Для передачи данных между элементами в пределах одного шаблона используются локальные переменные. Наиболее близкой аналогией в AngularJS, пожалуй, можно считать доступ к элементам формы по имени через ngForm. Конечно, это не совсем корректное сравнение, так как работает это только за счет директивы
ngForm
. В Angular2 вы можете использовать ссылку на любой объект или DOM элемент в пределах элемента шаблона и его потомков, используя локальные переменные #
.<video #movieplayer ...>
<button (click)="movieplayer.play()">
</video>
В данном примере мы можем видеть, как через переменную
movieplayer
мы можем получить доступ к API медиа элемента прямо в шаблоне. Помимо символа
#
, вы так же можете объявлять переменные используя префикс var-
. В этом случае вместо #movieplayer
мы могли бы записать var-movieplayer
.Благодаря локальным переменным нам больше не нужно делать новые директивы всякий раз, когда действия над одними элементами должны менять что-то у других. Например, в примере выше, мы можем быстро добавить кнопку которая запускает просмотр видео. В этом собственно главное концептуальное отличие Angular2 от AngularJS, меньше бесполезных микро-директив, большая концентрация внимания на компонентах.
Звездочка (символ *)
Символ
*
вызывает больше всего вопросов. Давайте разберемся, зачем он понадобился. Для осознания причин добавления этого символа, нам стоит вспомнить о таком элементе как template
.Элемент
template
позволяет нам задекларировать кусочек DOM, который мы можем инициализировать позже, что дает нам более высокую производительность и более рациональное использование ресурсов. Чем-то это похоже на documentFragment
в контексте HTML.Пожалуй, будет проще показать зачем оно надо на примере:
<div style="display:none">
<img src="path/to/your/image.png" />
</div>
В этом небольшом примере мы можем видеть, что блок скрыт (
display:none
). Однако браузер все равно будет пытаться загрузить картинку, даже если она не понадобится. Если подобных вещей на странице много, это может пагубно отразиться на общей производительности страницы.Решением этой проблемы станет использование элемента
template
.<template>
<img src="path/to/your/image.png" />
</template>
В этом случае браузер не будет загружать изображение, пока мы не инициализируем шаблон.
Но вернемся к нашим баранам. Использование символа
*
перед директивой элемента позволит ангуляру при компиляции обернуть элемент в шаблон. Проще посмотреть на примере:<hero-detail *ngIf="isActive" [hero]="currentHero"></hero-detail>
Этот шаблон будет трансформирован в такой:
<template [ngIf]="isActive">
<hero-detail [hero]="currentHero"></hero-detail>
</template>
Теперь должно стать ясно, что этот символ предоставляет синтаксический сахар для достижения более высокой производительности при использовании условных директив вроде
ngFor
, ngIf
и ngSwitch
. Логично что нет нужды в создании экземпляра компонента hero-detail
пока isActive
не является истиной.Пайпы
Пайпы — это прямой аналог фильтров из AngularJS. В общем и целом синтаксис их применения не особо поменялся:
<p>My birthday is {{ birthday | date:"MM/dd/yy" }} </p>
Зачем понадобилось менять название с уже привычных фильтров на новые пайпы — отдельный вопрос. Сделано это было что бы подчеркнуть новую механику работы фильтров. Теперь это не синхронные фильтры, а асинхронные пайпы (по аналогии с unix pipes).
В AngularJS фильтры запускаются синхронно на каждый $digest цикл. Этого требует механизм отслеживания изменений в AngularJs. В Angular2 же отслеживание изменений учитывает зависимости данных, посему это позволяет оптимизировать целый ряд концепций. Так же появилось разделение на stateful и stateless пайпы (в то время как фильры AngularJS заведомо считались stateful).
Stateless пайпы, как это может быть понятно из названия, не имеют собственного состояния. Это чистые функции. Они выполняются только один раз (или если изменились входящие данные). Большинство пайпов в Angular2 являются stateless пайпами. Это позволяет существенно увеличить производительность.
Stateful пайпы напротив, имеют свое состояние и они выполняются часто в связи с тем что внутренне состояние может поменяться. Примером подобного пайпа является
Async
. Он получает на вход промис, подписывается на изменения и возвращает заресолвленное значение.// это не TypeScript, это babel со stage-1, ну так, к сведенью
@Component({
selector: 'my-hero',
template: 'Message: {{delayedMessage | async}}',
})
class MyHeroAsyncMessageComponent {
delayedMessage = new Promise((resolve, reject) => {
setTimeout(() => resolve('You are my Hero!'), 500);
});
}
// повелись? Неее, это просто TypeScript без определения типов.
В этом примере компонент
my-hero
выведет Message: You are my Hero!
только после того, как будет заресолвлен промис delayedMessage
.Для того что бы сделать stateful пайпы мы должны явно объявить это в метаданных оного. Иначе Angular2 будет считать его stateless.
Elvis оператор
В AngularJS мы могли делать обращения к чему угодно совершенно безболезненно, что частенько выливалось в весьма коварные баги и затрудняло отладку. Вам наверняка приходилось сталкиваться с опечатками в
ngClick
, при которых код не выполнял требуемых действий а фреймворк не давал нам никаких подсказок что же пошло не так. В Angular2 мы наконец будем получать ошибки! Однако подобное решение не всем может придтись по душе без дополнительного сахара.В Javascript нам частенько приходится проверять наличие каких-либо свойств. Думаю все мы писали что-то подобное:
if (cordova && cordova.plugins && cordova.plugins.notification){
// use cordova.plugins.notification
}
Делая такие проверки мы конечно же хотим избежать подобного:
TypeError: Cannot read property 'notification' of undefined.
Для решения этой проблемы был введен Elvis оператор, как сокращенная версия тернарного оператора. Вы могли видеть подобный в Coffeescript. В Angular2 решили эту проблему используя тот же оператор, но на уровне шаблонов:
<button (click)="employer?.goToWork()">Go To Work</button>
Данная запись означает, что свойство
employer
опционально, и если оно имеет пустое значение, то остальная часть выражения игнорируется. Если убрать сахар, то эта запись будет выглядеть так:<button (click)="employer === undefined ? : employer.goToWork()">Go To Work</button>
Если бы мы не воспользовались этим оператором и при этом не делали бы подобной проверки, то в этой ситуации мы бы получили
TypeError
.Так же как и в coffescript данный оператор можно использовать сколько угодно раз в рамках одного выражения, например так:
a?.b?.c?.d
Выводы
Разработчики Angular2 проделали огромную работу для того что бы сделать синтаксис шаблонов более гибким и мощным. Да, некоторые вещи требуют немного времени для адаптации, но в целом со временем все это кажется более чем естественным. А главное, не так все страшно как кажется на первый взгляд.