Skip to content

Single Page Application example using AngularJS, Backbone.js and PHP Slim

Run the app or view demo.

git clone https://github.com/mmukhin/spa-tutorial
cd spa-tutorial
php -S localhost:8080 # built-in web server requires PHP >= 5.4.0

Navigate to: localhost:8080

This is an example Single Page Application (SPA) comparing and contrasting Backbone.js and AndularJS. The API is written using Slim for PHP.

API using PHP Slim

The api runs from api/index.php.

Write models to create, update, retrieve data from storage using Object Oriented Programming.

class TeamModel extends AbstractModel {

    protected $_id;
    protected $_city;
    protected $_name;

    public function fetchOne($id)
    {
        if (array_key_exists($id, $_SESSION['teams'])) {
            return $_SESSION['teams'][$id];
        }
        else {
            return false;
        }
    }

    /* more methods */
}

Write routes to calls models which create, update, retrieve data with Slim routes.

$app->get('/teams/:id', function ($id) use ($app) {
    $TeamModel = new TeamModel();
    $app->response()->body( json_encode($TeamModel->fetchOne($id)) );
});

That’s it for the api.

Client Javascript MVC

Routing

In the browser, the user is able to navigate around the app with the help of routes. When a link is clicked, the router either listens for a navigation event or parses the URL, then executes the logic within.
Consider how the URLs look (each type is available in Backbone.js & AngularJS):

Managing State

SPAs execute from a single page.. ;). When you copy links to pages within your app to send to friends, you intend your friend to see the page rendered the same exact way as you see it (e.g. paginated to page 3, sorted by date, purple). This principle is called managing application state, which refers to the specific configuration of information (how the interface looks) in an application. Mismanaging application state sucks. Keep this in mind, google is your friend.

HTML Templates

Do you really have to update code in each JavaScript file that uses a certain chunk of HTML? No! Use templates!
Do not write HTML within JavaScript code. Write HTML within templates. Underscore.js templates (Backbone.js dependency) and Mustache templates are helpful.

UI Flow

User clicks around the application.
Routes listen for clicks or URL changes.
Routes execute necessary business logic (e.g. calling controllers, views).
Business logic handles models and views.
Models synchronize data with AJAX requests to the API.
Views generate HTML using templates.

API Flow

Web server (e.g. PHP test server, apache, nginx) accepts HTTP requests and calls executes API code.
API routes match incoming request URLs and execute necessary business logic.
Models synchronize data with the database/storage.
API routes respond with a status code, headers and data.

Backbone.js Code Samples

Router (link)

NBAApp.Router = Backbone.Router.extend({
    routes: {
        'about':        'about',
        'league':       'league',
        '*catchAll':    'league'
    },

    /* more methods */
});

Models (link)

NBAApp.Models.Team = Backbone.Model.extend({
    url: function(){
        return apiUrl+'/teams/' + this.get('id');
    },
    defaults: function() {
        return {
            id: null,
            name: '',
            city: ''
        };
    },
    initialize: function() {
    }
});

NBAApp.Collections.Teams = Backbone.Collection.extend({
    url: apiUrl+'/teams/',
    model: NBAApp.Models.Team,
    initialize: function() {
    },
    comparator: function(a) {
        return a.get('name');
    }
});

Views (link)

var LeagueView = Backbone.View.extend({
    el: '#league',
    events: {
        'click #new-team-add': 'onTeamCreate',
        'keyup #backbone-search': 'keywordSearch'
    },
    initialize: function() {

        this.teams = new NBAApp.Collections.Teams;
        this.teams.fetch();

        this.listenTo(this.teams, 'sync', this.onCollectionSync);
        this.listenTo(this.teams, 'destroy', this.onCollectionModelDestroy);

        this.input = $('#new-team-add');
        this.keyword = $('#backbone-search');
    },
    /**
     * Override parent method, render HTML from template
     */
    render: function(optionalCollection) {

        var that = this;
        optionalCollection = optionalCollection || this.teams;

        if (optionalCollection.length > 0) {
            this.clear();
            _.each(optionalCollection.models, function(v) {
                that.onCollectionAdd(v);
            });
        }
        else {
            this.clear();
        }

        this.$el.show();

        this.updateTeamsCount();

        // enable chained calls
        return this;
    },

    /* more methods */
});

Templates (link)

<!-- Template: editing a team -->
<script type="text/template" id="backbone-edit-team">
    <!-- determine whether or not to add the "hide" class -->
    <form>
        <div style="display:inline-block;min-width: 500px;">
            <input type="text" id="edit-team-city" value="<%- city %>" placeholder="Team City" />
            <input type="text" id="edit-team-name" value="<%- name %>" placeholder="Team Name" />
        </div>
        <div style="display:inline-block;min-width: 500px;">
            <!-- click listeners bound to $scope -->
            <input type="submit" id="backbone-team-save" class="button" value="Save" />
            <input type="button" id="backbone-team-cancel" class="button" value="Cancel" />
        </div>
    </form>
