Jorge is the author of three software development books: "Building a Sencha Touch Application", "How to Build a jQuery Mobile Application", and the "Ext JS 3.0 Cookbook". He runs a software development and developer education shop that focuses on mobile, web and desktop technologies. Jorge is a DZone MVB and is not an employee of DZone and has posted 50 posts at DZone. You can read more from them at their website. View Full User Profile

How to Create a Sencha Touch 2 App: Part 3

04.11.2012
| 9519 views |
  • submit to reddit

In this third part of this tutorial on how to create a Sencha Touch application, we are going to create the Note Editor View. This is the View that will allow our users to create, edit and delete notes.

When we finish this article, our application will have the ability to create notes, and edit existing notes. Let’s get started building the Note Editor View.

Creating a Form Panel in Sencha Touch

We will place the View’s source code in the  NoteEditor.js file, which we will create in the view directory:

In the file, we will define an empty NoteEditor Class like so:

Ext.define("NotesApp.view.NoteEditor", {
    extend: "Ext.form.Panel",
    requires: "Ext.form.FieldSet",
    alias: "widget.noteeditor",
    config:{
        scrollable:'vertical'
    },
    initialize: function () {

        this.callParent(arguments);

    }
});

The Note Editor is an extension of the Ext.form.Panel Class. As we will use a FieldSet Class instance in the View, we are asking the loader to download its source by using the requires config. We are also using the scrollable config to allow the contents of the Panel to scroll vertically. This stops the form from being cropped when its height is larger than the screen height of the device.

As we did with the Notes ListContainer Class, we are going to use the initialize() function to define the Note Editor’s components:

Ext.define("NotesApp.view.NoteEditor", {
    extend: "Ext.form.Panel",
    requires: "Ext.form.FieldSet",
    alias: "widget.noteeditor",
    config:{
        scrollable:'vertical'
    },
    initialize: function () {

        this.callParent(arguments);

        var backButton = {
            xtype: "button",
            ui: "back",
            text: "Home"
        };

        var saveButton = {
            xtype: "button",
            ui: "action",
            text: "Save"
        };

        var topToolbar = {
            xtype: "toolbar",
            docked: "top",
            title: "Edit Note",
            items: [
                backButton,
                { xtype: "spacer" },
                saveButton
            ]
        };

        var deleteButton = {
            xtype: "button",
            iconCls: "trash",
            iconMask: true,
            scope: this
        };

        var bottomToolbar = {
            xtype: "toolbar",
            docked: "bottom",
            items: [
                deleteButton
            ]
        };

        var noteTitleEditor = {
            xtype: 'textfield',
            name: 'title',
            label: 'Title',
            required: true
        };

        var noteNarrativeEditor = {
            xtype: 'textareafield',
            name: 'narrative',
            label: 'Narrative'
        };

        this.add([
            topToolbar,
            { xtype: "fieldset",
                items: [noteTitleEditor, noteNarrativeEditor]
            },
            bottomToolbar
        ]);
    }

});

Within initialize(), after invoking callParent(), we proceed to define the top Toolbar, along with its two buttons, the Home Button and the Save Button. We then define the bottom Toolbar and the Delete Button.

The noteTitleEditor and noteNarrativeEditor are the fields we will use to edit the note’s title and narrative. They are instances of the Ext.form.Text and Ext.form.TextArea Classes.

Once all the View’s components are defined, we proceed to add them to the View. This is where we also add an Ext.form.FieldSet instance to enhance the appearance of the form. The title and narrative editors will render within the FieldSet:

this.add([
    topToolbar,
    { xtype: "fieldset",
        items: [noteTitleEditor, noteNarrativeEditor]
    },
    bottomToolbar
]);

Rendering a View In Sencha Touch

Before we start developing the features of the Note Editor, we are going to work on the code that will render this View when the New Button in the Notes List Container is tapped.

In the previous chapter of this tutorial we created the tap handler for the New Button, which fires the newNoteCommand event. We also added the onNewNoteCommand() listener to the Notes Controller. We will use this function to activate the Note Editor View.

To activate the Note Editor View in the Controller, we first need to acquire a reference to the View. We will use the noteeditor ref for this purpose:

