Sencha Touch and node.js: Part 3 - Fully functional backend with Ext.Direct support

Sencha Touch and node.js: Part 3 - Fully functional backend with Ext.Direct support

In previous parts of this tutorial we set up everything which will be needed to dive into developing on frontend side. Now it’s a time to look under the curtain. In this post I will answer on question how to build full stack solution to allow Sencha Touch and Node.JS work together.

Before we start, for purpose of this post you will need to set up empty Sencha Touch project and configure Grunt. You can find how to do it in my earlier article.

Here you are able to find final result of our work in this post.

1. Change project architecture

Firstly, we need to make few changes in application architecture. Create five folders in root path of your project, they will be called application, application-test, direct, uploads and public. In the /application directory we will also need model and in the /application-test test folder.
Next move \.sencha, \app, \app-test, \build, \packages, \resources, \touch, app.js, app.json, app-test.js, bootstrap.js, bootstrap.json, build.xml, index.html and packager.json to \public directory.

2. Install and setup dependencies

Now, we need to deal with new dependencies. We will install express, framework for building app with node, nconf, which will help us read configurations file, moongose, for communicate with database, extdirect for Ext.Direct support on the backend, nodeunit for server side unit test and a lot of other stuff.
Run the following commend to install listed below packages globally:

npm install node-inspector nodemon watch -g

Nodemon and node-inspector are amazing tools. First one helps us to keep our app up to date by restarting it after every change in watched files. Second tool gives us possibility to debug our app from browser via blink inspector. We even have live editing!
OK, now we need to install rest of the libraries locally in our project.

npm install express nconf mongoose extdirect nodeunit nodeunit-client grunt-contrib-nodeunit grunt-node-inspector grunt-contrib-jshint grunt-nodemon grunt-concurrent grunt-contrib-watch async --save-dev

When all necessary packages are installed it’s time to make some configuration. We will start from setup Grunt and then we will build our server.
Open Gruntfile.js and modify your script:

.........................
    grunt.initConfig({
        /**
         * Validate the source code files to ensure they
         * follow our coding convention and
         * dont contain any common errors.
         */
        jshint: {
            all: [
                 // add public directory
                 // on the beginning of three paths below
                "Gruntfile.js",
                "public/app.js",
                "public/app/**/*.js",
                "public/app-test/specs/*.js",
                "!touch/**/*.js",
                //add new created directory
                "application/**/*.js",
                "application-test/**/*.js",
                "direct/*.js"
            ],
            options: {
                trailing: true,
                eqeqeq: true
            }
        },
        /**
         * Setup Jasmine and run them using PhantomJS.
         */
        sencha_touch_jasmine: {
            options: {
                // also add public/ below
                specs: ["public/app-test/specs/**/*.js"],
                extFramework: "public/touch",
                extLoaderPaths   : {
                    "TutsApp" : "public/app"
                },
                extAppName: 'TutsApp'
            },
            app: {
                extLoaderPaths   : {
                    "TutsApp" : "public/app"
                }
            }
        },
        // point nodeunit where test for backend stuff is stored
        nodeunit: {
            all: ['application-test/test/**/*.js']
        },
        // configure watch to relaunch tests after every change
        watch: {
            scripts: {
                files: [
                    'application-test/**/*.js',
                    'application/**/*.js',
                    'direct/*.js'
                ],
                tasks: ['jshint', 'nodeunit'],
                options: {
                    spawn: true
                }
            }
        },
        // configure nodemon 
        nodemon: {
            dev: {
                options: {
                    file: 'app.js',
                    args: ['production'],
                    // tell nodemon to run app in debug mode
                    nodeArgs: ['--debug'],
                    ignoredFiles: ['README.md', 'node_modules/**', '/public'],
                    watchedExtensions: ['js'],
                    watchedFolders: [
                        'application-test',
                        'application',
                        'application/helper',
                        'application/model',
                        'direct'
                    ],
                    delayTime: 1,
                    legacyWatch: true,
                    env: {
                        PORT: '8181'
                    },
                    cwd: __dirname
                }
            }
        },
        // configure node inspector
        'node-inspector': {
            custom: {
                options: {
                    // set up on which port it will be available
                    'web-port': 8182,
                    // and on which host
                    'web-host': 'localhost',
                    'debug-port': 5858,
                    'save-live-edit': true
                }
            }
        },
        // set up concurrent library which allow to run multiple
        // tasks in same time
        concurrent: {
            dev: {
                tasks: ['node-inspector', 'nodemon'],
                options: {
                    logConcurrentOutput: true
                }
            }
        }
    });

    grunt.loadNpmTasks("grunt-contrib-jshint");
    grunt.loadNpmTasks("grunt-sencha-jasmine");
    // load new tasks
    grunt.loadNpmTasks("grunt-contrib-nodeunit");
    grunt.loadNpmTasks("grunt-contrib-watch");
    grunt.loadNpmTasks('grunt-nodemon');
    grunt.loadNpmTasks('grunt-node-inspector');
    grunt.loadNpmTasks('grunt-concurrent');

    grunt.registerTask("default", [
        "jshint"
    ]);
    grunt.registerTask("test-frontend", ["sencha_touch_jasmine:app"]);
    // and register them 
    grunt.registerTask("test-backend", ["watch"]);
    grunt.registerTask("server", ["concurrent"]);