</script>

AngularJS Code Samples

Service (API) Module (link)

NBAappModule.factory('teamStorage', function ($http) {

    var apiUrl = 'http://localhost:8080/api';

    return {
        getAll: function () {
            return $http({
                method: 'GET',
                url: apiUrl+'/teams/'
            });
        },
        put: function (team) {
            return $http({
                method: 'PUT',
                url: apiUrl+'/teams/' + team.id,
                data: JSON.stringify(team)
            });
        },
        post: function (team) {
            return $http({
                method: 'POST',
                url: apiUrl+'/teams/',
                data: JSON.stringify(team)
            });
        },
        delete: function (team) {
            return $http({
                method: 'DELETE',
                url: apiUrl+'/teams/' + team.id
            });
        }
    };
});

Controller (link)

NBAappModule.controller('NBAController', function ($scope, teamStorage, filterFilter) {

    // array of teams, used with ng-repeat in HTML
    $scope.teams = [];

    teamStorage.getAll().success(function(data){
        $scope.teams = data;
    });

    // listener for changes in teams array
    $scope.$watch('teams', function (newValue, oldValue) {

        // update binding in HTML
        $scope.totalTeams = $scope.teams.length;
    }, true);

    // brings up editing view
    $scope.editTeam = function (team) {

        // HTML displays team view by checking this property
        team.editing = true;

        // clone the original team to restore it later
        $scope.originalTeam = angular.extend({}, team);
    };

    /* more methods */
});

Templates (link)

<form ng-class="{ hide: ! team.editing }">
    <div style="display:inline-block;min-width: 500px;">
        <input type="text" ng-model="team.city" placeholder="Team City" />
        <input type="text" ng-model="team.name" placeholder="Team Name" />
    </div>
    <div style="display:inline-block;min-width: 500px;">
        <!-- click listeners bound to $scope -->
        <input type="submit" class="button" ng-click="saveEditing(team)" value="Save" />
        <input type="button" class="button" ng-click="cancelEditing(team)" value="Cancel" />
    </div>
</form>

Final Important Topics

CORS

