Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

Implementing RequireJs on BackboneJs

5.00/5 (4 votes)
3 May 2012CPOL2 min read 25.3K   680  
Updating the MVC music store app with RequireJS on BackboneJs

Introduction

In this tutorial we will be implementing RequireJs Asynchronous Module definition pattern on our BackboneJs application.   

Background

What is AMD n why AMD? 

Well a video speaks more than words. Here are the links which is definitely going to help you understand AMD and RequireJs. 

Why AMD? - Part 1 

Why AMD? - Part 2 

Using the code 

Since our existing code BackboneJs application has the backbone model, view and collections on one script file, to induce AMD we would need to  modularize them. So the obvious answer would be to take the views, models and collections to different js files. For better understanding lets folderise the  javascript files as follows.

1, Scripts/Model/GenreModel.js 

2, Scripts/Collection/GenreCollection.js

3, Scripts/View/StoreManager/GenreApplicationView.js 

4, Scripts/View/StoreManager/GenreView.js 

5, Scripts/View/StoreManager/GenreEditView.js 

6, Scripts/View/StoreManager/GenreDeleteView.js

Well if you have downloaded the source code, you would be thinking what are these following scripts for

1, Scripts/RequireUtil/RequireJquery.js 

2, Scripts/RequireUtil/RequireJqueryUI.js  

3, Scripts/RequireUtil/RequireUnderscore.js 

4, Scripts/RequireUtil/RequireBackbone.js  

The problem here is latest version of jQuery, jQueryUI, Underscore and Backbone are not modularised anymore, in simple sense they do not support RequireJs or AMD.  What we are doing is a simple workaround, convert jQuery et all into a named module and pass it as a dependency to our other modules. If you want to skip this step google for Modularised Jquery, Underscore and Backbone.js. They perfectly work fine. 

JavaScript
 
//RequireJquery.js
define(['order!../jquery'], function () {
    return $;
});

//RequireJqueryUI
define(['order!../jquery-ui'], function () {
    return $;
});

//RequireUnderscore
define(['order!../underscore'], function () {
    return _;
});

//RequireBackbone
define(['order!../backbone'], function () {
    _.noConflict();
    $.noConflict();
    return Backbone.noConflict();
});

//GenreApp - This is the main file we will be loading on the Genre.shtml
require.config({
    paths: {
        jQuery: '../RequireUtil/RequireJquery',
        jQueryUI: '../RequireUtil/RequireJqueryUI',
        order: "../RequireUtil/order",
        Underscore: '../RequireUtil/RequireUnderscore',
        Backbone: '../RequireUtil/RequireBackbone',
        GenreModel: '../Model/GenreModel',
        GenreCollection: '../Collection/GenreCollection',
        GenreApplicationView: '../View/StoreManager/GenreApplicationView',
        GenreView: '../View/StoreManager/GenreView',
        GenreDeleteView: '../View/StoreManager/GenreDeleteView',
        GenreEditView: '../View/StoreManager/GenreEditView'
    }
});

require(['order!jQuery', 'order!jQueryUI', 'GenreCollection', 'GenreApplicationView'], function ($, jqueryui, GenreCollection, AppView) {
    $(function () {
        $("#GenreEditDialog").dialog({
            autoOpen: false,
            show: "blind",
            hide: "explode",
            modal: true,
            height: 250,
            width: 600,
            open: function (event, ui) {
                $('.ui-dialog-titlebar', ui.dialog || ui).hide();
            }
        });

        var genres = new GenreCollection();
        var view = new AppView({ collection: genres });

        genres.fetch({ success: function () {
            view.render();
        } 
        });
    });
});

//GenreApplicationView
define(['order!jQuery', 'order!Underscore', 'order!Backbone', 'GenreView', 'GenreEditView', 'GenreModel', 'GenreCollection'],
function ($, _, Backbone, GenreView, EditView, Genre, GenreCollection) {
    // Actual App view
    var AppView = Backbone.View.extend({
        initialize: function () {
            this.collection.bind('add', this.AppendGenre, this);
        },
        el: '#Genre_Container',
        events: {
            "click #btnCreateNew": "AddNewGenre"
        },
        AddNewGenre: function () {
            var newGenre = new Genre();
            var editView = new EditView({ model: newGenre, collection: this.collection });
            editView.render();
        },
        AppendGenre: function (genre) {
            var genreView = new GenreView({ model: genre });
            $(this.el).find('#Genre_List').append(genreView.render().el);
        },
        render: function () {
            if (this.collection.length > 0) {
                this.collection.each(this.AppendGenre, this);
            }
            $("input:button", "#Genre_List").button();
        }
    });

    return AppView;
});

