Разбираемся с синтаксисом шаблонов в Angular2
Многие впервые увидев синтаксис шаблонов Angular2 начинают причитать, мол ужас какой сделали, неужто нельзя было как в Angular1 хотя-бы. Зачем нужно было вводить это разнообразие скобочек, звездочек и прочей ерунды! Однако при ближайшем рассмотрении все становится куда проще, главное не пугаться.
Так как шаблоны в AngularJS являются неотъемлемой его частью, важно разобраться с ними в самом начале знакомства с новой версии этого фреймворка. Заодно обсудим, какие преимущества дает нам данный синтаксис по сравнению с angular 1.x. Причем лучше всего будет рассматривать это на небольших примерах.
Данная статья во многом основана на материалах этих двух статей:
Для того, что бы упростить подачу материала, давайте разберемся. Под AngularJS я буду подразумевать всю ветку Angular 1.x, в то время как под Angular2 — ветку 2.x.
Примечание: вечер выходного дня, потому о опечатках и т.д. сообщайте в личку. Премного благодарен и приятного чтения.
В случае простого вывода данных разницы вы не почувствуете, однако, если вы решите передать часть состояния в качестве значения какого-либо атрибутов элементов, можно уже наблюдать интересные решения.
Давайте освежим в памяти как работают биндинги на атрибуты в AngularJS. Для этого мы обычно напрямую пробрасывали выражения (интерполируемые при компиляции шаблона) прямо в атрибут элемента, либо пользовались одной из множества директив. Например ngValue
для установки значения свойства value
у инпутов.
Приведем пример как это работает в AngularJS:
Так же не забываем что мы можем просто интерполировать результат выражения напрямую в качестве значения аргумента:
Заметим интересную особенность. Второй вариант многие его избегают, так как можно увидеть промежуточное состояние до того, как angular интерполирует значения. Однако первый вариант использует директивы. То есть для того что бы у нас все было хорошо, красиво и удобно, нам надо сделать по директиве на каждое свойство всех элементов. Согласитесь, не слишком то удобно. Почему бы не добавить какое-то обозначение для атрибута, которое бы говорило ангуляру замэпить значение на него. Причем было бы неплохо что бы синтаксис был валидным. И они добавили, теперь для этого надо всего-лишь обернуть интересующий нас атрибут (любой атрибут) в квадратные скобки — []
.
Однако, у нас все еще есть возможность мэпить интерполируемое значение, так же как это было в AngularJS:
В AngularJS мы могли подписаться на события элементов используя специальные директивы. Так же, как и в случае со свойствами, нам приходится иметь дело с целой кучей возможных событий. И на каждое событие приходилось делать директиву. Пожалуй, самой популярной из таких директив является ngClick
:
Учитывая что мы уже решили такую проблему для свойств элементов, то наверное так же стоит решать и эту проблему? Именно это и сделали! Для того, что бы подписаться на событие, достаточно прописать атрибут используюя следующий синтаксис: (eventName)
. Таким образом у нас есть возможность подписаться на любое событие генерируемое нашим элементом, при этом без надобности писать отдельную директиву:
Существует распространенное мнение, что двусторонний биндинг это плохо, и что это основной грех ангуляра. Это не совсем так. Проблема с двусторонним биндингом в AnugularJS была в том, что он используется повсеместно, не давая разработчикам альтернативы (возможно ситуация с этим в скором времени изменится).
Все же иногда возникают случаи когда двусторонний биндинг здорово упрощает разработку, особенно в случае форм. Так как же реализован оный в Angular2? Давайте подумаем, как организовать двусторонний биндинг имея односторонний биндинг свойст элементов и биндинг событий:
Опять же, не очень то удобно. Посему в Angular2 так же есть синтаксический сахар с использованием ngModel
. Результат будет идентичен тому, что мы привели выше:
Для передачи данных между элементами в пределах одного шаблона используются локальные переменные. Наиболее близкой аналогией в AngularJS, пожалуй, можно считать доступ к элементам формы по имени через ngForm. Конечно, это не совсем корректное сравнение, так как работает это только за счет директивы ngForm
. В Angular2 вы можете использовать ссылку на любой объект или DOM элемент в пределах элемента шаблона и его потомков, используя локальные переменные #
.
В данном примере мы можем видеть, как через переменную movieplayer
мы можем получить доступ к API медиа элемента прямо в шаблоне.
Помимо символа #
, вы так же можете объявлять переменные используя префикс var-
. В этом случае вместо #movieplayer
мы могли бы записать var-movieplayer
.
Символ *
вызывает больше всего вопросов. Давайте разберемся, зачем он понадобился. Для осознания причин добавления этого символа, нам стоит вспомнить о таком элементе как template
.
Элемент template
позволяет нам задекларировать кусочек DOM, который мы можем инициализировать позже, что дает нам более высокую производительность и более рациональное использование ресурсов. Чем-то это похоже на documentFragment
в контексте HTML.
Пожалуй, будет проще показать зачем оно надо на примере:
В этом небольшом примере мы можем видеть, что блок скрыт (display:none
). Однако браузер все равно будет пытаться загрузить картинку, даже если она не понадобится. Если подобных вещей на странице много, это может пагубно отразиться на общей производительности страницы.
Решением этой проблемы станет использование элемента template
.
В этом случае браузер не будет загружать изображение, пока мы не инициализируем шаблон.
Но вернемся к нашим баранам. Использование символа *
перед директивой элемента позволит ангуляру при компиляции обернуть элемент в шаблон. Проще посмотреть на примере:
Этот шаблон будет трансформирован в такой:
Теперь должно стать ясно, что этот символ предоставляет синтаксический сахар для достижения более высокой производительности при использовании условных директив вроде ngFor
, ngIf
и ngSwitch
. Логично что нет нужды в создании экземпляра компонента hero-detail
пока isActive
не является истиной.
Пайпы — это прямой аналог фильтров из AngularJS. В общем и целом синтаксис их применения не особо поменялся:
My birthday is {{ birthday | date:"MM/dd/yy" }}
Зачем понадобилось менять название с уже привычных фильтров на новые пайпы — отдельный вопрос. Сделано это было что бы подчеркнуть новую механику работы фильтров. Теперь это не синхронные фильтры, а асинхронные пайпы (по аналогии с 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.
В AngularJS мы могли делать обращения к чему угодно совершенно безболезненно, что частенько выливалось в весьма коварные баги и затрудняло отладку. В Angular2 мы наконец будем получать ошибки! Однако подобное решение не всем может придтись по душе без дополнительного сахара.
В Javascript нам частенько приходится проверять наличие каких-либо свойств. Думаю все мы писали что-то подобное:
if (cordova && cordova.plugins && cordova.plugins.notification){
// use cordova.plugins.notification
}
Делая такие проверки мы конечно же хотим избежать подобного:
TypeError: Cannot read property 'notification' of undefined.
В coffescript для решения этой проблемы был введен Elvis оператор. В Angular2 решили эту проблему используя тот же оператор, но на уровне шаблонов:
Employer: {{employer?.companyName}}
Данная запись означает, что свойство employer
опционально, и если оно имеет пустое значение, то остальная часть выражения игнорируется. Если бы мы не воспользовались этим оператором, то в этой ситуации мы бы получили TypeError
.
Так же как и в coffescript данный оператор можно использовать сколько угодно раз в рамках одного выражения, например так: a?.b?.c?.d
Разработчики Angular2 проделали огромную работу для того что бы сделать синтаксис шаблонов более гибким и мощным. Да, некоторые вещи требуют немного времени для адаптации, но в целом со временем все это кажется более чем естественным. А главное, не так все страшно как кажется на первый взгляд.