Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Backbone Marionette and RequireJS Modules

I'm beginning a large scale javascript application with Marionette. A Marionette application has a concept of application Modules and RequireJS is also used to break code into modules,

currently I have this for the start of my application:

require([ "jquery", "underscore", "backbone", "marionette" ],
function ($, _, Backbone, Marionette) {
    $(function() {

        App = new Marionette.Application();
        App.addInitializer(function(options) {
            App.addRegions({
                mainArea: "#mainArea"
            });
        });

        App.on("start", function() {
            // done starting up, do stuff here
        });

        App.start();
    });
});

If I wanted to add a view would I do something like the following in a file?

require([ "jquery", "underscore", "backbone", "marionette" ],
function($, _, Backbone, Marionette) {

    App.module("FirstView", function(FirstView, App, Backbone, Marionette, $, _) {
        return Marionette.ItemView.extend({
            //define view stuff in here
         });
    });

});

I'm not sure how I'd get this code to actually run, any help is much appreciated

like image 860
Adam Langsner Avatar asked Jul 06 '12 14:07

Adam Langsner


2 Answers

Marionette's modules are meant to be a simple alternative to RequireJS (and other) module formats. I would not recommend using them together, as noted in the wiki:

https://github.com/marionettejs/backbone.marionette/wiki/AMD-Modules-vs-Marionette's-Modules

like image 132
Derick Bailey Avatar answered Nov 18 '22 13:11

Derick Bailey


IMHO I like to differ from the view point stated above "Marionette's modules are meant to be a simple alternative to RequireJS (and other) module formats."

I like to draw a comparison between Require.js modules and Marionette.js modules with C#'s assembly and namespace concepts. Marionette.js's modules help us group definitions of various building blocks based on functionality, while Require.js could be used to load / inject dependencies.

Again, this is my view / understanding (based on discussions with David Sulc on his book 'Structuring Backbone Code with RequireJS and Marionette Modules'), which has helped in my implementation. In a way we can use Marionette.js and Require.js together as described below.

The example below is a small Library Manager app (sample) which could be found online @ https://github.com/srihari-sridharan/LibraryManagement. The code below (omitting insignificant bits and pieces) creates the application object and renders the list of books after initialization. Please find it here - https://github.com/srihari-sridharan/LibraryManagement/blob/master/app/js/app.js

define([
    'marionette',
    'modules/config/marionette/regions/dialog'], function (Marionette) {

    // Create the application object
    var LibraryManager = new Marionette.Application();

    // Add regions to the application object
    LibraryManager.addRegions({
        //Header
        headerRegion: "#header-region",
        //Main
        mainRegion: "#main-region",
        //Footer
        footerRegion: "footer-region",
        //Overlay Dialog
        dialogRegion: Marionette.Region.Dialog.extend({
            el:"#dialog-region"
        })
    });

    // Subscribe to Initialize After event.
    LibraryManager.on('initialize:after', function() {
        if(Backbone.history){
            require(['modules/books/booksModule', 'modules/about/aboutModule'], function (){
                Backbone.history.start();    
                if(LibraryManager.getCurrentRoute() === ''){
                    LibraryManager.trigger("books:list");
                }                    
            });
        }
    });

    // Return the application object.
    return LibraryManager;
});

Next we define the module / sub-modules based on the functionality. This will also have a module specific router and will wire controllers and handle routes. Note the require call to controllers. This code is present in https://github.com/srihari-sridharan/LibraryManagement/blob/master/app/js/modules/books/booksModule.js

define(['app'], function (LibraryManager) {
    // Define a new module for Books - BooksModule
    LibraryManager.module('BooksModule', function (BooksModule, LibraryManager, Backbone, Marionette, $, _) {

        BooksModule.startWithParent = false;

        BooksModule.onStart = function () {
            console.log('Starting BooksModule.');
        };

        BooksModule.onStop = function () {
            console.log('Stopping BooksModule.');
        };

    });

    // Define a new module for a Router specific to BooksModule
    LibraryManager.module('Routers.BooksModule', function (BooksModuleRouter, LibraryManager, Backbone, Marionette, $, _) {

        BooksModuleRouter.Router = Marionette.AppRouter.extend({
            appRoutes: {
                'books': 'listBooks',
                'books(?filter:=criterion)': 'listBooks',
                'books/:id': 'showBook',
                'books/:id/edit': 'editBook'
            }
        });

        var executeAction = function (action, arg) {
            LibraryManager.startSubModule('BooksModule');
            action(arg);
            LibraryManager.execute('set:active:header', 'books');
        };

        var API = {
            // This is where we are using / referring to our controller
            listBooks: function (criterion) {
                require(['modules/books/list/listController'], function (ListController) {
                    executeAction(ListController.listBooks, criterion);
                });
            },

            showBook: function (id) {
                require(['modules/books/show/showController'], function (ShowController){
                    executeAction(ShowController.showBook, id);
                });
            },

            editBook: function (id) {
                require(['modules/books/edit/editController'], function (EditController) {
                    executeAction(EditController.editBook, id);
                });
            }

        };

        // Navigating routes.
        LibraryManager.on('books:list', function () {
            LibraryManager.navigate('books');
            API.listBooks();
        });

        LibraryManager.on('books:filter', function(criterion) {
            if(criterion){
                LibraryManager.navigate('books?filter=' + criterion);
            }
            else{
                LibraryManager.navigate('books');
            }
        });

        LibraryManager.on('book:show', function (id) {
            LibraryManager.navigate('books/' + id);
            API.showBook(id);
        });

        LibraryManager.on("book:edit", function(id){
            LibraryManager.navigate('books/' + id + '/edit');
            API.editBook(id);
        });

        LibraryManager.addInitializer(function () {
            new BooksModuleRouter.Router({
                controller: API
            });
        });
    });

    return LibraryManager.BooksModuleRouter;
});