Cross Origin Resource Sharing is a simple concept closely related to JS MVCs. If you foresee your API being routed to a subdomain (e.g. api.website.com), you will encounter CORS.
CORS boils down to:

  • By default, cookies are NOT sent with cross-domain requests. To send cookies, use
    jQuery’s withCredentials: true, while the API response should send header Access-
    Control-Allow-Credentials: true
  • To allow client access from subdomains, you must specify the domains which are allowed (e.g. Access-Control-Allow-Origin: *, Access-Control-Allow-Origin: http://api.website.com)

The API will be required to have OPTIONS methods for all non-GET methods if CORS is used. These are called pre-flight requests, the browser verifies which methods are available on the certain route.

Important: If credentials are allowed (Access-Control-Allow-Credentials: true), Access-Control-Allow-Origin must not use the wildcard “*”. (Reference)

Client-Side Error Handling

How can you know whether the JS MVC is working for your users? One way is to quietly log all errors on the client, and send them to your server or a service like Rollbar for review.

window.onerror = function(msg, url, line) {
    alert("Error caught: " + msg + url + line);
    // @todo for you: ajax request to server goes here
};

This function is triggered if there is an uncaught exception or a compile time error.

Server-Side Error Handling

When building the API, consider standardizing responses that contain errors (e.g. HTTP Status Code 401, 400, 500). Responses with a JSON payload { error: true, message: ‘Reason for error’ }. Your client’s service layer (AJAX) can check for non-200 status codes by looking at response headers, response.error == true, or using jQuery’s error() callback.

If you are using a framework, it’s likely to provide a global exception handler. If you have a bug in a database query and do not surround the query execution with a try{} catch(){} block, a fatal error will occur. This will cause the script to stop executing, and send very little information to the client. To tell the client whether the error was the client’s or the server’s, a global error handler should catch all errors, log the information to a file, and respond with a human readable message:

{ error: true, message: ‘Reason for error’ }

For Fun

Interact with your application through the console. Makes for good practice.

// In developer tools -> console
var m = new NBAApp.Models.Team();
m.set('name','Lakers').set('city','Los Angeles').save();
// Refresh app

how to make mistakes

To learn from mistakes, one must make mistakes. To make mistakes one must push forward, despise inertia, and be soberly impatient. For this impatience to be tolerated by others, one must be diligent and continually progress. To progress, one must introspect and evaluate made mistakes. Allow for time to reflect.

Warning: There is a level of effort and diligence one must maintain, as it directly relates to a near supervisor’s loss of sleep.

node.js and mongo (using mongoose) tutorial

NoSQL databases came back into the mainstream when developers needed better performance and were ok with giving up the relational aspect of RDBs (unions, joins, etc). NoSQL is said to be easily scalable horizontally, unlike SQL databases. To use the NoSQL datastore MongoDB with your node instance, you need to install:

The mongoDB schema is dynamic, meaning it gets defined every single time you launch your node server.

So to begin, this is how you define a schema:

var Schema = mongoose.Schema
  , ObjectId = Schema.ObjectID;

var Hobby = new Schema({
    name            : { type: String, required: true, trim: true }
});

var Person = new Schema({
    first_name      : { type: String, required: true, trim: true }
  , last_name       : { type: String, required: true, trim: true }
  , username        : { type: String, required: true, trim: true }
  , hobbies         : [Hobby]
  , shoe_size       : Number
  , eye_color       : String
});

Where Schema is a mongoose interface and ObjectId is a MongoDB datatype. Inside Person (called a collection) you can see the different ways you can define SchemaTypes. The [Hobby] is an array or collection of Embedded Documents (which is itself a Schema, must be defined prior to the main schema). Mongoose also comes with validation middleware, so this acts as your server-side data validation (opposed to client side js).

function validateThis(v){
    return v == 'mike';
}
... first_name      : { type: String, required: true, trim: true, validate: [validateThis, 'your name isnt mike'] } ...

Useful reading on Injection in MongoDB.

And to be able to modify Person we need:

var Person = mongoose.model('Person', Person);

And thats about it for designing schemas!

To save/insert data to a schema, we use the .save() function, which has a callback (as all other db interaction calls).

var person_data = {
    first_name: req.params.first
  , last_name: req.params.last
  , username: req.params.username
};

var person = new Person(person_data);

person.save( function(error, data){
    if(error){
        res.json(error);
    }
    else{
        res.json(data);
    }
});

So person_data is the data we have and want to insert, and as you can see it matches the minimum requirements. After creating a new person object, we can save it to the database. By default, the callbacks have an error and data parameters. They return null by default – no error and no data.

To retrieve some data:

Person.find({}, function(error, data){
    console.log(data);
    res.json(data);
});

Where the first parameter of .find() can be a json object such as

{ username: 'mike' }

which acts as

WHERE username="mike"

(mongoose reference).

And to update a document within a collection, I chose to first check to see if there are actually any users with this username. Then we use the callback parameter

person

which is the actual document with username match to perform alterations and save it. There are other ways to do this.

Person.findOne({ username: req.params.username }, function(error, person){
    if(error){
        res.json(error);
    }
    else if(person == null){
        res.json('no such user!')
    }
    else{
        person.hobbies.push({ name: req.params.hobby });
        person.save( function(error, data){
            if(error){
                res.json(error);
            }
            else{
                res.json(data);
            }
        });
    }
});

I think that’s about it, pull or copy/paste the git repo. Let’s start the server:

node app.js

And add a single document by going to the url:

localhost:3001/adduser/michael/mukhin/mmukhin

To view all documents, go to:

localhost:3001/

And to add hobbies to any user:

localhost:3001/addhobby/mmukhin/beer

GIT repo: https://github.com/mmukhin/psitsmike_mongodb_tutorial

Additional reading: NoSQL server-side javascript injection.

node.js with express, quick facebook connect tutorial

This is a super quick tutorial on how to implement “login with facebook” using the Facebook JavaScript SDK. Bits of this tutorial were taken from the Facebook connect example. To begin, lets install express:

npm install express

Now let’s create an app.js file which we will use to create the server.

var app = require('express').createServer();

// routing
app.get('/', function (req, res) {
  res.sendfile(__dirname + '/index.html');
});

app.listen(8080);

Now in the index.html file, we must first include 2 scripts – jQuery and the Facbeook SDK. The app, first gets initialized with facebook by sending the API Key. Then, we query for the login status to see if we are currently logged in and/or user authorized the app for logins. We add a couple listeners for button clicks, and write the session response function. There is a console.log in there so you can see all that actually gets returned.

<!doctype html>
<html>
<head>
   <title>nodejs - facebook</title>
</head>
<body>
	<div>
           <button id="login">Login</button>
           <button id="logout">Logout</button>
	</div>
    <br>
    <span id="user-info" style="border:1px solid #888"></span>
    <span id="fb-root"></span>

    <script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.7.min.js"></script>
    <script src="http://connect.facebook.net/en_US/all.js"></script>
    <script>
        // initialize the library with the API key
        FB.init({ appId:'YOUR_APP_ID' });

        // fetch the status on load
        FB.getLoginStatus(handleSessionResponse);

        $('#login').bind('click', function() {
            FB.login(handleSessionResponse);
        });

        $('#logout').bind('click', function() {
            FB.logout(handleSessionResponse);
        });

        // handle a session response from any of the auth related calls
        function handleSessionResponse() {
            FB.api('/me', function(response) {
                console.log(response);
                $('#user-info').html(response.id + ' - ' + response.name);
            });
        }
    </script>
</body>
</html>

Now just load up

node app.js

and go to localhost:8080.