В четвёртой части мы создали базовый мобильный интерфейс Ext Mail на Modern Toolkit: NavigationView, MessagesGrid с аватарами, тулбар действий и row actions. Но для полноценной работы не хватало двух ключевых элементов: навигации по меткам и возможности создавать письма.
В пятой части мы добавим:
-
дерево меток (LabelsTree) для Modern Toolkit;
-
выдвижное меню (slide-out menu) в стиле нативных мобильных приложений;
-
плавающую кнопку Compose в правом нижнем углу;
-
форму написания письма для Modern;
-
интеграцию формы с общей бизнес-логикой.
Все примеры кода взяты из оригинальной книги Stuart Ashworth (Sencha MVP) и адаптированы для русскоязычного читателя.
🌳 Создаём дерево меток для Modern
В Classic Toolkit дерево меток было построено на Ext.tree.Panel. В Modern Toolkit для этого используется компонент Ext.list.Tree. Создаём класс ExtMail.view.labels.LabelsTree в папке modern/src/view/labels/:
Ext.define('ExtMail.view.labels.LabelsTree', { extend: 'Ext.list.Tree', alias: 'widget.labels-LabelsTree', defaults: { xtype: 'treeListitem', textProperty: 'combined' } });
Мы указываем, что для отображения каждого элемента дерева нужно использовать поле combined. Это поле мы создали в модели Label ранее — оно объединяет название метки и количество непрочитанных сообщений, оборачивая счётчик в HTML-элемент с CSS-классом unread.
Создаём SCSS-файл LabelsTree.scss рядом с компонентом:
.unread { font-weight: bold; }
Этого достаточно, чтобы дерево работало и выглядело так же, как в Classic-версии.
📂 Создаём выдвижное меню (Slide-out Menu)
Для мобильного приложения привычно, когда метки скрыты в выдвижном меню слева. Создаём компонент ExtMail.view.menu.Menu, который будет панелью с деревом меток внутри:
Ext.define('ExtMail.view.menu.Menu', { extend: 'Ext.panel.Panel', alias: 'widget.menu-Menu', requires: [ 'ExtMail.view.labels.LabelsTree' ], defaultListenerScope: true, scrollable: 'vertical', layout: 'fit', items: [{ xtype: 'labels-LabelsTree', style: { background: 'white' }, bind: { store: '{labels}', selection: '{selectedLabel}' }, listeners: { selectionchange: 'onLabelSelectionChange' } }], onLabelSelectionChange: function() { this.fireEvent('closemenu'); }, // Поведение меню getReveal: function() { return true; }, getCover: function() { return false; }, getSide: function() { return 'left'; } });
Мы привязываем дерево к общему хранилищу labels и свойству selectedLabel из MainViewModel. При выборе метки генерируем событие closemenu, чтобы меню автоматически закрывалось.
Три метода — getReveal, getCover и getSide — определяют поведение меню:
-
getReveal: true— меню появляется из-под основного контента, сдвигая его вправо. -
getCover: false— меню не накрывает контент сверху. -
getSide: 'left'— меню выезжает слева.
🔌 Подключаем меню к приложению
Меню создаётся на уровне приложения, а не внутри Main-компонента. Добавляем метод setupMenu в Application.js:
setupMenu: function() { Ext.Viewport.setMenu(Ext.create('ExtMail.view.menu.Menu', { width: 250, viewModel: this.getMainView().getViewModel(), listeners: { closemenu: function() { Ext.Viewport.hideMenu('left'); } } })); }
Мы передаём меню ViewModel главного вида, чтобы оно использовало те же данные и бизнес-логику. Меню прикрепляется к Viewport, поэтому не наследует ViewModel автоматически — мы передаём её вручную.
Вызываем setupMenu в методе launch:
launch: function() { this.setupMenu(); }
☰ Добавляем кнопку меню в тулбар
Теперь нужно дать пользователю возможность открывать меню. Переопределяем метод buildItems в Modern-версии тулбара:
buildItems: function() { var items = this.callParent(arguments); items.unshift({ xtype: 'button', iconCls: 'x-fa fa-list', bind: { hidden: '{selectedMessage}' }, handler: this.onMenuButtonTap, scope: this }); return items; }, onMenuButtonTap: function() { if (Ext.Viewport.getMenus().left.isHidden()) { Ext.Viewport.showMenu('left'); } else { Ext.Viewport.hideMenu('left'); } }
Мы вызываем родительский buildItems, чтобы получить стандартный набор кнопок, а затем добавляем кнопку меню в начало массива. Она видна только на экране списка писем.
🖼️ Добавляем логотип приложения
Чтобы меню выглядело законченным, добавим логотип Ext Mail в его верхнюю часть как dockedItem:
items: [{ xtype: 'component', docked: 'top', style: { textAlign: 'center', padding: '20px 0' }, html: '<img src="resources/images/ext-mail-logo.png" />' }, { xtype: 'labels-LabelsTree', // ... }]
➕ Плавающая кнопка Compose
Для запуска процесса написания письма добавим плавающую кнопку в правом нижнем углу. Создаём класс ExtMail.view.compose.ComposeButton:
Ext.define('ExtMail.view.compose.ComposeButton', { extend: 'Ext.Button', alias: 'widget.compose-ComposeButton', floated: true, width: 60, height: 60, cls: 'compose-button', iconCls: 'x-fa fa-edit' });
Создаём SCSS-файл ComposeButton.scss:
.compose-button.x-button { background: #d8372d; border-radius: 50%; position: absolute; right: 40px; bottom: 40px; .x-icon-el { color: #FFF; } }
Кнопка круглая, красная, с иконкой карандаша, расположена в правом нижнем углу.
Инициализируем кнопку в Application.js методом setupComposeButton:
setupComposeButton: function() { this.composeButton = Ext.create('ExtMail.view.compose.ComposeButton', { hidden: false, handler: function() { this.getMainView().getController().onComposeMessage(); }, scope: this }); this.getMainView().getViewModel().bind('{selectedMessage}', function(selectedMessage) { var setDelay = selectedMessage ? 0 : this.getMainView().getLayout().getAnimation().getDuration(); setTimeout(Ext.bind(function() { this.composeButton.setHidden(selectedMessage); }, this), setDelay); }, this); }
Мы привязываемся к свойству selectedMessage: когда пользователь открывает письмо, кнопка скрывается, когда возвращается к списку — показывается. Задержка setDelay нужна, чтобы кнопка появлялась после завершения анимации перехода.
Вызываем метод в launch:
launch: function() { this.setupMenu(); this.setupComposeButton(); }
📝 Создаём общую основу формы Compose
Форма для Classic и Modern имеет общую логику: встроенную ViewModel, обработку кнопок, заполнение данных получателя. Но API полей различается, поэтому создаём базовый класс ComposeFormBase в общей папке app/:
Ext.define('ExtMail.view.compose.ComposeFormBase', { extend: 'Ext.form.Panel', viewModel: { data: { selectedRecipient: null } }, constructor: function() { this.callParent(arguments); this.getViewModel().bind('{selectedRecipient}', this.onSelectedRecipientChange, this); }, onSendClick: function() { this.fireEvent('send', this.getViewModel().get('messageRecord')); }, onDiscardClick: function() { this.fireEvent('discarddraft', this.getViewModel().get('messageRecord')); }, onSelectedRecipientChange: function(selectedRecipientRecord) { var firstName, lastName, email; if (selectedRecipientRecord) { firstName = selectedRecipientRecord.get('firstName'); lastName = selectedRecipientRecord.get('lastName'); email = selectedRecipientRecord.get('email'); } if (this.getViewModel().get('messageRecord')) { this.getViewModel().get('messageRecord').set({ firstName: firstName, lastName: lastName, email: email }); } } });
Теперь Classic-версия формы просто расширяет этот базовый класс, а мы создаём Modern-версию с теми же полями, но с использованием API Modern Toolkit:
Ext.define('ExtMail.view.compose.ComposeForm', { extend: 'ExtMail.view.compose.ComposeFormBase', alias: 'widget.compose-ComposeForm', defaultListenerScope: true, padding: 10, layout: { type: 'vbox', align: 'stretch' }, items: [ { xtype: 'combobox', placeholder: 'Recipient', displayField: 'email', valueField: 'email', queryMode: 'local', required: true, bind: { store: '{contacts}', selection: '{selectedRecipient}', value: '{messageRecord.email}' } }, { xtype: 'textfield', placeholder: 'Subject', bind: { value: '{messageRecord.subject}' } }, { xtype: 'textareafield', placeholder: 'Compose email', flex: 1, bind: { value: '{messageRecord.message}' } } ], dockedItems: [{ xtype: 'toolbar', docked: 'bottom', margin: 0, items: [ { xtype: 'button', scale: 'medium', text: 'Send', handler: 'onSendClick' }, '->', { xtype: 'button', iconCls: 'x-fa fa-trash', tooltip: 'Discard', handler: 'onDiscardClick' } ] }], onSendClick: function() { if (this.validate()) { this.callParent(arguments); } } });
Обратите внимание на переопределённый onSendClick: в Modern Toolkit нет конфига formBind, поэтому мы вручную проверяем валидность формы перед вызовом родительского метода.
🔗 Интегрируем форму Compose
Интеграция формы в Modern использует тот же паттерн, что и в Classic. Переопределяем showComposeWindow в Modern-контроллере:
showComposeWindow: function(messageRecord) { this.getViewModel().set('selectedMessage', messageRecord); }
Обновляем обработчик onSelectedMessageChange, чтобы он открывал форму для черновиков:
onSelectedMessageChange: function(selectedMessage) { if (selectedMessage && !selectedMessage.get('draft')) { this.getView().push(this.getMessageDetailsConfig(selectedMessage)); } else if (selectedMessage && selectedMessage.get('draft')) { this.getView().push(this.getComposeMessageConfig(selectedMessage)); } else { this.getView().pop(); } }
Метод getComposeMessageConfig возвращает конфигурацию формы:
getComposeMessageConfig: function(messageRecord) { return { xtype: 'compose-ComposeForm', viewModel: { data: { messageRecord: messageRecord } }, listeners: { send: this.onSendMessage, discarddraft: this.onDiscardDraftMessage, scope: this } }; }
Здесь мы используем уже готовые методы onSendMessage и onDiscardDraftMessage из MainControllerBase — именно та самая общая бизнес-логика, которую мы сохранили для обоих тулкитов.
💎 Заключение
В пятой части мы завершили создание мобильного интерфейса Ext Mail:
-
Создали дерево меток для Modern Toolkit.
-
Реализовали выдвижное меню в стиле нативных приложений.
-
Добавили кнопку открытия меню в тулбар.
-
Разместили логотип приложения в меню.
-
Создали плавающую кнопку Compose.
-
Построили форму написания письма на основе общего базового класса.
-
Интегрировали форму с существующей бизнес-логикой.
Теперь приложение имеет полный паритет функций между Classic и Modern Toolkit: одинаковый набор возможностей, единая модель данных и общая бизнес-логика. В следующей части мы перейдём от статических JSON-файлов к настоящему REST API с использованием RAD Server.