Singool.js

Lightweight JavaScript framework for developing single-page web applications

View the Project on GitHub fahad19/singool

Table of contents


Introduction

Singool.js is a lightweight framework built on top of Backbone.js that helps you develop single-page web applications. It has a structure similar to popular server-side MVC frameworks, and supports plugins and themes.

It is released under the MIT License.

Most of the code examples are written in CoffeeScript below. If you want to view them in JavaScript, click here.


Getting started

Let's get Singool up and running!

Requirements

We will require Node.js with npm installed.

Once we have Node, we need to install CoffeeScript and Bower. It is not necessary for you to write your Singool application in CoffeeScript, but we need the cake command to run from command line.

$ npm install -g coffee-script bower

Installation

$ git clone git@github.com:fahad19/singool.git
$ cd singool
$ npm install .
$ cake install

Usage

Singool gives you a number of options to perform build tasks via the cake command.

$ cake

The quickest way to get up and running is by starting a real time server that lets you run your app in the browser as you make changes to your code without having to compile them again manually.

$ cake server
Server running at http://localhost:3000

Demo


Developing with Singool

Configuration

You can specify basic configuration values instructing whether to compress JS/CSS when compiling, what plugins and theme to load, etc from the config file located at /config.js.

File structure

A basic Singool app looks like this:

/public
  /app
    /collections
    /config
      /bootstrap.coffee
      /router.coffee
      /vendors.json
    /helpers
    /lib
    /models
    /templates
    /tests
    /vendors
    /views
  /lib
  /plugins
  /vendors
  /themes

CommonJS

Singool uses a fork of @sstephenson's Stitch which lets us access our JavaScript files as CommonJS modules in the browser.

For example, we have have a directory with two files like this:

/public/js/app
  a.coffee
  b.coffee

Content of a.coffee:

class A
  foo: (bar) ->
    bar

module.exports = A

Now from file b.coffee, we can require() the class A like this:

A = require 'a'

Bootstrap

Bootstrap file located at /public/js/app/config/bootstrap.coffee is responsible for doing anything before your app loads.

It is mainly used for instantiating Router classes like:

AppRouter = require 'config/router'
window.appRouter = new AppRouter

Routes

Router file located at /public/js/app/config/router.coffee is responsible for connecting client-side pages to actions and events.

For example, if you want your app to do something when the user visits http://localhost:3000/#!/foo, your router.coffee file will look something like:

class AppRouter extends require('router')
  
  routes:
    '!/foo': 'foo'
  
  foo: ->
    console.log 'foo page loaded'
  
module.exports = AppRouter

Read more about Backbone.Router here.

Models

If you want an example from a RDBMS point of view, the best way to describe a model would be a record in the table.

For example, a particular User can be a represented by a model at /public/js/app/models/user.coffee

class User extends require('model')
  
module.exports = User

Read more about Backbone.Model here.

Schema

Models have their own schema. This helps at later stages when generating forms or validating user input data against validation logic.

For User model, the schema would look something like:

class User extends require('model')
  
  schema:
    name:
      label: 'Name'
      type: 'text'
    email:
      label: 'Email'
      type: 'text'
    gender:
      label: 'Gender'
      type: 'select'
      options:
        f: 'Female'
        m: 'Male'
      empty: ' - Please choose one - '

module.exports = User

From the above schema, we understand that User has four fields: name, email and gender.

Input types

Model schema in Singool supports various input types:

Default attributes

It is likely sometimes you want models to have some default values when they are first instantiated. You can do so like this:

class User extends require('model')

  defaults:
    name: null
    email: null
    gender: null

module.exports = User

Validation

Model validation can be specified in the schema. For example, if we do not want User model to be saved with the name field left blank, we can:

class User extends require('model')
  
  schema:
    name:
      label: 'Name'
      type: 'text'
      validate:
        notEmpty:
          rule: 'notEmpty'
          message: 'Field cannot be empty.'

module.exports = User

You can also pass your own RegExp as a rule, as well as functions. Function takes (value, attributes, model) as arguments.

Built in validation rules

Model validation works on set() and save():

User = require 'models/user'
user = new User

# set
if user.set attributes
  alert 'successfully set!'
else
  alert 'validation fail'

# save
user.save attributes, 
  error: (model, response) ->
    alert 'validation fail'
  success: (model, response) ->
    alert 'successfully saved!'

You can also check if attributes are valid without having to set() or save():

