В первых двух частях мы построили фундамент почтового клиента Ext Mail: создали Universal-приложение, описали модели и хранилища, построили Grid для списка писем, Reader для просмотра, дерево меток и динамический тулбар. Во третьей части мы научим приложение создавать новые письма:
-
добавим кнопку Compose над деревом меток;
-
создадим форму для написания письма с полями получателя, темы и текста;
-
реализуем автодополнение получателя из списка контактов;
-
покажем форму в отдельном окне;
-
научимся отправлять и откладывать черновики;
-
добавим возможность вернуться к редактированию черновика.
Все примеры кода взяты из оригинальной книги Stuart Ashworth (Sencha MVP) и адаптированы для русскоязычного читателя.
✏️ Кнопка Compose
Начнём с добавления кнопки Compose в верхнюю часть дерева меток. Она будет закреплена как dockedItem и всегда видна пользователю.
В компоненте LabelsTree в методе initComponent добавляем dockedItems:
dockedItems: [{ xtype: 'toolbar', dock: 'top', items: [ { xtype: 'tbfill' }, { xtype: 'button', scale: 'large', iconCls: 'x-fa fa-pencil-square-o', text: 'Compose', handler: 'onComposeClick', scope: this }, { xtype: 'tbfill' } ] }]
Два компонента tbfill используются как распорки, чтобы центрировать кнопку на панели. Сама кнопка имеет scale: 'large' и использует иконку из встроенного набора Font Awesome.
По клику вызывается метод onComposeClick, который генерирует пользовательское событие compose:
onComposeClick: function() { this.fireEvent('compose'); }
Мы не создаём форму прямо здесь — вместо этого сообщаем родительскому компоненту, что пользователь хочет написать письмо. Это позволяет держать логику разделённой.
📝 Форма для написания письма (ComposeForm)
Создаём новый компонент ExtMail.view.compose.ComposeForm, расширяющий Ext.form.Panel:
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: [] });
Устанавливаем defaultListenerScope: true, чтобы имена обработчиков событий искались внутри этого компонента. Используем VBox-макет, чтобы поле сообщения растягивалось на всю доступную высоту.
Добавляем поля Subject и Message:
items: [ { xtype: 'textfield', emptyText: 'Subject', bind: { value: '{messageRecord.subject}' } }, { xtype: 'textareafield', flex: 1, bind: { value: '{messageRecord.message}' } } ]
Поле subject — обычное текстовое поле. Поле message — текстовая область с flex: 1, чтобы занимать всё свободное пространство. Оба поля привязаны к соответствующим свойствам записи messageRecord.
Внизу формы добавляем тулбар с кнопками Send и Discard:
dockedItems: [{ xtype: 'toolbar', dock: 'bottom', margin: 0, items: [ { xtype: 'button', scale: 'medium', text: 'Send', formBind: true, handler: 'onSendClick' }, '->', { xtype: 'button', iconCls: 'x-fa fa-trash', tooltip: 'Discard', handler: 'onDiscardClick' } ] }]
Кнопка Send имеет formBind: true — это автоматически блокирует её, если какое-либо поле формы невалидно. Отличный способ предотвратить отправку некорректных данных.
Обработчики генерируют пользовательские события:
onSendClick: function() { this.fireEvent('send', this.getViewModel().get('messageRecord')); }, onDiscardClick: function() { this.fireEvent('discarddraft', this.getViewModel().get('messageRecord')); }
👤 Поле получателя с автодополнением
Для поля получателя мы хотим использовать выпадающий список с автодополнением из базы контактов. Для этого нужно добавить инфраструктуру данных.
Модель Contact (app/model/Contact.js)
Ext.define('ExtMail.model.Contact', { extend: 'Ext.data.Model', fields: [ { name: 'name', calculate: function(data) { var firstName = data.firstName || ''; var lastName = data.lastName || ''; return Ext.String.trim(firstName + ' ' + lastName); } }, 'firstName', 'lastName', 'email', 'phone' ] });
Хранилище Contacts (app/store/Contacts.js)
Ext.define('ExtMail.store.Contacts', { extend: 'Ext.data.Store', alias: 'store.Contacts', model: 'ExtMail.model.Contact', autoLoad: true, proxy: { type: 'ajax', url: 'data/contacts.json', reader: { type: 'json', rootProperty: 'rows' } } });
Добавляем хранилище в MainModel, чтобы оно загружалось при старте приложения:
stores: { contacts: { type: 'Contacts' } }
Поле Recipient в форме
{ xtype: 'combobox', emptyText: 'Recipient', width: '100%', displayField: 'email', valueField: 'email', queryMode: 'local', allowBlank: false, bind: { store: '{contacts}', selection: '{selectedRecipient}', value: '{messageRecord.email}' } }
Здесь displayField и valueField оба указывают на поле email — то есть пользователь видит email и он же отправляется с формой. Режим queryMode: 'local' означает, что поиск идёт по уже загруженным данным, без обращения к серверу. Для больших баз данных можно использовать queryMode: 'remote'.
Мы привязываем selection к свойству selectedRecipient, которое нужно создать во встроенной вьюмодели формы:
viewModel: { data: { selectedRecipient: null } }
Зачем нам отслеживать selection, если значение уже привязано? Затем, что при выборе контакта мы хотим сохранить в запись письма не только email, но и имя с фамилией. Добавляем привязку в конструкторе формы:
constructor: function() { this.callParent(arguments); this.getViewModel().bind('{selectedRecipient}', this.onSelectedRecipientChange, this); }, 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 }); } }
Теперь при выборе получателя в запись письма автоматически попадут имя, фамилия и email.
🪟 Окно для написания письма (ComposeWindow)
Форма готова, но её нужно где-то показать. Создаём ExtMail.view.compose.ComposeWindow, расширяющий Ext.window.Window:
Ext.define('ExtMail.view.compose.ComposeWindow', { extend: 'Ext.window.Window', alias: 'widget.compose-ComposeWindow', viewModel: { data: { messageRecord: null } }, config: { messageRecord: null }, minimizeable: true, resizable: false, draggable: false, title: 'New Message', layout: 'fit', constrain: true, constrainHeader: true, items: [{ xtype: 'compose-ComposeForm', bubbleEvents: ['send', 'discarddraft'] }], updateMessageRecord: function(messageRecord) { this.getViewModel().set('messageRecord', messageRecord); } });
Ключевой момент — bubbleEvents: ['send', 'discarddraft'] в конфигурации формы. Это говорит о том, что события send и discarddraft, сгенерированные формой, должны всплывать до окна. Так мы можем слушать их на уровне окна, не залезая внутрь формы.
🚀 Запуск процесса создания письма
Теперь свяжем всё вместе. В MainControllerBase добавляем метод onComposeMessage:
onComposeMessage: function() { var messageRecord = Ext.create('ExtMail.model.Message', { labels: [ExtMail.enums.Labels.DRAFTS], outgoing: true, draft: true }); this.getViewModel().getStore('messages').add(messageRecord); this.getViewModel().getStore('messages').commitChanges(); this.showComposeWindow(messageRecord); }
Создаём новый экземпляр Message, помечаем его как черновик (draft: true), исходящее (outgoing: true) и добавляем метку DRAFTS. Затем добавляем запись в хранилище messages и сразу вызываем commitChanges() — поскольку у нас нет реального бэкенда, мы хотим, чтобы запись сразу считалась «сохранённой».
Метод showComposeWindow должен быть специфичен для каждого тулкита, поэтому добавляем его в подкласс Classic:
showComposeWindow: function(messageRecord) { var win = Ext.create('ExtMail.view.compose.ComposeWindow', { messageRecord: messageRecord, height: 500, width: 500 }); win.on({ send: Ext.bind(this.onSendMessage, this, [win], true), discarddraft: Ext.bind(this.onDiscardDraftMessage, this, [win], true), scope: this }); win.show(); }
Мы создаём окно, передаём ему запись письма, подписываемся на события send и discarddraft. Используем Ext.bind, чтобы добавить ссылку на окно (win) в качестве дополнительного параметра — это позволит закрыть окно после отправки или удаления.
Осталось подключить кнопку Compose к обработчику в конфигурации LabelsTree внутри Main:
{ xtype: 'labels-LabelsTree', region: 'west', width: 300, bind: { store: '{labels}', selection: '{selectedLabel}' }, listeners: { compose: 'onComposeMessage' } }
📤 Отправка и удаление черновика
Реализуем обработчики событий окна.
Отправка письма (onSendMessage):
onSendMessage: function(messageRecord, e, composeWindow) { messageRecord.removeLabel(ExtMail.enums.Labels.DRAFTS); messageRecord.addLabel(ExtMail.enums.Labels.SENT); messageRecord.set({ draft: false, sent: true, date: new Date() }); messageRecord.commit(); composeWindow.close(); }
Убираем метку DRAFTS, добавляем SENT, сбрасываем флаг черновика, устанавливаем флаг отправки и текущую дату. После этого фиксируем изменения и закрываем окно.
Удаление черновика (onDiscardDraftMessage):
onDiscardDraftMessage: function(messageRecord, e, composeWindow) { this.getViewModel().getStore('messages').remove(messageRecord); composeWindow.close(); }
Просто удаляем запись из хранилища и закрываем окно.
✏️ Редактирование черновика
После сохранения черновика он появляется в папке Drafts. Но при клике на него открывается обычный экран чтения письма — а нам хотелось бы снова открыть форму редактирования.
Исправим это в методе onMessageClick в MainControllerBase:
onMessageClick: function(grid, messageRecord, rowEl, index, e) { // Игнорируем клики по иконкам действий (звёздочка и т.д.) if (e.getTarget('.x-action-col-icon')) { return; } if (messageRecord.get('draft')) { this.showComposeWindow(messageRecord); } else { this.getViewModel().set('selectedMessage', messageRecord); } }
Теперь при клике на черновик открывается окно редактирования, и пользователь может продолжить написание письма, отправить его или удалить.
💎 Заключение
В третьей части мы научили наш почтовый клиент создавать и редактировать письма:
-
Добавили кнопку Compose над деревом меток.
-
Создали форму с полями получателя, темы и текста.
-
Реализовали автодополнение получателя из базы контактов.
-
Показали форму в отдельном окне с возможностью перетаскивания и сворачивания.
-
Настроили отправку и удаление черновиков.
-
Добавили возможность вернуться к редактированию черновика.
В следующей части мы интегрируем Modern Toolkit и создадим мобильную версию приложения с оптимизированным интерфейсом для сенсорных устройств. Следите за обновлениями!