Ampersand.js in a nutshell

Ever since I’ve started working with ampersand.js (which is for about three months now), I’ve been wondering why not more people have discovered this wonderful non-frameworky ‘framework’ and written about it. I decided to share my knowledge and experience, of course starting with a short introduction to get everyone up to speed.

The framework that is not a framework at all

Ampersand was created by the guys and gals of &yet, who wanted to use the good parts of backbone.js (which is awesome for clientside, single page webapps), and combine them with the simple, modular philosophy you can find in the node.js world.

It consists of different modules that you can easily add to your project with npm. If you only want to use one or two Ampersand modules, that’s perfectly fine: they’re just plain old CommonJS modules you can declare in your package.json.

Those of you who are familiar with Backbone, or other single-page app frameworks, will immediately recognize the concepts I will be talking about, but those of you who aren’t I’ll bring up to speed with some important terminology.

Model: the heart of your application

In Ampersand, like in backbone, models are the heart of your app. They contain the very objects your app works with, all their data, and the logic surrounding them. You will, for example, keep a User of your application in a model. The model will define how one User will look. Maybe something like this?

firstName,
lastName,
email,
address: {
    street,
    number,
    postalCode
},
birthDate

The User model should also define what you can do with a User object: login, delete, add, edit, flag, …

Collection: grouping your models

Imagine you’re building an app like Spotify, where one User has a collection of songs. In that case, one Song is a new model, but all Songs a User has added together form a Collection. An Ampersand Collection is a very simple, but extremely powerful object to group models of the same type together.

View: bringing user interaction and application data together

With Models and Collections you can set up and define your application data in no time, but every client-side web app needs user interaction. All contact with the user happens through a View: the glue between your HTML template and the state. Views make sure that a template can remain a template, without any logic intertwining with the declaration of your HTML DOM.

Router: navigating a single-page web application

Before single-page applications, a web url’s main function was to link to the right page of a website. Since a single-page web app only has one page, the url becomes something entirely different. The url is used to connect the user to certain actions and events in the app.
ampersandiscool.com/user/login, for example, connects to the action ‘open the login View’. The login View then renders the right HTML template linked to it, and the login screen is shown in the browser.

Getting to know Ampersand

Now that everyone is up to speed, we can dive a bit deeper into Ampersand. Let’s go over the main modules and check out how they can work together to get you a kick ass client-side web app.

ampersand-state

Ampersand-state is in fact the base object for an Ampersand Model, keeping track of state and making it observable by watching changes to the state. A big difference between Backbone.Model and ampersand-state, is that Backbone.Model does not presume anything about the properties you assign to a Model. If one developer, for example, adds the property isRegistered to a User Model in a certain view, and another one adds a very similar property named registered, semantically the same state is being tracked in two properties. Ampersand-state enforces you to declare the properties you want to store for a Model in ampersand-state, so properties are not scattered across the codebase. This is what our User model would look like as ampersand-state:

var AmpersandState = require('ampersand-state');

var User = AmpersandState.extend({
    // Simple properties of the User, when syncing with a db, 
    // these properties' state is synced with the db.
    props: {
        firstName: 'string',
        lastName: 'string',
        email: 'string',
        birthDate: 'string',
    },

    // Session properties' state is not saved after the session ends.    
    session: {
        signedIn: ['boolean', true, false],
    },

    // Derived properties are derived from `props`.
    derived: {
        fullName: {
            deps: ['firstName', 'lastName'],
            fn: function () {
                return this.firstName + ' ' + this.lastName;
            }
        }
    },

    // You can extend existing methods of ampersand-state or write your own methods
    initialize: function() {
        // Your code here.
    }
});

ampersand-model

Ampersand-model is a more elaborate version of ampersand-state that provides methods and properties that could come in handy when your data comes from an API. Ampersand-model has some extra methods to save, sync, fetch and destroy data. You can map ampersand-model to an API endpoint very easily by adding the uri property like this:

var AmpersandModel = require('ampersand-model');

var User = AmpersandModel.extend({
    // API endpoint.
    uri: '/user'
    props: {
        firstName: 'string',

...

ampersand-collection

The cool thing about ampersand-collection is that is does not make assumptions about what you are going to store in it. The collection for songs a User has added to his Spotify favorites doesn’t need more declaring than this:

var AmpersandCollection = require('ampersand-collection');

var Library = AmpersandCollection.extend({
    model: Song
});

A collection is in fact a very smart container for models, you just have to throw a bunch of Models in, and ampersand-collection takes care of them. If a Model is already part of the Collection, it won’t be added.

Collections can be easily sorted too, using the comparator property.

var AmpersandCollection = require('ampersand-collection');

var Library = AmpersandCollection.extend({
    model: Song,

    // The comparator sorts the collection by title of the Song model.
    comparator: function(a, b) {
        if (a.title.toLowerCase() > b.title.toLowerCase()) return 1;
        else if (a.title.toLowerCase() < b.title.toLowerCase()) return -1;
        else return 0;
});

ampersand-view

Ampersand-view is still very much based on Backbone.View, but also extends ampersand-state. This means that ampersand-view can also have its own state. Realizing that simple fact has helped me a lot during my development. Another important thing concerning Views, is that when the view is removed, all its events, subviews and rendered collection will be removed too. Ampersand kind of cleans up after you.

One of the main functionalities of a View is binding state to a DOM-element. If, for example, we want to set up the view for a User’s profile, we can easily start this way:

var AmpersandView = require('ampersand-view');

var ProfileView = new AmpersandView.extend({
    // A View template is not more than a string of HTML. 
    // You can add in a hardcoded string, use a function that returns a template, or use something like Browserify.
    // Please do make sure the template only has one root element, otherwise it won't be rendered! 
    template: "<div> <span data-hook='name'></span> <span class='email'></span> <a data-hook='edit'>edit</a> </div>",

    // Bind state to DOM element.
    bindings: {
        'model.fullName': {
            type: 'innerHTML',
            hook: 'name'
        },
        'model.email': {
            type: 'innerHTML',
            selector: '.email'
        }
    },

    // Render function should always at least look like this.
    render: function() {
        this.renderWithTemplate(this);
        return this;
    }

});

We also added an edit button to the template for this view, so let’s take a look at how we can handle a click on this button:

var AmpersandView = require('ampersand-view');

var ProfileView = new AmpersandView.extend({
    template: "<div> <span data-hook='name'></span> <span class='email'></span> <a class='edit'>edit</a> </div>",

    // All eventlisteners should be declared in the events property.
    // They are removed when the view is removed.
    events: {
        'mousedown .edit': 'edit'
    },

    bindings: {
        'model.fullName': {
            type: 'innerHTML',
            hook: 'name'
        },
        'model.email': {
            type: 'innerHTML',
            selector: '.email'
        }
    },

    render: function() {
        this.renderWithTemplate(this);
        return this;
    }

    edit: function(e) {
        // Call the edit method in the model.
        this.model.edit();
    }
});

Final thoughts

I could go on and on about what I have learned about Ampersand, and how we use it at neoScores, but that will be for a next post. There are tons of stuff I haven’t even talked about yet, so just give Ampersand a spin, and let me know in the comments what you think I should write about next.

If you’re interested, you should definitely read The Human Javascript. Even if you’re not going to use ampersand for your own projects, the concepts described in this book can be real eye-openers.

Take a look at this blogpost by &yet introducing Ampersand. It tells the complete story of how &yet decided to build this extreme minimalist non-framework.

Also, be sure to check out the beautifully compact documentation for ampersand. Everything you need to know is here! Take a look around the ampersand website too while you’re there, it provides some very nice tutorials too.

If you’re completely new to the concept of single page webapps, I suggest you also take a look at backbone.js. There are lots of other frameworks out there, but Backbone is obviously the most similar to Ampersand.