This is a follow up to Chicago Boss and Backbone.js.
We use Chicago Boss as an MVC framework and SpineJS on the client side to bind the models to the DOM. We think this is a really great setup and are building our future projects with this architecture.
In this blog post we want to show you how to get up and running with CB and Spine!
First of all, make sure you have Chicago Boss installed on your computer. Then create a new CB app:
make app PROJECT="todomanager"
It should now set up a skeleton project under ../todomanager.
This is what my output looks like:
==> ChicagoBoss-0.6.10 (create)
Writing ../todomanager/start-dev.sh
Writing ../todomanager/start.sh
Writing ../todomanager/start-server.bat
Writing ../todomanager/Makefile
Writing ../todomanager/todomanager.app.src
Writing ../todomanager/boss.config
Writing ../todomanager/src/mail/todomanager_outgoing_mail_controller.erl
Writing ../todomanager/src/mail/todomanager_incoming_mail_controller.erl
Writing ../todomanager/priv/init/todomanager_01_news.erl
Writing ../todomanager/priv/static/chicago-boss.png
Writing ../todomanager/priv/static/favicon.ico
Writing ../todomanager/priv/todomanager.routes
Cool. Now we are ready to start setting up spine. The easiest way to get Spine is to use npm (Node Package Manager). If you don’t have npm yet, you can get it with this sweet one-liner:
curl http://npmjs.org/install.sh | sh
Once we have that, we should install spine.app. It takes care for us of setting up all the required skeletons for our JS app. We can also already install hem, we will need that later:
npm install -g hem spine.app
After npm has done its magic, set up a spine project. Execute this in the todomanager folder:
spine app frontend
cd frontend
npm install
This creates a lot more files and installs all npm dependencies. But bear with me.
We will use Hem to bundle and package all these files created by Spine. Hem is a pretty neat little packaging tool written by Alex MacCaw, the creator of Spine. What it does is bundle all your Javascript and CSS resources and puts them into one single JS and CSS file. Pretty neat, huh?
When you look at the files created, you should see a file called slug.json. Open it.
This is the configuration file for hem. We now need to change some settings, so that spine plays along nicely with our Chicago Boss setup.
It should look like this:
{
"dependencies": [
"es5-shimify",
"json2ify",
"jqueryify",
"spine",
"spine/lib/local",
"spine/lib/ajax",
"spine/lib/route",
"spine/lib/tmpl",
"spine/lib/manager"
],
"libs": []
}
Change the file, so that it looks like this:
{
"dependencies": [
"es5-shimify",
"json2ify",
"jqueryify",
"spine",
"spine/lib/local",
"spine/lib/ajax",
"spine/lib/route",
"spine/lib/tmpl",
"spine/lib/manager"
],
"libs": [],
"public": "../priv/static"
}
This tells hem to put the generated javascript and css file into the publicly accessible folder of Chicago Boss. Give it a try by running
hem build
You should see those files being added at the right location.
Let’s see what we want to achieve:
We will start by setting up the Chicago Boss model.
Create a file src/model/todo.erl:
-module(todo, [Id, Subject, Done]).
-compile(export_all).
That’s our model. Chicago Boss takes care of all the remaining magical functions like persisting etc. Cool, huh?
Now we create the todo REST resource.
Create the controller src/controller/todomanager_todo_controller.erl:
-module(todomanager_todo_controller, [Req]).
-compile(export_all).
-default_action(index).
%%
%% List
%%
%% GET todo/index
%%
index('GET', []) ->
Todos = boss_db:find(todo, []),
case Todos of
[] ->
{output, <<"[]">>, [{"Content-Type", "application/json"}]};
_Else ->
{json, Todos}
end;
%%
%% Read
%%
%% GET todo/index/todo-1
%%
index('GET', [Id]) ->
Todo = boss_db:find(Id),
% TODO for some reason, when we have a non-existent todo, we still output the json
% data and don't jump to the not_found section.
case Todo of
Todo ->
{json, [{todo, Todo}]};
[] ->
not_found
end;
%%
%% Create
%%
%% POST todo/index
%%
index('POST', []) ->
Body = element(2, mochijson:decode(Req:request_body())),
io:format("~p", [proplists:get_value("subject", Body)]),
Todo = todo:new(id, proplists:get_value("subject", Body), false),
%io:format("~p", [Todo]),
{json, [{todo, element(2, Todo:save())}]};
%%
%% Update
%%
%% PUT todo/index/123
%%
index('PUT', [Id]) ->
Todo = boss_db:find(Id),
Body = element(2, mochijson:decode(Req:request_body())),
%% Set the new values
NewTodo = Todo:attributes([
{subject, proplists:get_value("subject", Body)},
{done, proplists:get_value("done", Body)}
]),
{json, [{todo, element(2, NewTodo:save())}]}.
This creates all our required end points for our REST controller.
However, we also need an index controller in Chicago Boss to deliver our required initial HTML, so that our client-side Spine app can actually start working.
So create this index controller in src/controller/todomanager_index_controller.erl:
-module(todomanager_index_controller, [Req]).
-compile(export_all).
index('GET', []) ->
{ok, []}.
We also need the view script. It is placed in src/view/index/index.html:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/static/application.css">
<title>My sweet Erlang Todo app</title>
</head>
<body>
<div id="app">
<h1>Todos:</h1>
<form action="/todo/create" method="post">
<input type="text" name="subject" placeholder="Enter your todo item">
<input type="submit" value="Save">
</form>
<ul class="items">
</ul>
<footer>
<span class="countVal">0</span> todos left.
</footer>
</div>
<script type="text/javascript" src="/static/application.js"></script>
<script type="text/javascript">
$ = require("jqueryify");
App = require("index");
$(function() {
new App({el: $("#app")});
});
</script>
</body>
</html>
This will deliver our initial HTML, include the files generated by hem and bootstrap our app.
Cool! By now we are pretty much done with our Erlang setup.
So now we can start writing our Spine app.
Spine apps are built in a pretty similar fashion. You have models, views and controllers.
So let’s start by writing our client-side model. We will place this file in frontend/app/models/todo.coffee:
Spine = require("spine")
class Todo extends Spine.Model
@configure "Todo", "id", "subject", "done"
@extend Spine.Model.Ajax
@url: "/todo/index"
@active: ->
@select (item) -> !item.done
@done: ->
@select (item) -> !!item.done
module.exports = Todo
By calling
@extend Spine.Model.Ajax
@url: "/todo/index"
we tell Spine to persist via AJAX to a REST resource located at “/todo/index”.
Let’s create our todo controller /frontend/app/controllers/todo.coffee
In Spine it is best practice when you create a controller for each “widget” that we have on our side.
When looking at our todo app, we have two “widgets”. One to contain the list of todos, and then one widget per each todo, that takes care of all the events for this single todo.
So we have to create two controllers in our todo controller file. First we have to import some dependencies:
Spine = require("spine")
$ = Spine.$
Model = require("models/todo")
This is our TodoItem widget controller:
class TodoItem extends Spine.Controller
events:
"change input[type=checkbox]": "toggle"
"dblclick": "edit"
"blur input[type=text]": "close"
"keypress input[type=text]": "blurOnEnter"
elements:
"input[type=text]": "input"
constructor: ->
super
@item.bind("update", @render)
@item.bind("destroy", @remove)
render: =>
@replace $(require("views/todo/todo")(@item))
@
toggle: ->
@item.done = !@item.done
@item.save()
edit: ->
@el.addClass("editing")
@input.focus()
blurOnEnter: (e) ->
if e.keyCode is 13 then e.target.blur()
close: ->
@el.removeClass("editing")
@item.updateAttributes({subject: @input.val()})
remove: =>
@el.remove()
Let’s look at some interesting parts here:
events:
"change input[type=checkbox]": "toggle"
"dblclick": "edit"
"blur input[type=text]": "close"
"keypress input[type=text]": "blurOnEnter"
This binds dom events to certain actions in our controller. Spine takes care for us of attaching these events to our dom.
render: =>
@replace $(require("views/todo/todo")(@item))
@
This renders our todo view and passes it our todo item as parameter so we can display some of its data.
class Todo extends Spine.Controller
events:
"submit form": "create"
"click .clear": "clear"
elements:
".items": "items"
"form input": "input"
".countVal": "count"
constructor: ->
super
@log "Initialited"
Model.bind "create", @addOne
Model.bind "refresh", @addAll
Model.bind "refresh change", @renderCount
Model.fetch()
# Add a single todo item
addOne: (todo) =>
view = new TodoItem(item: todo)
@items.append view.render().el
# After a refresh
addAll: =>
Model.each @addOne
# Create a new todo
create: (e) ->
e.preventDefault()
@log @input.val()
Model.create(subject: @input.val())
@input.val ""
renderCount: =>
active = Model.active().length
@count.text(active)
module.exports = Todo
This is the remaining controller that takes care of managing all our TodoItem instances.
We also need our todo view script /frontend/app/views/todo/todo.eco:
<li id="<%= @id %>">
<input type="checkbox" name="todo[]" value="<%= @id %>"
id="checkbox-<%= @id %>" <% if @done: %>checked="checked"<% end %>>
<label for="checkbox-<%= @id %>"><%= @subject %></label>
<div class="edit">
<input type="text" value="<%= @subject %>">
</div>
</li>
Remaining bootstrap code /frontend/app/index.coffee:
require('lib/setup')
Spine = require('spine')
Todo = require("controllers/todo")
module.exports = Todo
Some basic CSS /frontend/css/index.styl:
@import './mixin'
body, html
font-family:Georgia
font-size:25px
background:#f0f0f0
margin:0
padding:0
#app
margin:50px auto
padding:2em
width:500px
background:#fff
border-radius:5px
box-shadow:0px 0px 10px rgba(0,0,0,0.4)
h1
text-align:center
input[type=text]
font-size:25px
width:500px
padding:8px 0
input[type=submit]
display:none
ul
list-style:none
padding-left:0
ul li
padding:10px 0
border-bottom:solid 1px #ddd
ul li input
position:relative
top:-4px
label:hover
cursor:pointer
li .edit
display:none
li:hover
background:lightyellow
li.editing .edit
display:block
li.editing label,
li.editing input[type=checkbox]
display:none
And we are ready to give it a test run!
Execute hem watch in our frontend folder, and ./start-dev.sh in the todomanager root dir.
Then open a browser and navigate to “localhost:8001” and you should see our todo app. You can add entries, double click existing entries to edit them and mark todos as done.
You can find the existing source code on Github.
I hope this was helpful for you! I appreciate any feedback :-)