This should adjust Grunt configuration to current files structure.

3. Build server

OK, now server, create file app.js in root directory of your project and add to it all of those fragments:

/**
 * Module dependencies.
 */
var express = require('express'),
    nconf = require('nconf'),
    http = require('http'),
    path = require('path'),
    gzip = require('connect-gzip'),
    extdirect = require('extdirect'),
    mongoose = require('mongoose'),
    io = require('socket.io'),
    app,
    server;

nconf.env().file({ file: 'config.json'});

var ServerConfig = nconf.get("ServerConfig"),
    MongooseConfig = nconf.get("MongooseConfig"),
    ExtDirectConfig = nconf.get("ExtDirectConfig");

Small explanation. We load all of packages (line 4-11) and configuration file (line 15) which will be used in our app. Next, we split our options to separate variables (in lines 17-19). File config.json of course not exist at the moment, but will be created after we finish with current script.

app = express(gzip.gzip());

if (ServerConfig.enableSessions) {
    var store = new express.session.MemoryStore;
}

mongoose.connect(MongooseConfig.dbPath + MongooseConfig.db, function onMongooseError(err) {
    if (err) throw err;
});

global['db'] = mongoose;

Here, in first line, we instantiate express application. After that (in line 4) we created store for our session and in the end we make connection to db via mongoose driver (line 7). It’s important to notice that we saved our connector as global variable, thanks to that it will be available in whole application.

app.configure(function () {
    app.set('port', process.env.PORT || ServerConfig.port);
    app.use(express.logger(ServerConfig.logger));

    if (ServerConfig.enableUpload) {
        app.use(express.bodyParser({uploadDir: './application/uploads'})); //take care of body parsing/multipart/files
    }

    app.use(express.methodOverride());

    if (ServerConfig.enableCompression) {
        app.use(express.compress()); //Performance - we tell express to use Gzip compression
    }

    if (ServerConfig.enableSessions) {
        //Required for session
        app.use(express.cookieParser());
        app.use(express.session({ secret: ServerConfig.sessionSecret, store: store }));
    }

    app.use(express.static(path.join(__dirname, ServerConfig.webRoot)));
});

Next step in the process is to configure few options directly connected with application, port, logs level, upload directory, compression, session and server root directory.

//Important to get CORS headers and cross domain functionality
if (ServerConfig.enableCORS) {
    app.all('*', function (req, res, next) {
        res.header("Access-Control-Allow-Origin", "*");
        res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
        next();
    });

    // Restrict connection methods
    app.options(ExtDirectConfig.classPath, function (request, response) {
        response.writeHead(200, {'Allow': ServerConfig.allowedMethods});
        response.end();
    });
}

//GET Method returns API
app.get(ExtDirectConfig.apiPath, function (request, response) {
    try {
        var api = extdirect.getAPI(ExtDirectConfig);
        response.writeHead(200, {'Content-Type': 'application/json'});
        response.end(api);
    } catch (e) {
        console.log(e);
    }
});