User = require 'models/user'
user = new User

# check validation
if user.validates(attributes)
  alert 'valid!'
else
  alert 'validation fail :('
  console.log 'validation errors', user.validationErrors

Once validation has failed, it will store the errors in the model's validationrErrors property.

Take a look at form helper section on how it all works together.

Collections

Collections are, well, collections of models. For User model, we can have a collection called Users. A collection is usually a plural form of the model's name.

We can place our Users collection at /public/js/app/collections/users.coffee

class Users extends require('collection')
  
  model: require 'models/user'

module.exports = Users

Read more about Backbone.Collection here.

URL

Location on the server:

class Users extends require('collection')
  
  model: require 'models/user'

  url: '/users'

module.exports = Users

Local Storage

If you choose not to interact with the server, and store data in HTML5 localStorage by using backbone-localstorage.js:

class Users extends require('collection')
  
  model: require 'models/user'

  localStorage: new Store 'users'

module.exports = Users

This will require you to have backbone-localstorage.js file included as a vendor, and we will cover this in the Vendors section later.

Views

Views are used for the logic behind your interface. They themselves do not contain the HTML/CSS, but used for rendering and binding any changes to your elements in the DOM.

For example, we may want to have a view for http://localhost:3000/#!/foo. And we write our View at /public/js/app/views/foo.coffee:

class FooView extends require('view')

  template: require 'templates/foo/index'

  render: =>
    @$el.html @template()
    @

module.exports = FooView

Just in case your are wondering, the template file will be located at /public/js/app/templates/index.underscore. We dill discuss about Templates in later section.

We need to instantiate our view from the Router too so that it is shown when #!/foo page is visited:

class AppRouter extends require('router')
  
  routes:
    '!/foo': 'foo'
  
  foo: ->
    Foo = require 'views/foo'
    fooView = new FooView
    $('#main').html fooView.render().el
  
module.exports = AppRouter

Read more about Backbone.View here.

Templates

Templates are where your HTML comes from. Singool already uses Underscore.js since that is a dependency for using Backbone.js. So it uses its templating system by default. You are of course free to use your own if needed.

From the example of Views above, we needed a template for FooView at /public/js/app/templates/foo/index.underscore:

<p>This is my <strong>HTML</strong> code here</p>

Helpers

Helpers are made available to your Views and Templates for helping with common tasks.

An example helper can be placed at /public/js/app/helpers/foo.coffee:

class FooHelper extends require('helper')

  bar: ->
    "string from helper"

module.exports = FooHelper

They are then made available to your Views like:

class FooView extends require('view')

  helpers:
    foo: require 'helpers/foo'

  render: =>
    stringFromHelper = @foo.bar()
    @

module.exports = FooView

And from your Underscore templates:

<p>The string from helper is: <%= this.foo.bar() %></p>

You can access View from within a Helper like this:

class FooHelper extends require('helper')

  hide: ->
    @view.$el.find('.hide').hide()

module.exports = FooHelper

Singool comes with two helpers built in, and they are always made available to your views and Underscore templates:

HTML helper

Methods:

Form helper

Form helper, as anyone would guess, is used for handling generation of form elements both manually or automatically from Model's schema.

An example of generating a form for User model in a View:

class FooView extends require('view')

  render: =>
    User = require 'models/user'
    @$el.html @template
      model: new User

    @

module.exports = FooView

Now from our template we utilize the Form helper for generating a form according to User model's schema with a submit and reset button:

<%= this.form.create(model) %>
<fieldset>
  <%= this.form.inputs() %>

  <%= this.form.submit() %>
  <%= this.form.reset() %>
</fieldset>
<%= this.form.end() %>

For showing validation errors on form submission from a view:

class FooView extends require('view')

  events:
    'submit form': 'submit'

  submit: (e) =>
    e.preventDefault()
    attributes = @model.extract e.target

    if @model.isNew()
      if !@model.set attributes, (validate: true)
        # validation fail on adding
        @form.showErrors e.target, @model
      else
        @collection.create @model
        window.appRouter.navigate '!/some-other-page', true
    else
      if !(@model.save attributes, 
        error: (model, response) =>
          # validation fail on editing
          @form.showErrors e.target, model
        success: (model, response) ->
          window.appRouter.navigate '!/some-other-page', true)
        @form.showErrors e.target, @model

    false

module.exports = FooView

Methods:

Libraries

Any first-party classes that do not fit in as a View, Model, Collection, Router or Helper and needed to be used as CommonJS (that is, via require() function) is expected to go inside /public/js/app/lib.

Vendors

Vendors are JavaScript code that you want to be loaded in your app, but NOT as CommonJS. A perfect example would be a jQuery plugin.

Vendors are defined in /public/js/app/config/vendors.json file as an array:

["jquery.plugin.js"]

This will then load the jQuery plugin located at /public/js/app/vendors/jquery.plugin.js.

Plugins

Plugins are like self contained mini-apps themselves, only that they are loaded after the main app has been loaded.

Suppose you have a plugin called Tasks at /public/js/plugins/tasks, you need to add it in your config file at /config.js:

var plugins = ['tasks'];

All plugin classes can be accessed by require() function with the plugin named prefixed in the path:

File structure

/public/js/plugins/my_plugin
  /config
    /bootstrap.coffee
    /router.coffee
    /vendors.json
  /collections
  /lib
  /models
  /templates
  /vendors
    /plugin.less
  /views

Themes

Singool is also themeable. It comes with a default themed called, well, default which uses the very cool Twitter Bootstrap. Themes are placed under /public/themes directory.

To load your chosen theme, edit /config.js file:

var theme = 'my_theme';

File structure

/public/themes/my_theme
  /config
    /vendors.json
  /css
    /theme.less
  /layouts
    /index.html
  /vendors

Layout

All themes must come with a layout. The default theme has a layout like this at /public/themes/default/layouts/index.html:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Singool</title>
    <link rel="stylesheet" href="./css/app.css" />
    <script type="text/javascript" src="./js/app.js"></script>
  </head>
  
  <body>
    <header></header>
    <div id="main" class="container"></div>
    <footer></footer>
  </body>
</html>

app.js and app.css files will be compiled and served by Singool, the layout just need to point them.

CSS

Singool uses LESS for its CSS, mainly because of having compatiblity with Twitter Bootstrap. So it expects you to write your CSS in LESS too.

You are of course allowed to write your CSS like the way you do already. You do not have to learn anything new unless you want to explore the features of LESS.

Themes are required to have a LESS file at /public/themes/my_theme/css/theme.less.

JavaScript

Theme's JavaScript files are treated as vendors. To load a custom JS file of your theme, edit the file at /public/themes/my_theme/config/vendors.json

["my_theme.js"]

This will then load the file at /public/themes/my_theme/vendors/my_theme.js


Testing

Singool uses Mocha in combination with Expect.js for testing BDD style.

Run tests in the browser

To run tests, start the testing server:

$ cake test-server
Test server running at http://localhost:3000

Visit http://localhost:3000 from your browser and it will start the Mocha test runner.

If you are using Chrome as your browser, it is best if you run tests from Incognito mode, otherwise the tests may fail because of JavaScript injections by Chrome extensions.

Headless testing

You can also run the tests directly from the CLI using PhantomJS. Make sure you have it in your machine first:

$ brew update && brew install phantomjs

Now you can simply run the tests by:

$ cake test

File structure

/public/js/app/tests
  /cases
    /app.coffee
  /fixtures

Writing tests

An example test case comes with Singool.js at: /public/js/app/tests/cases/app.coffee

describe 'App', ->
  
  foo = 'bar'
  
  it 'test foo', ->
    expect(foo).to.be.a 'string'
    expect(foo).to.eql 'bar'

We first describe our test case at the beginning of the file, then we test whether the variable foo is a string and the string is 'bar'.

Test cases do not require to be exported by module.exports at the end of the file.


Deploying

Build files

Via the cake command, you can compile your app into 3 files (layout, CSS and JS):

$ cake build
Theme layout file written at: /public/index.html
CSS file written at: /public/css/app.css
JS file writen at: /public/js/app.js

Heroku

Deploying a Singool.js app on Heroku is very easy. It already comes with a Procfile at /Procfile.

Create the app on the Cedar stack:

$ heroku create --stack cedar
Creating sharp-rain-871... done, stack is cedar
http://sharp-rain-871.herokuapp.com/ | git@heroku.com:sharp-rain-871.git
Git remote heroku added

Push to Heroku:

$ git push heroku master
...
-----> Launching... done, v2
       http://sharp-rain-871.herokuapp.com deployed to Heroku

Scale the web process:

$ heroku ps:scale web=1