Ext.define("NotesApp.controller.Notes", {

    extend: "Ext.app.Controller",
    config: {
        refs: {
            // We're going to lookup our views by xtype.
            notesListContainer: "noteslistcontainer",
            noteEditor: "noteeditor"
        },

   // Remainder of the controller’s code omitted for brevity.

});

Remember, this ref automatically creates a getNoteEditor() function in the controller, which we can use to refer to the NoteEditor instance and make it the active View in the application.

Next, we need to modify the onNewNoteCommand() function:

onNewNoteCommand: function () {

    console.log("onNewNoteCommand");

    var now = new Date();
    var noteId = (now.getTime()).toString() + (this.getRandomInt(0, 100)).toString();

    var newNote = Ext.create("NotesApp.model.Note", {
        id: noteId,
        dateCreated: now,
        title: "",
        narrative: ""
    });

    this.activateNoteEditor(newNote);
}

Here things get more interesting. We are creating a new note, and passing it to the activateNoteEditor() function.

We are going to use the getRamdomInt() helper function to generate the unique id for a note:

getRandomInt: function (min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

The activateNoteEditor() function will load the new note into the Note Editor, and make the Editor active:

activateNoteEditor: function (record) {

    var noteEditor = this.getNoteEditor();
    noteEditor.setRecord(record); // load() is deprecated.
    Ext.Viewport.animateActiveItem(noteEditor, this.slideLeftTransition);
}

Here we are taking advantage of the Ext.form.Panel’s setRecord() function, which allows us to load the values of a model instance into the form’s fields whose names match those of the model’s fields.

This function also uses the slideLeftTransition variable, which we need to define like so:

slideLeftTransition: { type: 'slide', direction: 'left' }

 

The transition will bring the Note Editor into view with a slide motion from the right to the left.

This is all the Controller needs to do in order to activate the Note Editor View when the New Button is tapped. However, we need to make the application aware of the NoteEditor Class.

Making The Application Aware Of A View

In the app.js file, we will add the new View to the views config:

views: ["NotesList", "NotesListContainer", "NoteEditor"]

We are also going to instantiate the View in the Application’s launch function:

Ext.application({
    name: "NotesApp",

    models: ["Note"],
    stores: ["Notes"],
    controllers: ["Notes"],
    views: ["NotesList", "NotesListContainer", "NoteEditor"],

    launch: function () {

        var notesListContainer = {
            xtype: "noteslistcontainer"
        };
        var noteEditor = {
            xtype: "noteeditor"
        };

        Ext.Viewport.add([notesListContainer,noteEditor]);
    }
});

Ready to check it out? Well, start the emulator. :-)

A tap on the New Button should render the Note Editor View:

Editing A Record Rendered In A Sencha Touch List

We also want to activate the Note Editor View when a note’s disclosure button is tapped:

In order to accomplish this, we need to revisit the onEditNoteCommand() function in the Controller, and add the code that will activate the NoteEditor:

onEditNoteCommand: function (list, record) {

    console.log("onEditNoteCommand");

    this.activateNoteEditor(record);
}

Incredibly simple thanks to the fact that the disclose event supplies the selected Note model instance to the handler through the record argument. All we need to do here is call activateNoteEditor(), which we created a few minutes ago.

Back in the emulator, we should be able to see the Note Editor render the selected note:

Now it is time to save the note. We need to take care of a few things in order for this to happen.

Using A LocalStorage Proxy In Sencha Touch

First, we need to stop using hard-coded notes as the data for the Notes store.

Up to this point, we have been using the data config to define a few hard-coded records in the store:

Ext.define("NotesApp.store.Notes", {
    extend: "Ext.data.Store",
    config: {
        model: "NotesApp.model.Note",
        data: [
            { title: "Note 1", narrative: "narrative 1" },
            { title: "Note 2", narrative: "narrative 2" },
            { title: "Note 3", narrative: "narrative 3" },
            { title: "Note 4", narrative: "narrative 4" },
            { title: "Note 5", narrative: "narrative 5" },
            { title: "Note 6", narrative: "narrative 6" }
        ],
        sorters: [{ property: 'dateCreated', direction: 'DESC'}]
    }
});

As we intend to cache notes on the device that runs the app, we are going to discontinue the data config in the store, and define a LocalStorage proxy as follows:

Ext.define("NotesApp.store.Notes", {
    extend: "Ext.data.Store",
    requires:"Ext.data.proxy.LocalStorage",
    config: {
        model: "NotesApp.model.Note",
        proxy: {
            type: 'localstorage',
            id: 'notes-app-store'
        },
        sorters: [{ property: 'dateCreated', direction: 'DESC'}]
    }
});

LocalStorageProxy uses the HTML5 localStorage API to save Model data on the client browser. This proxy is ideal for storing multiple records of similar data. And it requires that we provide the id config, which is the key that will identify our data in the localStorage object.

Now the Notes store has the ability to save and read data from localStorage, and we can go back to the Views and Controller to take care of the functions that will save the data.

Adding Event Listeners To A Sencha Touch Controller

In the NoteEditor Class, let’s modify the saveButton declaration in the initialize() function like so:

var saveButton = {
    xtype: "button",
    ui: "action",
    text: "Save",
    handler: this.onSaveButtonTap,
    scope: this
};

The handler config defines the handler function that will run when a user taps the button. To make sure we run the handler in the scope of the NoteEditor, we set the scope config to this.

Now we can define onSaveButtonTap() as follows:

onSaveButtonTap: function () {
    console.log("saveNoteCommand");
    this.fireEvent("saveNoteCommand", this);
}

No surprises here, right? As we’ve done with other handlers, we are capturing the Button tap event within the View and defining a View event, saveNoteCommand.

As we already know, the fact that the NoteEditor View broadcasts the saveNoteCommand event is not enough for the Controller to be able to listen to it. We need to tell the Controller where this event is coming from, which we did when we added the noteEditor entry in the refs config of the Controller:

refs: {
    // We're going to lookup our views by xtype.
    notesListContainer: "noteslistcontainer",
    noteEditor: "noteeditor"
}

We will use the Controller’s control config to map the saveNoteCommand event to a handler function. Therefore, need an entry for the Note Editor in the control config:

control: {
    notesListContainer: {
        // The commands fired by the notes list container.
        newNoteCommand: "onNewNoteCommand",
        editNoteCommand: "onEditNoteCommand"
    },
    noteEditor: {
        // The commands fired by the note editor.
        saveNoteCommand: "onSaveNoteCommand"
    }
}

Finally, we will define the onSaveNoteCommand() function like so:

onSaveNoteCommand: function () {

    console.log("onSaveNoteCommand");

    var noteEditor = this.getNoteEditor();

    var currentNote = noteEditor.getRecord();
    var newValues = noteEditor.getValues();

    // Update the current note's fields with form values.
    currentNote.set("title", newValues.title);
    currentNote.set("narrative", newValues.narrative);

    var errors = currentNote.validate();

    if (!errors.isValid()) {
        Ext.Msg.alert('Wait!', errors.getByField("title")[0].getMessage(), Ext.emptyFn);
        currentNote.reject();
        return;
    }

    var notesStore = Ext.getStore("Notes");

    if (null == notesStore.findRecord('id', currentNote.data.id)) {
        notesStore.add(currentNote);
    }

    notesStore.sync();

    notesStore.sort([{ property: 'dateCreated', direction: 'DESC'}]);

    this.activateNotesList();
}

We begin onSaveNoteCommand() acquiring references to the note being edited and the values in the form’s fields:

var noteEditor = this.getNoteEditor();
var currentNote = noteEditor.getRecord();
var newValues = noteEditor.getValues();

Then, we transfer the new values to the loaded note:

currentNote.set("title", newValues.title);
currentNote.set("narrative", newValues.narrative);

Model Validation In Sencha Touch

Next comes an important part, which is validation. To validate the new values we loaded into the model instance, we first call the model’s validate() function, and then call the isValid() function on the errors object returned by validate():

var errors = currentNote.validate();

if (!errors.isValid()) {
    Ext.Msg.alert('Wait!', errors.getByField("title")[0].getMessage(), Ext.emptyFn);
    currentNote.reject();
    return;
}

The Ext.data.Model’s validate() function iterates over the validations defined for the model, and returns an Ext.data.Errors instance containing Ext.data.Error instances for each model field that is invalid.

In our case, the only field with an attached validator is the note’s title. When we find that the model is invalid, we first display an alert using the validator’s configured message, and then call the model’s reject() function. This function reverts the modified fields back to their original values before we exit the onSaveNoteCommand() handler.

Saving Data Using LocalStorageProxy

In onSaveNoteCommand(), after confirming that the modified note is valid, we move on to save it on the device:

var notesStore = Ext.getStore("Notes");

if (null == notesStore.findRecord('id', currentNote.data.id)) {
    notesStore.add(currentNote);
}

notesStore.sync();

As this routine works for new or edited notes, we need to find out if the note is new by searching the store using its findRecrod() function. If the note is new, we add it to the store.

The store’s sync() function asks the store’s proxy to process all the changes, effectively saving new or edited notes, and removing deleting notes from localStorage.

After the store’s records have been updated, we sort them by date:

notesStore.sort([{ property: 'dateCreated', direction: 'DESC'}]);

Returning To The Main View

Our last step in onSaveNoteCommand() consists of invoking the activateNotesList() function. This is a helper function, similar to activateNoteEditor(), that will make the app’s main View active:

activateNotesList: function () {
    Ext.Viewport.animateActiveItem(this.getNotesListContainer(), this.slideRightTransition);
}

In this case we are using a right-slide transition, which we will define like so:

slideRightTransition: { type: 'slide', direction: 'right' }

OK. What do you think about doing another quick check? This time we should be able to save a note:

Summary

In this article we added a couple of features to the Notes App: the ability to create notes, and edit existing notes.

We started by learning how to use Sencha Touch’s Form components to edit data in an application, and how to use a LocalStorageProxy instance to cache data on the device running the application. We also learned how to validate a Sencha Touch data Model, synchronize a Data Store with its Proxy, and revert Model changes when its data is invalid.

In addition, we learned how to create transitions and connect different Views in a multi-view application.

At this point the application is missing the ability to delete notes. We will add this feature in the next part of this tutorial, where we will also modify the Notes List so it renders the cached notes grouped by date.

Stay tuned!

Downloads

Download the source code for this article: NotesApp-ST2-Part-3.zip

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Published at DZone with permission of Jorge Ramon, author and DZone MVB. (source)

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)