// Ignoring any GET requests on class path
app.get(ExtDirectConfig.classPath, function (request, response) {
    response.writeHead(200, {'Content-Type': 'application/json'});
    response.end(JSON.stringify({success: false, msg: 'Unsupported method. Use POST instead.'}));
});

// POST Request process route and calls class
app.post(ExtDirectConfig.classPath, function (request, response) {
    extdirect.processRoute(request, response, ExtDirectConfig);
});

app.configure('development', function () {
    app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});

app.configure('production', function () {
    app.use(express.errorHandler());
});

server = http.createServer(app).listen(app.get('port'), function () {
    console.log("Express server listening on port %d in %s mode", app.get('port'), app.settings.env);
});

io.listen(server);

In last fragment of code we setup Ext.Direct library, error handling and in the end we create a server.
Now, we need to prepare our configuration file called config.json (should be place in root directory near app.js).

{
    // Configuration for server
    "ServerConfig": {
        "port": 3000,
        "logger": "dev",
        "enableUpload": true,
        "enableCompression": true,
        "webRoot": "/public",
        "enableSessions": true,
        "sessionSecret": "vdW3F6y3506h",
        "enableCORS": true,
        "allowedMethods": "GET,POST,OPTIONS"
    },
    // Db
    "MongooseConfig": {
        "dbPath": "mongodb://localhost/",
        "db" : "devjs"
    },
    // And extdirect
    "ExtDirectConfig": {
        "namespace": "ExtRemote",
        "apiName": "REMOTING_API",
        "apiPath": "/directapi",
        "classPath": "/direct",
        "classPrefix": "DX",
        "server": "localhost",
        "port": "3000",
        "protocol": "http"
    }
}

As you see anything special here, all of this variables should be readable for you, rest of them shouldn’t be changed (especially if we talking about extdirect configuration). I have only one advice for you, if you want to test your direct services from other device than your local machine, you should use your IP address instead of host (property “server” in extdirect config part).

4. Write your first model and direct controller

We will start our coding from write a test suit for model using nodeunit (please read documentation first) very simple and clear library for unit test on server side.
Here is content of \application-test\test\userModel.js with explanations

//As in our application, load modules first
var nconf = require('nconf'),
    mongoose = require('mongoose'),
    User = require('../../application/model/User'),
    conf,
    id;
// load db config
nconf.env().file({ file: 'config.json'});
conf = nconf.get("MongooseConfig");

User.remove({}, function (err) {
});

// call before each test
exports.setUp = function (callback) {
    // connect to a test database
    mongoose.connect(conf.dbPath + conf.db + 'Test');
    callback();
//clear table
};

// call after each test
exports.tearDown = function (callback) {
    // disconnect from db
    mongoose.disconnect();
    callback();
};

exports.create = function (test) {
    console.log('running create user test');
    // tell nodeunit  how many assertion
    // should expect
    test.expect(2);
    var item = new User({
        email: 'sebakpl@test.com',
        firstName: 'First',
        lastName: 'Last',
        password: '2342j323h4'
    });
    //save model
    item.save(function (err, record) {
        if (!err) {
            // check if record was created (first assertion)
            test.ok(record, "Record was not created");
            id = record._id;
        }
        // check if any error occurred (second assertion)
        test.ifError(err);
        // finish test
        test.done();
    });
};

exports.read = function (test) {
    console.log('running read user test');
    test.expect(3);
    User.find({}, function (err, records) {
        if (!err) {
            console.log('results' + records);
            test.ok(records, "Response was empty");
            // check if we have only one record
            test.equal(records.length, 1);
        } else {
            console.log('error' + err);
        }
        test.ifError(err);
        test.done();
    });
};

exports.update = function (test) {
    console.log('running update user test');
    test.expect(3);
    User.update({"_id": id}, {
        firstName: 'First2'
    }, {upsert: false}, function (err, numberAffected, raw) {
        if (!err) {
            test.ok(raw.ok, 'Update failed');
            test.equal(numberAffected, 1, 'Exactly one record should be updated');
        } else {
            console.log('error' + err);
        }
        test.ifError(err);
        test.done();
    });
};

