|

Using ColtJS in Modular Single Page Applications

A guest post by Jonnie Spratley, who currently works for GE as a UI Developer on the Industrial Internet Team building user interfaces for next generation products, and teaches a variety of programming courses at AcademyX. Check out his earlier AngularJS Directives: Using HTML5 Web Speech post.

Building modular single page applications that are scalable has always been a nontrivial task, but with proper tooling, achieving this is a lot easier than you think.

A new framework called ColtJS allows the easy development of JavaScript applications using Asynchronous Module Definition (AMD). It depends upon the highly popular script loader, RequireJS, and aims to be lightweight, flexible and highly efficient.

coltjs-banner

Overview

There are plenty of fully stacked frameworks out there, but ColtJS aims to give a solid, reliable foundation with a minimal footprint. It is easily expandable with utility modules that plug into the system that are only called when and if they are needed.

The framework uses the standard model-view-controller approach, which is great for separating out logic from individual modules and the systems core, but in order to effectively create a modular application, it’s always best to provide a sandbox that the modules can use.

A sandbox allows for changes of libraries or logic to be completed without having to adjust each and every modules implementation of that logic. Think it of as a proxy which handles common tasks that the modules should be able to perform, without actually writing the logic inside of the module. You just add the sandbox core to the dependencies and invoke the methods as you wish. We will discuss this in detail in this post, but first the concept of MVC should be understood.

Model-View-Controller

Generally, in single page applications you use modules that contain a set of functionality controller, logic model, and template view.

The MVC pattern is composed of three core components:

mvc-diagram

Getting Started

Now let’s start to implement in code, a single page application that has a central sandbox that the modules will use to hook into our sample application.

The Application

The following is the directory structure layout, which is pretty self-explanatory:

app/
    index.html
    modules/
        module_1.coffee
        module_2.coffee
        module_3.coffee
    scripts/
        app.coffee
        bootstrap.coffee
        sandbox.coffee
    styles/
        app.css
    templates/
        module_1.tpl
        module_2.tpl
        module_3.tpl
    utils/
        some_utility.coffee
    README.md

Note: There is yeoman-coltjs on github that will automatically create this.

1. Index

The following is the index.html page that will be the application’s main view:

<!DOCTYPE html>
<html>
    <head>

        <!-- Styles -->
        <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/
          css/bootstrap.min.css" />
        <link rel="stylesheet" href="styles/app.css" />

        <!-- Scripts -->
        <script data-main="scripts/bootstrap" src="//cdnjs.cloudflare.com/ajax/libs/
          require.js/2.1.4/require.js"></script>
    </head>
    <body>

        <!-- Navbar -->
        <div class="navbar navbar-inverse navbar-static-top">
            <div class="container">
                <div class="navbar-header">
                    <a class="navbar-brand" href="#">Dashboard</a>
                </div>
                <div class="collapse navbar-collapse">
                    <ul id="app-nav" class="nav navbar-nav pull-right">
                      </ul>
                </div>
            </div>
        </div>

        <!-- Container -->
        <div class="container">
            <div class="starter-template jumbotron">
                <h1>spa starter template</h1>
                <p class="lead">
                    Use this document as a way to quickly start any new project.
                </p>
            </div>

            <!-- App root element that all modules get injected to. -->
            <div id="app"></div>

            <!-- Footer -->
            <hr/>
            <footer>
                <p>
                    Copywrite 2013
                </p>
            </footer>
        </div>
    </body>
</html>

2. Bootstrap

The following bootstrap.coffee file is the RequireJS configuration file, which specifies the application’s base URL, libraries, and named references to other modules, and then loads the application at the end (app/scripts/bootstrap.coffee):

# RequireJS configuration - Wire up names to dependencies to load.
require.config
  # Base path for scripts
  baseUrl: './'
  paths:
    colt: 'https://dl.dropboxusercontent.com/u/26906414/ge/cdn/colt.min'
    mustache: '//cdnjs.cloudflare.com/ajax/libs/mustache.js/0.7.2/mustache'
    bootstrap: '//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min'
    jquery: '//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.3/jquery.min'
    angular: '//cdnjs.cloudflare.com/ajax/libs/angular.js/1.1.5/angular.min'
    xcharts: 'https://dl.dropboxusercontent.com/u/26906414/cdn/js/xcharts.min'
    d3: 'https://dl.dropboxusercontent.com/u/26906414/cdn/js/d3.min'

    # Named references to modules
    app: 'scripts/app'
    sandbox: 'scripts/sandbox'

