Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

386 lines (288 sloc) 11.794 kB

@page organizing Organizing Your App @parent tutorials 7

The secret to building large apps is to NEVER build large apps. Break up your applications into small pieces. Then assemble those testable, bite-sized pieces into your big application.

JavaScriptMVC 3.X is built with this pattern in mind. As opposed to a single flat 'scripts' folder, JavaScriptMVC breaks up your app into manageable, isolated modules. This tutorial discusses the reasons for doing this and patterns for doing it.

Why

Traditionally JavaScript, CSS and static resources were seen as second-class citizens when compared to server code. JavaScript code was put in a single flat 'scripts' folder that looked like:

button.js
jquery.ui.calendar.js
contactmanager.js
tabs.js
jquery.js
nav.js
resizer.js
\test
  button_test.js
  contactmanager.js
  tabs_test.js
  nav_test.js

This was OK for a limited amount of JavaScript; however; client code increasingly represents a larger percentage of an app's codebase. What works for 10 files does not work for 100.

Complicating matters, an individual JavaScript file might have dependencies on non-JavaScript resources. It's easy to imagine a menu needing a specific stylesheet, images, or [can.view client side template] to run.

Spreading these dependencies across images, styles, templates etc folders leads to bad organization and potentially bad performance. For example, it can be hard to know if a particular style rule is needed.

The Fix

JavaScriptMVC gives each resource you author it's own folder. Typically, the folder will hold the resource, its demo page, test page, test script, and any other files specific to that resource.

For example, a tabs folder might look like:

\tabs
  tabs.js        - the code for a tabs widget 
  tabs.html      - a demo page
  funcunit.html  - a test page
  tabs_test.js   - test code
  tabs.css       - css for the tab

The idea is that we can work on tabs.js in complete isolation.

How

Before we discuss best practices for organizing your application, a little throat clearing ...

Every app is different. Providing a single folder structure for all applications is impossible. However, there are several useful patterns that when understood can keep your application under control. JavaScriptMVC is extremely flexible so use your best judgement!

This guide walks you through starting with a small-ish example app and where you would add features over time. Before the example, it's good to know some JavaScript terminology:

App and Library Folders

In general, a JavaScriptMVC application is divided into two root folders: an app folder and library folder. The app folder code typically 'steals' and configures 'library' code.

Application Folder

The application (or app) folder houses code specific to a particular application. The code in this folder is very unlikely to be used in other places. The folder name reflects the name of the application being built.

Create an application folder structure with:

js jmvc\generate\app cms

Library Folders

A library folder is for general code that can be reused across several applications. It is the perfect place for reusable controls like a tabs widget. Typically folder names reflect the name of the organization building the controls.

Resource Types

An application is comprised of various resources. JavaScriptMVC's code generators can be used to create these resources.

Model - A model represents a set of services. Typically, they exist within an application folder's models directory and are used to request data by other controls.

Generate a model like:

js jmvc\generate\model cms\models\image

Controller - A controller is a widget or code that combines and organizes several widgets. Reusable widgets are added to library folders. Controllers specific to an application should be put in a folder within an application folder.

Generate a controller like:

js jmvc\generate\controller bitovi\tabs

Plugin - A plugin is a low-level reusable module such as a special event or dom extension. It does not typically have a visible component. These should be added to library folders.

js jmvc\generate\plugin bitovi\range

Example Application

The example is a content management system that organizes 'videos', 'images', and 'articles' under a tabbed layout. For each content type, the user needs to be able to edit a selected item of that type.

@image tutorials/cms.png

If the application's name is cms and it is built by Bitovi, a basic version's folder structure might look like:

\cms
  \models    - models for the CMS
  \views     - views to configure the grid
  cms.js
\bitovi
  \tabs      - a basic tabs widget
  \edit      - binds a form to edit a model instance
  \grid      - a configurable grid
    \views

This basic version assumes that we can configure the grid and edit widget enough to produces the desired functionality. In this case, cms/cms.js might look like:

// load dependencies
steal('bitovi/tabs',
      'bitovi/grid',
      'bitovi/create',
      './models/image',
      './models/video',
      './models/article',function(){

  // add tabs to the page
  var tabs = new Bitovi.Tabs('#tabs');

  // Configure the video grid
  var videos = new Bitovi.Grid('#videos', {
    model: Cms.Models.Video,
    view: "//cms/views/videos.ejs"
  })

  // listen for when a video is selected
  videos.element.find('li').bind('selected', 
    function(ev, video){
      // update the edit form with the selected 
      // video's attributes
      new Bitovi.Edit('#videoEdit', { model: video });
    }
  );

  // Do the same for images and articles
  var images = new Bitovi.Grid('#images', {
    model: Cms.Models.Image,
    view: "//cms/views/images.ejs"
  });

  images.element.find('li').bind('selected', 
    function(ev, image){
      new Bitovi.Edit('#imageEdit', { model: image });
    }
  );

  var articles = new Bitovi.Grid('#articles', {
    model: Cms.Models.Article,
    view: "//cms/views/article.ejs"
  })

  articles.element.find('li').bind('selected', 
    function(ev, article){
      new Bitovi.Edit('#articleEdit', { model: article });
    }
  );

})