exports.destroy = function (test) {
    test.expect(2);
    console.log('running destroy user test');
    User.remove({"_id": id}, function (err) {
        if (!err) {
            console.log('object destroyed');
            test.ok(true);
        } else {
            test.ok(false, "Document not destroyed");
            console.log('error ' + err);
        }
        test.ifError(err);
        test.done();
    });
};

OK, when we got our first unit test it’s time to run our war machine, in console, from project root directory fire this command:

grunt test-backend

and you should get something similar to this:

2a

Yep, nothing happen. Our test will not run until we make any change in existing files or until we create new one.
Right, so we need to have something to test. In \application\model add file User.js and past this:

var mongoose = require('mongoose'),
    UserSchema = new mongoose.Schema({
        email: {
            type: String,
            unique: true
        },
        password: {
            type: String
        },
        firstName: {
            type: String
        },
        lastName: {
            type: String
        }
    });

module.exports = mongoose.model('User', UserSchema);

Save, and check what happen in console.

2b

Tests were performed automatically.
If you read earlier post about how to connect Ext JS with node and mongoose you should notice that model is nearly the same as in Łukasz article. Only change is that we return it directly and not in the another object.
Let’s try to prepare something similar for the direct controller. It will be a little more complicated than User model, so it will be good to make some assumption.
Direct controller for User model should have methods which allow to:

  • add/get/update/remove one model
  • add/get/update/remove more than one model
  • filter records by different fields
  • filter records by offset and limit
  • check if email is unique

Another important thing, all of our direct methods have the same arguments which are:

  • params: store sent data
  • callback: need to be called when all of our logic is done
  • sessionId: id of session (session need to be enabled)

And in the end will return object with properties like:

  • success: true (required) – if false, exception will be thrown, if true, conditions where meet
  • model name: array (optional) – an array of records
  • total: integer (optional) – total number of current model records in db

It will be a long task, so we do it step by step, firstly writing test method and next proper one in the direct controller.
Start from creating script called userDirect.js in \application-test\test\ and after that past this:

var nconf = require('nconf'),
    mongoose = require('mongoose'),
    User = require('../../application/model/User'),
    DirectUser = require('../../direct/DXUser.js'),
    users,
    user,
    conf,
    id,
    userNumber20,
    n = 0;
// load db config
nconf.env().file({ file: 'config.json'});
conf = nconf.get("MongooseConfig");

// clear documents
User.remove({}, function (err) {
});

// called before each test
exports.setUp = function (callback) {
    console.log('set up');
    mongoose.connect(conf.dbPath + conf.db + 'Test');
    callback();
};

// called after each test
exports.tearDown = function (callback) {
    callback();
    mongoose.disconnect();
    console.log('disconnect');
};

/**
 * Generate unique string
 * @returns {string}
 */
function uniqueString() {
    var text = "",
        possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
        i;
    for (i = 0; i < 5; i++)
        text += possible.charAt(Math.floor(Math.random() * possible.length));
    return text + n++;
}

Configuration for this test is similar to earlier one. Only difference is small helper function which will return random unique string.

a) Create methods

File: \application-test\test\userDirect.js

.......................................
//create
exports.createOne = function (test) {
    console.log('testing createOne');
    test.expect(2);
    DirectUser.create({
        _id : 0,
        email: 'user@test.com',
        firstName: 'First',
        lastName: 'Last',
        password: '2342j323h4'
    }, function (response) {
        test.ok(response.success, "Cant create User");
        test.equal(response.User.length, 1, "User not created");
        user = response.User[0];
        test.done();
    });
};

exports.createMoreThanOne = function (test) {
    console.log('testing createMoreThanOne');
    var params = [],
        len = Math.floor((Math.random() * 98) + 2),
        i;
    for (i = 0; i < len; i++) {
        params.push({
            _id: i+1,
            email: uniqueString() + '@test.com',
            firstName: 'Temporary',
            lastName: 'Person',
            password: '2342j323h4'
        });
    }
    DirectUser.create(
        params,
        function (response) {
            test.ok(response.User.length > 1, "Cant create Users");
            users = response.User;
            test.done();
        });
};

OK, we had cover create method, so now it’s proper time to write it in the direct controller. In \direct directory create file called DXUser (as we set in config.json every of direct controller should have ‘DX’ prefix) and fill it with this code:

    // we load new module called async 
    // which will be helpfull to synchronize
    // asynchronous task