#Load the app
requirejs(['app'])

3. Sandbox

The sandbox is created so that the application can be framework-agnostic, as well serve as a single entry point. It is composed of commonly used methods for separating out the logic that is usually performed by single page applications (SPAs).

This SPA needs the following:

The application’s sandbox is just a plain AMD module, and it is going to be the base of the application. This is where we define our dependencies using RequireJS. In the define method we pass in a list of sandbox dependencies that our application will need.

The following module sets up a object with named references to commonly used scripts. Instead of calling these scripts directly in the module, you can call on the sandbox that provides access, which is used for a cleaner implementation (app/scripts/sandbox.coffee):

define ['colt', 'jquery', 'mustache'], (Colt, $, Mustache) ->

  # Sandbox object
  Sandbox =

    # In this case jQuery is our dom library of choice.
    dom: $

    # In this case ColtJS is our framework of choice.
    mvc: Colt

    # Template object, later we could attach a bindable property and use
        Object.observe() to bind data. 
    template:

      # Simple template.render method that takes data and a string then 
          returns them compiled.
      render: (data, html) ->
        return Mustache.to_html(data, html);

    # Init the application with modules.
    init: (@options) ->

      # Set the modules on the mvc
      @mvc.modules = options.modules

      # Invoke the setup method that just adds elements to the root el.
      @setup()

      # Invoke the underlying framework (ColtJS) Colt.init() method.
      @mvc.init(options.modules)

      # Return the sandbox
      return @

    #Setup the UI add a div for each module onto the dom
    setup: () ->
      self = @
      @dom.each(@mvc.modules, (i, o ) -> 
       console.log o 
       name = o.split('/')
       el = self.dom('<div/>').attr('data-view', name[1] )
       self.dom(self.options.el).addClass('row').append(el)
       navEl = self.dom('<a/>').attr('href', '').html(name[1])
       self.dom('#app-nav').append('<li/>');
      )

4. App

The following is the application module. This module depends upon the sandbox which holds the core functionality. We define a set of options and pass it to the Sandbox.init method that initializes the application (app/scripts/app.coffee):

define ['sandbox'], (Sandbox) ->

  # Define options for the application
  options = 
    el: '#app'
    modules: [
      'modules/module_1',
      'modules/module_2',
      'modules/module_3'
    ]

  # Generally you shouldn't do this
  window.App = Sandbox.init(options)

5. Utility

The following is a utility module that the other modules can reference to use the functionality. There is a changeColor method that should change the color of the module (app/utilities/some_utility.coffee):

# Wrap the module in define() + anon function for RequireJS
define ['jquery'], ($) ->
  some_utility = 

    log: (what) ->
      console.log what

    # I will change the color of the module specified
    changeColor: (module, color) ->
      $("##{module}").find('.panel').toggleClass("panel-#{color}")
      console.log "Change #{module} to color #{color}"

  # Return the module object
  some_utility

The Module

The following is how we setup the application’s module. This module is a very basic implementation using a model, a view and controller.

1. Model

This is the model definition for the module. It is a very basic model – the underlying framework has a model class that is implemented here (app/models/module.coffee):

define ['sandbox'], (Sandbox) ->
  ModuleModel = Sandbox.mvc.model(
    name: 'module'
    data: 
      title: 'Module Title'
      content: 'The description'
    url: '/api/v2/modules'
    onchange: (data) ->
      console.log('onchange', data)
    onsync: (data) ->
      console.log('onsync', data)
  )

2. View

The view is composed of standard HTML with the usage of Handlebars tempting (app/templates/module_1.tpl):

<div class="panel panel-default">
  <div class="panel-heading">
    <h3 class="panel-title">{{title}}</h3>
  </div>
  <div class="panel-body">
    {{content}}
  </div>
  <div class="panel-footer">
    <button class="something" class="btn">Click Me</button>
  </div>
</div>

3. Controller

The following is an example of using the sandbox to create a module (app/modules/module_1.coffee):

