Статьи

Композиция в Aurelia.io: создание построителя отчетов

Когда мы узнаем о новой платформе, мы часто видим тривиальные демонстрации, изображающие ее основные функции, например, хорошо известное приложение TodoMVC . И это здорово — я имею в виду, кому не нравятся приложения Todo, верно? Что ж, сегодня мы собираемся пойти немного другим путем. Мы собираемся избегать общего и вместо этого сосредоточимся на одной из уникальных ключевых особенностей фреймворка Aurelia: визуальной композиции.

Aurelia, новичок в этом блоке , уже была представлена ​​в предыдущей статье вместе с ее возможностями расширения HTML . К концу этой статьи мы должны лучше понять, как композиция помогает собирать сложные экраны из небольших компонентов, которые можно многократно изменять. Для этого мы собираемся создать приложение для построения отчетов. Вы можете найти демо-версию приложения здесь и найти полный исходный код здесь .

Что такое визуальная композиция?

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

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

Диаграмма, изображающая гомогенный и гетерогенный состав

Сравнение типов визуальной композиции

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

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

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

Использование Aurelia’s Compose Element

Чтобы использовать визуальную композицию в Aurelia, мы можем использовать предопределенный пользовательский элемент compose . Он работает по одному из ключевых соглашений Aurelia — парам вида и модели представления (VM) (о которых эта статья также будет называться страницей). Короче говоря, compose позволяет нам включать страницу в любой конкретной позиции в другое представление.