var async = require('async'),
    User = require('../application/model/User'),
    DXUser = {};
/**
 * @param record
 * @param callback
 * Helper for save each record
 * separately
 */
function saveItem(record, callback) {
    var item,
    // For prevent throwing error from mongodb
    // we need to remove id created on frontend
    // from record. But we will use it later so 
    // need to be save to new variable.
        id = record._id;
    delete record._id;
    // create new document
    item = User(record);
    item.save(function (err, _record) {
        if (err) {
            // Assign id to property clientId
            // it will help sencha touch to
            // connect row from server response
            // with model on the frontend
            err.clientId = id;
            // Return result of our function as 
            // second parameter, as first pass 
            // boolean value. False mean that 
            // no error occurred (we cheating 
            // here, but we want finish all of 
            // the tasks
            callback(false, err);
        } else {
            record.clientId = id;
            record._id = _record._id;
            callback(false, record);
        }
    });
}
// api method
DXUser.create = function (params, callback) {
    // params always need to be array
    var _params = params instanceof Array ? params : [params];
    // map function from async library works exactly in the same way
    // like map from the array. It will iterate through params and
    // pass it to the saveItem 
    async.map(_params, saveItem, function (err, results) {
        callback({success: true, User: results});
    });
};
module.exports = DXUser;

Look at the console. The number of finished assertion should increase to 13. Every of them should be passed.

b) Read method

Now time for read method. It will be the most complex function. We will need to cover getting element by id, with filters, offset and limit. Back again to the \application-test\test\userDirect.js

.......................................
//read
exports.readById = function (test) {
    console.log('testing readById');
    test.expect(2);
    DirectUser.read({_id: user._id},
        function (response) {
            test.ok(response.success, "Cant read user with id " + user._id);
            test.equal(response.User.length, 1, "Number of returned models different than expected");
            test.done();
        });
};
//
exports.readAll = function (test) {
    console.log('testing readAll');
    test.expect(3);
    DirectUser.read({},
        function (response) {
            test.ok(response.success, "Cant read users");
            test.ok(users.length + 1 === response.total, "Total number of rows should be equal users in array");
            test.ok(response.User.length > 1, "Number of returned models different than expected");
            test.done();
        });
};
//
exports.readAllWithLimit = function (test) {
    console.log('testing readAllWithLimit');
    test.expect(3);
    DirectUser.read({
            limit: 20
        },
        function (response) {
            test.ok(response.success, "Cant read users");
            test.equal(response.User.length, 20, "Total number of rows should be equal users in array");
            test.ok(response.User.length > 1, "Number of returned models different than expected");
            userNumber20 = response.User[19];
            test.done();
        });
};
//
exports.readAllWithStart = function (test) {
    console.log('testing readAllWithLimit');
    test.expect(3);
    DirectUser.read({
            start: 20,
            limit: 1
        },
        function (response) {
            test.ok(response.success, "Cant read users");
            test.equal(response.User[0].email, userNumber20.email, "Email should be the same");
            test.equal(response.User.length, 1, "Number of returned models different than expected");
            test.done();
        });
};
//
exports.readWithOneFilter = function (test) {
    console.log('testing readWithOneFilter');
    test.expect(2);
    DirectUser.read({
            filter: [
                {
                    property: 'firstName',
                    value: 'First'
                }
            ]
        },
        function (response) {
            test.ok(response.success, "Cant read users");
            console.log('readWithOneFilter: Number of returned models ' + response.User.length);
            test.equal(response.User.length, 1, "Number of returned models different than expected");
            test.done();
        });
};
//
exports.readWithMultipleFilters = function (test) {
    console.log('testing readWithMultipleFilters');
    test.expect(2);
    DirectUser.read({
            filter: [
                {
                    property: 'email',
                    value: 'user@test.com'
                },
                {
                    property: 'lastName',
                    value: 'Last'
                }
            ]
        },
        function (response) {
            test.ok(response.success, "Cant read users");
            console.log('readWithMultipleFilters: Number of returned models ' + response.User.length);
            test.equal(response.User.length, 1, "Number of returned models different than expected");
            test.done();
        });
};

