(For those looking to dig right into the code, make your way down to Section 3!)
Section 1: Creative Days
There is always an aura of excitement around Barrel’s Creative Days internal “hack-a-thon”—two months of brainstorming and planning followed by two days of hack-and-slash design and coding. Creative Days is an excellent opportunity to try out new ideas and technologies, and for Barrel’s second annual event, my team chose to tackle a Backbone.js application. At that point—around August 2013—my experience with Backbone.js was almost non-existent—but after seeing the framework in action on higher-profile websites like Hulu and Rdio, I felt it was important to familiarize myself with it’s inner-workings.
Earlier in 2013, the Barrel team had taken the Gallup “Find Your Strengths” test as a way of highlighting each team member’s positive qualities. After answering a list of questions, each person was assigned five “strength characteristics” from a list of thirty-four. Alongside these strengths were lists of strengths which complemented each other. My Creative Days team—aptly named the “Strength Force”—was tasked with creating a data-visualization for Barrel’s results. We named our product the Barrel “Strength Finder”.
On the surface, learning a new framework in two days is not completely unreasonable.
On the surface, learning a new framework in two days is not completely unreasonable, but in my excitement for the project I also chose to use Creative Days as my inaugural attempt at using the Grunt/Bower workflow, as well as an opportunity to test the RESTful capabilities of the extremely simple CodeIgniter PHP framework. So the tech specs looked like this:
A data-visualization / web application that will highlight the strengths of the Barrel team using the following technologies:
- MySQL database to store Barrel Team member “strength” relationships and descriptions.
- CodeIgniter PHP controller and model translating and serving database information through a RESTful API.
- Grunt workflow leveraging the Bower package manager and LESS CSS pre-processing to speed up development.
- Backbone.js JavaScript framework for requesting and interpreting JSON data and producing views.
Needless to say, it was a tall order for two days, and as you will see in Section 3, the choice to use the technologies above came with both pros and cons.
Section 2: Planning
2.1 Design
Inspired by the bold look and visual feel of the Colorplan color picker tool our team came up with a playful way to represent each strength as a circle with a unique color.
In our visualization, the largest circles represent the strengths exhibited by the most team members. Hovering over a circle reveals the list of team members associated with that particular strength.
Tabs at the top of the application filter strengths by Barrel disciplines (Web Ops, Design, Development, etc.) as well as the four major Gallup strength categories: Relaters, Influencers, Strivers, and Thinkers.
When viewing a team member’s individual strengths, the largest circle represents the team member’s greatest strengths, and smaller circles represent lesser strengths.
Finally, each strength has a set of complementary strengths, which helps to identify which other members of the Barrel team would work well with that individual.
2.2 Building the Database
We began the project by creating a MySQL database that would hold all of the strength data and relationships. In the database we created tables for categories, strengths, disciplines, people, and the various has_and_belongs_to_many relationships necessary for keeping track of each member’s strengths and how they relate to other complementary strengths.
The final set of database tables looked like this:
sf-categories // For strength categories sf-disciplines // For discipline groups (web-ops, design development, etc.) sf-people // For team members, with index for discipline sf-strengths // For strengths, with index for category sf-people_meta // Relationship between team members and strengths with a 1-5 rating for each strength for that user sf-strength_meta // Relationship between strengths and their complementary strengths
Foregoing building a content management system for this project, our team entered the data and relationships manually.
Section 3: Back-end
(This section assumes basic understanding of the CodeIgniter PHP framework. However, the principles involved can be applied to other frameworks such as Ruby on Rails or Laravel which have more integrated ways of serving RESTful JSON and stronger ORMs for database relationship management. For those looking to focus on Backbone.js exclusively, please skip to Section 4.)
3.1 CodeIgniter
Development began quickly on the back-end, setting up an instance of CodeIgniter 2.3.2 using my own personal Default CodeIgniter repository on Github and adding in Phil Sturgeon’s CodeIgniter Rest Server to enable serving RESTful data from our API controller.
Set-up of the CI Rest Server was relatively simple—dropping in the required files—however, planning to serve our API from a subdomain, I had to make an important change to Phil’s application/libraries/REST_Controller.php file, adding the following to the bottom of the construct method:
public function __construct() { ... header('Access-Control-Allow-Origin: *'); }
This little line of code allowed us to accept cross-domain AJAX requests. You can specify which domains and IP addresses can make these requests, but for the sake of this example, I’ve just inserted a wildcard, allowing anyone to connect.
Because CodeIgniter’s ActiveRecord is more of an elegant SQL-agnostic way to write database queries (see the WordPress WPDB Class) rather than a full-blown ORM with relational modeling (see Laravel), the next step was to write model methods to gather the necessary strengths and people data from the database.
In this case, methods were written to grab strengths or people and then join in the various relationship data that might have been needed on the fly once it was converted to JSON and JavaScript got ahold of it.
3.2 Strengths
For strengths, we joined in category data, as well as queried and pushed arrays of person and discipline data into the final array. The categories and disciplines were used for sorting on the front-end.
// Get strength(s) function get_strengths(){ // Get the strengths $this->db->select(' s.id AS id, s.name AS name, c.name AS category_name '); $this->db->from('sf-strengths AS s'); $this->db->join('sf-categories AS c', 's.category = c.id', 'left outer'); $query = $this->db->get(); // Validate if ($query->num_rows() > 0){ $strengths = $this->sort_results($query->result_array(), 'id', TRUE); // People foreach ($strengths AS $strength){ // Get the people $this->db->select(' pm.user_id AS id, p.name AS name, d.name AS discipline_name '); $this->db->from('sf-people_meta AS pm'); $this->db->join('sf-people AS p', 'pm.user_id = p.id', 'left outer'); $this->db->join('sf-disciplines AS d', 'p.discipline = d.id', 'left outer'); $this->db->where('pm.strength_id', $strength['id']); $query = $this->db->get(); // Validate if ($query->num_rows > 0){ $people = $this->sort_results($query->result_array(), 'id', TRUE); $strengths[$strength['id']]['people'] = array_values($people); // Append } } } // Return return $strengths; }
3.3 People
For people, it was necessary to query and push several arrays of sub-data into the final array, including strengths, complementary strengths, and sub-arrays of people based off both. The result was a hairy looking method that required several foreach loops to manage the results.
// Get people function get_people(){ // Get the people $this->db->select(' p.id AS id, p.name AS name '); $this->db->from('sf-people AS p'); $query = $this->db->get(); // Validate if ($query->num_rows() > 0){ $people = $this->sort_results($query->result_array(), 'id'); // Strengths foreach ($people as $person){ // Get the strengths $this->db->select(' pm.strength_id AS id, s.name AS name, pm.rating AS rating, '); $this->db->from('sf-people_meta AS pm'); $this->db->join('sf-strengths AS s', 'pm.strength_id = s.id', 'left outer'); $this->db->where('pm.user_id', $person['id']); $query = $this->db->get(); // Validate if ($query->num_rows() > 0){ $strengths = $this->sort_results($query->result_array(), 'id', TRUE); $connections = array(); // Connections foreach ($strengths AS $strength){ // Get the people $this->db->select(' pm.user_id AS id, p.name AS name, '); $this->db->from('sf-people_meta AS pm'); $this->db->join('sf-people AS p', 'pm.user_id = p.id', 'left outer'); $this->db->where('pm.strength_id', $strength['id']); $this->db->where('p.id !=', $person['id']); $query = $this->db->get(); // Validate if ($query->num_rows > 0){ $matches = $this->sort_results($query->result_array(), 'id', TRUE); $strengths[$strength['id']]['people'] = array_values($matches); // Append } // Get the connections $this->db->select(' sm.connect_id AS id, s.name AS name, '); $this->db->from('sf-strengths_meta AS sm'); $this->db->join('sf-strengths AS s', 'sm.connect_id = s.id', 'left outer'); $this->db->where('sm.strength_id', $strength['id']); $query = $this->db->get(); // Validate if ($query->num_rows > 0){ $results = $this->sort_results($query->result_array(), 'id', TRUE); foreach ($results as $result){ if (!isset($connections[$result['id']])){ $result['rating'] = 1; // Get the people $this->db->select(' pm.user_id AS id, p.name AS name, '); $this->db->from('sf-people_meta AS pm'); $this->db->join('sf-people AS p', 'pm.user_id = p.id', 'left outer'); $this->db->where('pm.strength_id', $result['id']); $this->db->where('p.id !=', $person['id']); $query = $this->db->get(); // Validate if ($query->num_rows > 0){ $duders = $this->sort_results($query->result_array(), 'id', TRUE); $result['people'] = array_values($duders); // Append } $connections[$result['id']] = $result; }else{ $connections[$result['id']]['rating']++; } } } } $people[$person['id']]['connections'] = array_values($connections); // Append $people[$person['id']]['strengths'] = array_values($strengths); // Append } } // Return return $people; } }
It was important to use the array_values function on each of the sub-arrays we pushed into the strengths and people arrays. Later, I found that the indexes on these sub-arrays behaved badly when converted to JSON and pulled into Backbone, so array_values was necessary to strip the indexes and make the data useable.
3.4 API
Building the API controller to serve the data was simple. After taking a quick look at Phil’s example controller, I was able to quickly craft a set of four methods to delivery the arrays to the front.
class API extends REST_Controller { // Construct function __construct(){ parent::__construct(); // Load the API model $this->load->model('m_api'); } /*-----STRENGTH METHODS-----*/ // Get strength data function strength_get() { $id = $this->get('id'); if (!$id) { $this->response(NULL, 400); // 400 } // Get array $rows = $this->m_api->array_strengths(); // Get row $row = @$rows[$id]; // Return if ($row){ $this->response($row, 200); // 200 }else{ $this->response(array('error' => 'Strength could not be found!'), 404); // 404 } } // Get strengths data function strengths_get() { // Get array $rows = $this->m_api->array_strengths(); // Return if ($rows){ $this->response(array_values($rows), 200); // 200 }else{ $this->response(array('error' => 'Couldn\'t find any strengths!'), 404); // 404 } } /*-----PEOPLE METHODS-----*/ // Get person data function person_get() { $id = $this->get('id'); if (!$id) { $this->response(NULL, 400); // 400 } // Get array $rows = $this->m_api->array_people(); // Get row $row = @$rows[$id]; // Return if ($row){ $this->response($row, 200); // 200 }else{ $this->response(array('error' => 'Person could not be found!'), 404); // 404 } } // Get people data function people_get() { // Get array $rows = $this->m_api->array_people(); // Return if ($rows){ $this->response(array_values($rows), 200); // 200 }else{ $this->response(array('error' => 'Couldn\'t find any people!'), 404); // 404 } } }
With the necessary pieces in place, it was time to move onto the front-end of the site.
Section 4: Front-end
4.1 Grunt / Bower
Having only dabbled with the Grunt workflow, I reached out to Barrel’s own Scott Polhemus—a seasoned Grunt user—for some guidance, and he provided me with a loose file-structure and time-tested Gruntfile to help speed up the process. Included in the Gruntfile were the basics needed to run Grunt using the Grunt Connect server, and helpful build tasks to minify JavaScript and process LESS stylesheets.
I quickly edited the basic structure and stylesheets to my liking, and pushed the results up to Github, creating my Default Grunt repository.
The Bower package manager was used to manage JavaScript dependencies. For this project I used the following bower.json to install the packages needed to develop using jQuery and Backbone.
{ "name": "strength-force", "version": "1.0.0", "author": "Kevin Kneifel", "dependencies": { "jquery": "~2.0.3", "jquery-ui": "~1.10.3", "underscore": "~1.5.1", "backbone": "~1.0.0" } }
4.2 Backbone Basics
Backbone is dependent on Underscore to run, so when declaring blocks of JavaScript at the bottom of index.html I broke the JavaScript out into three sections for usemin to compress, one for jQuery:
<!-- build:js(src) /scripts/jquery.js --> <script src="/bower_components/jquery/jquery.min.js"></script> <script src="/bower_components/jquery-ui/ui/jquery-ui.js"></script> <script src="/scripts/jquery.easing.1.3.js"></script> <!-- endbuild -->
One for Backbone’s core:
<!-- build:js(src) /scripts/lib.js --> <script src="/bower_components/underscore/underscore.js"></script> <script src="/bower_components/backbone/backbone.js"></script> <!-- endbuild -->
And finally, one for my Backbone framework:
<!-- build:js(src) /scripts/app.js --> <script src="/scripts/models.js"></script> <script src="/scripts/collections.js"></script> <script src="/scripts/routes.js"></script> <script src="/scripts/views.js"></script> <script src="/scripts/app.js"></script> <!-- endbuild -->
As you can see from above, I split my Backbone application into five major parts. Below I will describe how each of these files contributes to the final application.
4.3 App.js
The app.js file serves two roles in my Backbone application. First, it acts as a place to hold global functions that may be needed in other areas of the application. Second, it is where I initialize my application.
For the sake of following JavaScript OOP best practices, my entire application takes place inside the app object, which I re-declare at the top of each of the five main JavaScript files.
var app = app || {};
All of the functions I will need throughout the application are then declared as methods of the app object. For instance, the functions that brings each of the “strength circles” into view.
app.randomFromTo = function(from, to){ var self = this; return Math.floor(Math.random() * (to - from + 1) + from); }; app.comeAtMeBro = function(element, parent){ var self = this; // Get container position and size var cPos = parent.offset(), cHeight = parent.height(), cWidth = parent.width(); // Get movable box size var bHeight = element.width(), bWidth = element.height(); // Set maximum position var maxY = cPos.top + cHeight - bHeight, maxX = cPos.left + cWidth - bWidth; // Set minimum position var minY = cPos.top, minX = cPos.left; // Set new position var newY = self.randomFromTo(minY, maxY), newX = self.randomFromTo(minX, maxX); element.animate({ top: newY, left: newX }, 1000, 'easeOutBack', function(){ $(this).removeClass('loading'); }); };
Or the function that will initialize the app.
app.initialize = function(){ // Define the host path app.host = 'http://api.strengthforce.dev'; // Fetch the location app.location = window.location.pathname.substring(1); // Create homepage app.home = new app.HomePageView(); // Create person page app.person = new app.PersonPageView(); // Create router app.router = new app.Router(); // Start history Backbone.history.start({ pushState: true }); // Set route if (app.location){ app.router.navigate(app.location, { trigger: true }); }else{ app.router.navigate('home', { trigger: true }); } // Home click $('.js-home').on('click', function(event){ app.router.navigate('home', { trigger: true }); }); };
You will notice that when I initialize the app, I am setting the host variable to point at the API we created in CodeIgniter and grabbing the browser’s location. I am also declaring instances of the HomePageView and PersonPageView objects. These are “Backbone View” objects and are where I will declare most of my application’s behavior later on in the views.js file.
Finally, I am instantiating an instance of my “Backbone Router”, tracking history, and setting some conditional routing behaviors. All of this will be explained in greater detail in section 4.6, so for now, I am simply going to end by initializing my application after listening for jQuery’s document ready.
$(document).ready(function(){ // Initialize the app app.initialize(); });
4.4 Models.js
The models.js file is where I define my “Backbone Model” objects, whose jobs are simply holding onto the JSON data queried from CodeIgniter using the REST API.
For the “Strength Finder” app, I defined two models, the PersonItem model and the StrengthItem model, and each of these has been optionally linked to their respective RESTful CodeIgniter route using Backbone’s url method. By defining the route, our models will know where to look when retrieving data from the server using Backbone’s fetch method.
// Person item model app.PersonItem = Backbone.Model.extend({ url: function(){ return app.host +'/api/person/id/'+ this.get('id'); } }); // Strength item model app.StrengthItem = Backbone.Model.extend({ url: function(){ return app.host +'/api/strength/id/'+ this.get('id'); } });
Using the get method, I can pass in parameters when declaring instances of these models—in this case the ID of the person or strength I’d like to query.
4.5 Collections.js
The collections.js file is where I define my “Backbone Collection” objects, which are basically a collection or set of Backbone Models. When defining a collection, it is important to define the type of model that it will contain using the model method. It is also possible to use Backbone’s url method to define the default route to fill the collection—but like a model, it is not necessary for every collection to have default data, and in the case of my PeopleItems and StrengthItems collections, I will be adding content later.
// People item collection app.PeopleItems = Backbone.Collection.extend([], { model: app.PersonItem }); // Strength item collection app.StrengthItems = Backbone.Collection.extend([], { model: app.StrengthItem }); // Strength collection app.Strengths = Backbone.Collection.extend({ model: app.StrengthItem, url: function(){ return app.host +'/api/strengths/'; } });
4.6 Routes.js
The routes.js file is where I define the basic behaviors of the “Backbone Router”, including the individual application routes, and the methods that define their behavior.
For “Strength Finder” there are only two routes, a route to the homepage, and a route to an individual person page. Each of these routes execute methods of the view objects I declared in my initialize method—in this case, the user defined closePage and openPage methods are triggered.
app.Router = Backbone.Router.extend({ routes: { 'home': 'home', // #home 'person/:id': 'person' // #person/id }, home: function(){ // Close other pages app.person.closePage(); // Open homepage app.home.openPage(); }, person: function(id){ // Close other pages app.home.closePage(); // Open person page app.person.openPage(id); } });
Looking back at the intitialize method in app.js, I instantiated the router after the view objects were declared, this way the router object will recognize the methods that are defined in those objects.
The “Backbone History” object takes care of the HTML5 History API—with a hashtag fallback for browsers that don’t support the pushState declaration—and I’ve added a conditional statement to route to the homepage unless I am explicitly trying to reach another page every time the app is initialized.
Backbone’s router uses the navigate method to save a URL in the browser’s history, and the trigger parameter tells the browser to execute the corresponding routing method we defined in our routes.js file.
app.initialize = function(){ ... // Create router app.router = new app.Router(); // Start history Backbone.history.start({ pushState: true }); // Set route if (app.location){ app.router.navigate(app.location, { trigger: true }); }else{ app.router.navigate('home', { trigger: true }); } ... };
Backbone’s router is extremely powerful, and an integral part of how the “Strength Finder” application works. However, much like the router in a server-side framework, it is necessary to make sure that all interactions begin at the application root.
In a Linux PHP application, it is the job of .htaccess and mod_rewrite to re-route all requests to index.php—so that the basic framework can be loaded, and the request can be processed properly. That rewrite usually looks something like this:
<IfModule mod_rewrite.c> RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ /index.php [L,QSA] </IfModule>
But, because “Strength Finder” uses the Grunt Connect server locally, it was necessary conduct the rewrite using an additional Node package. For this purpose, I installed Grunt Connect Rewrite and added the middleware to my Gruntfile.
... var rewriteRulesSnippet = require('grunt-connect-rewrite/lib/utils').rewriteRequest; var mountFolder = function(connect, dir) { return connect.static(require('path').resolve(dir)); }; module.exports = function(grunt) { require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); grunt.initConfig({ ... connect: { ... server: { options: { middleware: function (connect) { return [ ... rewriteRulesSnippet, mountFolder(connect, '.tmp'), mountFolder(connect, 'src') ]; } } } }, rewriteRules: { rules: { '^/home$': '/', '^/person/(.*)$': '/' } }, configureRewriteRules: { options: { rulesProvider: 'rewriteRules.rules' } }, ... }); grunt.registerTask('server', [ ... 'configureRewriteRules', 'connect', ... ]); ... }
4.7 Views.js
The views.js file is where I define my “Backbone View” objects, and is where the meat of my application is written. The Backbone documentation has an excellent description of the view’s usage:
“The general idea is to organize your interface into logical views, backed by models, each of which can be updated independently when the model changes, without having to redraw the page.”
For “Strength Finder” I created two views, HomePageView and PersonPageView.
All Backbone views are represented by elements in the DOM. These elements do not have to exist yet, but must be referenced by the el variable when defining the view object, and the $el jQuery object acts as a cached version of the view’s element.
app.HomePageView = Backbone.View.extend({ el: '#HomePage', ... }); app.PersonPageView = Backbone.View.extend({ el: '#PersonPage', ... });
If the view’s element already exists in the DOM—as both do in “Strength Finder”—the view’s internal initialize method acts as a constructor, and is an appropriate place to bind events to child elements that already exist in the DOM. In my PersonPageView, I use the constructor to bind events to the filtering links at the top of the #PersonPage section in my index.html.
app.PersonPageView = Backbone.View.extend({ el: '#PersonPage', initialize: function(){ var self = this; // Filter click self.$el.find('.js-filter_personal').on('click', function(event){ // Personal filter self.$el.find('#Nav li').removeClass('active') .end().find('#Nav .filter_personal').addClass('active'); self.$el.find('#Connections').removeClass('active').find('article').each(function(){ app.getTheHellOut($(this), self.$el); }); self.$el.find('#Personal').addClass('active').find('article').each(function(){ app.comeAtMeBro($(this), self.$el); }); }); ... }, ... });
The basics of using Backbone are extending or overwriting the Backbone’s core objects. This means I can add as many custom methods into each model, collection, or view that I want. In the case of my “Strength Finder” views, I have written openPage and closePage methods for each view. As seen earlier, these methods are triggered in the router, and the openPage method is directly responsible for fetching the model or collection data needed in the view, and calling the object’s render method.
app.HomePageView = Backbone.View.extend({ ... openPage: function(){ var self = this, collection = new app.Strengths(); // Fetch the data collection.fetch({ success: function(){ self.$el.find('.graph').empty() .end().fadeIn(); self.render(collection); } }); }, closePage: function(){ var self = this; self.$el.fadeOut(); }, ... });
In the case of the HomePageView I am pulling data for a collection of StrengthItem models using our REST API, and then passing the collection into the render function if the fetch method is successful. Since Backbone fetches the data asynchronously, it is important to render the data in the success callback, or else the render function may fire before there is data to use.
For my PersonPageView, the openPage method grabs data for a single PersonItem model, but then uses that person’s joined strengths data to fill up additional StrengthItems and PeopleItems collections.
app.PersonPageView = Backbone.View.extend({ ... openPage: function(id){ var self = this, model = new app.PersonItem({ id: id }); // Fetch the data model.fetch({ success: function(){ self.$el .find('h2, .graph').removeClass('flipInX').empty() .end().fadeIn(); self.render(model); } }); }, ... render: function(model){ var self = this, strengths = new app.StrengthItems(model.get('strengths')), connections = new app.StrengthItems(model.get('connections')); ... // Add strengths strengths.each(function(strength){ var sView = '<article class="strength animated personal '+ strength.get('name').toLowerCase() +' loading"><h4>'+ strength.get('name') +'</h4><ul></ul></article>', sSize = strength.get('people') ? strength.get('people').length : 0, sRate = (6 - strength.get('rating')), sElement = $(sView).appendTo(self.$el.find('#Personal')), sPeople = new app.PeopleItems(strength.get('people')); ... sPeople.each(function(person){ var pView = '<li class="person"><h3><a href="javascript:void(0)" rel="'+ person.get('id') +'" class="js-person">'+ person.get('name') +'</a></h3></li>', pElement = $(sElement).find('ul').append(pView); ... }); }); // Add connections connections.each(function(connection){ var cView = '<article class="strength animated connection '+ connection.get('name').toLowerCase() +' loading"><h4>'+ connection.get('name') +'</h4><ul></ul></article>', cSize = connection.get('people') ? connection.get('people').length : 0, cRate = (connection.get('rating')), cElement = $(cView).appendTo(self.$el.find('#Connections')), cPeople = new app.PeopleItems(connection.get('people')); ... cPeople.each(function(person){ var pView = '<li class="person"><h3><a href="javascript:void(0)" rel="'+ person.get('id') +'" class="js-person">'+ person.get('name') +'</a></h3></li>', pElement = $(cElement).find('ul').append(pView); ... }); ... // Person click self.$el.find('.js-person').on('click', function(event){ var id = $(this).attr('rel'); app.router.navigate('person/'+ id, { trigger: true }); }); } });
Although the Backbone.js documentation recommends the use of a JavaScript templating library to render view elements, for the sake of time I chose to write the HTML for new view elements directly into the render method and append it to the DOM using jQuery. Although it’s a bit hack-and-slash, it worked perfectly. Finally, I added event binding for new elements added into the DOM.
By splitting my Backbone application into five distinct sections, I was able to keep better track of objects by their type, and could rely on Grunt’s usemin task to join and compress the parts into a single file when building the final distribution.
Section 5: Lessons Learned
Although the “Strength Force” team was successful in completing our “Strength Finder” application, we grossly over-scoped the amount of work we could accomplish in two days, and Day One of Creative Days ended up being a 16-hour day for development.
The best way to save time would have been to use a RESTful framework to serve the API. Ruby on Rails would have been an excellent choice—allowing us to scaffold a basic CMS for entering the data (rather than entering it into the database by hand), generate JSON format views for delivering API data, and use ActiveRecord relationships to eliminate the need for complex functions for querying and joining the needed data from the database.
The best way to save time is to use a RESTful framework.
Still, as an exercise in serving a RESTful API from CodeIgniter, the project was a success, and although the CodeIgniter framework is certainly dated (in many ways because of it’s continued support of PHP 4), it was more than capable of behaving as a platform for our API.
Finally, the final code for the “Strength Finder” Backbone application is still “scrappy at best”, and although it is a good starting point, only touches on the flexibility of the framework. For anyone choosing to use this article as an introduction, I hope the code serves to clarify some of the basic functionality of Grunt and Backbone, and I encourage you—should you choose to “specialize”—to seek out other tutorials and the Backbone documentation to dig even deeper.
Illustration by Cindy Leong
Pingback: payday
Pingback: direct affordable payday loans hamilton lender
Pingback: drugrehabcentershotline.com malibu rehab