Следующий фрагмент демонстрирует, как его использовать. В позиции, в которую мы хотели бы включить страницу Hello World , мы просто определяем пользовательский элемент и устанавливаем значение его атрибута view-model равным имени файла, содержащего определение VM.

 <template> <h1>Hello World</h1> <compose view-model="hello-world" model.bind="{ demo: 'test' }"></compose> </template> 

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

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

 export class HelloWorld { constructor() { } activate(modelData) { console.log(modelData); // --> { demo: 'test' } } } 

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

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

 <compose view-model="hello-world" model.bind="{ demo: 'test' }" view="alternative-hello-world.html"></compose> 

В этом случае виртуальная машина все равно будет загружена, но вместо загрузки hello-world.html механизм компоновки вставит содержимое alternative-hello-world.html в элемент compose. А что если нам нужно динамически решить, какой вид следует использовать? Один из способов добиться этого — привязать атрибут view к свойству вызывающей виртуальной машины, значение которого будет определяться некоторой логикой.

 // calling VM export class App { pathToHelloWorld = "alternative-hello-world.html"; } // calling view <compose view-model="hello-world" model.bind="{ demo: 'test' }" view.bind="pathToHelloWorld"></compose> 

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

 export class HelloWorld { constructor() { } activate(modelData) { this.model = modelData; } getViewStrategy() { if( this.model.demo === 'test' ) return 'alternative-hello-world.html'; else return 'hello-world.html'; } } 

Подготовка настройки проекта

Теперь, когда мы увидели, как работает элемент compose, давайте взглянем на приложение построителя отчетов. Чтобы начать разработку, мы построили ее на Skeleton Navigation App . Некоторые части, такие как маршрутизатор, были удалены, так как это приложение использует только одно сложное представление, состоящее из других вложенных представлений. Для начала зайдите в репозиторий GitHub , загрузите ветку master и распакуйте ее в папку, либо локально клонируйте, открыв терминал и выполнив следующую команду:

 git clone https://github.com/sitepoint-editors/aurelia-reporter.git 

Чтобы завершить установку, выполните действия, перечисленные в разделе «Запуск приложения» в README проекта.

Создание представления отчета

Точкой входа нашего приложения является страница app.html (находится в папке src ). VM ( app.js ) — это просто пустой класс с предварительной загрузкой Twitter Bootstrap. Представление, как показано во фрагменте ниже, действует как контейнер основного приложения. Вы заметите, что он составляет экран из двух отдельных страниц, называемых toolbox и report . Первый действует как наш контейнер для различных перетаскиваемых инструментов, а второй — это лист, на который вы помещаете эти виджеты.

 <template> <div class="page-host"> <h1 class="non-printable">Report Builder</h1> <div class="row"> <compose class="col-md-2 non-printable" view-model="toolbox"></compose> <compose class="col-md-10 printable" view-model="report"></compose> </div> </div> </template> 

Глядя на toolbox.html мы видим, что представление toolbox.html список доступных виджетов вместе с кнопками для печати или очистки отчета.

 <template> <h3>Toolbox</h3> <ul class="list-unstyled toolbox au-stagger" ref="toolboxList"> <li repeat.for="widget of widgets" class="au-animate" title="${widget.type}"> <i class="fa ${widget.icon}"/> ${widget.name} </li> </ul> <button click.delegate="printReport()" type="button" class="btn btn-primary fa fa-print"> Print</button> <button click.delegate="clearReport()" type="button" class="btn btn-warning fa fa-remove"> Clear Report</button> </template> 

toolbox предоставляет эти виджеты, объявляя свойство с одинаковым именем и создавая его экземпляр внутри своего конструктора. Это делается путем импорта виджетов из их соответствующих местоположений и передачи их экземпляров — созданных инъекцией зависимостей Aurelia — в массив widgets . Кроме того, EventAggregator объявляется и присваивается свойству. Мы вернемся к этому чуть позже.

 import {inject} from 'aurelia-framework'; import {EventAggregator} from 'aurelia-event-aggregator'; import {Textblock} from './widgets/textblock'; import {Header} from './widgets/header'; import {Articles} from './widgets/articles'; import {Logo} from './widgets/logo'; @inject(EventAggregator, Textblock, Header, Articles, Logo); export class Toolbox { widgets; constructor(evtAgg, textBlock, header, articles, logo) { this.widgets = [ textBlock, header, articles, logo ]; this.ea = evtAgg; } ... } 

Так что же содержат эти виджеты? Рассматривая структуру проекта, мы можем найти их все в подпапке src/widgets . Начнем с простого: виджет логотипа. Этот виджет просто показывает изображение внутри своего вида. ВМ следует шаблону по умолчанию, реализуя type свойств, name и icon . Мы видели, как они используются в блоке репитеров панели инструментов.

 // logo.html <template> <img src="images/main-logo.png" /> </template> // logo.js export class Logo { type = 'logo'; name = 'Logo'; icon = 'fa-building-o'; } 

Глядя на виджет textblock блока, мы видим дополнительный метод активации, принимающий исходные данные модели из механизма компоновки.

 // textblock.js export class Textblock { type = 'textblock'; name = 'Textblock'; icon = 'fa-font'; text = 'Lorem ipsum'; activate(model) { this.text = model; } } 

Чтобы увидеть, как эта модель доступна для представления, давайте взглянем на страницу report . То, что мы видим в его представлении, представляет собой смесь как гомогенного, так и гетерогенного состава. Отчет, по сути неупорядоченный список, выведет все добавленные в него виджеты — это однородная часть. Теперь каждый виджет имеет различное отображение и поведение, которое составляет гетерогенную часть. Тег compose передает исходную модель, а также имя модели представления вложенных видов. Кроме того, отображается значок удаления, который можно использовать для удаления виджета с листа отчета.

 <template> <ul class="list-unstyled report" ref="reportSheet"> <li repeat.for="widget of widgets" class="au-animate"> <compose model.bind="widget.model" view-model="widgets/${widget.type}" class="col-md-11"></compose> <i class="remove-widget fa fa-trash-o col-md-1 non-printable" click.trigger="$parent.removeWidget(widget)"></i> </li> </ul> </template> 

Удаление выполняется путем поиска id соответствующего виджета и report.widget его из массива report.widget . Ретранслятор Aurelia позаботится об обновлении представления для фактического удаления DOM-элементов.

 removeWidget(widget) { let idx = this.widgets.map( (obj, index) => { if( obj.id === widget.id ) return index; }).reduce( (prev, current) => { return current || prev; }); this.widgets.splice(idx, 1); } 

Межкомпонентная связь через события

Мы упоминали, что на панели инструментов есть кнопка «Очистить отчет», но как это запускает очистку всех виджетов, добавленных на страницу report ? Одной из возможностей будет включение ссылки на виртуальную машину report внутри панели инструментов и вызов метода, который будет предоставлен. Этот механизм, однако, привел бы к жесткой связи между этими двумя элементами, так как набор инструментов не был бы применим без страницы отчета. По мере роста системы все больше и больше частей становятся зависимыми друг от друга, что в конечном итоге приведет к чрезмерно сложной ситуации.

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

Снимок экрана приложения построителя отчетов с двумя речевыми пузырями, детализирующими пользовательские события

События, использованные для создания функции очистки всех

Для реализации этого мы можем использовать EventAggregator от Aurelia . Если вы посмотрите на toolbox.js кода toolbox.js выше, то увидите, что EventAggregator уже EventAggregator в EventAggregator с EventAggregator toolbox . Это можно увидеть в действии в методе clearReport , который просто публикует новое событие с именем clearReport .

 clearReport() { this.ea.publish('clearReport'); } 

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

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

 import {inject} from 'aurelia-framework'; import {EventAggregator} from 'aurelia-event-aggregator'; import sortable from 'sortable'; @inject(EventAggregator) export class Report { constructor(evtAgg) { this.ea = evtAgg; this.ea.subscribe('clearReport', () => { this.widgets = []; }); } ... 

Используйте внешний код через плагины

До сих пор мы не рассматривали фактическую функцию перетаскивания, которую мы будем использовать для перетаскивания виджетов из панели инструментов на лист отчета. Конечно, можно создать функциональность с помощью встроенного перетаскивания в HTML5 , но зачем изобретать колесо, когда уже есть куча хороших библиотек, таких как Sortable , чтобы сделать работу за нас.

Таким образом, общая модель при разработке приложений — полагаться на внешние базы кода, которые предоставляют готовые функции. Но таким способом может передаваться не только сторонний код. Мы можем сделать то же самое с нашими собственными возможностями многократного использования, используя систему плагинов Aurelia. Идея та же. Вместо того, чтобы переписывать код для каждого приложения, мы создаем собственный плагин Aurelia, содержащий желаемую функциональность и экспортирующий его с помощью простых помощников. Это не ограничивается чисто компонентами пользовательского интерфейса, но может также использоваться для общей бизнес-логики или сложных функций, таких как сценарии аутентификации / авторизации.

Используйте тонкие анимации

В этом ключе давайте посмотрим на Aurelia Animator CSS , простую библиотеку анимации для Aurelia.

Библиотека анимации Aurelia построена на простом интерфейсе, который является частью репозитория шаблонов . Он действует как своего рода универсальный интерфейс для реальных реализаций. Этот интерфейс вызывается внутри Aurelia в определенных ситуациях, когда встроенные функции работают с DOM-Elements. Например, repeater использует это для запуска анимации на вновь вставленных / удаленных элементах в списке.

Следуя подходу согласия, чтобы использовать анимации, необходимо установить конкретную реализацию (такую ​​как CSS-Animator), которая делает свое волшебство, объявляя CSS3-анимации внутри вашей таблицы стилей. Для его установки мы можем использовать следующую команду:

 jspm install aurelia-animator-css 

После этого последний шаг заключается в регистрации плагина в приложении, что делается на этапе начальной загрузки вручную в файле main.js нашего примера построителя отчетов.

 export function configure(aurelia) { aurelia.use .standardConfiguration() .developmentLogging() .plugin('aurelia-animator-css'); // <-- REGISTER THE PLUGIN aurelia.start().then(a => a.setRoot()); } 

Примечание: сам плагин — это просто еще один проект Aurelia, который следует соглашению о наличии в файле index.js предоставляющего функцию configure , которая получает экземпляр Aurelia в качестве параметра. Метод configure выполняет инициализацию для плагина. Например, он может регистрировать такие компоненты, как пользовательские элементы, атрибуты или преобразователи значений, чтобы их можно было использовать «из коробки» (как в случае с пользовательским элементом compose ). Некоторые плагины принимают обратный вызов в качестве второго параметра, который можно использовать для настройки плагина после инициализации. Примером этого является плагин i18n .

Построитель отчетов использует тонкие анимации на этапе компоновки и указывает на удаление виджета из отчета. Первое сделано в представлении toolbox . Мы добавляем класс au-stagger в неупорядоченный список, чтобы указать, что каждый элемент должен быть анимирован последовательно. Теперь каждому элементу списка нужен класс au-animate , который сообщает Animator, что мы бы хотели, чтобы этот DOM-элемент был анимирован.

 <ul class="list-unstyled toolbox au-stagger" ref="toolboxList"> <li repeat.for="widget of widgets" class="au-animate" title="${widget.type}"> <i class="fa ${widget.icon}"/> ${widget.name} </li> </ul> 

То же самое мы делаем для виджета-повторителя представления reports :

 <li repeat.for="widget of widgets" class="au-animate"> 

Как уже упоминалось, CSS-Animator будет добавлять определенные классы к элементам во время фазы анимации. Все, что нам нужно сделать, это объявить их в нашей таблице стилей .

Добавление Drag & Drop

Что касается включения сторонних библиотек, мы можем воспользоваться стандартным менеджером пакетов Aurelia JSPM. Чтобы установить ранее упомянутую библиотеку Sortable.js, нам нужно выполнить следующую команду, которая установит пакет под именем sortable .

 jspm install sortable=github:rubaxa/[email protected] 

После установки JSPM автоматически обновит файл config.js и добавит сопоставления его пакетов:

 System.config({ "map": { ... "sortable": "github:rubaxa/[email protected]", ... } }); 

Теперь, когда пакет установлен, мы можем использовать его в нашей виртуальной toolbox , импортируя его сначала, а затем регистрируя функцию перетаскивания для нашего списка виджетов внутри attached хука. Это важно сделать в это время, так как это когда представление полностью генерируется и подключается к DOM.

 import sortable from 'sortable'; ... export class Toolbox { ... attached() { new sortable(this.toolboxList, { sort: false, group: { name: "report", pull: 'clone', put: false } }); } } 

Вы можете спросить, откуда этот this.toolboxList . Взгляните на атрибут ref представления toolbox в разделе анимации выше. Это просто создает сопоставление для элемента между представлением и виртуальной машиной.

Финальная часть — принять пропущенные элементы внутри виртуальной машины report . Для этого мы можем использовать обработчик onAdd в Sortable.js. Так как сам перетаскиваемый элемент списка не будет помещен в отчет, а будет ссылочным виджетом, составленным представлением, сначала мы должны удалить его. После этого мы проверяем тип виджета и в случае текстового блока мы инициализируем подсказку для текста, который будет использоваться в качестве данных модели виджета. Наконец, мы создаем объект-оболочку, включающий в себя id , type и model виджета, которые будут использоваться представлением report для создания виджета.

 attached() { new sortable(this.reportSheet, { group: 'report', onAdd: (evt) => { let type = evt.item.title, model = Math.random(), newPos = evt.newIndex; evt.item.parentElement.removeChild(evt.item); if(type === 'textblock') { model = prompt('Enter textblock content'); if(model === undefined || model === null) return; } this.widgets.splice(newPos, 0, { id: Math.random(), type: type, model: model }); } }); } 

Вывод

Вот и все. Мы видели, как элемент compose Aurelia может помочь нам создать сложную визуальную композицию и красиво разделить все наши компоненты на небольшие многократно используемые части. Кроме того, я продемонстрировал концепцию плагинов Aurelia для совместного использования кода несколькими проектами, а также использования сторонних библиотек. Мы, команда Aurelia, надеемся, что вам понравилось читать эту статью, и мы будем рады ответить на любые вопросы, либо здесь, в комментариях, либо на нашем канале Gitter .