Data Entry

A data entry app supporting CRUD (Create Read Update Delete) actions.



Launch App


Description


Data entry involves CRUD operations. CRUD stands for Create, Read, Update, and Delete which are the four basic operations that you will find in any app in which records are maintained (probably over 95% of the apps). These operations exactly map to the four database SQL statements: Insert, Select, Update, and Delete.

In this page we maintain customer records that have a first name and a last name. The page starts off with two existing records. You can add new records and edit or delete existing ones: fairly straightforward.

What is different is that everything takes place on the client for which we use the popular Backbone MVC framework. Backbone is built around the notion of REST services, but since there is no server we built a Mock server using the open source fauxServer library. This is a valuable tool to help in automated testing. You'll see that doing CRUD on the client, as opposed to calling server pages, makes the app highly responsive.

The page elements are template driven. The templates are found in <script> tags with a type="text/template" attribute. The three templates are: customers-template which renders a list of customers, the customer-template, which renders a single record, and form-template which allows adding a new record or editing an existing record. Because of the type="text/template" attribute the browser will recognize these as templates rather than script. This is commonly used in Backbone templating. By the way, the SPA example (another Patterns-in-Action app) will use a different technique in which templates are loaded from disk.

Here are the three template scripts:

<script type="text/template" id="customers-template">
<div class="row">
   <div class="span2"><button class="btn btn-primary add"> New Customer </button></div>
   <div class="span4" style="padding-top:5px;"><h4>Currently we have {{ count }} customers</h4></div>
</div>
<div class="row">
    <div class="span6 offsethalf">
        <div class="customers"></div>
    </div>
</div>
</script>

<script type="text/template" id="customer-template">
    <div class="row">
      <div class="span3">{{ first }} {{ last }}</div>
      <div class="span2"><button class='btn btn-primary edit'>Edit</button>  
                         <button class='btn btn-danger delete'>Delete</button>
      </div>
    </div>
</script>

<script type="text/template" id="form-template">
   <legend>Add/Edit a customer</legend>
   <fieldset>
   <div class='control-group'>
       <label class='control-label' for='first'>First Name</label>
       <div class='controls'>
        <input type='text' value="{{ first }}" class='input-large' id='first'>
       </div>
   </div>
   <div class='control-group'>
      <label class='control-label' for='last'>Last Name</label>
      <div class='controls'>
        <input type='text' value="{{ last }}" class='input-large' id='last'>
      </div>
   </div>
   <div class="form-actions">
       <button type="submit" class="btn btn-primary">Save</button>
       <button type="reset" class="btn">Cancel</button>
   </div>
   </fieldset>
</script>

Notice that the three script tags have type="text/template" attribute.

By default Backbone uses the underscore.js templating engine. Its template token markers has a rather awkward <% =   %> syntax. We changed this to double braces {{ and }} which is also more in line with Mustache, another popular templating engine. The _.templateSettings method is used to change this. You can see it in the code below.

Here is the code for the Data Entry app:

var Patterns = {
    // ** namespace pattern
    namespace: function (name) {
        // ** single var pattern
        var parts = name.split(".");
        var ns = this;

        // ** iterator pattern
        for (var i = 0, len = parts.length; i < len; i++) {
            // ** || idiom
            ns[parts[i]] = ns[parts[i]] || {};
            ns = ns[parts[i]];
        }

        return ns;
    }
};