Above code simulate nearly all of the possible situation while reading data from server. Only story, which still need to be cover, is when we have some sorters. But to make this post a little bit shorter I will skip it.
Right now we need to take care of controller, code below should allow us to pass all the tests (remember to past it before line module.exports = DXUser;).
\direct\DXUser.js

.......................................
DXUser.read = function (params, callback) {
    // if we pass an id it's means that we want to
    // get only one record
    if (params._id) {
        User.find({'_id': params._id}, function (err, records) {
            callback({success: true, User: records, total: records.length});
        });
        // in other case
    } else {
        var limit = params.limit,
            skip = params.start ? params.start - 1 : null,
            filters = {};
        // if filters exists
        if (params.filter) {
            // join them in one object
            params.filter.map(function (filter) {
                filters[filter.property] = filter.value;
            });
        }
        // build query
        User.find(filters)
            .limit(limit)
            .skip(skip)
            // and execute them
            .exec(function (err, records) {
                // run query to get count
                User.count().exec(function (err, count) {
                    callback({success: true, User: records, total: count});
                });
            });
    }
};
.......................................

c) Update

Cover update (and destroy too) is the easier part of current task. We only need to make few small changes in saveItem function and rest of code should be similar.
\application-test\test\userDirect.js

.......................................
//update
exports.editOneUser = function (test) {
    console.log('testing editOneUser');
    DirectUser.update({
        id: user._id,
        firstName: 'EditTest'
    }, function (response) {
        test.ok(response.success);
        test.done();
    });
};
//
exports.editMultipleUsers = function (test) {
    console.log('testing editMultipleUsers');
    var modifiedUsers = [];
    for (var i = 0, l = users.length; i < l; i++) {
        modifiedUsers.push({
            _id: users[i]._id,
            firstName: uniqueString()
        });
    }
    DirectUser.update(modifiedUsers, function (response) {
        test.ok(response.success);
        test.done();
    });
};

\direct\DXUser.js

.......................................
/**
 * @param record
 * @param callback
 * Helper for update each record
 * separately
 */
function updateItem(record, callback) {
    var id = record._id;
    delete record._id;
    User.update({"_id": id}, { $set: record}, {upsert: false}, function (err) {
        if (err) {
            err._id = id;
            callback(false, err);
        }
        else {
            record._id = id;
            callback(false, record);
        }
    });
}
DXUser.update = function (params, callback) {
    var _params = params instanceof Array ? params : [params];
    async.map(_params, updateItem, function (err, results) {
        callback({success: true, User: results});
    });
};
.......................................

d) Custom methods

For now, we will have only one custom method, to check if sent email exist in database. Firstly, we will pass email which for sure should be unique and after that we will try with duplicated one.
\application-test\test\userDirect.js

.......................................
//custom
exports.emailShouldBeUnique = function (test) {
    test.expect(1);
    console.log('testing emailShouldBeUnique');
    DirectUser.isEmailUnique(uniqueString() + "@test.com",
        function (response) {
            test.ok(response.success, "Email should be unique but seems to be duplicated");
            test.done();
        });
};
//
exports.emailShouldBeDuplicated = function (test) {
    test.expect(1);
    console.log('testing emailShouldBeDuplicated');
    DirectUser.isEmailUnique("user@test.com",
        function (response) {
            test.ok(!response.success, "Email should be duplicated but seems to be unique");
            test.done();
        });
};

\direct\DXUser.js

.......................................
DXUser.isEmailUnique = function (params, callback) {
    User.where('email', params).count(
        function (err, count) {
            if (err) throw err;
            callback({success: count < 1});
        }
    );
};
.......................................

e) Destroy

OK, last thing and we are home.
\application-test\test\userDirect.js

//destroy
exports.destroyOneUser = function (test) {
    console.log('testing destroyOneUser');
    test.expect(1);
    DirectUser.destroy([
        {
            id: user._id
        }
    ], function (result) {
        test.ok(result.success, "Cant remove record");
        test.done();
    });
};

exports.destroyMultipleUsers = function (test) {
    console.log('testing destroyMultipleUsers');
    test.expect(1);
    DirectUser.destroy(users, function (response) {
        test.ok(response.success, "Cant remove records");
        test.done();
    });
};

