This is a second part of a Backbone.js and Slim PHP framework. In the first part of the tutorial we’ve dealt with creating server side (using Slim) and now we’ll get started with client side (Backbone part of the story).
Table of Contents
Server side modifications
First we need to modify some bits of our PHP code. We need to modify GET method for fetching tasks to be able to accept project ID and return only tasks tied to specific project. Route won’t be modified, only the code inside the method.
[code lang=”php”]
$projectId = $app->request()->get(‘project’);
$data = array();
/*
* We’re fetching tasks and filling an array that we’ll return to the client
*/
foreach ($db->task()->where(‘project_id’, $projectId) as $task) {
$data[] = array(
‘id’ => $task[‘id’],
‘task’ => $task[‘task’],
‘project_id’ => $task[‘project_id’],
‘date_created’ => $task[‘date_created’],
‘date_due’ => $task[‘date_due’],
‘status’ => $task[‘status’]
);
}
[/code]
Beside that we need to modify return values on both objects (task and project) on POST and PUT methods. Earlier we were returning just the object ID, but if we return the whole object as an array, Backbone will automatically update our newly created object/model with ID (only the echo statement is modified):
[code lang=”php”]
echo json_encode((array)$data->getIterator());
[/code]
HTML markup
We’ll use Twitter Bootstrap to create user interface to save ourselves time and frustration of designing and coding it from scratch. We’ll also use additional date-time picker as Twitter Bootstrap doesn’t have one. My pick is http://tarruda.github.io/bootstrap-datetimepicker/.
User interface will look similar to:
Project list and “Add project” button on the left column, task list, “Add task” button and delete project button in the right column.
Both “Add project” and “Add task” buttons will open Twitter Bootstrap modal dialog window, edit form (dialog) will be opened on double click (either on project name or task title/due date).
Full markup without any Backbone or JS parts can be found in /client/markup.html file.
Now nothing stands in a way between us and the Backbone.
I won’t go through what Backbone is and why and where you should use it. If you are reading this article then you already know this stuff. Download it from the official Backbone website. As it is dependent on Underscore.js don’t forget to download it also (bet you already encountered Underscore). One resource for Backbone that I found very useful (beside Backbone website and of course Stackoverflow) is http://addyosmani.github.io/backbone-fundamentals/.
After dividing our initial markup into views/templates (Underscore.js templates) that we’ll use in our view objects, our /client/index.html looks like this:
[code lang=”html”]
<!DOCTYPE html>
<html lang="en">
<head>
<title>Tasks</title>
<link href="css/bootstrap.min.css" rel="stylesheet">
<link href="css/bootstrap-datetimepicker.min.css" rel="stylesheet">
<link href="css/tasks.css" rel="stylesheet">
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-inner">
<div class="container-fluid">
<a class="brand" href="#">Tasks</a>
</div>
</div>
</div>
<div class="container-fluid">
<div class="row-fluid">
<div class="span3">
<div class="well sidebar-nav">
<ul class="nav nav-list" id="projects">
<li class="nav-header">Projects</li>
</ul>
</div>
<a href="#new-project" id="add-project" class="btn pull-right"><i class="icon-folder-open"></i>&nbsp;&nbsp;Add project</a>
</div>
<div class="span9 main-content">
<div class="page-header">
<div class="row-fluid">
<div class="span9">
<h3 id="project-title"><span>Project</span> tasks</h3>
</div>
<div class="span3">
<a href="#new-task" id="add-task" role="button" class="btn pull-right" data-toggle="modal"><i class="icon-tasks"></i>&nbsp;&nbsp;Add task</a>
<a href="#delete-project" id="remove-project" role="button" class="btn pull-right offset-right"><i class="icon-remove"></i></a>
</div>
</div>
</div>
<table class="table table-hover table-striped">
<thead>
<tr>
<th class="status-col">Status</th>
<th>Task</th>
<th class="date-col">Due date</th>
<th class="action-col">&nbsp;</th>
</tr>
</thead>
<tbody id="tasks">
</tbody>
</table>
</div>
</div>
</div>
<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script src="js/bootstrap.min.js"></script>
<script src="js/bootstrap-datetimepicker.min.js"></script>
<script type="text/javascript" src="js/underscore.min.js"></script>
<script type="text/javascript" src="js/backbone.min.js"></script>
<script type="text/javascript" src="js/tasks.js"></script>
<script type="text/template" id="project-dialog">
<div class="modal fade in" tabindex="-1" role="dialog" aria-labelledby="Create project" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">x</button>
<h3 id="myModalLabel">Create project</h3>
</div>
<div class="modal-body">
<form class="form-inline">
<input type="text" name="name" class="input-block-level project-title" placeholder="Project name" value="<%= name %>">
</form>
</div>
<div class="modal-footer">
<button class="btn close-action" data-dismiss="modal" aria-hidden="true">Cancel</button>
<button class="btn btn-primary save-action">Save</button>
</div>
</div>
<div class="modal-backdrop fade in"></div>
</script>
<script type="text/template" id="project-item">
<a href="#<%= id %>" data-id="<%= id %>"><%= name %></a>
</script>
<script type="text/template" id="task-dialog">
<div class="modal in fade" tabindex="-1" role="dialog" aria-labelledby="Create task" aria-hidden="true">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">x</button>
<h3 id="myModalLabel">Create task</h3>
</div>
<div class="modal-body">
<form class="form-inline">
<input type="text" name="task" class="input-xlarge input-task" placeholder="Task name" value="<%= task %>">
<div id="dp1" class="input-append date">
<input class="input-medium" name="date_due" value="<%= date_due %>" data-format="yyyy-MM-dd hh:mm:ss" type="text" placeholder="Due date">
<span class="add-on">
<i data-time-icon="icon-time" data-date-icon="icon-calendar"></i>
</span>
</div>
</form>
</div>
<div class="modal-footer">
<button class="btn close-action" data-dismiss="modal" aria-hidden="true">Cancel</button>
<button class="btn btn-primary save-action">Save</button>
</div>
</div>
<div class="modal-backdrop fade in"></div>
</script>
<script type="text/template" id="task-item">
<td class="center-col">
<input type="checkbox" name="status" value="1"<% if (status == 1) { %> checked="checked" <% } %> />
</td>
<td<% if (status == 1) { %> class="finished" <% } %>><%= task %></td>
<td<% if (status == 1) { %> class="finished" <% } %>><%= date_due %></td>
<td class="center-col">
<a href="#delete" class="delete-action">
<i class="icon-remove"></i>
</a>
</td>
</script>
</body>
</html>
[/code]
Most of it is a plain HTML so I won’t describe it in detail. Interesting parts here are the Underscore templates. There are four templates:
- project dialog – modal dialog with create/edit form
- project item – simple project output markup that will be located in UL element
- task dialog – modal dialog with create/edit form
- task item – table row contents for single task output, will be placed in TR and TABLE elements
There are two interesting things in templates, first being the variable output, <%= VARIABLE-NAME %> which will output the value of variable. We’ll push the variables (entire model) when initializing/rendering template. The second one (only in task-item template) is the if statements (<% if (status == 1) { %> checked=”checked” <% } %>). As you can see, “normal” javascript code is embedded in <% and %>.
Other notable parts of HTML markup are the buttons and UL and TABLE element where our functionalities will be attached (ul#projects, a#add-project, a#add-task, a#remove-project and tbody#tasks).
Backbone.js models
Now lets get started with Backbone part, all of our code can be found at /client/js/tasks.js file. File is pretty much well documented which should give you the details. We’ll cover main aspects and Backbone concepts in more detail but for coding details be sure to check comments.
Lets discuss what we need to accomplish first. We need to be able to create new projects, add tasks to them, delete tasks, modify tasks, deleting project (and its tasks) and modify project. We also need to be able to display tasks for selected project.
OK, first we are declaring and initializing some variables which aren’t directly connected to Backbone functionalities.
After that we are creating our first Backbone model (Backbone.Model.extend) and defining two main parts – default values (which will be used in the process of creating new task and also to let the Backbone know how are object looks like) and URI at which Backbone will be doing the CRUD operations. Beside defining models we are also defining collection (Backbone.Collection.extend) which will hold our items (and will be communicating with view objects).
[code lang=”javascript”]
/*
* This is our Backbone model representation (as you can see attributes are the same as in the database table)
*/
Task = Backbone.Model.extend({
/*
* We need to define default values which will be used in model creation (and to give Backbone some info what our model look like)
*/
defaults: {
id: null,
date_created: today.getFullYear() + ‘-‘ + (1 + today.getMonth()) + ‘-‘ + today.getDate() + ‘ ‘ + today.getHours() + ‘:’ + today.getMinutes() + ‘:’ + today.getSeconds(),
date_due: tomorrow.getFullYear() + ‘-‘ + (1 + tomorrow.getMonth()) + ‘-‘ + tomorrow.getDate() + ‘ ‘ + tomorrow.getHours() + ‘:’ + tomorrow.getMinutes() + ‘:’ + tomorrow.getSeconds(),
project_id : null,
status: 0,
task: ”
},
/*
* This is the URI where the Backbone will communicate with our server part
*/
urlRoot: serverUrl + ‘task’
});
/*
* We also need a collection object which will hold our models (so it will be able to communicate with Backbone views more easily)
*/
TaskCollection = Backbone.Collection.extend({
/*
* Model which this collection will hold and manipulate
*/
model: Task,
/*
* URI for fetching the tasks
*/
url: serverUrl + ‘task’
});
[/code]
Backbone.js view objects
The most important part of our application are the view objects which are defining and consuming events which transforms and persist our data to database. There are three type of views in our tasks application – list view, item view and form modal dialog view (every model has one which makes 6 view total).
[code lang=”javascript”]
ProjectItem = Backbone.View.extend({
/*
* Defining the parent tag that will be created
*/
tagName: ‘li’,
/*
* Constructor like method where we can do needed intializations
*/
initialize: function() {
/*
* We are attaching context to render method
*/
this.render = _.bind(this.render, this);
/*
* Defining template, we are loading underscore template here
*/
this.template = _.template($(‘#project-item’).html());
/*
* Bind any change in the model to render method
*/
this.model.bind(‘change’, this.render);
},
/*
* Defining events which will trigger our methods
*/
events: {
‘dblclick a’: ‘edit’,
‘click a’: ‘loadTasks’
},
/*
* Rendering template
*/
render: function() {
/*
* Filling template with model attributes
*/
this.$el.html(this.template(this.model.attributes));
return this;
},
/*
* Triggers when user double clicks on project
*/
edit: function() {
/*
* Opening project dialog and filling it with current model (the one that was clicked)
*/
new ProjectDialog({model: this.model}).show();
},
/*
* Method which loads currently selected project tasks
*/
loadTasks: function() {
/*
* First lets handle visual side of selecting a project, we’ll remove active class for project that was selected earlier and
* add class to currently selected project
*/
$projects.find(‘li.active’).removeClass(‘active’);
this.$el.addClass(‘active’);
/*
* We also need to update project title (above the tasks table)
*/
$(‘#project-title span’).html(this.model.get(‘name’));
/*
* Cleaning bit, we’ll remove tasks that was tied to project selected earlier
*/
$tasks.empty();
/*
* We’ll initialize new task colleci
*/
tasks = new TaskCollection();
/*
* We’ll assign current project ID to "global" variable as we need it on several other places
*/
currentProjectId = this.model.id;
/*
* Lets fetch tasks for currently selected project (we can access currently selected project through this.model)
* processData param here only informs the system that params provided through data param needs to be added to URL as GET params
*/
tasks.fetch({data: {project: this.model.id}, processData: true, success: function() {
/*
* Initializing task list view and passing tasks collection to it
*/
taskListView = new TaskList({
collection: tasks,
/*
* Telling view to which DOM elements it needs to attach itself
*/
el: $tasks
});
/*
* Rendering list view
*/
taskListView.render();
}});
}
});
[/code]
First is the single item view object (for project). It is the LI element inside of the UL element which holds our project list. We are defining two events, click event which loads tasks for clicked project and initializes task list view (loadTasks method) and double click event which triggers editing of currently clicked project and shows modal dialog with populated data (edit method).
[code lang=”javascript”]
ProjectList = Backbone.View.extend({
initialize: function() {
_(this).bindAll(‘add’, ‘remove’);
/*
* Holder of single project views
*/
this._projects = [];
/*
* For each element in collection we run the ‘add’ method
*/
this.collection.each(this.add);
/*
* Binding collection events to our methods
*/
this.collection.bind(‘add’, this.add);
this.collection.bind(‘remove’, this.remove);
},
render: function() {
/*
* Initializing and setting flag from which we’ll know if our view was rendered or not
*/
this._rendered = true;
/*
* We render single project items and append it to DOM element
*/
_(this._projects).each(function(item) {
$projects.append(item.render().el);
});
},
/*
* Method that fires when project item is added (either from collection after fetching or creating a new one)
*/
add: function(project) {
var projectItem = new ProjectItem({model: project});
/*
* Adding project item view to the list
*/
this._projects.push(projectItem);
/*
* If view is rendered then we add our rendered item to this view
*/
if (this._rendered) {
this.$el.append(projectItem.render().el);
}
},
/*
* Fires when removing project item
*/
remove: function(project) {
/*
* Determining which view we need to remove from markup
*/
var view = _(this._projects).select(function(cv) { return cv.model === project; })[0];
if (this._rendered) {
$(view.el).remove();
}
/*
* Triggering click to the first project item
*/
$projects.find(‘li:nth-child(2)’).find(‘a’).trigger(‘click’);
}
});
[/code]
The most important thing to notice in project list view is the binding of collection events to view object methods. We are attaching collection events (like adding and removing items) to objects add and remove methods where we are doing the respective view handling, rendering new project item view (for every collection item) and removing the view (LI element) from the list.
[code lang=”javascript”]
projects = new ProjectCollection();
/*
* Fetching projects from DB
*/
projects.fetch({success: function() {
projectListView = new ProjectList({
collection: projects,
el: $projects
});
projectListView.render();
/*
* Triggering click on first project which will load its tasks
*/
$projects.find(‘li:nth-child(2)’).find(‘a’).trigger(‘click’);
}});
[/code]
With the code snippet above, we are starting our app. After we initialize ProjectCollection object, its fetch method is being called which then pulls projects from the server part. Upon success we are initializing ProjectList view object and giving it our fetched projects and view where it should attach it self.
The third remaining view object type is the modal dialog form.
[code lang=”javascript”]
ProjectDialog = Backbone.View.extend({
/*
* Events which we are listening and their respective selectors that triggers them
*/
events: {
‘click .save-action’: ‘save’,
‘click .close,.close-action’: ‘close’,
‘change input’: ‘modify’
},
initialize: function() {
this.template = _.template($(‘#project-dialog’).html());
},
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
/*
* Displaying the dialog
*/
show: function() {
$(document.body).append(this.render().el);
},
/*
* Removing the dialog
*/
close: function() {
this.remove();
},
/*
* Fires when we click save on the form
*/
save: function() {
/*
* If this is new project it won’t have the ID attribute defined
*/
if (null == this.model.id) {
/*
* We are creating our model through its collection (this way we’ll automatically update its views and persist it to DB)
*/
projects.create(this.model);
} else {
/*
* Simple save will persist the model to the DB and update its view
*/
this.model.save();
}
/*
* Hiding modal dialog window
*/
this.remove();
},
/*
* We listen to every change on forms input elements and as they have the same name as the model attribute we can easily update our model
*/
modify: function(e) {
var attribute = {};
/*
* We’ll fetch name and value from element that triggered "change" event
*/
attribute[e.currentTarget.name] = e.currentTarget.value;
this.model.set(attribute);
}
});
[/code]
First, we initialize events that when triggered will call our view objects method. As you can see the syntax is ‘event selector’: ‘method’.
Save method will check if id attribute is null (which means that we are doing a create action) and will add model to the project collection object. As we have listeners attached on collection (and its view object), model will automatically be added to the list (UL element in this instance) and will automatically be persisted to the DB.
If we are doing an update, we just need to save our model. Item view will do its job, and of course, changes will be persisted to the DB by Backbone itself.
One other thing to notice here is the modify method which is triggered by the change event on input elements (in the modal dialog form). When called it inspects event that was triggered and pulls out form field name and value and modifies the model.
As you can see most of the code is in the view objects. Detailed line-by-line description can be found in the file comments. There are some small differences between project and task views/code but you can spot them and figure them out from the comments.
You can see the demo or download files below:
What’s next
These concludes the second part of this tutorial. With the things learned here (and the previous part) you can achieve a lot. Only your imagination is the limit 🙂
In the third part I was planning to do task ordering (with Underscore array methods), notifications and confirmation dialogs as well as error handling. Also, we’ll take a look at Backbone router component.
Thanks for this post..! This post has great information to me..!
This is great work! …. Tutorial was really easy and informative.
Thanks Luka!
excuse me: input validation, error handling…?
I’m in the heavy frameworks side (like Zend) and i’m disturbed to see that this kind of examples seem so easy and fast doable but lack serious (and tedious) standard web app development routines.
great job, this is really helpful article
Your way of explaining all in this article is really nice, all be capable of simply know it, Thanks a lot.
thumbs up for the two part article.
great job.
Comments are closed.