Не так давно в мире web-разработки произошло важное событие — вышла бета Angular 2. И уже можно строить предположения о том, как он будет выглядеть после релиза.
Но оценка сама по себе, в вакууме, напоминает выбор электроники по рекламным буклетам производителя. Все фишки, которые есть в гаджете, упомянуты. А все, которых нет — не упомянуты и мы можем про их существование даже не задуматься. Поэтому гораздо эффективнее оценивать сравнивая с чем-то еще.
Так и родилась мысль сравнить Angular 2 с новым, но весьма амбициозным проектом Aurelia, который так же недавно вышел в бету. А заодно пополнить копилку Хабра информацией об этом фреймворке, поскольку пока ее гораздо меньше, чем информации об Angular 2.
Код примеров из статьи в виде готовых для запуска проектов выложен на github. Примеры написаны на TypeScript и оба используют systemjs. Конфигурации systemjs были взяты из quick start guide для каждого фреймворка и достаточно сильно отличаются. Мне показалось разумным оставить их в том виде, в котором их предоставили авторы фреймворков, и не пытаться сделать похожими. Также отмечу, что используемый в проекте http-server не поддерживает pushState (надеюсь, скоро будет поддерживать, ибо одобренный pull request есть), и поэтому в Angular пришлось включить hash based routing.
Мы не будем сравнивать досконально каждую архитектурную особенность, а скорее оценим фреймворки с точки зрения конечного пользователя, который просто хочет понять какие плюшки он получит. Сравнить все в одной статье не получится, поэтому данная статья будет своего рода знакомством с общей структурой. А более сложные вопросы останутся на вторую часть.
Мы пойдем по следующему сценарию:
- Узнаем, что такое Aurelia и почему ее правомерно сравнивать с Angular
- Рассмотрим плюсы и минусы каждого с высоты птичьего полета
- Обозначим фичи, термины и что с чем сранивать
- Создадим на каждом фреймворке простейший компонент
- Настроим routing
- Добавим вложенные компоненты
- Рассмотрим простейшие варианты data binding
- Рассмотрим управляющие конструкции в data binding
Что такое Aurelia и почему ее правомерно сравнивать с Angular
Поскольку на том же Хабре информации по Aurelia совсем немного, можно предположить, что не каждый читатель слышал о ней. Поэтому расскажу предысторию создания Aurelia. Тем более, что она довольно интересна и даже немного драматична.
Aurelia это проект Роба Эйзенберга, автора весьма популярного MV*-фреймворка для XAML-платформ Caliburn.Micro. Позже он разработал MV*-фреймворк для web, получивший название Durandal. Durandal не стал супер популярным, но, тем не менее, в нем были очень интересные и элегантные решения и фреймворк собрал свою аудиторию приверженцев, которые его очень полюбили.
Но Роб Эйзенберг понимал все недостатки Durandal, поэтому вместе с его сопровождением занимался разработкой так называемого NextGen фреймворка.
В январе 2014 года, на конференции ngConf небезызвестный в мире веб-разработки John Papa поделился с менеджером Angular team Брэдом Грином идеями, которые были заложены Робом Эйзенбергом в Durandal и в NextGen framework. Эти идеи заинтересовали Грина и он решил пообщаться с Эйзенбергом.
Встретившись, Брэд Грин и Роб Эйзенберг поняли, что их взгляды на будущее веба и веб-разработки во многом совпадают, и они приняли решение объединить усилия и Эйзенберг начал работать в команде Angular над второй версией фреймворка.
Однако, спустя десять месяцев, он принял решение покинуть команду Angular, поскольку направление развития Angular 2, по его мнению, слишком сильно изменилось и отличалось от того Angular 2, на работу с которым он соглашался.
Эйзенберг за короткий срок собрал достаточно большую команду, в составе которой есть такие звезды как, например, Scott Allen, и вернулся к работе над фреймворком своей мечты. Итогом этой работы и стал Aurelia.
Общественность приняла фреймворк с интересом (как простейший способ оценки, на момент написания статьи Aurelia собрала на github 5000 звезд против 8000 у Angular 2).
Общие принципы, заложенные в Angular 2 и Aurelia, очень похожи — это игроки одного класса. Но видение в деталях и набор возможностей у них достаточно сильно отличаются, что и делает их сравнение интересным.
Плюсы и минусы Angular 2 и Aurelia с высоты птичьего полета
Из осязаемых характеристик, которые можно сравнить при выборе фреймворка, посмотрим на perfomance. Aurelia показывает интересные результаты в бенчмарке dbmonster, выбивая чуть лучшие баллы, чем Angular 2, и заметно лучшие, чем React и Angular1.
При оценке бенчмарка полезно обратить внимание на следующие моменты:
- Плавный скролл — страница должна скроллиться без «прыжков»
- Всплывающие подсказки — если вести курсор мыши по списку, то отрисовывается всплывающая подсказка. Она должна отрисовываться плавно и данные должны отображаться без задержек
- Repaint rate и memory rate — в правом нижнем углу есть два индикатора. Первый показывает количество перерисовок в секунду, второй показывает расход памяти
- Скорость изменения данных — в верхней части страницы есть слайдер, который позволяет регулировать частоту изменения данных. Если при уменьшении скорости изменения данных repaint rate не становится больше, то это значит, что рассматриваемый фреймворк неэффективно отслеживает изменения и принимает решения об обновлении DOM
Для того, чтобы все индикаторы работали корректно и для получения наиболее «чистого» результата, рекомендуется использовать браузер chrome и запускать его следующей командой:
"C:Program Files (x86)GoogleChromeApplicationchrome.exe" --user-data-dir="C:chromedev-sessionsperf" --enable-precise-memory-info --enable-benchmarking --js-flags="--expose-gc"
Из неосязаемых характеристик, попробую оценить самые сильные и слабые стороны обоих фреймворков (осторожно, ИМХО автора).
Главные плюсы Angular
- У Angular заведомо больше сообщество, а значит будет больше идей, больше расширений, проще найти ответ на имеющийся вопрос.
- У Angular в три раза больше команда и они гораздо быстрее развивают свой проект. Также команда Angular уже заручилась поддержкой авторов инструментов для разработчиков и поддержка Angular будет в очень многих инструментах (а еще уже есть работающий и очень крутой Batarangle, пусть он еще и в developer preview).
Главные плюсы Aurelia
- Aurelia может противопоставить Angular-у достаточно много интересных фишек. Например, это продвинутый механизм composition, и template parts. Aurelia разработана с акцентом на unobtrusive, количество конструкций фреймворка в конечном коде минимально. Aurelia более компактен и сопровождаем, в то время как Angular порой просто вынуждает плодить копипаст.
- Команда Aurelia будет обеспечивать коммерческую поддержку клиентов, и есть реальная возможность повлиять на направление развития проекта. А в случае с Angular 2 мы вынуждены работать с тем, что есть.
Главные минусы Angular
- Если смотреть на историю развития Angular, то складывается впечатление, что команда не очень четко видит, каким должен получиться Angular 2 в итоге. Я следил за Angular 2 с момента анонса работ над ним и вижу, что фреймворк действительно очень сильно и не всегда последовательно меняется. По этой же причине Эйзенберг покинул команду Angular 2 из-за того, как поменялась архитектура проекта по сравнению с изначальной. И по этой же причине появляются подобные посты.
Главные минусы Aurelia
- Главный вопрос по поводу Aurelia — сдюжит ли она против Angular 2. Это не праздный вопрос, поскольку Эйзенберг противопоставляет Aurelia и Angular 2. О некоторой ангажированности говорят его записи с блогах (раз, два, три) его выступления (NDC London последние минуты видео) и даже некоторые комментарии к статьям про Angular 2. Мне очень нравится aurelia, но у меня вызывает опасения вероятность, что выбранный фреймворк в один прекрасный день перестанет поддерживаться ибо автору надоело. С другой стороны, не прошло и недели, как большой facebook объявил о закрытии parse.com, так что не застрахован никто и ни от чего.
Возможности Angular 2 и Aurelia, термины и что с чем сранивать
Архитектуры Angular 2 и Aurelia очень похожи. Ниже попытаюсь в пару абзацев сформулировать принципы работы обоих, обозначая курсивом основные термины и конструкции, которые имеет смысл рассматривать и сравнивать. Надеюсь, это получится читаемым текстом, а не месивом из терминов.
Основу приложения как на Angular 2, так и на Aurelia составляют компоненты, ассоциированные с соответствующим шаблоном.
Обязательно наличие root-компонента, который олицетворяет собой приложение (app). К компонентам могут/должны быть привязаны метаданные при помощи декораторов.
Инициализация компонентов выполняется при помощи dependency injection. Также, каждый компонент имеет декларированный жизненный цикл, в который можно встраиваться при помощи lifecycle hooks. Компоненты могут быть составлены в иерархическую структуру.
Синхронизация состояний и коммуникация между компонентом и шаблоном выполняется при помощи data binding. В процесс рендеринга шаблона в конечный HTML можно встроиться при помощи pipes (Angular) или value converters+binding behaviours (Aurelia).
Для перехода между изолированными областями приложения используется routing. Для коммуникации между модулями приложения могут быть использованы события.
И, наконец, переходим к примерам.
Создаем первый компонент
Angular 2
Начнем с создания простейшего компонента, который будет представлять собой root component.
import {Component} from 'angular2/core';
@Component({selector: 'angular-app', templateUrl: 'app/app.html'} })
export class App {
message: string = 'Welcome to Angular 2!';
}
Для того, чтобы Angular понял что наш модуль представляет из себя компонент, необходимо обернуть его декоратором @Component.
Примечание — строго говоря, @Component это не декоратор (decorator), а аннотация (annotation). Про разницу можно почитать здесь. В статье оставим термин декоратор, поскольку в документации к Angular 2 аннотации лежат в разделе Decorators.
Как минимум, декоратор должен указывать селектор, куда отрисовывать шаблон. В данном случае это элемент <angular-app>.
Для декларации шаблона есть два варианта:
- Указать html-строку в качестве параметра template декоратора @Component. Такой подход полезен на тот случай, если шаблон компактный и не хочется делать ради него отдельную html-страницу
- Указать url шаблона в качестве параметра templateUrl. У нас потребуется отдельная страница, поэтому мы будем использовать этот вариант
Шаблон будет выглядеть следующим образом:
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">{{message}}</a>
</div>
</div>
Вот, собственно, и весь компонент. Достаточно просто.
Что хорошо — код чище и свободнее от конструкций самого фреймворка, нежели это было в Angular 1. Это не может не радовать.
Что смущает — необходимость использования декораторов даже для простейшего компонента. Цитата из документации:
Each Angular component requires a single @Component and at least one View annotation. The @Component annotation specifies when a component is instantiated, and which properties and hostListeners it binds to.
При этом параметры, конифгурируемые в данном примере, у большинства разработчиков в любом случае подчиняются какой-то логике, и вместо явной конфигурации можно было использовать конвенции, а декораторы подключать для более сложных сценариев.
Aurelia
Поскольку Aurelia построен основываясь на конвенциях, компонент это обычный модуль без каких-либо метаданных:
export class App {
message: string = "Welcome to Aurelia!";
}
По стандартной конвенции, для декларации шаблона нам необходимо создать html-файл с аналогичным компоненту именем, то есть для нашего примера это будет файл app.html:
<template>
<div class="container">
<div class="navbar-header">
<a class="navbar-brand" href="#">${message}</a>
</div>
</div>
</template>
Каждый шаблон в Aurelia должен быть обернут в элемент template. Для создания inline шаблона по аналогии с Angular 2 можно использовать декоратор inlineView. Также в Aurelia можно изменять конвенции и выполнять дополнительную настройку. Детали можно посмотреть здесь.
Что хорошо — код предельно чистый и понятный. Никаких конструкций фреймворка.
Что смущает — для настройки может понадобиться множество аннотаций, решающих схожие вопросы. Например, это inlineView, noView, useView и useViewStrategy. Документацию пока что обильной не назовешь, и в ней даже нет поиска, так что есть риск просто запутаться, что и где использовать.
Настраиваем routing
В нашем случае каждое приложение будет иметь несколько страничек, на которых будут рассматриваться аспекты, обозначенные в статье. Вложенный routing мы рассматривать не будем, поскольку как в Angular, так и в Aurelia, он по сути аналогичен routing-у как таковому.
Angular 2
Для того, чтобы настроить routing в Angular 2 нам необходимо импортировать декоратор @RouteConfig, импортировать модули, которые будут привязаны к маршрутам и декларировать нашу карту роутинга. Сделаем это в нашем компоненте app:
...
import {RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router';
import {BindingSample} from './binding-sample/binding-sample';
import {ComponentSample} from './component-sample/component-sample';
...
@RouteConfig([
{ path: '/component-sample', name: 'ComponentSample', component: ComponentSample, useAsDefault: true },
{ path: '/binding-sample', name: 'BindingSample', component: BindingSample }
])
export class App {
...
}
Для каждого маршрута мы указываем:
- паттерн маршрута
- имя маршрута (оно понадобится для привязки в разметке)
- модуль компонента, который Angular будет создавать при активации маршрута
- опциональный параметр useAsDefault, указывающий что это маршрут по умолчанию
Если смущает необходимость уже на старте загружать все модули, которые имеют маршрут, то можно воспользоваться асинхронной загрузкой. Для этого вместо параметра component используем параметр loader, указывая функцию, которая вернет promise, импортирующий нужный модуль. Мы так и сделаем, но пример кода чуть позже.
Чтобы использовать routing в шаблонах для отрисовки навигации и указания секции, куда отрисовывать текущий компонент, нам необходимо дополнительно импортировать коллекцию директив ROUTER_DIRECTIVES и добавить их в параметр directives декоратора @Component. Итого, модуль app.ts получается вот такой:
import {Component} from 'angular2/core';
import {View} from 'angular2/core';
import {RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router';
@Component({
selector: 'angular-app', templateUrl: 'app/app.html', directives: [ROUTER_DIRECTIVES]
})
@RouteConfig([
{ path: '/component-sample', name: 'ComponentSample',
loader : () => System.import('app/component-sample/component-sample').then(m => m.ComponentSample), useAsDefault: true },
{ path: '/binding-sample', name: 'BindingSample',
loader : () => System.import('app/binding-sample/binding-sample').then(m => m.BindingSample) }
])
export class App {
message: string = "Welcome to Angular 2!";
}
Теперь добавляем навигацию в app.html. Для этого используем директиву routerLink, передавая в качестве параметра массив, первым элементом в котором является строка с именем маршрута, которое мы указали при настройке @RouteConfig.
…
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li class="active">
<a [routerLink]="['ComponentSample']">Component sample</a>
</li>
<li>
<a [routerLink]="['BindingSample']">Binding sample</a>
</li>
</ul>
</div>
...
Параметр является массивом на случай вложенного routing, тогда мы передаем массив из нескольких строк с именами маршрутов.
Если маршрут необходимо параметризовать, то объект с параметрами передается последним элементом в массиве:
...
<a [routerLink]="['BindingSample', {someParameter: ‘someString’}]">Binding sample</a>
...
И последний момент. Нам необходимо в разметке указать область, куда будут отрисовываться шаблоны для текущего маршрута. Делаем это при помощи директивы router-outlet в том же app.html:
<div class="container">
<router-outlet></router-outlet>
</div>
Что хорошо — новый routing приятнее, чем routeProvider, живший в Angular 1.x, но все равно вызывает целый ряд вопросов. Абсолютное большинство типов, касающихся routing в документации вообще еще не имеют описания, поэтому пока трудно что-то говорить окончательно.
Что смущает — смущает несколько вещей. Прежде всего, это необходимость настройки через декоратор — если в нашем приложении, к примеру, 50 маршрутов, то код нашего модуля просто потеряется во всех этих настройках. И если нам понадобится при построении схемы навигации какая-либо if-логика, то наш код рискует превратиться в кошмар.
Второе, это отсутствие явного доступа ко всей коллекции route-ов, которую можно было бы просто перебрать в разметке для отрисовки всей навигации, а не ручная отрисовка каждой ссылки (которую мы будем забывать делать). Опять же, при наличии if-логики для построения маршрутов, нам придется дублировать эту логику в шаблоне, чтобы не нарисовать лишнего.
Aurelia
Согласно конвенции в Aurelia, чтобы настроить routing для компонента нам необходимо реализовать метод configureRouter, который Aurelia вызовет автоматически. То же самое верно и для вложенного routing-а — любой компонент, имеющий метод configureRouter, будет формировать схему routing-а.
export class App {
message: string = "Welcome to Aurelia!";
router: any;
configureRouter(config, router) {
config.title = 'Welcome to Aurelia!';
config.map([
{
route: ['', 'component-sample'], moduleId: 'app/component-sample/component-sample', nav: true, title: 'Component sample'
},
{
route: 'component-sample', moduleId: 'app/binding-sample/binding-sample', nav: true, title: 'Binding sample'
}
]);
this.router = router;
}
}
Для каждого маршрута мы указываем:
- паттерн маршрута (или набор паттернов, паттерн в виде пустой строки означает маршрут по умолчанию).
- идентификатор модуля, который инициировать при активации маршрута.
- опционально свойство title — при активации маршрута его значение будет дописываться в title страницы.
- опционально свойство nav — оно сообщает должен ли маршрут попасть в navigation model. Если указать число, то оно будет означать порядок элемента в коллекции navigation model.
На основании переданной конфигурации Aurelia составит navigation model, который в шаблоне можно будет перебрать и отрисовать навигацию:
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav">
<li repeat.for="row of router.navigation" class="${row.isActive ? 'active' : ''}">
<a href.bind="row.href">${row.title}</a>
</li>
</ul>
</div>
Последним шагом указываем в разметке область, куда будут отрисовываться шаблоны для текущего маршрута. Делаем это при помощи директивы router-view в том же app.html
<div class="container">
<router-view></router-view>
</div>
Что хорошо — настройка простая и вполне очевидная. Приятно, что Aurelia пытается нам помочь и строит коллекцию для навигации.
Что смущает — по сравнению с навигацией в Durandal убрали возможность добавлять в настройки маршрутов произвольные свойства. С одной стороны это, конечно, правильно, ибо нефиг. С другой стороны, это сильно снижает вероятность использования navigation model в сторону ручной отрисовки. Navigation model будет бесполезен если захочется добавить к пунктам меню не только title, но и, например, tooltip-ы.
Добавляем вложенные компоненты
Angular 2
Для примера работы с вложенными компонентами в тестовом проекте есть папка component-sample, в ней лежит все, что нам нужно.
Итак, для создания вложенного компонента в Angular 2 нам необходимо:
- Объявить компонент и привязать к нему метаданные. Для инициализации свойств данными от родительского компонента, мы добавляем параметр inputs в декораторе @Component, передавая ему имена соответствующих свойств:
import {Component} from 'angular2/core'; @Component({ selector: 'test-child-component', inputs: ['inputMessage'], template: `<div class="panel panel-default"> <div class="panel-heading">Child component title</div> <div class="panel-body"> Message from parent component is: {{inputMessage}} </div> </div>` }) export class TestChildComponent { inputMessage: string }
- Импортируем в родительском компоненте (component-sample.ts) дочерний и передаем его в массив directives декоратора @Component
- В шаблоне родительского компонента размещаем элемент соответствующий указанному селектору из вложенного компонента и передающий параметры
В итоге, наш родительский компонент выглядит следующим образом:
import {Component} from 'angular2/core';
import {TestChildComponent} from './test-сhild-сomponent';
@Component({
template: `
<div class="sample-header">
<h1>{{message}}</h1>
</div>
<test-child-component [inputMessage]="messageForChild"></test-child-component>`,
directives: [TestChildComponent]
})
export class ComponentSample
{
message: string = 'This is a component with child component sample';
messageForChild: string = 'Hello to child component!';
}
Что хорошо — в целом, все просто и понятно.
Что смущает — не хватает приятных плюшек, которые есть в Aurelia. Они описаны ниже.
Aurelia
В Aurelia отрисовать вложенный компонент можно двумя способами: custom element и composition.
Первый способ, в целом, аналогичен Angular 2 и используется для создания сложных контролов и т.д. В коде тестового проекта этот подход демонстрируется в файле test-custom-element.html.
Второй используется преимущественно для сценариев master-detail и более гибок, поскольку мы можем динамически указывать какой загрузить компонент, какой отрисовать шаблон и какие данные передать. Данный подход продемонстрирован в тестовом проект в файле test-сhild-сomponent.ts.
Разберем оба варианта по очереди.
Вариант 1 — custom element:
Для создания вложенного компонента при помощи custom element нам необходимо:
- Создать обычный компонент, дополнительно отметив декоратором @bindable свойства, значения для которых мы будем передавать через шаблон как параметры (по аналогии с параметром inputs в Angular 2). Дополнительно, если наш элемент простой и не имеет поведения, то можно обойтись без создания компонента, а просто создать шаблон и в нем перечислить bindable-свойства при помощи одноименного атрибута. Компонент Aurelia создаст «на лету»:
<template bindable="message"> <div class="panel panel-default"> <div class="panel-heading">Custom element title</div> <div class="panel-body"> Message from parent component is: ${message} </div> </div> </template>
- Добавить в шаблон родительского компонента элемент require, указывающий на нужный нам модуль (аналогично параметру directives в Angular 2, но в шаблоне) и отрисовать кастомный элемент в разметке там, где он нам понадобится. Передача параметров выполняется при помощи атрибута <имя свойства>.bind. В итоге, шаблон родительского компонента будет выглядеть следующим образом:
<template> ... <require from="app/component-sample/test-custom-element.html"></require> <test-custom-element message.bind="messageForCustomElement"></test-custom-element> </template>
Если элемент используется повсеместно, то его можно зарегистрировать как global resource. Это позволит не писать в каждом шаблоне <require…>. Как это сделать написано здесь в разделе “Making Resources Global”
Еще одна приятная опция, которая есть в Aurelia и которую мне не удалось найти в Angular — передача разметки из родительского шаблона. Если в родительском шаблоне внутри элемента test-custom-element декларировать разметку, а в дочернем добавить элемент
<content></content>
то разметка из родительского шаблона будет отрисована в дочернем. Также при работе с custom elements можно использовать уже упомянутые ранее template parts и объявить несколько заменяемых областей.
Что хорошо — все просто и логично.
Что смущает — возможно, не всем понравится необходимость декларировать зависимости в разметке.
Вариант 2 — Сompose:
Для создания вложенного компонента при помощи composition нам необходимо:
- Создать обычный компонент. Поскольку compose предполагает слабую связанность компонент, для передачи данных нам необходимо использовать lifecycle hooks (они также имеются и у Angular 2, но про них в следующей статье). В данном случае мы используем метод activate. Шаблон мы сделаем inline, поскольку он маленький:
import {inlineView} from 'aurelia-templating'; @inlineView('<template><h3>${inputMessage}</h3><template>') export class TestChildComponent { inputMessage: string; activate(inputMessage: string) { this.inputMessage = inputMessage; } }
- В шаблоне родительского элемента помещаем элемент compose, в котором указываем идентификатор модуля с дочерним компонентом. Для передачи данных от родительского компонента дочернему используем атрибут model.bind:
<compose model.bind="messageForChild" view-model="app/testChildComponent"></compose>
Что хорошо — на мой взгляд compose это очень крутой способ борьбы со сложностью проекта. Он обеспечивает изоляцию между компонентами при этом давая большую гибкость.
Что смущает — я долго размышлял над этим пунктом, но так и не смог ничего написать. Мне не к чему придраться.
Data binding
Angular 2
Основная работа, которую команда Angular провела при разработке нового data binding, это минимизация количества используемых директив и четкое разделение направлений движения данных: to the DOM, from the DOM, both directions. Посмотрим на все по очереди.
To the DOM
Однонаправленный binding to the DOM выполняется при помощи двух форм:
string interpolation:
<div class="panel-body">
This string is builted with {{interpolationString}} syntax
</div>
и property binding:
<img [src]="iconUrl" />
В случае string interpolation текст, заключенный в двойные угловые скобки, оценивается как expression, который будет выполнен и его результат будет помещен в шаблон. То есть можно привязывать не только свойства компонента, но и писать выражения подобного вида:
<h1>2 = {{1+1}}</h1>
Однако, рекомендуется не злоупотреблять данной возможностью и сложные выражения помещать в свойства компонент.
В случае property binding ключевыми являются квадратные скобки, которые сигнализируют template engine, что это название свойства, которому необходимо присвоить результат выражения справа. В данной форме также поддерживаются выражения, но злоупотреблять ими не рекомендуется. Помимо стандартных свойств, есть дополнительный набор директив — routerLink, textContent и т.д.
Примечание — разрабатывая шаблоны в Angular 2, вы пишете, по сути, не на HTML, а на специальном синтаксисе Angular 2. Для разбора шаблона он использует собственный парсер. И, когда вы пишете, к примеру:
<input value="some text" />
то это не «чистый» HTML, в котором атрибуту присваивается значение, а синтаксический сахар, раскладывающийся в присвоение константы:
<input [value]="'some text'" />
Важным моментом является то, что для привязки данных к атрибутам, именам классов и стилям требуется отдельный синтаксис с использованием префиксов class, style, и attr. Например, вот так:
<img [style.width]="iconWidth" [style.height]="iconHeight" [src]="iconUrl" />
Для установки одновременно нескольких свойств стилей или нескольких классов имеются директивы ngClass и ngStyle. Как результат выражения они ожидают объект, каждое свойство которого будет оценено как свойство стиля или имя класса.
Смысл отделения property binding от attribute binding заключается в том, что html атрибуты и свойства DOM-элементов имеют четкое различие. Атрибуты используются для инициализации элементов, в то время как свойства предназначены для изменения состояния элементов и именно их использует Angular 2. Attribute binding является исключением на тот случай, если html атрибут не имеет соответствующего свойства в DOM-element.
From the DOM
В данном случае все просто, это привязка обработчиков событий:
<input type="button" (click)="onClicked()" value="Click me!" />
Ключевыми в данном варианте являются круглые скобки, внутри которых пишем название события (без префикса “on”). Внутри выражения в правой части binding-а доступна переменная $event, представляющая собой, собственно, DOM event. Это позволяет не декларировать отдельные функции для простейших обработчиков и писать inline выражения. Однако, опять же, увлекаться и писать таким образом тяжеловесные конструкции не стоит.
Both directions
Двусторонняя привязка данных, по сути, может быть выполнена при помощи комбинации from the DOM и to the DOM:
<input [value]="twoWayBindedProperty" (input)="twoWayBindedProperty=$event.target.value" />
Однако, поскольку это абсолютно типичный сценарий, команда Angular разработала специальную директиву ngModel, которая вполне логично, хоть и немного жутко, объединяет синтаксис from the DOM и to the DOM в одну конструкцию:
<input [(ngModel)]="twoWayBindedProperty" />
При необходимости, можно разложить все это дело на отдельные части, тогда для направления from the DOM нам понадобится директива ngModelChange:
<input [ngModel]="twoWayBindedProperty" (ngModelChange)="twoWayBindedProperty=$event">
Примечание: если вас смущает синтаксис с [], () b [()] и вам больше нравится канонический вариант, то можно использовать приставки bind-,on- и bindon- соответственно. Например:
<img bind-src="iconUrl">
Что хорошо — несмотря на то, что синтаксис, мягко говоря, необычный и уже много было негативных высказываний в его адрес, лично мне он нравится. Во-первых, он очень заметный. Во-вторых, его очень легко запомнить. В-третьих, он хорошо подружится с intellisense различных редакторов, поскольку с первого же символа понятно, что это конструкция Angular 2 и мы сразу получим варианты директив и не придется запоминать все эти ngClass, ngModel и т.д. В-четвертых, его можно поменять на каноническую форму, если вам совсем уж противно от всех этих скобочек.
Что смущает — в противовес Aurelia не хватает возможности one-time привязки и event delegation. Но критичными эти вещи, в общем случае, не назовешь.
Aurelia
В Aurelia data binding сосредоточен на возможности тонкой настройки направления движения данных. Фреймворк делает некие разумные предположения, но оставляет разработчику возможность решать самому. В конечном счете у нас есть следующие варианты:
String Interpolation
<div class="panel-body">
This string is builted with ${interpolationString} syntax
</div>
Аналогичен string interpolation в Angular 2, отличается только синтаксис (${} вместо {{}}):
Property binding
Cводится к выражениям <имя свойства>.<тип привязки> = “выражение”. Типы привязки есть следующие:
- .bind — принимает решение в зависимости от того, где он применен. Для input-элементов будет использован two-way binding, на все остальные случаи one-way
- .one-way — аналог to the DOM в Angular
- .two-way — классический двунаправленный биндинг
- .one-time — вариант для одноразовой отрисовки данных без дальнейшего отслеживания изменений. Позволяет сэкономить ресурсы там, где это возможно. Типичный сценарий использования — отрисовать список с данными в режиме read-only.
Наглядно, синтаксис для property binding, в общем виде, следующий:
<input value.bind="iconUrl" />
Помимо привязки стандартных свойств, как и в Angular 2, имеются custom атрибуты innerHTML, textContent, style. Возможны два варианта написания — с добавлением .bind и указанием свойства, или без .bind и с использованием string interpolation:
<div innerhtml.bind="htmlProperty"></div>
<div innerhtml="${htmlProperty}"></div>
Обработчики событий
Для привязки обработчиков событий у нас есть два варианта:
- .trigger — создает обработчик, привязанный непосредственно к элементу
- .delegate — создает один обработчик событий, присоединяя его к объекту document (или к ближайшей границе на случай shadow DOM) который обрабатывает все события указанного типа, соответствующим образом передавая их элементу, объявившему обработчик. Помогает сэкономить ресурсы за счет создания меньшего количества обработчиков. Однако, не работает для событий, не поддерживающих bubbling (пример, показывающий разницу, можно найти в демо-проекте в файле binding-sample.html).
Для доступа к событию внутри выражения обработчика, как и в Angular 2, доступна переменная $event.
Примечание: в своем выступлении на NDC London Эйзенберг говорит о том, что синтаксис Aurelia можно легко переписать при помощи плагинов и демонстрирует data binding в Aurelia с использованием синтаксиса Angular 2. Однако, найти этот плагин где-либо мне не удалось.
Что хорошо — возможность тонкой настройки, если она вам нужна. Вариант привязки one time. Полная совместимость со стандартами HTML.
Что смущает — достаточно многословный синтаксис для вариантов one-time, one-way и two-way. И поддержка intellisense будет не такой крутой, как в Angular.
Управляющие конструкции в data binding
Angular
Управляющие конструкции (for, if, switch)
Вдаваться в детали не будем, поскольку, полагаю, читатель уже начал уставать. Просто посмотрим общий синтаксис:
<div class="panel-body">
Select something:
<select [(ngModel)]="selectedClass">
<option *ngFor="#alertClass of alertClasses" [value]="alertClass">{{alertClass}}</option>
</select>
</div>
<div class="panel-body">
<div [ngSwitch]="selectedClass">
<template [ngSwitchWhen]="'success'">
<div class="alert alert-success" role="alert">You will be successfull if you learn Angular</div>
</template>
...
<template ngSwitchDefault>You must choose option</template>
</div>
<div *ngIf="selectedClass=='success'">
<div class="alert alert-success" role="alert">Extra message with *ngIf binding</div>
</div>
</div>
В целом ничего необычного. На что стоит обратить внимание, это синтаксис ngFor и ngIf — директивы предваряет символ *. Как объясняется в документации, это своего рода синтаксический сахар, который позволяет избежать оборачивания шаблонов в элемент template.
Создание локальных переменных в шаблонах
Локальные переменные необходимы для доступа к данным из разных областей html-шаблона. Простейший пример создания переменной мы уже увидели в цикле ngFor:
<option *ngFor="#alertClass of alertClasses" [value]="alertClass">{{alertClass}}</option>
С помощью спецсимвола # мы указали Angular, что объявляем переменную, к которой будем обращаться внутри шаблона. Также можно создать переменную, указывающую индекс текущего элемента массива:
<option *ngFor="#alertClass of alertClasses, #index=index" [value]="alertClass">{{index}} {{alertClass}}</option>
Еще объявление переменных при помощи # (или канонической альтернативы “val-”) можно применять вне цикла ngFor. В таком случае, переменная будет указывать на html элемент в котором переменная была объявлена. Эту переменную можно использовать, например, в обработчиках событий. Но в шаблоне такие переменные не работают, то есть watch не выполняется:
<input #i placeholder="Type something">
<input type="button" class="btn btn-success" (click)="displayTextboxValue(i.value)" value="And click" />
<br/>
But templating doesn't work with it - {{i.value}}
Aurelia
Управляющие конструкции (for, if, show)
Начать стоит с того, что в Aurelia switch не реализован (по крайней мере пока). Также, стоит отметить, что в терминологии Aurelia управляющие конструкции относятся не к data binding, а к HTML extensions, как и виденный нами ранее compose.
Итого, из управляющих конструкций у нас есть repeat, if и притянутый сюда же за компанию show:
<div class="panel-body">
Select something:
<select value.bind="selectedClass">
<option repeat.for="alertClass of alertClasses" value.bind="alertClass">${alertClass}</option>
</select>
</div>
<div class="panel-body">
<div if.bind="selectedClass=='success'" class="alert alert-success" role="alert">You will be successfull if you learn Aurelia</div>
...
<div show.bind="selectedClass=='success'">
<div class="alert alert-success" role="alert">Extra message with show extension</div>
</div>
</div>
Дополнительно, отмечу, что при написании выражений внутри repeat доступны следующие переменные:
- $index — индекс текущего элемента в массиве
- $first — true, если это первый элемент в массиве
- $last — true, если это последний элемент в массиве
- $even — true, если это четный элемент в массиве
- $odd — true, если это нечетный элемент в массиве
Создание локальных переменных в шаблонах
Синтаксис для создания локальных переменных внутри repeat мы уже увидели выше. Для сравнения с Angular 2 осталось посмотреть, как создавать переменные, указывающие на HTML элементы. Для этого используется атрибут ref. Созданную переменную можно использовать как в обработчиках событий, так и в шаблоне, то есть watch выполняется:
<div class="panel-body">
<input ref="i" placeholder="Type something">
<input type="button" class="btn btn-success" click.delegate="displayTextboxValue(i.value)" value="And click" />
<br/> And templating works with it - ${i.value}
</div>
Что смущает — внимательный читатель мог заметить, что в части Angular 2 не было заметок «Что хорошо — Что смущает». Я решил объединить этот раздел и написать по поводу обоих, ибо проблема у них одна. Эта проблема заключается в том, что при ошибках в шаблонах, далеко не всегда оба бросают ошибки. Например, попробуйте написать неправильно имя свойства в примере с if для обоих фреймворков — оба продолжат втихушку работать.
А значит мы остаемся наедине со своими ошибками и опечатками.
На этом все. Спасибо дочитавшим :)
Автор: fshchudlo