\direct\DXUser.js

.......................................
DXUser.destroy = function (params, callback) {
    var _params = params instanceof Array ? params : [params];
    async.map(_params, destroyItem, function (err, results) {
        callback({success: true, User: results});
    });
};
.......................................

Ladies and Gentlemen that’s all. We cover whole direct controller for User. If you check console, you should see 34 assertion passed without any errors.

5. Meet Sencha Touch app with your new RESTful api

Playing with backend it’s a great fun but in the end we have a reason why we prepared our server. So, now it’s a time to test it with our Sencha Touch app. We will start from writing model which should be persistent to the backend one. And after that we will create store for it.
Add new file User.js in \public\app\model and fill it with this code:

// remember to set proper app name
Ext.define('TutsApp.model.User', {
    extend: 'Ext.data.Model',
    config: {
        idProperty: '_id',
        fields: [
            {
                name: 'firstName'
            },
            {
                name: 'lastName'
            },
            {
                name: 'email'
            },
            {
                name: 'password'
            },
            {
                name: '_id'
            },
            {
                name: 'createdAt',
                defaultValue: +new Date(),
                convert: function (value) {
                    return new Date(value);
                }
            }
        ],
        validations: [
            {type: 'presence', field: 'firstName'},
            {type: 'length', field: 'firstName', min: 5},
            {type: 'presence', field: 'lastName'},
            {type: 'presence', field: 'password'},
            {type: 'length', field: 'password', min: 7},
            {type: 'presence', field: 'email'},
            {type: 'format', field: 'email', matcher: /[a-z0-9!#$%&'*+/=?^_{|}~-]+(?:.[a-z0-9!#$%&'*+/=?^_{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/}
        ],
        proxy: {
            type: 'direct',
            api: {
                create: 'ExtRemote.DXUser.create',
                read: 'ExtRemote.DXUser.read',
                update: 'ExtRemote.DXUser.update',
                destroy: 'ExtRemote.DXUser.destroy'
            },
            reader: {
                type: 'json',
                rootProperty: 'User'
            }
        }
    },
    statics: {
        isEmailUnique : function (value, callback) {
            ExtRemote.DXUser.isEmailUnique(value, callback);
        }
    }
});

Our User model on the client side should implement exactly the same rules like his backend clone. Additionally we need to add static method which will cover our custom API function.
Now create file Users.js in \public\app\store, fill it with this:

Ext.define('TutsApp.store.Users', {
    extend: 'Ext.data.Store',

    requires: [
        'TutsApp.model.User'
    ],

    config: {
        autoLoad: false,
        autoSync: false,
        model: 'TutsApp.model.User',
        storeId: 'UsersStore',
        proxy: {
            type: 'direct',
            api: {
                create: 'ExtRemote.DXUser.create',
                read: 'ExtRemote.DXUser.read',
                update: 'ExtRemote.DXUser.update',
                destroy: 'ExtRemote.DXUser.destroy'
            },
            reader: {
                type: 'json',
                rootProperty: 'User'
            }
        }
    }
});

Code above is typical store definition, so it’s nothing special to explain.
Next step is to prepare appropriate view. To test our new functionality we will need simple form, for adding new user and list view to display them.
Open \public\app\view\Main.js and edit it to something like that:

Ext.define('TutsApp.view.Main', {
    extend: 'Ext.Panel',
    xtype: 'main',
    requires: [
        'Ext.TitleBar',
        'Ext.form.Panel',
        'Ext.dataview.List',
        'Ext.field.Password'
    ],
    config: {
        layout: {
            type: 'fit'
        },
        items: [
            {
                xtype: 'container',
                layout: {
                    type: 'vbox'
                },
                scrollable: true,
                items: [
                    {
                        xtype: 'formpanel',
                        itemId : 'form',
                        height: 220,
                        items : [
                            {
                                xtype : 'textfield',
                                label: 'Name:',
                                name : 'firstName'
                            },
                            {
                                xtype : 'textfield',
                                label: 'Surname:',
                                name : 'lastName'
                            },
                            {
                                xtype : 'textfield',
                                label: 'Email:',
                                name : 'email'
                            },
                            {
                                xtype : 'passwordfield',
                                label: 'Password:',
                                name : 'password'
                            },
                            {
                                xtype : 'button',
                                padding: 0,
                                text : 'add',
                                itemId: 'add'
                            }
                        ]
                    },
                    {
                        xtype: 'list',
                        itemId: 'user-list',
                        minHeight: 350,
                        flex: 1,
                        itemTpl: [
                            '<div>{firstName} {lastName} ({email})</div>'
                        ],
                        store: 'UsersStore',
                        items: [
                            {
                                xtype: 'toolbar',
                                cls: 'portrait',
                                docked: 'bottom',
                                layout: {
                                    align: 'center',
                                    pack: 'center',
                                    type: 'hbox'
                                },
                                items: [
                                    {
                                        xtype: 'button',
                                        text: 'Refresh',
                                        itemId: 'list-refresh'
                                    }
                                ]
                            }
                        ]
                    }
                ]
            }
        ]
    }
});

Which should’ve gave you results like this:

1-sencha-touch-nodejs

OK, model, store and view are behind us, but we still need to write controller and modify our main application files app.js and index.html.
Analogously to previous steps, in \public\app\controller\ create file called User.js:

Ext.define('TutsApp.controller.User', {
    extend: 'Ext.app.Controller',
    config: {
        stores: [
            'TutsApp.store.Users'
        ],
        refs: {
            // bind references
            refreshButton: '[itemId=list-refresh]',
            addButton: '[itemId=add]',
            form: '[itemId=form]',
            list: '[itemId=user-list]'
        },
        control: {
            // bind callbacks on events
            refreshButton: {
                tap: 'onRefresh'
            },
            addButton: {
                tap: 'onAdd'
            }
        }
    },
    launch: function () {
        this.callParent(arguments);
    },
    /**
     * get list store
     * @returns {Store}
     */
    getListStore: function () {
        return this.getList().getStore();
    },
    /**
     * refresh list view
     */
    onRefresh: function () {
        this.getListStore().load();
    },
    /**
     * add new records
     */
    onAdd: function () {
        var me = this,
            values = me.getForm().getValues(),
            users = me.getListStore(),
            user = users.getModel(),
            data = "",
            errors,
            instance;
        // create new instance of model
        // with data from form
        instance = user.create(values);
        // validate it
        errors = instance.validate();
        if (instance.isValid()) {
            // if all data are valid
            user.isEmailUnique(values.email, function (result) {
                // check if email is unique
                if(result.success){
                    // if yes add to store
                    users.add(values);
                    users.sync();
                }else{
                    // or display error message
                    data = "Email should be unique";
                    Ext.Msg.alert("Validation Failed", data);
                }
            });
        } else {
            // if errors occurred show
            // alert with description
            errors.each(function (item) {
                data = data + item.getField() + ' - ' + item.getMessage() + '<br>';
            });
            Ext.Msg.alert("Validation Failed", data);
        }
    }
});

So, what’s left? Two small things. Firstly open index.html and after line

    <!-- The line below must be kept intact for Sencha Command to build your application -->
    <script id="microloader" type="text/javascript" src=".sencha/app/microloader/development.js"></script>

add

    <script type="text/javascript" src="/directapi"></script>

and the last step, in \public\app.js we need to initialize our new remoting API and tell application to load User controller.
So before

Ext.application({
    name: 'TutsApp',

add

Ext.require([
    'Ext.direct.*'
]);
Ext.onReady(function () {
    Ext.direct.Manager.addProvider(ExtRemote.REMOTING_API);
});

and after

    views: [
        'Main'
    ],

put

    controllers: [
        'User'
    ],

That’s all! Now you are ready to test what we have done. Click add, when fields are empty, to see error message. Fill form with proper data to add new record and in the end try to add them again to see information that email should be unique.

6. Conclusion

In this part of the tutorial we learned how to build proper backend solution for Sencha Touch applications and how to communicate with it. What is also important, we cover how to write it with TDD in mind using Grunt and nodeunit. In next part, I hope a lot shorter that this, we will back to the client side, and I will show you how to use sprites to build simple games or animation. So if you are interested, please subscribe our blog on G+.

Comments:

Leave a Reply

Please fill all required fields


Drag circle to the rectangle on the right!

Send