// Genre View - el returns the template enclosed within a tr
define(['order!jQuery', 'order!Underscore', 'order!Backbone', 'order!GenreEditView', 'order!GenreDeleteView'], function ($, _, Backbone, EditView, DeleteView) {
    var GenreView = Backbone.View.extend({
        tagName: "tr",
        initialize: function () {
            this.template = _.template($('#Genre-Template').html());
            this.model.bind('change', this.render, this);
            this.model.bind('remove', this.unrender, this);
        },
        render: function () {
            $(this.el).html(this.template(this.model.toJSON()));
            return this;
        },
        unrender: function () {
            $(this.el).remove();
            return this;
        },
        events: {
            "click .Edit": 'EditGenre',
            "click .Delete": 'DeleteGenre'
        },
        EditGenre: function () {
            var editView = new EditView({ model: this.model });
            editView.render();
        },
        DeleteGenre: function () {
            var deleteView = new DeleteView({ model: this.model });
            deleteView.render();
        }
    });

    return GenreView;
});

//Genre-Edit view
define(['order!jQuery', 'order!jQueryUI', 'order!Underscore', 'order!Backbone'], function ($, jqueryui, _, Backbone) {
    var GenreEditView = Backbone.View.extend({
        tagName: "table",
        container: "#Genre_Edit",
        initialize: function () {
            this.template = _.template($('#Genre-Edit-Template').html())
        },
        events: {
            "change .gName": 'NameChange',
            "change .gDesc": 'DescChange',
            "click .Update": 'UpdateGenre',
            "click .Cancel": 'CancelGenre'
        },
        NameChange: function () {
            // Directly bind the name change to the model, best use of back bone.
            // Otherwise set the data during save which ever you prefer
            this.model.set({ Name: $(".gName").val() });
        },
        DescChange: function () {
            this.model.set({ Description: $(".gDesc").val() });
        },
        render: function () {
            $(this.container).append($(this.el).html(this.template(this.model.toJSON())));
            $("input:button", $(this.el)).button();
            $("#GenreEditDialog").dialog("open");
            var title = 'Create Genre';
            if (this.collection == null)
                title = 'Edit Genre - ' + this.model.get('Name');
            $(this.container).find('legend').html(title);
            $('.ui-dialog-titlebar').hide();
            $('#valSum').hide("slow");
            return this;
        },
        unrender: function () {
            $(this.el).remove();
            return this;
        },
        UpdateGenre: function () {
            if (this.model.validateModel()) {
                var self = this;
                if (this.collection != null) {
                    this.collection.add(this.model);
                }
                this.model.save(this.model, { success: function () {
                    self.unrender();
                    $("#GenreEditDialog").dialog("close");
                    var genreId = self.model.get('GenreId');
                    $("input:button", '#Genre_List').button();
                }
                });
            }
            $("input:button", '#Genre_List').button();
        },
        CancelGenre: function () {
            this.model.fetch({ success: function () {
                $("input:button", '#Genre_List').button();
            }
            });
            this.unrender();
            $("#GenreEditDialog").dialog("close");
        }
    });

    return GenreEditView;
});

// Genre Delete View
define(['order!jQuery', 'order!jQueryUI', 'order!Underscore', 'order!Backbone'], function ($, jqueryui, _, Backbone) {
    var GenreDeleteView = Backbone.View.extend({
        container: "#Genre_Edit",
        initialize: function () {
            this.template = _.template($('#Genre-Delete-Template').html());
        },
        events: {
            "click .Update": 'DeleteGenre',
            "click .Cancel": 'CancelGenre'
        },
        render: function () {
            $(this.container).append($(this.el).html(this.template(this.model.toJSON())));
            $("input:button", $(this.el)).button();
            $("#GenreEditDialog").dialog("open");
            $('.ui-dialog-titlebar').hide();
            var title = 'Delete Genre - ' + this.model.get('Name');
            $(this.container).find('legend').html(title);
            return this;
        },
        unrender: function () {
            $(this.el).remove();
            return this;
        },
        CancelGenre: function () {
            this.unrender();
            $("#GenreEditDialog").dialog("close");
        },
        DeleteGenre: function () {
            var self = this;
            this.model.destroy({ success: function () {
                self.unrender();
                $("#GenreEditDialog").dialog("close");
            }
            });
        }
    });

    return GenreDeleteView;
});

// Genre Model
define(['jQuery', 'Underscore', 'Backbone'], function ($, _, Backbone) {
    var GenreModel = Backbone.Model.extend({
        url: function () {
            var base = '/StoreManager/GenreList/';
            if (this.isNew())
                return base;
            return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + this.id;
        },
        initialize: function () {
            this.bind("change:GenreId", function () {
                var genreId = this.get('GenreId');
                this.set({ id: genreId });
            });
        },
        defaults: {
            GenreId: 0,
            Name: '',
            Description: ''
        },
        validateModel: function () {
            var validate = true;
            var divHtml = "<div class='ui-state-error ui-corner-all' style='display: block; padding-top: 0px; padding-bottom: 0px; width: 500px; padding-left: 0.7em; padding-right: 0.7em;'>";
            divHtml += "<p><span class='ui-icon ui-icon-alert' style='float: left; margin-right: .3em;'></span><strong>Please correct these Errors</strong> </p><div class='validation-summary-errors' data-valmsg-summary='true'>";
            divHtml += "</div><p></p></div>";
            var ulHtml = "<ul></ul>";
            var div = $(divHtml);
            var ul = $(ulHtml);
            if (this.get('Name') == "") {
                ul.append("<li>Name is Mandatory</li>");
                validate = false;
            }
            if (this.get('Description') == "") {
                ul.append("<li>Description is Mandatory</li>");
                validate = false;
            }
            div.find(".validation-summary-errors").append(ul);

            if (!validate) {
                $('#valSum').show("slow");
                $('#valSum').html(div);
            }
            else {
                $('#valSum').hide("slow");
                $('#valSum').html("");
            }

            return validate;
        }
    });

    return GenreModel;
});

