Создаем почтовый клиент на Ext JS с нуля. Часть 3: Создание писем и работа с черновиками

В первых двух частях мы построили фундамент почтового клиента Ext Mail: создали Universal-приложение, описали модели и хранилища, построили Grid для списка писем, Reader для просмотра, дерево меток и динамический тулбар. Во третьей части мы научим приложение создавать новые письма:

  • добавим кнопку Compose над деревом меток;

  • создадим форму для написания письма с полями получателя, темы и текста;

  • реализуем автодополнение получателя из списка контактов;

  • покажем форму в отдельном окне;

  • научимся отправлять и откладывать черновики;

  • добавим возможность вернуться к редактированию черновика.

Все примеры кода взяты из оригинальной книги Stuart Ashworth (Sencha MVP) и адаптированы для русскоязычного читателя.

✏️ Кнопка Compose

Начнём с добавления кнопки Compose в верхнюю часть дерева меток. Она будет закреплена как dockedItem и всегда видна пользователю.

В компоненте LabelsTree в методе initComponent добавляем dockedItems:

javascript
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:

javascript
onComposeClick: function() {
    this.fireEvent('compose');
}

Мы не создаём форму прямо здесь — вместо этого сообщаем родительскому компоненту, что пользователь хочет написать письмо. Это позволяет держать логику разделённой.

📝 Форма для написания письма (ComposeForm)

Создаём новый компонент ExtMail.view.compose.ComposeForm, расширяющий Ext.form.Panel:

javascript
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:

javascript
items: [
    {
        xtype: 'textfield',
        emptyText: 'Subject',
        bind: {
            value: '{messageRecord.subject}'
        }
    },
    {
        xtype: 'textareafield',
        flex: 1,
        bind: {
            value: '{messageRecord.message}'
        }
    }
]

Поле subject — обычное текстовое поле. Поле message — текстовая область с flex: 1, чтобы занимать всё свободное пространство. Оба поля привязаны к соответствующим свойствам записи messageRecord.

Внизу формы добавляем тулбар с кнопками Send и Discard:

javascript
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 — это автоматически блокирует её, если какое-либо поле формы невалидно. Отличный способ предотвратить отправку некорректных данных.

Обработчики генерируют пользовательские события:

javascript
onSendClick: function() {
    this.fireEvent('send', this.getViewModel().get('messageRecord'));
},

onDiscardClick: function() {
    this.fireEvent('discarddraft', this.getViewModel().get('messageRecord'));
}

👤 Поле получателя с автодополнением

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

Модель Contact (app/model/Contact.js)

javascript
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)

javascript
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, чтобы оно загружалось при старте приложения:

javascript
stores: {
    contacts: {
        type: 'Contacts'
    }
}

Поле Recipient в форме

javascript
{
    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, которое нужно создать во встроенной вьюмодели формы:

javascript
viewModel: {
    data: {
        selectedRecipient: null
    }
}

Зачем нам отслеживать selection, если значение уже привязано? Затем, что при выборе контакта мы хотим сохранить в запись письма не только email, но и имя с фамилией. Добавляем привязку в конструкторе формы:

javascript
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:

javascript
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:

javascript
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:

javascript
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:

javascript
{
    xtype: 'labels-LabelsTree',
    region: 'west',
    width: 300,
    bind: {
        store: '{labels}',
        selection: '{selectedLabel}'
    },
    listeners: {
        compose: 'onComposeMessage'
    }
}

📤 Отправка и удаление черновика

Реализуем обработчики событий окна.

Отправка письма (onSendMessage):

javascript
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):

javascript
onDiscardDraftMessage: function(messageRecord, e, composeWindow) {
    this.getViewModel().getStore('messages').remove(messageRecord);
    composeWindow.close();
}

Просто удаляем запись из хранилища и закрываем окно.

✏️ Редактирование черновика

После сохранения черновика он появляется в папке Drafts. Но при клике на него открывается обычный экран чтения письма — а нам хотелось бы снова открыть форму редактирования.

Исправим это в методе onMessageClick в MainControllerBase:

javascript
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 и создадим мобильную версию приложения с оптимизированным интерфейсом для сенсорных устройств. Следите за обновлениями!

Оставьте комментарий