Building An Email Client From Scratch Part3
Building An Email Client From Scratch Part3
Stuart Ashworth
Sencha MVP
Building an Email Client from Scratch - Part 3
This e-book series will take you through the process of building an email client from scratch,
starting with the basics and gradually building up to more advanced features.
By the end of all the 7 series, you will have a fully functional email client that is ready to be
deployed in production and used in your daily life. So, get ready to embark on an exciting
journey into the world of email client development, and buckle up for an immersive learning
experience!
2
Building an Email Client from Scratch - Part 3
1
Start with Part-1 and work your way through each subsequent series in order. Each
series builds upon the previous one and assumes that you have completed the previous
part.
2
As you read each series, follow along with the code examples in your own development
environment. This will help you to better understand the concepts and see how they
work in practice.
3
Take breaks and practice what you have learned before moving on to the next series.
This will help to reinforce your understanding of the concepts and ensure that you are
ready to move on to the next step.
4 Don’t be afraid to experiment and customize the code to meet your own needs. This will
help you to better understand the concepts and make the email client your own.
5
If you encounter any issues or have any questions, don’t hesitate to reach out to the
community or the authors of the articles. They will be happy to help you and provide
guidance along the way.
6 Once you have completed all the series, take some time to review the entire email client
application and make any necessary adjustments to fit your specific needs.
7
Finally, enjoy the satisfaction of having built your own fully functional email client from
scratch using Ext JS!
3
Building an Email Client from Scratch - Part 3
Table of Contents
Introduction 6
Compose Button 7
Compose Form 10
Recipient Field 14
Compose Window 20
Editing a Draft 27
Summary 28
4
Building an Email Client from Scratch - Part 3
Executive Summary
In the previous sections, we set up our data models for our Messages grid and Labels tree
and also created the UI for users to view Messages in each label and to view a Message in full.
We also created a number of action buttons in a dynamic toolbar for refreshing the grid and
deleting and archiving a single message.
In this article we will cover the Compose workflow, allowing a user to create a new message,
select a recipient from a data-backed combo box and ‘send’ or discard the message.
We will also let users re-open a draft message to continue editing their email.
Introduction
We assume you have followed the first & second parts of this series where we set up the
foundations of our application and built a dynamic toolbar and unread message count display
for label-based message filtering, if you haven’t, we suggest going back to that article to get a
grasp of the application’s code before jumping in to add these new features.
Compose Button
At the moment we can’t create a new message so we will create a compose button that will sit
above the Labels tree as a docked item.
To add the button we add the `dockedItems` config to the `LabelsTree` component’s
`initComponent` method.
> Docked items can be added to any sub-component of an Ext.Panel and they can be docked to
the `top`, `bottom`, `left` or `right` of the panel with the main `items` being shifted across
to accommodate.
In this case, we want to add a `toolbar` with 3 children, 2 special spacer components to center
the button, and the button itself.
7
Building an Email Client from Scratch - Part 3
initComponent: function() {
Ext.apply(this, {
rootVisible: false,
dockedItems: [
{
xtype: ‘toolbar’,
dock: ‘top’,
weight: -1,
items: [
‘->’,
{
xtype: ‘button’,
scale: ‘large’,
text: ‘Compose’,
iconCls: ‘x-fa fa-edit’,
width: 150,
handler: function () {
this.fireEvent(‘compose’);
},
scope: this
},
‘->’
]
}
],
...
});
...
}
8
Building an Email Client from Scratch - Part 3
We use the `dock` config to locate the toolbar at the top of the LabelsTree.
Our Compose button is a simple `button` configuration. The `scale` is set to “large” so it is as
big as possible and we use the `x-fa` CSS class to make use of the built-in Font Awesome icon
set.
The `handler` function will be executed when the button is clicked and will run the scope of
the LabelsTree itself (thanks to the `scope: this` config).
We don’t actually create the compose form here, instead, we fire a custom event named
`compose` and let the parent component handle it.
Compose Form
To compose an email we want a form with 3 fields - a recipient, a subject and a message field.
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: []
});
We set the `defaultListenerScope` to `true` so any event listener function names are resolved
within this component. We add some padding and make use of a VBox layout so that the
message field will always be stretched to fill the form’s remaining space.
Next, we add the Subject and Message fields (we will talk about the Recipient field shortly). The
former will just be a simple `Ext.form.field.Text` component, using the `textfield` xtype. We
bind it to the `subject` field of the `messageRecord` object (which we will add later) so the
value in the field will be stored in this field.
The Message field is a TextArea component and is given a `flex: 1` config so it fills the form’s
height. We bind its value to the `message` property of the `messageRecord`.
10
Building an Email Client from Scratch - Part 3
...
items: [
{
xtype: ‘textfield’,
emptyText: ‘Subject’,
bind: {
value: ‘{messageRecord.subject}’,
},
},
{
xtype: ‘textareafield’,
flex: 1,
bind: {
value: ‘{messageRecord.message}’,
},
}
]
...
11
Building an Email Client from Scratch - Part 3
Finally, we add the Send and Discard buttons to a toolbar docked to the bottom of the form.
...
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’
}
]
}
]
...
The Send button has `formBind: true` set which will automatically disable the button if any of
the form’s fields are invalid. This is really useful for preventing invalid form submissions.
The `handler` for each button is set to a string which will be resolved to a method of that
name on the component itself.
We add them now and simply fire a custom event for each action, passing the
`messageRecord` currently active in the form as its single parameter.
12
Building an Email Client from Scratch - Part 3
...
onSendClick: function () {
this.fireEvent(‘send’, this.getViewModel().get(‘messageRecord’));
},
onDiscardClick: function () {
this.fireEvent(‘discarddraft’,
this.getViewModel().get(‘messageRecord’));
}
...
Recipient Field
Our Compose form is now ready for the Recipient field to be added. For this field, we want
to use a combo box that will autocomplete the recipient from a list of Contacts stored in the
application.
Ext.define(‘ExtMail.model.Contact’, {
extend: ‘Ext.data.Model’,
fields: [
{
name: ‘name’,
calculate: function (data) {
var firstName = data.firstName || ‘’;
var lastName = data.lastName || ‘’;
14
Building an Email Client from Scratch - Part 3
Now we can add a Contacts store to load our contact data into and have it available for our
Recipients field.
The Contacts store uses the new Contact model and a simple AJAX proxy to load our static
`contacts.json` data. It has `autoLoad: true` set so when it is instantiated the contacts data is
loaded.
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’
}
}
});
Finally we add this store to our MainViewModel class so it is created and loaded when the
application starts.
...
stores: {
...
contacts: {
type: ‘Contacts’
}
}
...
15
Building an Email Client from Scratch - Part 3
Recipients Field
Now we have our data store ready we can add the Recipient combo box and bind it to the
store.
...
{
xtype: ‘combobox’,
emptyText: ‘Recipient’,
width: ‘100%’,
displayField: ‘email’,
valueField: ‘email’,
queryMode: ‘local’,
allowBlank: false,
bind: {
store: ‘{contacts}’,
selection: ‘{selectedRecipient}’,
value: ‘{messageRecord.email}’
}
}
...
We set it up so the `displayField` (the Contact model field that is shown to the user) is the
same as the `valueField` (the field that is submitted with the form) to the `email` field.
We also use `queryMode: local` so when a user types in the field it will just query the local
data we already have, rather than sending a query to the server requesting new data that
matches the value entered. If you were dealing with large datasets using `queryMode: remote`
might be favourable to avoid loading all the data upfront.
We bind the `store` config to the `contacts` store we added to the MainViewModel class and
the `value` to the `email` field of the `messageRecord`.
We have also bound the `selection` property, which stores the record that is currently selected
in the combobox, to the `selectedRecipient` View Model property, which doesn’t yet exist. So
we will add it now.
Instead of creating a standalone View Model for this form, we will just create an inline one, as
we only need one data field.
...
viewModel: {
data: {
selectedRecipient: null
}
}
...
17
Building an Email Client from Scratch - Part 3
The reason we want to keep track of the `selection` value, as well as the `value` (the email),
is that we want to add the first and last name fields to the underlying Message record so we
can display them in other parts of the app. We have to extract the parts of the Contact model
that we want manually, so we add a binding to the `selectedRecipient` data field and do this
ourselves.
...
constructor: function() {
this.callParent(arguments);
this.getViewModel().bind(‘{selectedRecipient}’,
this.onSelectedRecipientChange, this);
}
...
18
Building an Email Client from Scratch - Part 3
Then we can define the handler function to add the first name, last name, and email to the
`messageRecord`.
...
onSelectedRecipientChange: function(selectedRecipientRecord) {
var firstName, lastName, email;
Compose Window
With the Compose Form complete we need to be able to show it to the user. We will display
this in a Window and will create our own sub-class so we can add some additional functionality.
This will be a simple window with a single item - the ComposeForm - and it will have a basic
inline View Model that holds the `messageRecord` (which we referenced multiple times in the
ComposeForm). We will also add a `messageRecord` config that we will use to pipe a bound
value into the view model.
Ext.define(‘ExtMail.view.compose.ComposeWindow’, {
extend: ‘Ext.window.Window’,
alias: ‘widget.compose-ComposeWindow’,
viewModel: {
data: {
messageRecord: null,
},
},
config: {
messageRecord: null,
},
minimizable: true,
resizable: false,
draggable: false,
title: ‘New Message’,
layout: ‘fit’,
constrain: true,
constrainHeader: true,
items: [
{
xtype: ‘compose-ComposeForm’,
bubbleEvents: [‘send’, ‘discarddraft’],
},
],
20
Building an Email Client from Scratch - Part 3
When configuring the ComposeForm we use the `bubbleEvents` config to tell it that we want
the `send` and `discarddraft` events to be bubble up and emitted from the ComposeWindow
too, this means we can listen for them on the ComposeWindow rather than having to delve
inside and attach handlers to the form itself.
We also add an `updater` for the `messageRecord` config which simply sets the View Model’s
property with the passed value, keeping these in sync.
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);
}
First, we create a new Message instance, marking it as `outgoing` and `draft`, along with
giving it the `DRAFTS` label.
We then add it to the Messages store and then pass it to the `showComposeWindow`
function.
> We immediately call the `commitChanges` method of the Messages store because we don’t
have a proper backend so we want the record to appear as ‘saved’ straight away.
22
Building an Email Client from Scratch - Part 3
win.show();
}
This method simply creates a new ComposeWindow, passing in the newly created Message
record, and shows it to the user.
Finally we need to hook the `onComposeMessage` method to the `compose` event that the
new Compose button we added at the start fires. We do this in the `LabelsTree` configuration
in the `Main` component.
{
xtype: ‘labels-LabelsTree’,
region: ‘west’,
width: 300,
bind: {
store: ‘{labels}’,
selection: ‘{selectedLabel}’
},
listeners: {
compose: ‘onCompose’
}
}
23
Building an Email Client from Scratch - Part 3
Clicking the Compose button will now open our new form and let us compose a message.
win.on({
send: Ext.bind(this.onSendMessage, this, [win], true),
discarddraft: Ext.bind(this.onDiscardDraftMessage, this, [win],
true),
scope: this
});
win.show();
}
We want to be able to close the Compose window after these operations are complete so we
use the `Ext.bind` function to wrap our handler functions and tack on the `win` variable as an
extra parameter.
The `onSendMessage` method will remove the `DRAFTS` label from the Message and add the
`SENT` label. It will then mark `draft` as false, `sent` as true, and stamp it with a `date`. We
will then commit the changes, and close the window.
25
Building an Email Client from Scratch - Part 3
messageRecord.set({
draft: false,
sent: true,
date: new Date()
});
messageRecord.commit();
composeWindow.close();
}
> Note the third parameter is the `composeWindow` reference that we added using the `Ext.
bind` method when we hooked up the event handler.
The `onDiscardDraftMessage` handler is very simple and removes the record from the
`Messages` store and then closes the window.
composeWindow.close();
}
Editing a Draft
After creating a new message and closing the window we will see it appear in the `Drafts`
folder. Clicking on this message will move us to the default MessageReader screen. Ideally, we
want this to reopen the Compose Window so the user can continue to write their email. We will
do this now.
if (e.getTarget(‘.x-action-col-icon’)) {
return;
}
if (messageRecord.get(‘draft’)) {
this.showComposeWindow(messageRecord);
} else {
this.getViewModel().set(‘selectedMessage’, messageRecord);
}
}
SUMMARY
In this article we’ve extended our Email Client to allow users to compose their own
new messages, having them appear in the Drafts folder in our main navigation.
We made use of the common base classes to enable our composing business logic
to be shared across toolkits - something we will be exploring in the next article.
We also created a simple form and added 3 different field types - text field, text
area, and a more complex type-ahead combo box. Each of these was bound to a
backing View Model using the `bind` configuration.
As well as the Form Panel component, we used the Window component to allow
the compose form to float about the main interface and be positioned correctly. In
the next part, we will be integrating the Modern toolkit into our application to bring
support to mobile and touch devices.
28
Building an Email Client from Scratch - Part 3
29
Try Sencha Ext JS
FREE for 30 DAYS