// GenreCollection
define(['jQuery', 'Underscore', 'Backbone', 'GenreModel'], function ($, _, Backbone, Genre) {
    var GenreCollection = Backbone.Collection.extend({
        model: Genre,
        url: '/StoreManager/GenreList/'
    });
    return GenreCollection;
});

Remember the above code is not in a single js file, they are in different js files. Now we need to refer Require.js in our Genre.shtml and make it load the GenreApp.js (note the data-main attribute on the script tag) with its dependencies. 

HTML
@{
    ViewBag.Title = "Genres";
}
<script data-main="@Url.Content("~/Scripts/App/GenreApp")" src="@Url.Content("~/Scripts/require.js")" type="text/javascript"></script>
<div class="styler">
    <fieldset class="ui-widget">
        <legend class="ui-state-legend-default ui-corner-top ui-corner-bottom">Genre List -
            Using Backbone</legend>
        <div id="Genre_Container">
            <input type="button" value="Create New" class="Add" id="btnCreateNew" />
            <table id="Genre_List">
                <tr>
                    <th>
                        Name
                    </th>
                    <th>
                        Description
                    </th>
                    <th>
                    </th>
                </tr>
            </table>
            <div id="GenreEditDialog" style="height: 350">
                <div class="styler">
                    <fieldset class="ui-widget" id="Genre_Edit" style="height: 350">
                        <legend class="ui-state-legend-default ui-corner-top ui-corner-bottom"></legend>
                        <br />
                        <div id="valSum" >
                        </div>
                    </fieldset>
                </div>
            </div>
        </div>
        <script id='Genre-Template' type='text/template'>
                <td><%=Name%></td> <td><%=Description%></td>
                <td><input type="button" value= "Edit" class="Edit" /> | <input type="button" value= "Delete" class="Delete" /> </td>
        </script>
        <script id='Genre-Edit-Template' type='text/template'>
               <tr>
                <td>
                    <div class="editor-label">
                        Name</div>
                    <div class="editor-field">
                        <input type="text" class="gName" value='<%=Name%>' />
                    </div>
                </td>
                <td>
                    <div class="editor-label">
                        Description</div>
                    <div class="editor-field">
                        <input type="text" class="gDesc" value='<%=Description%>' />
                    </div>
                </td>
            </tr>
            <tr>
                <td colspan="2">
                    <input type="button" value="Save" class="Update" /> | <input type="button" id="btnCancel" value="Cancel" class="Cancel" />
                </td>
            </tr> 
        </script>
        <script id='Genre-Delete-Template' type='text/template'>
            <div class="ui-state-highlight ui-corner-all" style="margin-top: 20px; padding: 0 .7em;
            width: 500px">
                <p>
                <span class="ui-icon ui-icon-info" style="float: left; margin-right: .3em;"></span>
                <strong>Confirm</strong><br />
                Are you sure you want to delete this Album - <%=Name%> ?</p>
            </div><br/>
            <table>
                <tr>
                    <td>
                        <div class="editor-label">
                            Name</div>
                        <div class="editor-field">
                            <%=Name%>
                        </div>
                    </td>
                    <td>
                        <div class="editor-label">
                            Description</div>
                        <div class="editor-field">
                            <%=Description%>
                        </div>
                    </td>
                </tr>
                <tr>
                    <td colspan="2">
                        <input type="button" value="Delete" class="Update" /> | <input type="button" id="btnCancel" value="Cancel" class="Cancel" />
                    </td>
                </tr>
            </table>
        </script>
    </fieldset>
</div>

Points of Interest

To appreciate the importance and advantages of RequireJs one has to use it profusely in his code. Also the advantages of RequireJs becomes really apparent when your project is growing, managing dependencies can be a headache.  Also RequireJs will help you test your javascript more effectively. We will see that in later tutorials like implementing jasmine or qunit. Until then give RequireJs a shot. Also here are some links which I hope will interest you. 

http://requirejs.org/docs/jquery.html 

http://developer.teradata.com/blog/jasonstrimpel/2011/12/part-1-backbone-js-require-js

http://www.bennadel.com/blog/2287-Using-jQuery-As-A-Named-Module-In-RequireJS.htm

http://www.davidgranado.com/2012/01/getting-jquery-1-7-andor-backbone-to-play-nicely-with-requirejs/ 

 -- Anon  

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)