# Wrap the module in define() + anon function for RequireJS
define ['sandbox', 'models/module'], (Sandbox, model) ->

  #Module definition
  module_1 =
    created: new Date().toString(),
    model: model

    # Specify path to dependencies using RequireJS convention, base path
    dependencies:
      utlis: "utils/some_utility"

    # Bind events
    events:
      "click .panel-heading": "doSomething"

    # Sets the routes in which this module should load
    routes:
      #Display on home page
      "": "renderModule1"
      #Display on this route
      route_to_module: "renderModule1"

      #Display on this route also
      'module1': 'renderModule1'

    # Handles rendering of the module
    renderModule1: ->
      console.log @
      @created = new Date().toString()
      @model.data.content = "I am #{@mid}, I was created #{@created}."

      #Build template
      html = Sandbox.template.render(@template, @model.data)
      @$el.html(html).fadeIn()
      @update()

      Sandbox.mvc.delegateEvents @events, @

    # Example of a method of the module object
    doSomething: (e) ->

      # Console logs the current module scope
      console.log e

      Sandbox.mvc.navigate('module1')

      #Access the module by name
      Sandbox.mvc.access "module_2", (scope) ->
        console.log scope
        alert scope.mid

  # Return the module object
  module_1

Other Controller

The following is an example of not using the sandbox to create a module, instead it uses individual libraries as its dependencies. The implementation is not as clean, and there is not a single point of entry with this way (app/modules/module_3.coffee):

# Wrap the module in define() + anon function for RequireJS
define ["colt", "mustache"], (Colt, Mustache) ->
  module_3 =

    # Specify path to dependencies using RequireJS convention, base path
    dependencies:
      some_utility: "utils/some_utility"

    # Bind events
    events:
      "click .something": "doSomething"

    # Sets the routes in which this module should load
    routes:
      "": "renderModule3"
      module3: "renderModule3"

    # Handles rendering of the module
    renderModule3: ->

      # Setup data for template
      data =
        title: @mid
        content: "I am #{@mid}, I can talk to other modules by using 
           Colt.access( module_name );"

      console.log @

      #Build template
      tmpl = @template
      html = Mustache.render(tmpl, data)
      elem = document.querySelectorAll("[data-view=\"" + @mid + "\"]")

      #Inject template
      elem[0].innerHTML = html
      elem[0].style.display = "block"

      Colt.delegateEvents @events, @

    # Example of a method of the module object 
    doSomething: (e) ->

      # Console logs the current module scope
      console.log this, e
      Colt.navigate('module3')
      self = @
      Colt.access "module_2", (scope) ->
        console.log scope
        self.some_utility.changeColor('module_2', 'success')

  # Return the module object
  module_3

Summary

Now that you have the knowledge about how to structure and create a modular AMD application, you can use this as a starting point for development of larger scale applications. There is always ways to improve the application and structure, so it is up to the developer to choose the correct structure and layout when creating single page applications.

Example

You can view a live example over at Plunkr, as well as checkout the generators source code over at github.

coltjs-spa

Be sure to look at the following resources in Safari Books Online.

Not a subscriber? Sign up for a free trial.

Safari Books Online has the content you need

Instant Dependency Management with RequireJS How-to will guide you on how to improve the performance and maintainability of your JavaScript applications with RequireJS. You will learn simple to advanced techniques for converting your JavaScript application to an AMD workflow.
Backbone.js Cookbook contains a series of recipes that provide practical, step-by-step solutions to the problems that may occur during front-end application development using an MVC pattern. You will learn how to build Backbone applications utilizing the power of popular Backbone extensions and integrating your app with different third party libraries. You will also learn how to fulfill the requirements of the most challenging tasks.
The Twitter Flight Edge was written for the curious Javascript developer who learns new libraries by hacking on examples or personal projects. We walk through developing a simple sample application – an RSS reader – using Twitter Flight and supporting libraries. Everything from basic components like tabs to remote data fetching is covered, highlighting the benefits Flight offers for event-driven applications.

About the author

jonnie Jonnie Spratley is currently working for GE as a UI Developer on the Industrial Internet Team building user interfaces for next generation products. He also teaches a variety of programming courses at AcademyX, and can be reached at @jonniespratley.

About Safari Books Online

Safari Books Online is an online learning library that provides access to thousands of technical, engineering, business, and digital media books and training videos. Get the latest information on topics like Windows 8, Android Development, iOS Development, Cloud Computing, HTML5, and so much more – sometimes even before the book is published or on bookshelves. Learn something new today with a free subscription to Safari Books Online.
|

Comments are closed.