This example will show you how to use Spine, Hem and Eco to build a basic contacts manager.
Some of the source will be omitted for the sake of brevity (such as the CSS). You can find the complete source on GitHub, as well as a live demo. This is what we’ll end up with:
Before we get started, I advise you to do the following:
Now you’ve got all that under your belt, let’s think about what we need in this Contact’s application from a high level architectural standpoint.
Basically your classic CRUD methods. Let’s get started!
Firstly, to make life easier, we’re going to install Spine.app and Hem. Spine.app is a Spine application generator. It’s not required to use Spine, but very useful all the same. Hem is bit like Bundler for JavaScript apps, see their respective guides for more information.
If you haven’t got them installed already, you’ll need Node and npm. Both projects’ sites include excellent installation instructions. Now we can get on with installing the two npm modules we need, spine.app
and hem
:
npm install -g spine.app hem
Now we’ve got an executable called spine
which we can use to generate new applications.
spine app contacts
cd contacts
Check out the article on Spine.app for more information concerning its usage. Now let’s install the default dependencies listed in our application’s package.json
:
npm install .
Finally we can use the hem
executable to run a Hem server, which will temporarily host our Spine application during development.
hem server
Now our server is running, let’s open up the application:
open http://localhost:9294
You’ll see Spine’s default welcome screen introducing you to the framework. Let’s remove that before we go any further. Open up public/index.html
, and remove the ‘getting started’ script tag:
<!-- Getting started script - should be removed -->
<script src="http://maccman-spine.herokuapp.com/start.js" type="text/javascript" charset="utf-8"></script>
Refresh the page, and it should be blank.
Let’s generate the basic models and controller’s our applications going to need. Firstly, a Contact
model:
spine model contact
This will generate a model under app/models/contact.coffee
which will come in handy later. Then let’s generate three controllers, Contacts
, Main
and Sidebar
.
spine controller contacts
spine controller contacts_main
spine controller contacts_sidebar
These controller’s will be created under app/controllers.
We could put all the controllers inside one file, but by splitting them up we’re de-coupling them, ensuring our code is clear and doesn’t descend into Spaghetti hell.
So earlier we generated a Contact
model. Let’s flesh that out, and add some functionality. Replace app/models/contact.coffee
with the following:
Spine = require('spine')
class Contact extends Spine.Model
# Configure name & attributes
@configure 'Contact', 'name', 'email'
# Persist with Local Storage
@extend @Local
@filter: (query) ->
return @all() unless query
query = query.toLowerCase()
@select (item) ->
item.name?.toLowerCase().indexOf(query) isnt -1 or
item.email?.toLowerCase().indexOf(query) isnt -1
module.exports = Contact
Ok, so that need’s some explaining. Let’s take that apart piece by piece. Firstly, we’re calling configure()
, passing in the name of the model and its attributes. This is something you’ll need to do whenever you create a model, and it should be done immediately before anything else inside the model. As you can see, our Contact
model is going to have two attributes, a name
and an email
.
The next line ensures our model is persisted with HTML5 Local Storage. Spine’s Local Storage module is included by default in our generated application, so this line is all that’s required to make sure that model data is persisted between page reloads.
The last part of the model is a class (static) function called filter()
. This takes a query string and returns an array of contacts that match that string, comparing both the email address and name of each contact stored in the model. This function will come in handy in our sidebar, allowing us to filter the list of contacts easily.
Lastly the model is exported, so it’s available to be required from other modules.
Right, now our model is setup we can move onto the controllers. Let’s tackle the Main
controller first. Replace the contents of app/controllers/contacts_main.coffee
with the following:
Spine = require('spine')
Contact = require('models/contact')
$ = Spine.$
That sets up some variables we’re going to use, such as the Contact
model and jQuery.
The main section of our application is going to have two main ‘views’, a show view and an edit view. This will translate into two controller’s Show
and Edit
respectively. We want to make sure only one of these controller is shown at any one time, so we’re going to use a Stack to manage them.
So let’s go ahead and implement the Show
controller, appending the following to app/controllers/contacts_main.coffee
:
class Show extends Spine.Controller
# Set the HTML class
className: 'show'
events:
'click .edit': 'edit'
constructor: ->
super
# Bind the change() callback
# to the *active* event
@active @change
render: ->
# Render a template, replacing the
# controller's HTML
@html require('views/show')(@item)
change: (params) =>
@item = Contact.find(params.id)
@render()
edit: ->
# Navigate to the 'edit' view whenever
# the edit link is clicked
@navigate('/contacts', @item.id, 'edit')
So, the first property in Show
is className
, which set a class of show
on the controller’s internal HTML element. This will help us style the controller later.
Next we’re setting up some events, specifically a click event on any element with a class of edit
. In this case, our template is going to contain a <a class="edit">Edit</a>
link which, when clicked, will invoke the controller’s edit()
function, navigating to the edit route.
In the controller’s constructor, we’re binding to the active event, specifying the change()
function as a callback. We’re going to pass some router params when triggering the event, which will be used by our callback to find the relevant contact, and then re-render the view. This part will make more sense when you see how the controller is activated later on in the tutorial.
Notice in the render()
function we’re requiring a template under views/show
, calling it and passing in the current item. Let’s go ahead now and define that template. Create a file under app/views/show.eco
containing the following:
<header>
<a class="edit">Edit</a>
</header>
<div class="content">
<p><%%= @name %></p>
<p><%%= @email %></p>
</div>
This is an eco template, and the syntax inside the template tags (<%%= %>
) is CoffeeScript. We’re pulling out the name
and email
properties from the contact, displaying them in the page. We’ve also got that edit link we talked about earlier, ready to be clicked.
Now we’ve defined our Show
controller which will show us information about the selected contacts, we can go ahead and define an Edit
controller for updating contacts. Append the following to app/controllers/contacts_main.coffee
.
class Edit extends Spine.Controller
className: 'edit'
events:
'submit form': 'submit'
'click .save': 'submit'
'click .delete': 'delete'
elements:
'form': 'form'
constructor: ->
super
@active @change
render: ->
@html require('views/form')(@item)
change: (params) =>
@item = Contact.find(params.id)
@render()
submit: (e) ->
e.preventDefault()
@item.fromForm(@form).save()
@navigate('/contacts', @item.id)
delete: ->
@item.destroy() if confirm('Are you sure?')
In a lot of ways, this is really similar to the Show
controller. We’re binding to the active active event with a change()
callback, rendering a template with the appropriate contact context. The only real difference here is there’s a few more events, mostly dealing with the controller’s form. When the form submits, the submit()
callback will be invoked and the item updated from the form’s inputs (using fromForm()
).
Again, we’re requiring a template under views/form
, rendering it with the current contact (@item
). Let’s define that template under app/views/form.eco
:
<header>
<a class="save">Save</a>
<a class="delete">Delete</a>
</header>
<div class="content">
<form>
<label>
<span>Name</span>
<input type="text" name="name" value="<%%= @name %>">
</label>
<label>
<span>Email</span>
<input type="email" name="email" value="<%%= @email %>">
</label>
<button>Save</button>
</form>
</div>
It’s pretty self explanatory; again we’re just pulling out properties from the current context using the <%%= %>
syntax.
The last step for our Main
controller, is to define a stack that will manage our other two controllers, Show
and Edit
. Both controllers, Show
and Edit
, need to be shown independently one at a time. Adding both controllers to a Spine Stack will ensures this happens automatically.
class Main extends Spine.Stack
controllers:
show: Show
edit: Edit
module.exports = Main
The last line exports the controller, so it’s available to other modules (see the CommonJS guide for more information). The full source for this controller is available on GitHub.
The Sidebar
controller is going to list contacts and let users filter them by name and email. Additionally users’s can select a contact, which is then displayed in the main view.
Although this controller is fairly large, it’s pretty straightforward. Let’s take a look at the full code, and then I’ll explain it in detail. Replace app/controllers/contacts_sidebar.coffee
with the following:
Spine = require('spine')
Contact = require('models/contact')
List = require('spine/lib/list')
$ = Spine.$
class Sidebar extends Spine.Controller
className: 'sidebar'
elements:
'.items': 'items'
'input[type=search]': 'search'
events:
'keyup input[type=search]': 'filter'
'click footer button': 'create'
constructor: ->
super
# Render initial view
@html require('views/sidebar')()
# Setup a Spine List
@list = new List
el: @items,
template: require('views/item'),
selectFirst: true
@list.bind 'change', @change
@active (params) ->
@list.change(Contact.find(params.id))
Contact.bind('refresh change', @render)
filter: ->
@query = @search.val()
@render()
render: =>
contacts = Contact.filter(@query)
@list.render(contacts)
change: (item) =>
@navigate '/contacts', item.id
create: ->
item = Contact.create()
@navigate('/contacts', item.id, 'edit')
module.exports = Sidebar
Ok, so that’s a fair amount of code that needs some explaining. In a nutshell, the Sidebar
controller sets up a Spine List, populates with contacts and handles filtering.
The elements
property is setting up some references to various HTML elements we’ll need subsequently in the class. The format is {elementSelector: propertyName}
, and ensures that references to properties such as @items
and @search
point to the appropriate elements.
The events
property sets up some event delegation, namely the contact filtering when users type in a search input, and responding to a click event on a ‘contact create’ button. The two callbacks referenced, @filter()
and @create()
are pretty self explanatory; @filter()
merely pulling out a query from the search input, and re-rendering the list, whilst @create()
makes a blank contact, navigating to the ‘edit’ route.
The Sidebar
‘s constructor is where all the magic happens, and where the list of contacts is setup. The first step is to replace the controller’s HTML with the views/sidebar
template (which we’ll define later). Next, we’re instantiating a Spine List, passing in the .items
element reference, the list template and some additional options. The list will take care of rendering and selecting contacts, we just have to add a change event listener onto it to know when a user selects a different contact.
Also notice we’re binding to two events, refresh and change on the Contact
model. These will be fired whenever the model fetches its records (i.e. on startup), and whenever a record changes (i.e. is created, updated or destroyed). Whenever these two events fire, we’re just going to redraw the whole list, reflecting model changes in the view.
Two templates were mentioned in the controller, let’s tackle the first, views/sidebar
. This is just going to contain a search input, a container for our list and a button to create new contacts. Replace app/views/sidebar.eco
with the following:
<header>
<input type="search" placeholder="search" results="0" incremental="true" autofocus>
</header>
<div class="items"></div>
<footer>
<button>New Contact</button>
</footer>
The other template is the views/item
template, used by the List for rendering list items. Replace app/views/item.jeco
with the following:
<div class="item">
<%%= @name or "<i>No Name</i>" %>
</div>
Notice we’re using the .jeco
extension for this template, rather then .eco
. This is a Hem specific extension to Eco, which allows us to associate data with template items, something the Spine List class requires.
The full source for this controller, and all its templates, is available on GitHub.
So we’ve got a Sidebar
controller for listing contacts, and a Main
controller for showing/editing them. The last step is to tie these two together by using Spine’s routes. This we’ll do in the Contacts
controller; replace app/controllers/contacts.coffee
with the following:
Spine = require('spine')
Contact = require('models/contact')
$ = Spine.$
Main = require('controllers/contacts_main')
Sidebar = require('controllers/contacts_sidebar')
class Contacts extends Spine.Controller
className: 'contacts'
constructor: ->
super
@sidebar = new Sidebar
@main = new Main
@routes
'/contacts/:id/edit': (params) ->
@sidebar.active(params)
@main.edit.active(params)
'/contacts/:id': (params) ->
@sidebar.active(params)
@main.show.active(params)
@append @sidebar, @main
Contact.fetch()
module.exports = Contacts
Right, so that’s a shorter controller than the previous two we implemented, lets have a look at what it’s up to. Firstly you’ll notice that we’re requiring in our other two controller’s Main
and Sidebar
. In the Contacts
constructor these are instantiated, saved as local variables and ultimately appended to the controller.
We’re also setting up some routes, /contacts/:id
and /contacts/:id/edit
. Spine will listen to changes in the URL’s hash, and invoke the matched route callbacks. For example, if the user navigates to #/contacts/1
, then the /contacts/:id
route will be activated, and the Show
controller activated. We’re passing through the route params, so our controllers change()
callbacks can pull out the contact’s ID, and match it up with one from the model.
Lastly we’re calling Contact.fetch()
, which fetches all the contacts out of local storage, populating the Contact
model.
So our contacts app is now finished, and has all the functionality we need for listing, creating and updating contacts. So how do we actually go about instantiating it, adding it to the page? Well this is where the App
controller comes in, Spine’s main controller. This is automatically instantiated and appended to the document’s body when the page loads.
All we need to do is instantiate Contacts
controller, appending it to App
. Replace app/index.coffee
with the following:
require('lib/setup')
Spine = require('spine')
Contacts = require('controllers/contacts')
class App extends Spine.Controller
constructor: ->
super
@contacts = new Contacts
@append @contacts
Spine.Route.setup()
module.exports = App
Awesome! Refresh the page, and you should see your working contacts application (albeit unstyled). Remember, you can clone the complete source from GitHub if you run into any difficulties, as well as copy some attractive CSS.
Congratulations if you’ve got this far. We’ve explored a lot of Spine, and you should have a good handle on the framework now. You’re all set to go off and create your own applications.