// ** namespace pattern 
// ** revealing module pattern
// ** singleton pattern
Patterns.namespace("InAction").DataEntry = (function () {

    // ** single var pattern
    // ** namespace pattern (Models, View, Routers)
    var Models = {};
    var Views = {};
    var Routers = {};

    var router;
       
    var start = function () {
        router = new Routers.Router();
        Backbone.history.start();
    }

    // Change template token markers to {{ and }} 
    _.templateSettings = {
        interpolate: /\{\{(.+?)\}\}/g,
        evaluate: /\{\{(.+?)\}\}/g
    };

    // ** extend pattern 
    // ** option hash idiom
    Routers.Router = Backbone.Router.extend({
        // ** init pattern
        initialize: function (options) {
            this.el1 = $("#customers-content");
            this.el2 = $("#form-content");
        },
        routes: {
            "": "main"  
        },
        main: function () {
            this.el1.html(new Views.Customers().el);
        }
    });

    // ** extend pattern
    // ** option hash idiom
    Models.Customer = Backbone.Model.extend({
            
        defaults: {
            first: "",
            last: ""
        }
    });

    // ** extend pattern
    // ** option hash idiom
    Models.Customers = Backbone.Collection.extend({
        model: Models.Customer,
        url: "customers"
    });

    // ** extend pattern
    // ** option hash idiom
    Views.Customers = Backbone.View.extend({
        template: _.template($('#customers-template').html()),

        events: {
            'click .add': 'add',
        },
        // ** init pattern
        initialize: function () {
            this.collection = new Models.Customers();
            // ** observer pattern
            this.collection.on('reset add update remove change', this.render, this);
            this.collection.fetch();
        },
        render: function (eventName) {

            // total customer count
            this.$el.html(this.template({ count: this.collection.length }));

            // ** iterator pattern
            this.collection.each(function (customer) {
                // ** option hash idiom
                var view = new Views.Customer({ collection: this.collection, model: customer });
                this.$el.append(view.render().el);
            }, this);

            return this;
        },
        add: function () {
            // ** option hash idiom
            var view = new Views.Customer.Form({ collection: this.collection, model: new Models.Customer() });
            $("#form-content").html(view.render().el);
            $('#first').focus();
        }
    });

    // ** extend pattern
    // ** option hash idiom
    Views.Customer = Backbone.View.extend({
        className: 'well',
        // ** chaining pattern
        template: _.template($('#customer-template').html()),
        events: {
            'click .edit': 'edit',
            'click .delete': 'remove'
        },
        // ** init pattern
        initialize: function() {
            this.model.bind("destroy", this.close, this);
        },
        render: function () {
            this.$el.html(this.template(this.model.toJSON()));
            return this;
        },
        edit: function () {
            // ** option hash idiom
            var view = new Views.Customer.Form({ collection: this.collection, model: this.model });
            $("#form-content").html(view.render().el);
            // ** chaining pattern
            $('#first').focus().val('').val(this.model.get("first"));  // focus, but deselect the current value
        },
        remove: function () {
            this.model.destroy();
        },
        close: function () {
            $(this.el).unbind();
            $(this.el).remove();
        }
    });

    // ** extend pattern
    // ** option hash idiom
    Views.Customer.Form = Backbone.View.extend({
        tagName: 'form',
        className: 'form-horizontal',
        // ** chaining pattern
        template: _.template($('#form-template').html()),
        events: {
            'submit': 'submit',
            'reset' : 'cancel'
        },
        render: function () {
            this.$el.html(this.template(this.model.toJSON()));
            return this;
        },
        submit: function (event) {
            event.preventDefault();
                
            if (this.model.isNew()) {     // add
                this.collection.create({
                    first: this.$('#first').val(),
                    last: this.$('#last').val()
                });

            } else {       // update
                var mod = this.collection.get(this.model.get("id"));
                   
                this.model.set("first", this.$('#first').val());
                this.model.set("last", this.$('#last').val());
                var url = this.model.url;
                   
                this.model.url = "customers";
                this.model.save();
                this.model.url = url;
            }
            this.$el.empty();
        },
        cancel: function (event) {
            event.preventDefault();
            this.$el.empty();
        }
    });

    // Helper: generate four random hex digits.
    var S4 = function () {
        return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
    };

    // Helper: generate a pseudo-GUID by concatenating random hexadecimal.
    var guid = function () {
        return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4());
    };

    // Used to mock database persistence
    if (!window.databaseCustomers) {
        window.databaseCustomers = [{ id: "5f294421-08af-135f-1d22-583245fb67b5", first: "Joan", last: "Kennedy" },
                                    { id: "461f92de-a7fc-a90d-4419-958423678d8f", first: "Kevin", last: "McGregor" }];
    }

    // server mock
    // ** options hash idiom
    fauxServer.addRoutes({
        listcustomers: {
            urlExp: "customers",
            httpMethod: "GET",
            handler: function (context) {
                context.data = databaseCustomers;
                return context.data;
            }
        },

        addCustomer: {
            urlExp: "customers",
            httpMethod: "POST",
            handler: function (context) {
                context.data.id = guid();
                databaseCustomers.push(context.data);
                return context.data;
            }
        },

        updateCustomer: {
            urlExp: "customers",
            httpMethod: "PUT",
            handler: function (context) {
                databaseCustomers.push(context.data);
                return context.data;
            }
        },

        deleteCustomer: {
            urlExp: "customers/:id",
            httpMethod: "DELETE",
            handler: function (context, bookId) {
                var len = databaseCustomers.length;
                for (var i = 0; i < len; i++) {
                    if (databaseCustomers[i].id === bookId) {
                        databaseCustomers.splice(i, 1);
                        break;
                    }
                }
            }
        }
    });

    return { start: start };
})();

$(function () {

    // ** facade pattern 
    Patterns.InAction.DataEntry.start();
});

The patterns and idioms used in this code are:

  • || and && idiom
  • Option Hash idiom
  • Namespace pattern
  • Single var pattern
  • Module pattern
  • Extend pattern
  • Init pattern
  • Chaining pattern
  • Iterator pattern
  • Singleton pattern
  • Observer pattern
  • Façade pattern

Backbone is a powerful MVC framework. It is not very opiniated meaning there are many ways to solve the same problem. This is good and bad: good because it offers flexibility, and bad because it allows you to make bad design decisions. As a result, Backbone has a fairly steep learning curve. Unfortunately, the details of Backbone are beyond the scope of this program; our focus is on JavaScript and Patterns.

Most of the patterns listed above are used in a similar way as in the previous example (Dashboard) and will not be discussed here. The only differences are 1) the use of the Namespace pattern and 2) the Extend and Init patterns that are part of the Backbone framework.

The Namespace pattern is used within the DataEntry module to organize the component categories in Backbone: Models, Views, and Routers. All model objects are stored in Models, all view objects in Views, and all router objects in Routes.

In Backbone there is usually a one-to-one relationship between models and views: for each model there is an associated view. The following naming convention has been adopted: when a model is named, for example, Widget we place it in Models.Widget. The associated view is also named Widget but we place it in Views.Widget. When we have collection of Widget model objects we pluralize it to Widgets and place this collection in Models.Widgets. The matching view will also be named Widgets and gets placed in Views.Widgets.

Backbone makes extensive use of the Extend pattern which lets you extend Backbone's base objects with your own custom objects. The extend method accepts a single argument which is an object literal with name/value pairs that are assigned as properties to the new object. This argument follows the Option Hash idiom. Backbone makes use of the Init Pattern which allows you to customize and complete the object initialization phase. The initialize method's argument is the same as the extend method so you have access to all object settings.


Launch App




  Dashboard
Search