Notice that the cms.js configures the grid and edit widgets with the cms folder's models and views. This represents an ideal separation between app specific code and reusable widgets. However, it's extremely rare that widgets are able to provide all the functionality an app needs to meet its requirements.

More complexity

Eventually, you won't be able to configure abstract widgets to satisfy the requirements of your application. For example, you might need to add specific functionality around listing and editing videos (such as a thumbnail editor).

This is application specific functionality and belongs in the application folder. We'll encapsulate it in a controller for each type:

\cms
  \articles - the articles tab
  \images   - the images tab
  \videos   - the videos tab
  \models  
  \views     
  cms.js
\bitovi
  \thumbnail
  \tabs     
  \edit      
  \grid      
    \views

cms/cms.js now looks like:

steal('cms/articles',
      'cms/images',
      'cms/videos',
      'bitovi/tabs',
      function(){

  new Bitovi.Tabs('#tabs');

  // add the video grid
  new Cms.Videos('#videos');

  // Do the same for images and articles
  new Cms.Images('#images');
  new Cms.Articles('#articles');

})

cms/articles/articles.js might look like:

steal('bitovi/grid',
      'bitovi/edit',
      'can/control', 
      'can/view/ejs',
      'bitovi/thumbnail',
      function(){

  can.Control('Cms.Articles',
  { },
  {
    init : function(){
      // draw the html for the tab
      this.element.html('//cms/articles/views/init.ejs',{});

      // configure the grid
      new Bitovi.Grid(this.find('.grid'), {
        model: Cms.Models.Article,
        view: "//cms/articles/views/article.ejs"
      })
      });
    },

    // when the grid triggers a select event
    " select" : function(el, ev){
      var editor = new Bitovi.Edit({
        model: el.data('model')
      })

      // add the thumbnail editor
      new Bitovi.Thumbnail(editor.find('.thumbs'));
    }
  });

});

Adding leaves to the tree

In the previous example, we moved most of the code in cms/cms.js into an articles, images, and videos plugin. Each of these plugins should work independently from each other, have it's own tests and demo page.

Communication between these high-level controls should be configured in cms/cms.js.

Essentially, as your needs become more specific, you are encouraged to nest plugins within each other.

In this example, after separating out each type into it's own plugin, you might want to split the type into edit and grid controls. The resulting folder structure would look like:

\cms
  \articles
    \grid
    \edit
  \images
    \grid
    \edit   
  \videos
    \grid
    \edit   
  \models  
  \views     
  cms.js
\bitovi
  \thumbnail
  \tabs     
  \edit      
  \grid      

cms/articles/articles.js might now look like:

steal('cms/articles/grid',
      'cms/articles/edit',
      'can/control',
      'can/view/ejs',
      function(){

  can.Control('Cms.Articles',
  { },
  {
    init : function(){
      // draw the initial html
      this.element.html('//cms/articles/views/init.ejs',{});

      // create the articles grid
      new Cms.Articles.Grid(this.find('.grid'));
    },
    " select" : function(el, ev, article){

      // update the articles edit control
      new Cms.Articles.Edit(this.find('.edit'), { 
        model: article 
      });
    }
  });
})

JavaScriptMVC encourages you to organize your application folder as a tree. The leaves of the tree are micro-controls that perform a specific task (such as allowing the editing of videos).

Higher-order controls (cms/articles/articles.js) combine leaves and other nodes into more complex functionality. The root of the application is the application file (cms/cms.js). It combines and configures all high-level widgets.

In general, low-level controls use can.trigger to send messages 'up' to higher-order controls. Higher-order controls typically call methods on lower-level controls

The Cms.Articles control listening to a 'select' event produced by Cms.Articles.Grid and creating (or updating) the Cms.Articles.Edit control is a great example of this.

The situation where this breaks down is usually when a 'state' needs to be shared and communicated across several controls. [can.Observe] and client [can.Model models] are useful for this situation.

Conclusion

This is an extremely abstract article, but hopefully illustrates a few important trends of JavaScriptMVC organization:

  • Put code specific to an app in the application folder.
  • Put reusable plugins, widgets, and other code into library folders.
  • Fill out the tree.
Jump to Line
Something went wrong with that request. Please try again.