В первых трёх частях мы построили полноценный почтовый клиент Ext Mail для десктопа на Classic Toolkit: создали Universal-приложение, модели и хранилища, Grid и Reader, дерево меток, динамический тулбар, форму создания письма и работу с черновиками. Однако современное корпоративное приложение должно одинаково хорошо работать и на мобильных устройствах.
В четвёртой части мы интегрируем Modern Toolkit и создадим мобильную версию интерфейса, максимально переиспользуя уже написанную бизнес-логику. Это одно из главных преимуществ Universal-приложений на Ext JS: общий код живёт в папке app/, а специфичные для тулкита компоненты — в classic/ и modern/.
Все примеры кода взяты из оригинальной книги Stuart Ashworth (Sencha MVP) и адаптированы для русскоязычного читателя.
🚀 Запуск Modern Toolkit
Чтобы запустить приложение в режиме Modern Toolkit, используйте команду:
sencha app watch modern
Затем откройте приложение в браузере и включите режим эмуляции мобильного устройства (например, в Chrome DevTools). Sencha Cmd автоматически подставит Modern-версию приложения.
🏗️ Создаём Main View для Modern
В Modern Toolkit корневой компонент приложения должен быть другим. Мы создадим класс ExtMail.view.main.Main, который расширяет Ext.NavigationView. Этот компонент даёт «нативный» вид и функциональность: заголовок, кнопку «Назад», анимацию скольжения при переходе между экранами.
Ext.define('ExtMail.view.main.Main', { extend: 'Ext.NavigationView', xtype: 'app-main', fullscreen: true, requires: [ 'ExtMail.view.main.MainController', 'ExtMail.view.main.MainModel' ], controller: 'main', viewModel: 'main' });
NavigationView позволяет «нажимать» (push) и «выталкивать» (pop) дочерние компоненты, обеспечивая навигацию в стиле мобильных приложений. Он автоматически отображает заголовок текущей карточки и кнопку «Назад».
Контроллер MainController для Modern расширяет общий MainControllerBase из папки app/:
Ext.define('ExtMail.view.main.MainController', { extend: 'ExtMail.view.main.MainControllerBase', alias: 'controller.main' });
Благодаря этому вся бизнес-логика (обработка писем, действия с метками, отправка черновиков) остаётся общей и не дублируется.
📱 Создаём Messages Grid для Modern
Для отображения списка писем создаём компонент ExtMail.view.messages.MessagesGrid, который расширяет Ext.grid.Grid (аналог Ext.grid.Panel в Modern).
Ext.define('ExtMail.view.messages.MessagesGrid', { extend: 'Ext.grid.Grid', alias: 'widget.messages-MessagesGrid', cls: 'messages-grid', // ... });
Колонки
Мы создаём две колонки:
-
Аватар — цветной кружок с первой буквой имени отправителя.
-
Информация о письме — полное имя, тема, фрагмент текста и дата.
columns: [ { dataIndex: 'firstName', width: 60, cell: { encodeHtml: false }, tpl: [ '<div class="avatar" style="background-color: {[this.getAvatarColour(values.firstName)]};">', '<span>{[values.firstName.substring(0, 1).toUpperCase()]}</span>', '</div>' ].join(''), getAvatarColour: function(name) { var alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split(''); var colours = ['#e6194B', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', '#42d4f4', '#f032e6', '#bfef45', '#fabed4', '#469990', '#dcbeff', '#9A6324', '#fffac8', '#800000', '#aaffc3', '#808000', '#ffd8b1', '#000075', '#a9a9a9']; var index = alphabet.indexOf(name.substring(0, 1).toUpperCase()); return colours[index % colours.length]; } }, { dataIndex: 'subject', flex: 1, cell: { encodeHtml: false }, tpl: [ '<div class="{[values.unread ? \'unread\' : \'\']}">', '<div class="top-line">', '<span class="name">{fullName}</span>', '<span class="date">{date:date("j M \'y")}</span>', '</div>', '<div class="subject">{subject}</div>', '<div class="message">{message}</div>', '</div>' ].join('') } ]
Шаблон первой колонки использует функцию getAvatarColour, которая по первой букве имени выбирает цвет из палитры. Вторая колонка выводит имя, дату, тему и фрагмент сообщения. Условие {values.unread ? 'unread' : ''} добавляет CSS-класс для непрочитанных писем.
Стили
Создаём SCSS-файл MessagesGrid.scss рядом с компонентом:
.messages-grid { .avatar { border-radius: 50%; display: flex; align-items: center; justify-content: center; width: 35px; height: 35px; color: #fff; font-weight: bold; } .unread { .name, .date, .subject { font-weight: bold; } } .top-line { display: flex; } .name { font-size: 1.05rem; flex: 1; } .subject, .message { text-overflow: ellipsis; overflow: hidden; } }
Sencha Cmd автоматически скомпилирует этот файл и добавит стили в приложение.
Интеграция в Main
Добавляем Grid в Main и привязываем к хранилищу messages:
items: [{ xtype: 'messages-MessagesGrid', hideHeaders: true, titleBar: false, bind: { store: '{messages}' }, listeners: { childtap: 'onMessageTap' } }]
Событие childtap в Modern Toolkit аналогично itemclick в Classic. Оно передаёт объект location, содержащий запись.
👆 Обработка тапа по сообщению
В Classic Toolkit при клике на письмо мы устанавливали свойство selectedMessage во ViewModel и переключали карточку. В Modern Toolkit мы хотим использовать ту же логику, но с учётом навигации.
Для этого вынесем общий код в MainControllerBase:
// app/view/main/MainControllerBase.js handleMessageClick: function(messageRecord) { if (messageRecord.get('draft')) { this.showComposeWindow(messageRecord); } else { this.getViewModel().set('selectedMessage', messageRecord); } }
Теперь в Modern-контроллере обрабатываем childtap:
// modern/src/view/main/MainController.js onMessageTap: function(grid, location) { this.handleMessageClick(location.record); }
А в Classic-контроллере обновляем onMessageClick:
// classic/src/view/main/MainController.js onMessageClick: function(grid, messageRecord, rowEl, index, e) { if (e.getTarget('.x-action-col-icon')) { return; } this.handleMessageClick(messageRecord); }
📖 Отображение MessageReader в Modern
Чтобы при выборе письма открывался экран чтения, используем возможности NavigationView. В MainController для Modern добавляем метод init, который подписывается на изменение selectedMessage:
init: function() { this.getViewModel().bind('{selectedMessage}', this.onSelectedMessageChange, this); }, onSelectedMessageChange: function(selectedMessage) { if (selectedMessage) { this.getView().push(this.getMessageDetailsConfig(selectedMessage)); } else { this.getView().pop(); } }, getMessageDetailsConfig: function(messageRecord) { return { xtype: 'reader-MessageReader', data: messageRecord.data, header: false }; }
Когда selectedMessage становится truthy, мы делаем push нового компонента MessageReader, иначе — pop (возврат к списку).
Компонент MessageReader изначально лежал в папке classic/. Поскольку он не использует ничего специфичного для Classic, мы можем переместить его в общую папку app/view/reader/. После этого он станет доступен обоим тулкитам.
🧰 Добавляем тулбар действий
В Classic Toolkit у нас был тулбар с кнопками Refresh, Back, Archive, Delete, Mark as Unread. Мы хотим такой же в Modern, но с учётом особенностей тулкита.
Создаём общий базовый класс MessagesToolbarBase в папке app/:
Ext.define('ExtMail.view.messages.MessagesToolbarBase', { extend: 'Ext.Toolbar', buildItems: function() { return [ { xtype: 'button', tooltip: 'Refresh', iconCls: 'x-fa fa-redo', handler: this.makeHandler('refresh'), scope: this, bind: { hidden: '{!visibleMessageButtons.refresh}' } }, { xtype: 'button', tooltip: 'Back', iconCls: 'x-fa fa-arrow-left', handler: this.makeHandler('back'), scope: this, hidden: true, bind: { hidden: '{!visibleMessageButtons.back}' } }, // ... Archive, Delete, Mark as Unread { xtype: 'component', tpl: '{count} messages', data: {}, bind: { hidden: '{!visibleMessageButtons.messageCount}', data: { count: '{messages.count}' } } } ]; }, makeHandler: function(event) { return function() { this.fireEvent(event); }; } });
Мы используем bind: { hidden: '{!visibleMessageButtons.refresh}' } — это привязка к вычисляемым свойствам, которые мы определим во ViewModel. Логика видимости кнопок остаётся общей.
В Classic тулбар расширяет этот базовый класс и в initComponent вызывает this.setItems(this.buildItems()). В Modern используется метод initialize:
Ext.define('ExtMail.view.messages.MessagesToolbar', { extend: 'ExtMail.view.messages.MessagesToolbarBase', alias: 'widget.messages-MessagesToolbar', initialize: function() { this.setItems(this.buildItems()); this.callParent(arguments); } });
Добавляем тулбар в Main как docked: 'top':
{ xtype: 'messages-MessagesToolbar', docked: 'top', listeners: { refresh: 'onRefreshMessages', back: 'onBackToMessagesGrid', delete: 'onDeleteMessage', markunread: 'onMarkMessageUnread', archive: 'onArchiveMessage' } }
Все обработчики уже есть в MainControllerBase и работают с данными, поэтому не требуют изменений.
⭐ Добавляем row action (звёздочка)
В Modern Toolkit для добавления действий в строку используется конфигурация tools у колонки. Мы создаём две «кнопки»: Star и Unstar.
{ dataIndex: 'subject', flex: 1, cell: { encodeHtml: false, tools: { star: { handler: 'onUnStarMessage', iconCls: 'x-fa fa-star', zone: 'end', bind: { hidden: '{!record.starred}' } }, unstar: { handler: 'onStarMessage', iconCls: 'x-fa fa-star-half-alt', zone: 'end', bind: { hidden: '{record.starred}' } } } } }
Важно: чтобы привязка record.starred работала, нужно добавить в конфигурацию Grid:
itemConfig: { viewModel: true }
Это говорит о том, что каждая ячейка должна иметь собственную ViewModel, привязанную к записи.
Также добавляем defaultListenerScope: true в Grid, чтобы обработчики onStarMessage и onUnStarMessage искались в самом компоненте.
onStarMessage: function(grid, info) { this.fireEvent('starmessage', info.record); }, onUnStarMessage: function(grid, info) { this.fireEvent('unstarmessage', info.record); }
В Main подписываемся на эти события:
listeners: { childtap: 'onMessageTap', starmessage: 'onStarMessage', unstarmessage: 'onUnStarMessage' }
Обработчики onStarMessage и onUnStarMessage уже есть в MainControllerBase и работают с моделью.
💎 Заключение
В четвёртой части мы создали мобильную версию почтового клиента Ext Mail на Modern Toolkit:
-
Настроили
Ext.NavigationViewкак корневой компонент. -
Создали адаптированный
MessagesGridс аватарами и условным форматированием. -
Реализовали обработку тапа и навигацию через
push/pop. -
Переиспользовали общий код благодаря рефакторингу.
-
Добавили тулбар действий и row actions (звёздочка).
Теперь приложение работает и на десктопе, и на мобильных устройствах, сохраняя единую бизнес-логику. В следующей части мы добавим выдвижное меню (sliding menu) и завершим создание интерфейса для написания писем в Modern Toolkit.