Finally we have the definitions for our views, models and controllers. These definitions will be tied to module / sub module objects.

The view code is shown below. Look at the .extend() methods. They are assigned to variables attached to the BooksModule.List.View sub module. https://github.com/srihari-sridharan/LibraryManagement/blob/master/app/js/modules/books/list/listView.js

define(['app',
        'tpl!modules/books/list/templates/layout.html',
        'tpl!modules/books/list/templates/panel.html',
        'tpl!modules/books/list/templates/none.html',
        'tpl!modules/books/list/templates/list.html',
        'tpl!modules/books/list/templates/listItem.html'], 
    function (LibraryManager, layoutTemplate, panelTemplate, noneTemplate, listTemplate, listItemTemplate) {

        LibraryManager.module('BooksModule.List.View', function(View, LibraryManager, Backbone, Marionette, $, _) {

            View.Layout = Marionette.Layout.extend({

                template: layoutTemplate,

                regions:{
                    panelRegion: '#panel-region',
                    booksRegion: '#books-region'
                }

            });

            View.Panel = Marionette.ItemView.extend({
                // More code here!
            });

            View.Book = Marionette.ItemView.extend({                
                // More code here!
            });

            var NoBooksView = Marionette.ItemView.extend({
                template: noneTemplate,
                tagName: "tr",
                className: "alert"
            });

            View.Books = Marionette.CompositeView.extend({
                // More code here!
            });
        });
    return LibraryManager.BooksModule.List.View; // Return the definition.
});

The controller code is shown below. This gets called from the code in booksModule.js. The controller definition is attached to BooksModule.List sub module.

define(['app', 'modules/books/list/listView'], function (LibraryManager, View) {

    LibraryManager.module('BooksModule.List', function (List, LibraryManager, Backbone, Marionette, $, _) {

        List.Controller = {

            listBooks: function (criterion) {

                require(['common/views', 'entities/book'], function (CommonViews) {

                    var loadingView = new CommonViews.Loading();
                    LibraryManager.mainRegion.show(loadingView);

                    var fetchingBooks = LibraryManager.request('book:entities');
                    var booksListLayout = new View.Layout();
                    var booksListPanel = new View.Panel();

                    require(['entities/common'], function (FilteredCollection) {

                        $.when(fetchingBooks).done(function (books) {
                            // More code here!
                            });

                            if(criterion){
                                filteredBooks.filter(criterion);
                                booksListPanel.once('show', function () {
                                    booksListPanel.triggerMethod("set:filter:criterion", criterion);
                                });
                            }

                            var booksListView = new View.Books({
                                collection: filteredBooks
                            });

                            booksListPanel.on('books:filter', function (filterCriterion) {
                                filteredBooks.filter(filterCriterion);
                                LibraryManager.trigger("books:filter", filterCriterion);
                            });

                            booksListLayout.on("show", function(){
                                booksListLayout.panelRegion.show(booksListPanel);
                                booksListLayout.booksRegion.show(booksListView);
                            });

                            booksListPanel.on('book:new', function () {

                                require(["modules/books/new/newView"], function (NewView) {
                                        // More code here!
                                    });

                                    LibraryManager.dialogRegion.show(view);
                                });
                            });

                            booksListView.on('itemview:book:show', function (childView, model) {
                                LibraryManager.trigger("book:show", model.get('id'));
                            });

                            booksListView.on('itemview:book:edit', function(childView, model) {
                                require(['modules/books/edit/editView'], function (EditView) {
                                    // More code here!
                                    LibraryManager.dialogRegion.show(view);
                                });
                            });

                            booksListView.on("itemview:book:delete", function (childView, model) {
                                model.destroy();
                            });

                            LibraryManager.mainRegion.show(booksListLayout);

                        });

                    });

                });

            }

        }

    });

    return LibraryManager.BooksModule.List.Controller; // Return the definition.
});

Thus require.js modules and marionette modules can coexist. The following are the advantages.

  • Much cleaner organization of source code and clearer separation of concerns.
  • Module start and stop methods provide provision to initialize and cleanup objects.
  • When you model functionalities and sub-functionalities as modules and sub modules, we have more granular control over what resides in memory and what should not.
  • Also, module definition can be split across multiple files.

Please post your thoughts. Thanks for reading.

PS: Based on the above view point, please find the changes to your example below:

require([ "jquery", "underscore", "backbone", "marionette" ],
function($, _, Backbone, Marionette) {
    App.module("FirstView", function(FirstView, App, Backbone, Marionette, $, _) {
        FirstView.View = Marionette.ItemView.extend({
            //define view stuff in here
        });

        return FirstView.View;
    });
});
like image 42
Srihari Sridharan Avatar answered Nov 18 '22 12:11

Srihari Sridharan