Lightweight JavaScript framework for developing single-page web applications
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.
Let's get Singool up and running!
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
$ git clone git@github.com:fahad19/singool.git
$ cd singool
$ npm install .
$ cake install
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
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
.
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
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 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
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.
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.
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.
class Task extends require('model')
schema:
title:
type: 'text'
label: 'Title'
module.exports = Task
class Task extends require('model')
schema:
description:
type: 'textarea'
label: 'Description'
module.exports = Task
class Task extends require('model')
schema:
mood:
type: 'select'
label: 'Mood'
options:
happy: 'Happy'
sad: 'Sad'
module.exports = Task
class Task extends require('model')
schema:
lucky:
type: 'radio'
label: 'Feeling lucky?'
options:
yes: 'Oh yes!'
no: 'Nope'
module.exports = Task
class Task extends require('model')
schema:
status:
type: 'checkbox'
label: 'Done?'
module.exports = Task
class Task extends require('model')
schema:
address:
type: 'object'
schema:
line1:
type: 'text'
label: 'Line 1'
line2:
type: 'text'
label: 'Line 2'
module.exports = Task
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
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.
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 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.
Location on the server:
class Users extends require('collection')
model: require 'models/user'
url: '/users'
module.exports = Users
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 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 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 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:
Methods:
Set some breadcrumbs in your view:
class FooView extends require('view')
render: =>
@settings.breadcrumbs = [
{title: 'Home', url: '#!/'}
{title: 'Foo', url: '#!/foo'}
]
@
module.exports = FooView
And use it from your Underscore template:
<%= this.html.breadcrumbs() %>
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:
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 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 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:
Router:
router = require 'tasks/config/router'
Models:
task = require 'tasks/models/task'
Collections:
tasks = require 'tasks/collections/tasks'
/public/js/plugins/my_plugin
/config
/bootstrap.coffee
/router.coffee
/vendors.json
/collections
/lib
/models
/templates
/vendors
/plugin.less
/views
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';
/public/themes/my_theme
/config
/vendors.json
/css
/theme.less
/layouts
/index.html
/vendors
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.
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
.
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
Singool uses Mocha in combination with Expect.js for testing BDD style.
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.
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
/public/js/app/tests
/cases
/app.coffee
/fixtures
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.
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
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