Angular 1.5: Компоненты
Не так давно увидел свет релиз Angular 1.5, который привносит множество интересных нововведений. Важной особенностью
данной версии является то, что это первый из череды релизов, который должен сгладить концептуальный разрыв между Angular1.x и Angular2.x. Для людей, у которых есть необходимость вести проекты на Angular сейчас, но в будущем планируется постепенная миграция на Angular2, это очень радостная новость.
В данной статье я постараюсь осветить основные нововведения:
- Компоненты!
- Односторонние биндинги!
- Мульти-слот трансклюды!
Полный список изменений доступен в репозитории ангуляра. Так же нас ждет небольшой примерчик использования перечисленных фич.
UI компоненты
Пожалуй только ленивый не слышал о концепции UI компонентов, так что эту главу можно пропустить. Или вы не слышали? А, слышали, но не до конца понимаете? Тогда извиняюсь, поясню суть концепции, а так же ее преимущества.
Под компонентом мы будем подразумевать кастомный элемент, который имеет при себе какое-то дополнительное поведение и шаблон. В качестве примера, вспомним элементы video
или audio
. Они, конечно, не являются «кастомными» (согласно спецификации W3C), но хорошо передают суть.
Идея далеко не новая и называется она — иерархическая декомпозиция. Ее применяют для снижения сложности оочень давно. И почему бы не применить ее к UI наших WEB приложений? Мы берем UI и делим его на отдельные блоки — компоненты. Каждый компонент в свою очередь состоит из других компонентов. А те — из других, и так пока мы не дойдем до минимальной единицы — стандартных элементов, которые можно воспринимать как компоненты без поведения. Если вам доводилось работать с Qt, GTK или другими GUI фреймворками, там компонентами являются виджеты, так что назвать идею новой никак нельзя.
При выборе границы каждого компонента, мы должны стараться делать их максимально независимыми от контекста использования. В иделе UI компоненту не должно быть дела где он используется, а так же из чего состоят компоненты, которые он использует.
Декомпозиция интерфейса
Насколько нужно дробить компоненты? Это вопрос здравого смысла. Естественно, что заворачивать каждый DOM элемент в компонент глупо, потому на каком-то этапе мы должны остановиться. Все же мы делаем это для удобства, а не просто так. Для примера, давайте возьмем какое-нибудь приложение с простым UI. Вы же знаете о TodoMVC? Давайте попробуем разделить его на компоненты:
Сразу же хочу заметить, что этот вариант разделения не является единственно верным. Потому стоит пояснить, откуда у нас взялись эти компоненты, и почему я решил разделить все именно так.
У нашего приложения можно сходу выделить элементы списка, описывающие отдельные задачи. Там явно прослеживается поведение и оно явно одинаковое. Назовем их todo-item
Выделив этот компонент, мы автоматически изолируем все поведение этой части UI в рамках этого компонента. Делать отдельно todo-list
не имеет смысла, так как это будет компонент пустышка, зачем зря тратить время. Мы могли бы запихнуть логику фильтрации списка в этот компонент, но это проще делать на уровне сервисного слоя.
Далее мы сходу видим todo-header
и todo-footer
как шапку и подвал нашего приложения. Компонент todo-header
будет отвечать за добавление новых задач в список. Мы конечно могли бы еще чуть раздробить этот компонент и вложить внутрь отдельный компонент, изолирующий логику добавления задач, а todo-header
бы отвечал только для оформление. Или еще интереснее — пробросит добавлялку через transclude но… это как-то сложно для такого простого приложения.
todo-footer
мы так же не будем дробить дальше, поскольку у нас будут слишком уж маленькие компоненты, и работать с ними будет уже не столь удобно.
У любого дерева компонентов должен быть корень, базовый элемент описывающий весь UI. У нас это todo-app
. Он является своего рода точкой входа, описывающей конкретный скрин нашего приложения. Но наши приложения обычно посложнее, и имеют множество скринов, в рамках которых можно выделить дополнительные скрины и т.д. Именно по этой причине у нас есть все эти роутеры и т.д. Но вернемся к этому чуть позже.
Влияние на процесс разработки
Поскольку мы можем изолировать поведение каждого компонента, а так же имея иерархию оных, мы очень легко можем распаралелить разработку на несколько разработчиков. Разработчики смогут делать отдельные самодостаточные компоненты, покрывать их тестами и предоставлять другим членам команды.
Это сильно влияет на гибкость планирования, и фраза »9 женщин не могут родить ребенка за 1 месяц» уже не настолько хорошо описывает процесс разработки нашего приложения. Повышая эффективность наших процессов, мы можем доставлять клиенту больше фич за меньшее время, а это пожалуй самое важное в нашей работе.
Есть так же еще один аспект, который не очень любят обсуждать. Это сегрегация обязанностей между фронтэнд-разработчиками и верстальщиками. Может прозвучать грубо, но намного эффективнее взять парочку дешевых верстальщиков и одного дорогого фронтэнд разработчика, нежели двух дорогих фронтэнд разработчиков. А с учетом того, что штуки вроде БЭМ не вчера появились, добиться модульной верстки не составляет особых проблем. Нужно просто добиться определенного уровня ответственности и взаимопонимания от разработчиков.
Мы так же можем вынести все шаблоны компонентов из JS файлов, и дать верстальщикам возможность работать как можно ближе к реальному месту применения шаблонов, что уменьшает риски, связанные с взаимодействием команды. Причем верстальщиу не нужно особо знать Angular для того, что бы доделать оформление. Или же фронтэнд разработчик может предоставлять верстальщикам уже готовые компоненты с примитивной разметкой и без стилей заранее. А верстальщики уже будут заниматься доводкой.
Так причем тут Angular 1.5?
По своей сути компоненты, это директивы, определяющие новый элемент со своим поведением (изолированным в контроллере) и шаблоном. А как всем известно, у Angular дико переусложенное API директив. Да, оно очень гибкое и позволяет делать много того, что обычно не стоит делать. А так как у нас есть необходимость сохранять обратную совместимость, упростить его не представляется возможным.
Именно по этому для объявления компонентов в Angular 1.5 мы получили новое API:
class MyComponentController {
// поведение компонента определяет контроллер
}
// вместо фабрики мы используем обычные объекты
const myComponentDefinition = {
// вместо scope + bindToController: true
// у нас появился более удобный способ объявлять биндинги
bindings: {
'name': '='
},
// так же как и для директив, мы можем либо применить шаблон
// либо воспользоваться `templateUrl`
// мы так же можем использовать функции
// для динамического объявления шаблонов
template: `
{{ $ctrl.name }}
`,
// тут примерно так же как и в случае с директивами
// единственное что `controllerAs` используется всегда
// в случае если вы явно не прописали элиас для контроллера
// будет использовано значение `$ctrl`.
controller: MyComponentController
}
// спецификация HTML5 требует наличия хотя бы одного дефиса
// в имени кастомных элементов. В нашем случае это my-component
angular.module('app').component('myComponent', myComponentDefinition);
Полное описание API, а так же сравнение с API директив, доступно в документации. Все же полезно туда иногда поглядывать.
Итак, мы теперь можем разделить UI на отдельные компоненты, осталось только разобраться с их состоянием. Что такое состояние компонента? Грубо говоря, это какие-то данные, которые компонент использует для формирования представления, наполнения биндингов и т.д. Откуда компонент берет эти данные? Запрашивает их из сервиса, или же получает через биндинги, или на основе данных из биндингов запрашивает у сервиса. Словом, вариантов куча. Но как лучше?
Stateless vs Stateful компоненты
Ребята из Facebook считают, что компоненты должны быть подобны чистым функциям. А UI в этом случае будет лишь композицией этих функций. То есть формула счастья от Facebook:
UI = сomponents(state)
Что это означает? Это означает, что состояние данных должно получаться извне директив и прокидываться внутрь через биндинги. Таким образом мы делаем компоненты более предсказуемыми. В компоненте верхнего уровня будет все состояние для скрина, он будет прокидывать нужную часть состояния в дочерние компоненты и так далее.
Делая компоненты независимыми от источника состояния, мы развязываем себе руки в том, каким образом мы будем получать и хранить состояние. Мы можем использовать redux, rx.js, можем использовать обычный подход с мутированием состояния, можем кешировать промежуточные данные, словом… нас ограничивает только наша фантазия. В этом изначально и была суть MVC, который придумали в далеком 79-ом году. Полное отделение логики обработки и хранения данных от логики формирования их представления. Сделав это разделение, у нас небудет никаких проблем с тем, что бы независимо менять и то и то. И в качестве бонуса, тестированием каждого отдельного компонента или сервиса становится очень простым.
Вместо того что бы менять состояние прямо в компоненте, имеет смысл просить это сделать сервисы, складывая всю ответственность на них. Они в свою очередь должны будут обновить состояние на верхнем уровне, что в итоге обновит состояние компонента, который и запустил всю эту цепочку.
Тут сразу стоит оговориться, что существует целая масса задач, когда это не очень удобно. А потому можно все же чуть чуть состояния хранить и менять прямо в контроллере. Обычно это состояние специфичное именно для этого конкретного компонента. Например — обрезка картинок. Согласитесь, звучит не очень разумно, если мы будем на каждое смещение курсора прогонять данные через сервисы и обратно в компонент. Лучше хранить такие вещи локально на уровне компонента и просить сервис что-то сделать, когда мы делаем какие-то более явные действия.
Компоненты и маршрутизация
Если рассматривать UI как иерархию компонентов, то каждый скрин будет веткой нашего дерева, и каждый вложенный скрин — еще одним ответвлением и т.д. И в рамках каждой новой ветки мы можем определить корень, и создать для него компонент. Решать же какой именно компонент будет отображаться будет решать наш раутер.
В принципе, подобные подходы существовали еще с первых версий Angular, просто было не так удобно делать декомпозицию. Вместо компонентов, за изоляцию отдельных частей UI отвечали целые стэйты/роуты со своим шаблоном и поведением, зашитым в контроллеры. Каждый отдельный роут можно воспринимать как полноценный компонент, просто очень жирный. Начиная с первых версий разработчики предложили вариант использования ресолверов для того, что бы сделать эти «компоненты» проще в обращении. Однако, делать декомпозицию все еще неудобно, а размеры шаблонов и контроллеров быстро росли.
Ребята из команды uiRouter попытались решить эту проблему введя вложенные вьюшки, что можно воспринимать как дробление UI на отдельные компоненты, просто не явное. Мы так же можем использовать ресолвы для отделения логики получения данных, а так же можем форсить обновление отдельных вьюшек.
Пойдем дальше! Уберем поведение из контроллера стэйта (по сути уберем контролер), заменим темплейт на один одинешенек компонент, прокинем в него состояние из ресолверов через биндинг атрибутов, и вуаля — все у нас теперь предсказуемо и легко.
Помимо ngRoute и uiRouter так же стоит посмотреть на angular-router, который является адаптацией роутера из angular2 для ветки 1.x. В целом же не стоит забывать что вскоре мир увидет релиз uiRouter 1.0, в котором так же много вкусностей.
$scope не нужен!
Пойдем еще дальше! $scope
не нужен! Ну как, в контексте директив, он все еще бывает нужен. В особенности, что бы подчищать за собой. Но в контроллерах/сервисах использовать его не рекомендуется. Мне очень нравится идея добавить правило в eslint, которое будет ругаться на наличие $scope
где-то кроме link-функций директив. Вместо использования $scope
мы можем использовать старый добрый javascript и биндинг на атрибуты контроллеров.
Это не то что бы что-то новое, возможность биндить значения на атрибуты контроллера это не новость, эта возможность появилась еще в angular 1.3, но так как большинство примеров в документации, а так же статей используют $scope
, я думаю было бы неплохо обсудить как можно жить без него.
Вопрос биндинга на атрибуты контроллера мы уже рассмотрели, когда обсуждали API компонентов. Теперь перейдем к остальным кейсам, когда нам очень хочется использовать $scope
. Первым из них, пожалуй, будет являться использование системы событий $emit/$broadcast/$on
. Просто не используете их. Они не случайно привязаны к иерархии скоупов, и служат именно для нотификации отдельных элементов о том, что что-то произошло. В частности, обычно использование листенеров ограничивается отслеживанием события $destroy
, на котором мы должны убирать все, что мы оставили после себя, и что не будет прибито сборщиком мусора. Например хэндлеры ивентов на document
.
Использовать события скоупов для организации pub/sub в сервисах, или еще хуже, завязывать какую-то логику приложения на них, это очень плохо. И хоть в оочень редких случаях это может быть полезным, я рекомендую 10 раз подумать прежде чем использовать $scope
или $rootScope
для реализации системы событий в вашем приложении, лучше воспользоваться отдельными библиотеками предназначенными для этого.
Кто у нас там дальше на очереди? $apply
и $digest
. Эти методы предоставляют нам возможность синхронизировать состояние после асинхронных операций. Они запускают $digest цикл, который собирает изменения и запускает обработчики. Использовать эти методы нужно только там, где непосредственно происходит асинхронная операция. И обычно у нас уже все это завернуто в сервисы. Делать же что-то эдакое в компонентах, просто неразумно. В крайнем случае используйте сервис $timeout
. Если же вы работаете с событиями DOM — то опять же для этого есть директивы, компоненты ничего не должны знать о DOM.
Ну и на сладкое — $watch
. Ох как это прекрасно, когда разработчик решает отслеживать изменения состояния в контроллерах, а еще слаже это потом отлаживать. Но как быть, если нам сверху через биндинги может придти обновление данных? Вдруг мы хотим отфильтровать коллекцию, или еще чего специфичного. Ну… давайте подумаем. Значения мэпятся на свойства нашего контроллера. В отличии от $scope
, который является частью фреймворка, у нас есть вся власть над нашим контроллером. Продолжая размышлять… у нас же есть геттеры/сеттеры! А это значит, что мы можем точно определить момент, когда наши данные поменялись.
class MyComponent {
get bindedValue() {
return this.$value;
}
set bindedValue(value) {
this.$value = value;
// а теперь вызовем метод, который должен обновить что-то
// вместо того, что бы вешать неявный ватчер
this.makeSomethingWithNewValue();
}
}
angular.module('app').component('myComponent', {
bindings: {
"bindedValue": "="
},
template: `Какой-то шаблон`,
controller: MyComponent
});
Вот так вот просто. Собственно именно по этой причине такая интересная концепция как Object.observe
была исключена черновиков стандарта. Ну что, все еще думаете что нам так уж нужен $scope
?
Односторонние биндинги и изоляция
Angular всегда ругали за навязывание двустороннего связывания данных. Не поймите неправильно, двусторонние биндинги это неплохо, особенно при работе со сложными формами. Но в большинстве случаев можно было бы обойтись односторонними. Именно по этой причине в Angular2 все построено исключительно на односторонних биндингах, а при необходимости двустороннего связывания, просто используется пара односторонних, действующих в противоположных направлениях.
Но почему все так хэйтят двусторонние биндинги, особенно в контексте компонентов? Как мы уже говорили, нашим компонентам не должно быть дело как работают компоненты на более низких уровнях. И нужную часть состояние мы передаем сверху вниз, и в принципе что с ними происходит далее нас не волнует. Но в случае с двусторонними биндингами мы легко можем потерять контроль над системой, так как внутренние компоненты могут переписать состояние внешних. Это как-то не хорошо. Посмотрим пример:
class MyComponent {
constructor() {
this.myValue = 'take my value';
}
}
angular.module('app').component('myComponent', {
template: `
My Value: {{ $ctrl.myValue }}
`,
controller: MyComponent
});
class MyNastyComponent {
constructor() {
this.passedValue = 'I\'m touching myself tonight!';
}
}
angular.module('app').component('myNastyComponent', {
bindings: {
passedValue: '='
},
template: `Mhahaa!`,
controller: MyNastyComponent
});
Как вы думаете, какое значение будет выведено? Явно не то что мы хотели. Да, конечно же пример надуманный, но мы можем сделать подобное случайно и потом долго искать виновника. Иногда все же стоит ограничивать наши возможности.
Итак, в Angular 1.5 появилась долгожданная фича: одностороннее связывание данных при изоляции скоупа директив (документация)! Действует оно, как и говорит нам название, за счет проброса значения с верхнего уровня на нижний, запрещая изменениям гулять в противоположном направлении. Давайте исправим пример выше, для этого нам всего-лишь надо изменить биндинги нашего MyNastyComponent
:
bindings: {
passedValue: '<' // вот так вот
},
И все, теперь когда контроллер нашего вредного компонента меняет значение свойства passedValue
, оно не уходит вверх и остается на этом уровне.
Важно отметить, что, хоть перезапись всего объекта на верхнем уровне не происходит, это не значит что вы не можете поменять состояние. Именно копирования объектов не происходит, происходит только присваивание значений.
class MyNastyComponent {
constructor() {
this.passedValue.message = 'I\'m touching myself tonight!';
}
}
В примере выше мы меняем значение поля объекта. А так как объекты в JS присваиваются по ссылке, оно меняется везде. С этим приходится мириться просто потому, что копирование объектов может убить производительность. Слишком большая цена за душевное спокойствие.
Итак, мы научились делать декомпозицию UI на отдельные реюзабельные элементы, а так же разобрались с односторонними биндингами. Так же не забудем что нам больше не нужен $scope
. Теперь было бы нелпохо это потыкать вживую. Или нет?
Мульти-слот трасклюд
Наши реюзабельные компоненты еще не полностью реюзабельные. Есть еще целый класс компонентов, которые отличаются по своему содержанию, и их можно описать как однотипные «обертки» для разных компонентов. Однако эти обертки так же могут иметь поведение. Давайте рассмотрим пример из material design. Предположим что у нас есть необходимость сделать много однотипных скринов, отличающихся по содержанию, но имеющих общую структуру:
Как бы мы устранили дублирование при помощи компонентов? Мы видим что у нас есть два «слота», в которые бы мы хотели поместить каке-то содержание. Интересно, можно ли поместить компоненты не в шаблоне, а вместе использования компонента? Конечно можно, у нас же есть трансклюды
Для начала вспомним что такое трансклюды (transclude или включения) и вспомним почему они вообще так называтюся. Как говорит нам википедия, включение, это внедрение части, или целого электронного документа в один или более других документов через гипертекстовые ссылки. Сложно, да? Посмотрим картинку:
Надеюсь теперь понятно. Поскольку в ангуляре мы работаем с представлением как с шаблонами, то этот механизм предоставляет нам декларативный способ включения других шаблонов. Для того, что бы компонент мог прокидывать кусок документа в свой шаблон, в определении компонента нам надо указать transclude: true
, и затем поместить директиву ngTransclude на нужный элемент. Еще можно воспользоваться transcludeFn
, но это уже тянет на отдельную статью.
Однако до недавнего времени, мы могли работать только с одним стотом. С версии angular 1.5 нам стали доступны мульти-слот трансклюды. Они действуют довольно просто. В рамках компонента мы должны вместо true
у свойства transclude
определить объект, описывающий слоты:
transclude: {
// при такой записи ангуляр будет искать элемент,
// подподающий под заданный селектор
// и выкинет ошибку в случае его отсутствия
slotName: 'elementSelector',
// но нам не всегда нужно, что бы элемент был обязательным
// иногда нам нужно дать опциональную возможность переопределить слот
optionalSlotName: '?optionalElementSelector'
}
Вот так вот, далее же мы можем при помощи директивы ngTransclude
разместить элементы наших слотов:
Если вы читаете это, значит для слота optionalSlotName не нашлось элемента.
Еще одной причиной, почему разработчики недолюбливали трансклюды, является проблема производительности. С введением мульти-слот трансклюдов пришлось решить и эту проблему, введя ленивую компиляцию вложенных шаблонов. Все вместе это открывает нам довоьно много возможностей в плане реализации реюзабельных компонентов. Например мы можем реализовать компонент listView
, который будет изолировать логику вывода списка элементов. Причем мы можем добавить этому списку поведение, вроде… показывать спиннер пока данные загружаются, или же отображать сообщение о том что данных нет. А за счет мульти-слот трансклюдов мы можем подменять части шаблонов и делать компонент еще более гибким. Спойлер: в конце статьи я дам ссылку на простенькую реализацию такого компонента.
А как же директивы?
Директивы можно расценивать как декораторы для компонентов. Их задача — инкапсулировать дополнительное поведение, и применять их к определенным компонентам. Таким образом мы можем уменьшить специфичность компонентов и добавлять ее декорацией.
Такие директивы как ngRepeat
, ngShow
или ngIf
можно рассмаривать как универсальные декораторы, которые можно применять для любых элементов. Они инкапсулируют DOM операции, и предоставляют декларативный способ управления представлением.
Классическими примерами декораторов для компонентов могут служить директивы для форм, которые добавляют компоненту input
дополнительное поведение, в виде специфичной валидации или трансформации значений. Делается это при помощи указания зависимости от другой директивы, указываемой в свойстве require
, которая в нашем случае будет компонентом.
В нашем примере с компонентом listView
, мы могли бы написать директиву-декоратор, которая бы позволяла при помощи одного атрибута добавить функциональность pull-to-refresh. Или же бесконечный скрол с подгрузкой данных. При всем при этом нам не нужно вносить изменения в сам компонент, что значит что мы сможем проще поддерживать наши решения!
Тестирование компонентов
Если вы начинающий разработчик, то вам смертельно важно научиться покрывать тестами ваш код. Как минимум для того, что бы иметь возможность продолжать экспериментировать с архитектурой вашего приложения, чистить его, пробовать новое с минимальными рисками того, что вас побьют за регрессии. Для этого так же важно, что бы написание тестов не отнимало много времени и не создавало оверхэд (иначе придут злые менеджеры и будут кричать что вы медленно работаете). А еще тесты должны быть относительно быстрыми, что бы иметь возможность запускать как можно чаще (проблемы решать проще когда вы знаете что изменилось с последнего прогона тестов). Итак, что могут предложить в этом плане компоненты?
Тестировать маленькие вещи проще чем большие, потому декомпозиция на компоненты уже должна упрощать тестирование. Stateless компоненты тестировать еще проще, как как нам не нужно заботиться о внутреннем состоянии компонента. Мы просто создаем экземпляр компонента, пробрасывая необходимое для теста состояние, и все. Но давайте подумаем что еще у нас упрощает жизнь.
Тот факт, что представление в Angular является полностью декларативным, существенно упрщает юнит тестирование UI компонентов. Отдельные директивы, предоставляющие нам строительные блоки для построения декларативного UI, и инкапсулирующие императивную логику по работе с DOM, уже должны быть покрыты тестами, нам нет смысла тестировать их еще раз.
Максимум что нам стоит проверить — так это состояние, которое наш компонент предоставляет шаблонам. Конечно было бы неплохо еще проверить, что бы в шаблоне биндинги были верные, но это проще делать в рамках e2e тестов. В Angular2 с этим будет проще, так как в случае опечаток в выражениях у нас появится возможность ловить ошибки. В ветке 1.x с этим немного грустно.
Итого, тест типичного компонента может быть примерно таким:
describe('my component', () => {
let component;
beforeEach(() => {
component = new MyComponent();
component.someState = [1, 3, 2];
});
it('sorts collection before render', () => {
expect(component.sortedCollection).toBe.equal([1, 2, 3]);
});
});
Согласитесь, это очень просто. Никакой специфичной Angular фигни. Только javascript! В этом основное преимущество dirty checking-а между данными и представлением, а не между представлением и DOM, как например в React. Мы полностью отделяем UI от состояния приложения, а такие вещи банально проще тестировать.
Однако я слегка смухлевал. При написании таких тестов я руководствуюсь простым допущением, что мы не делаем никаких действий с состоянием в конструкторе компонента. Иногда хочется делать подобное, потому пакет angular-mock
предоставляет вам сервис $componentController
, который вы можете использовать для получения экземпляра контроллера именно тем способом, которым он будет получаться в Angular. В прочем это лишь делает утверждение (ничего от ангуляра в тестах) невалидным, но дополнительной сложности это несете не много.
Светлое будущее?
Конечно же у нас все еще есть проблемы, вроде необходимости в принципе объявлять биндинги на уровне компонента, или же необходимость понимать разницу между аж 4-мя видами изоляции атрибутов скоупов… Но это уже намного лучше, нежели в былые времена. А главное разрыв между Angular 1.5 и Angular2 сводится уже к минимуму. Если вы хотите еще больше сократить этот разрыв, можно воспользоваться различными решениями, почитать ngUpdate. Так же есть масса пакетов, добавляющих декораторы из Angular2 для регистрации компонентов, сервисов и прочего, делая ваши приложения еще ближе к Angular2.
А пример?
Да да, я же обещал хоть какой-то примерчик. Выше я уже говорил о идее сделать реюзабельный компонент для вывода списков. К сожалению я оказался слишком ленив что бы подготовить полноценную реализацию в виде гитхаб репозитория, потому вот вам небольшой пример на планкере:
мой маленький листвью
В примере использованы компоненты, односторонние биндинги, реакция на изменения значений без $scope.$watch
, а так же мульти-слот трансклюды для стилизации. К сожалению я не нашел в себе сил добавить туда пример использования директив в качестве декораторов, а так же, хоть мне и стыдно, не добавил тестов. Так же не могу сказать что этот пример идеален и готов к использованию в бою. Это просто пример, который должен подтолкнуть вас к новым идеям. Если
Приятной разработки!