API Documentation

This page has consolidated API reference documentation for the core modules in a single page for the sake of being 'cmd+f' friendly.

For more in-depth guides, conventions, best practices, and how to use these tools together please see guides.

Suggest an edit

The Ampersand CLI

ampersand

latest v3.0.6

CLI tool for generating single page apps a. la. http://humanjavascript.com

Lead Maintainer: Drew Fyock

The idea behind the CLI is not to solve all your problems and write all your code, but to help you with the tedious parts of building an app, which is what computers are supposed to help us with.

Installation

npm install -g ampersand

Starting a new app

Just, cd into whatever directory you normally put your projects in and just run ampersand.

The CLI will walk you through some basic questions, and kick out an app that runs out of the box.

It's meant to be a loose guide, not an edict. Just delete whatever isn't relevant.

Generating stuff

ampersand gen {{type}}

Type can be form, view, model or collection.

Generating models (from JSON)

You can use the CLI to generate a model and collection for that model. If you already know what the JSON is going to look like you can pipe it into the generator to create a model with matching properties.

On a Mac, if you've copied some JSON to your clipboard you can do this from anywhere within your project folder:

pbpaste | ampersand gen model MyModel

And it'll kick out two files in your models folder (which is configurable, see below):

my-model.js
my-model-collection.js

And it will create the properties in the JSON object as model properties.

Don't worry, nothing will be overwritten unless you use the the --force (or -f) option

Generating forms from models

You can also use a model to generate the starting point of a form-view for editing that model.

ampersand gen form ./path/to/your/model.js

It will create a form view in your /client/forms folder.

Nothing will be overwritten unless you use the the --force (or -f) option, so it's safe to just experiment.

Configuring the generated code

The cli looks for config options from a number of sources, starting with default, applying configs from a .ampersandrc in your home folder, then your project root, then by parsing option flags from stdin.

Those files can be JSON or ini format.

The available options and defaults are as follows:

  • framework: default framework to be prompted with, options are express or hapi
  • indent: indent size
  • view: default template
  • router: default template
  • model: default template
  • page: default template
  • collection: default template
  • clientfolder: name for the 'client' folder
  • viewfolder: name for the 'views' folder
  • pagefolder: name for the 'pages' folder
  • modelfolder: name for the 'models' folder
  • formsfolder: name for the 'forms' folder
  • collectionfolder: name for the collection folder - grouped with 'models' by default
  • makecollection: whether to create collection when making a model
  • approot: if called without the 'gen' command build a new one, so we won't look for an application root. starts walking up folders looking for package.json.
  • quotes: options are 'single' or 'double'

Sample JSON with default options

{
    "framework": "hapi",
    "indent": 4,
    "view": "",
    "router": "",
    "model": "",
    "page": "",
    "collection": "",
    "clientfolder": "client",
    "viewfolder": "views",
    "pagefolder": "pages",
    "modelfolder": "models",
    "formsfolder": "forms",
    "collectionfolder": "models",
    "makecollection": true,
    "approot": "",
    "quotes": "single"
}
Suggest an edit

ampersand-app

ampersand-app

latest v2.0.0

Simple instance store for managing instances without circular dependency issues in ampersand apps.

Simple instance store and event channel that allows different modules within your app to communicate without requiring each other directly. The entire module is only ~30 lines of code, you can read the source here to see exactly what it does.

The Singleton pattern

Whenever you require('ampersand-app') it returns the same instance of a plain 'ol JavaScript Object.

This is called the Singleton pattern.

This object it returns is nothing special. It's just a plain old JavaScript Object that has been decorated with ampersand-events methods as well as an extend and reset method.

That's it!

Why is this useful?

It's quite common to create an app global to store collections and models on and then to reference that global whenever you need to look up related model instance from another module within your app. However, this creates many indirect interdependencies within your application which makes it more difficult to test isolated parts of your application.

It's also quite common to need "application-level" events that any number of pieces of your app may need to handle. For example, navigation events, or error events that could be triggered by any number of things within your app but that you want to handle by a single module that shows them as nice error dialogs.

This module provides a pattern to address both those cases without having to rely on globals, or have circular dependency issues within your apps. It also means you don't have to adjust code linting rules to ignore that app global.

Before ampersand-app

Module "A" (app.js):

var MyModel = require('./models/some-model');

// explicitly create global
window.app = {
    init: function () {
        this.myModel = new MyModel();
    }
};

window.app.init();

Module "B" (that needs access to app):

// note we're not requiring anything
module.exports = View.extend({
    someMethod: function () {
        // reference app and models directly
        app.myModel.doSomething():
    }
});

With ampersand-app you'd do this instead:

Module "A" (app.js):

// it just requires ampersand-app too!
var app = require('ampersand-app');
var MyModel = require('./models/some-model');

// Here we could certainly *chose* to attach it to
// window for better debugging in the browser 
// but it's no longer necessary for accessing the 
// app instance from other modules.
app.extend({
    init: function () {
        this.myModel = new MyModel();
    }
};

app.init();

Module "B" (that needs access to app):

// this just requires ampersand-app too!
var app = require('ampersand-app');


module.exports = View.extend({
    someMethod: function () {
        // reference app that we required above
        app.myModel.doSomething():

        // now as a bonus, since `app` supports events
        // we've also got a global "pubsub" mechanism
        // for app events, that any other modules can 
        // listen to.
        app.trigger('some custom event');
    }
});

Now when we go to write tests for module "B" we can easily mock things that it expects from app.

So our tests for module B might look like this:

var test = require('tape');
var ModuleB = require('../module-b');
// note we just require ampersand-app here
// and make sure it has what module b expects
var app = require('ampersand-app');


test('test module B', function (t) {
    // each test can clear it.
    app.reset();
    // stub out what it might need for the
    // test.
    app.myModel = {
        doSomething: function () {}
    };

    // check to make sure calling 
    // `someMethod` fires event on app
    app.on('some custom event', function () {
        t.pass('custom event fired');

        // app also has a `reset` for testing
        // purposes that purges it to start over
        // so this could be used to reset before each test
        app.reset();

        t.end();
    });

    var view = new ModuleB();

    t.doesNotThrow(function () {
        view.someMethod();
    }, 'make sure calling some method does not explode');
});

test('next test', function () {
    // now we can use `reset` if we want
    // to make sure we clear that state
    app.reset();

    // etc. etc.
});

Warning: Not for use in re-usable modules

If you're writing a re-usable module for distribution on npm it should not have ampersand-app as a dependency.

Doing so makes assumptions about how you want it to be used.

Say you want to make an error event handling module, that requires ampersand-app listens for error events from that app and shows a nice error dialog.

Rather than make all those assumptions about how its going to be used, just make the nice error dialog view and suggest in the readme how someone might use ampersand-app as an event channel to trigger them.

This allows people who don't use this particular application pattern to still use your npm module and leaves the event names, and application architecture up to the person building the app.

install

npm install ampersand-app

API Reference

event methods

The app object is an event object so it contains all the methods as described in the ampersand-events docs.

The app object becomes a handy way to communicate within your app so various modules can notify each other about "app-level" events such as user navigation, etc.

extend app.extend(obj, [*objs])

Convenience method for attaching multiple things to the app at once. This is simply an alias for amp-extend that pre-fills the app as the object being extended.

  • obj {Object} copy properties from this object onto app. You can pass as many objects to this as you want as additional arguments.
var app = require('ampersand-app');
var UserCollection = require('./models/user-collection');
var MeModel = require('./models/me');


app.extend({
    me: new MeModel(),
    users: new UserCollection(),
    router: new Router(),
    init: function () {
        this.router.history.start({pushState: true});
    }
});

reset app.reset()

Resets the app singleton to its original state, clearing all listeners, and deleting everything you've added to it, but keeping the same object instance.

This is primarily for simplifying unit testing of modules within your app. Whenever you require('ampersand-app') you get the same object instance (this is the Singleton pattern). So, having app.reset() lets you mock app state required for testing a given module.

Suggest an edit

ampersand-state

ampersand-state

latest v5.0.2

An observable, extensible state object with derived watchable properties.

Lead Maintainer: Philip Roberts

Coverage Status

An observable, extensible state object with derived watchable properties.

Ampersand-state serves as a base object for ampersand-model but is useful any time you want to track complex state.

ampersand-model extends ampersand-state to include assumptions that you'd want if you're using models to model data from a REST API. But by itself ampersand-state is useful for anytime you want something to model state, that fires events for changes and lets you define and listen to derived properties.

For further explanation see the learn ampersand-state guide.

Install

npm install ampersand-state --save

API Reference

extend AmpersandState.extend({ })

To create a State class of your own, you extend AmpersandState and provide instance properties and options for your class. Typically here you will pass any properties (props, session and derived) of your state class, and any instance methods to be attached to instances of your class.

extend correctly sets up the prototype chain, so that subclasses created with extend can be further extended as many times as you like.

Definitions like props, session, derived etc will be merged with superclass definitions.

var Person = AmpersandState.extend({
    props: {
        firstName: 'string',
        lastName: 'string'
    },
    session: {
        signedIn: ['boolean', true, false],
    },
    derived: {
        fullName: {
            deps: ['firstName', 'lastName'],
            fn: function () {
                return this.firstName + ' ' + this.lastName;
            }
        }
    }
});

AmpersandState.extend does more than just copy attributes from one prototype to another. As such it is incompatible with Coffeescript's class-based extend. TypeScript users may have similar issues.

For instance, this will not work since it never actually calls AmpersandState.extend:

class Foo extends AmpersandView
     constructor: (options)->
         @special = options.special
         super

constructor/initialize new AmpersandState([attrs], [options])

When creating an instance of a state object, you can pass in the initial values of the attributes which will be set on the state. Unless extraProperties is set to allow, you will need to have defined these attributes in props or session.

If you have defined an initialize function for your subclass of State, it will be invoked at creation time.

var me = new Person({
    firstName: 'Phil',
    lastName: 'Roberts'
});

me.firstName //=> Phil

Available options:

  • [parse] {Boolean} - whether to call the class's parse function with the initial attributes. Defaults to false.
  • [parent] {AmpersandState} - pass a reference to a state's parent to store on the state.

idAttribute state.idAttribute

The attribute that should be used as the unique id of the state. getId uses this to determine the id for use when constructing a model's url for saving to the server.

Defaults to 'id'.

var Person = AmpersandModel.extend({
    idAttribute: 'personId',
    urlRoot: '/people',
    props: {
        personId: 'number',
        name: 'string'
    }
});

var me = new Person({ personId: 123 });

console.log(me.url()) //=> "/people/123"

getId state.getId()

Get ID of state per idAttribute configuration. Should always be how ID is determined by other code.

namespaceAttribute state.namespaceAttribute

The property name that should be used as a namespace. Namespaces are completely optional, but exist in case you need to make an additional distinction between states, that may be of the same type, with potentially conflicting IDs but are in fact different.

Defaults to 'namespace'.

getNamespace state.getNamespace()

Get namespace of state per namespaceAttribute configuration. Should always be how namespace is determined by other code.

typeAttribute

The property name that should be used to specify what type of state this is. This is optional, but specifying a state type types provides a standard, yet configurable way to determine what type of state it is.

Defaults to 'modelType'.

getType state.getType()

Get type of state per typeAttribute configuration. Should always be how type is determined by other code.

extraProperties AmpersandState.extend({ extraProperties: 'allow' })

Defines how properties that aren't defined in props, session or derived are handled. May be set to 'allow', 'ignore' or 'reject'.

Defaults to 'ignore'.

var StateA = AmpersandState.extend({
    extraProperties: 'allow',
});

var stateA = new StateA({ foo: 'bar' });
stateA.foo === 'bar' //=> true


var StateB = AmpersandState.extend({
    extraProperties: 'ignore',
});

var stateB = new StateB({ foo: 'bar' });
stateB.foo === undefined //=> true


var stateC = AmpersandState.extend({
    extraProperties: 'reject'
});

var stateC = new StateC({ foo: 'bar' })
//=> TypeError('No foo property defined on this state and extraProperties not set to "ignore" or "allow".');

collection state.collection

A reference to the collection a state is in, if in a collection.

This is used for building the default url property, etc.

Which is why you can do this:

// some ampersand-rest-collection instance
// with a `url` property
widgets.url //=> '/api/widgets'

// get a widget from our collection
var badWidget = widgets.get('47');

// Without a `collection` reference this
// widget wouldn't know what URL to build
// when calling destroy
badWidget.destroy(); // does a DELETE /api/widgets/47

cid state.cid

A special property of states, the cid, or a client id, is a unique identifier automatically assigned to all states when they are first created. Client ids are handy when the state has not been saved to the server, and so does not yet have its true id, but needs a unique id (so it can be rendered in the UI, etc.).

var userA = new User();
console.log(userA.cid) //=> "state-1"

var userB = new User();
console.log(userB.cid) //=> "state-2"

isNew state.isNew()

Has this state been saved to the server yet? If the state does not yet have an id (using getId()), it is considered to be new.

escape state.escape()

Similar to get, but returns the HTML-escaped version of a state's attribute. If you're interpolating data from the state into HTML, use escape when retrieving attributes to help prevent XSS attacks.

var hacker = new PersonModel({
    name: "<script>alert('xss')</script>"
});

document.body.innerHTML = hacker.escape('name');

isValid state.isValid()

Check if the state is currently valid. It does this by calling the state's validate method (if you've provided one).

dataTypes AmpersandState.extend({ datatypes: myCustomTypes })

ampersand-state defines several built-in datatypes: string, number, boolean, array, object, date, state, or any. Of these, object, array and any allow for a lot of extra flexibility. However sometimes it may be useful to define your own custom datatypes. Then you can use these types in the props below with all their features (like required, default, etc).

Setting type is required and typeError will be thrown if it's missing or has not been choosen either from default types or your custom ones.

To define a type, you generally will provide an object with 4 member functions (though only 2 are usually necessary) get, set, default, and compare.

  • set : function(newVal){}; returns {type : type, val : newVal};: Called on every set. Should return an object with two members: val and type. If the type value does not equal the name of the dataType you defined, a TypeError should be thrown.
  • compare : function(currentVal, newVal, attributeName){}; returns boolean: Called on every set. Should return true if oldVal and newVal are equal. Non-equal values will eventually trigger change events, unless the state's set (not the dataTypes's!) is called with the option {silent : true}.
  • onChange : function (value, previousValue, attributeName){};: Called after the value changes. Useful for automatically setting up or tearing down listeners on properties.
  • get : function(val){} returns val;: Overrides the default getter of this type. Useful if you want to make defensive copies. For example, the date dataType returns a clone of the internally saved date to keep the internal state consistent.
  • default : function(){} returns val;: Returns the default value for this type.

For example, let's say your application uses a special type of date, JulianDate. You'd like to setup this as a type in state, but don't want to just use any or object as the type. To define it:

// Julian Date is a 'class' defined elsewhere:
// it has an 'equals' method and takes `{julianDays : number}` as a constructor

var Person = AmpersandState.extend({
   dataTypes : {
        julianDate : {
           // set called every time someone tried to set a property of this datatype
           set : function(newVal){
               if(newVal instanceof JulianDate){
                   return {
                       val : newVal,
                       type : 'julianDate'
                   };
               }
               try{
                   // try to parse it from passed in value:
                   var newDate = new JulianDate(newVal);

                   return {
                       val : newDate,
                       type : 'julianDate'
                   };
               }catch(parseError){
                   // return the value with what we think its type is
                   return {
                       val : newVal,
                       type : typeof newVal
                   };
               }
           },
           compare : function(currentVal, newVal, attributeName){
               return currentVal.equals(newVal);
           }
       }

   }
   props : {
       bornOn : 'julianDate',
       retiresOn : {
           type : 'julianDate',
           required : 'true',
           default : function(){
                  // assuming an 'add' function on julian date which returns a new JulianDate
                  return this.bornOn.add('60','years');
               }
           }
   }
});

var person = new Person({ bornOn : new JulianDate({julianDays : 1000}); }
// this will also work and will build a new JulianDate
var person = new Person({bornOn : {julianDays : 1000}});

// will construct a new julian date for us
// and will also trigger a change event
person.bornOn = {julianDays : 1001};

// but this will not trigger a change event since the equals method would return true
person.bornOn = {julianDays : 1001};

props AmpersandState.extend({ props: { name: 'string' } })

The props object describes the observable properties of your state class. Always pass props to extend; never set it on an instance, as it won't define new properties.

Properties can be defined in three different ways:

  • As a string with the expected dataType. One of string, number, boolean, array, object, date, or any. (Example: name: 'string'.) Can also be set to the name of a custom dataTypes, if the class defines any.
  • An array of [dataType, required, default]
  • An object { type: 'string', required: true, default: '' , values: [], allowNull: false, setOnce: false }
  • default will be the value that the property will be set to if it is undefined (either by not being set during initialization, or by being explicit set to undefined).
  • If required is true, one of two things will happen
    • If the property has a default, it will start with that value, and revert to it after a call to unset(propertyName).
    • If the property does not have a default, calls to unset(propertyName) will throw an error.
  • If values array is passed, then you'll be able to change a property to one of those values only.
  • If setOnce is true, then you'll be able to set property only once.
    • If the property has a default, and you don't set the value initially, the property will be permanently set to the default value.
    • If the property doesn't have a default, and you don't set the value initially, it can be set later, but only once.
  • If test function is passed, then a negative validation test will be executed every time this property is about to be set. If the validation passes, the function must return false to tell State to go ahead and set the value. Otherwise, it should return a string with the error message describing the validation failure. In this case State will throw a TypeError with "Property '<property>' failed validation with error: <errorMessage>".

Trying to set a property to an invalid type will throw an error.

See get and set for more information about getting and setting properties.

var Person = AmpersandState.extend({
    props: {
        name: 'string',
        age: 'number',
        paying: ['boolean', true, false], // required attribute, defaulted to false
        type: {
            type: 'string',
            values: ['regular-hero', 'super-hero', 'mega-hero']
        },
        numberOfChildren: {
            type: 'number',
            test: function(value){
                if (value < 0) {
                    return "Must be a positive number";
                }
                return false;
            }
        },
    }
});

reserved prop, session names

The following should not be used as prop names for any state object. This of course includes things based on state such as ampersand-model and ampersand-view.

If you're consuming an API you don't control, you can re-name keys by overwriting parse and serialize methods.

bind, changedAttributes, cid, clear, collection, constructor, createEmitter, escape, extraProperties, get, getAttributes, getId, getNamespace, getType, hasChanged, idAttribute, initialize, isNew, isValid, listenTo, listenToAndRun, listenToOnce, namespaceAttribute, off, on, once, parent, parse, previous, previousAttributes, serialize, set, stopListening, toJSON, toggle, trigger, typeAttribute, unbind, unset, url

defaulting to objects/arrays

You will get an error if you try to set the default of any property as either an object or array. This is because those two dataTypes are mutable and passed by reference. (Thus, if you did set a property's default to ['a','b'], it would return the same array on every new instantiation of the state.)

Instead, if you want a property to default to an array or an object, just set default to a function, like this:

AmpersandModel.extend({
    props: {
        checkpoints: {
            type: 'array',
            default: function () { return []; }
        }
    }
});

It's worth noting that both array and object have this behavior built-in: they default to empty versions of themselves. You would only need to do this if you wanted to default to an array/object that wasn't empty.

session AmpersandState.extend({ session: { name: 'string' } })

Session properties are defined and work in exactly the same way as props, but generally only exist for the lifetime of the page. They would not typically be persisted to the server, and are not returned by calls to toJSON() or serialize().

var Person = AmpersandState.extend({
    props: {
        name: 'string',
    },
    session: {
        isLoggedIn: 'boolean'
    }
);

derived AmpersandState.extend({ derived: { derivedProperties }})

Derived properties (also known as computed properties) are properties of the state object that depend on other properties (from props, session, or even derived or the same from state props or children) to determine their value. Best demonstrated with an example:

var Address = AmpersandState.extend({
  props: {
    'street': 'string',
    'city': 'string',
    'region': 'string',
    'postcode': 'string'
  }
});

var Person = AmpersandState.extend({
    props: {
        firstName: 'string',
        lastName: 'string',
        address: 'state'
    },
    derived: {
        fullName: {
            deps: ['firstName', 'lastName'],
            fn: function () {
                return this.firstName + ' ' + this.lastName;
            }
        },
        mailingAddress: {
            deps: ['address.street', 'address.city', 'address.region', 'address.postcode'],
            fn: function () {
                var self = this;
                return ['street','city','region','postcode'].map(function (prop) {
                    var val = self.address[prop];
                    if (!val) return val;
                    return (prop === 'street' || prop === 'city') ? val + ',' : val;
                }).filter(function (val) {
                    return !!val;
                }).join(' ');
            }
        }
    }
});

var person = new Person({
    firstName: 'Phil',
    lastName: 'Roberts',
    address: new Address({
        street: '123 Main St',
        city: 'Anyplace',
        region: 'BC',
        postcode: 'V6A 2S5'
    })
});
console.log(person.fullName) //=> "Phil Roberts"
console.log(person.mailingAddress) //=> "123 Main St, Anyplace, BC V6A 2S5"

person.firstName = 'Bob';
person.address.street = '321 St. Charles Pl'
console.log(person.fullName) //=> "Bob Roberts"
console.log(person.mailingAddress) //=> "321 St. Charles Pl, Anyplace, BC V6A 2S5"

See working example at RequireBin

Each derived property is defined as an object with the following properties:

  • deps {Array} - An array of property names which the derived property depends on.
  • fn {Function} - A function which returns the value of the computed property. It is called in the context of the current object, so that this is set correctly.
  • cache {Boolean} - Whether to cache the property. Uncached properties are computed everytime they are accessed. Useful if it depends on the current time for example. Defaults to true.

Derived properties are retrieved and fire change events just like any other property. However, they cannot be set directly. Caching ensures that the fn function is only run when any of the dependencies change, and change events are only fired if the result of calling fn() has actually changed.

children AmpersandState.extend({ children: { profile: Profile } })

Define child state objects to attach to the object. Attributes passed to the constructor or to set() will be proxied to the children/collections. Childen's change events are proxied to the parent.

var AmpersandState = require('ampersand-state');
var Hat = AmpersandState.extend({
    props: {
        color: 'string'
    }
});

var Person = AmpersandState.extend({
    props: {
        name: 'string'
    },
    children: {
        hat: Hat
    }
});

var me = new Person({ name: 'Phil', hat: { color: 'red' } });

me.on('all', function (eventName) {
    console.log('Got event: ', eventName);
});

console.log(me.hat) //=> Hat{color: 'red'}

me.set({ hat: { color: 'green' } });
//-> "Got event: change:hat.color"
//-> "Got event: change"

console.log(me.hat) //=> Hat{color: 'green'}

note: If you want to be able to swap out and get a change event from a child model, don't use children instead, define a prop in props of type state.

children and collections are not just a property of the parent, they're part of the parent. When you create the parent, an instance of any children or collections will be instantiated as part of instantiating the parent, whether they have any data or not.

Calling .set() on the parent with a nested object will automatically set() them on children and collections too. This is super handy for APIs like this one that return nested JSON structures.

Also, there will be no change events triggered if you replace a child with something else after you've instantiated the parent because it's not a true property in the props sense. If you need a prop that stores a state instance, define it as such, don't use children.

The distinction is important because without it, the following would be problematic:

var Person = State.extend({
    props: {
        child: {
            type: 'state'
        }
    }
});

var person = new Person()

// throws type error because `{}` isn't a state object
person.child = {};
// should this work? What should happen if the `child` prop isn't defined yet?
person.set({child: {name: 'mary'}});

So, while having children in addition to props of type state may feel redundant they both exist to help disambiguate how they're meant to be used.

collections AmpersandState.extend({ collections: { widgets: Widgets } })

Define child collection objects to attach to the object. Attributes passed to the constructor or to set() will be proxied to the collections.

Note: Currently, events don't automatically proxy from collections to parent. This is for efficiency reasons. But there are ongoing discussions about how to best handle this.

var State = require('ampersand-state');
var Collection = require('ampersand-collection');

var Widget = State.extend({
    props: {
        name: 'string',
        funLevel: 'number'
    }
});

var WidgetCollection = Collection.extend({
    model: Widget
});

var Person = State.extend({
    props: {
        name: 'string'
    },
    collections: {
        widgets: WidgetCollection
    }
});

var me = new Person({
    name: 'Henrik',
    widgets: [
        { name: 'rc car', funLevel: 8 },
        { name: 'skis', funLevel: 11 }
    ]
});

console.log(me.widgets.length); //=> 2
console.log(me.widgets instanceof WidgetCollection); //=> true

parse

parse is called when the state is initialized, allowing the attributes to be modified, remapped, renamed, etc., before they are actually applied to the state. In ampersand-state, parse is only called when the state is initialized, and only if { parse: true } is passed to the constructor's options:

var Person = AmpersandState.extend({
    props: {
        id: 'number',
        name: 'string'
    },

    parse: function (attrs) {
        attrs.id = attrs.personID; //remap an oddly named attribute
        delete attrs.personID;

        return attrs;
    }
});

var me = new Person({ personID: 123, name: 'Phil' },{ parse: true});

console.log(me.id) //=> 123
console.log(me.personID) //=> undefined

parse is arguably more useful in ampersand-model, where data typically comes from the server.

serialize state.serialize([options])

Serialize the state object into a plain object, ready for sending to the server (typically called via toJSON). By default, of the state's properties only props is returned, while session and derived are omitted. You can serialize session or derived attributes as well by passing in a options object. The options object should match that accepted by .getAttributes(...). This method will also serialize any children or collections by calling their serialize methods.

get state.get(attribute); state[attribute]; state.firstName

Get the current value of an attribute from the state object. Attributes can be accessed directly, or a call to the Backbone style get. So these are all equivalent:

person.get('firstName');
person['firstName'];
person.firstName

Get will retrieve props, session or derived properties all in the same way.

set state.set(attributes, [options]); state.firstName = 'Henrik';

Sets an attribute, or multiple attributes, on the state object. If any of the state object's attributes change, it will trigger a "change" event. Change events for specific attributes are also triggered, which you can listen to as well. For example: "change:firstName" and "change:content". If the changes update any derived properties on the object, their values will be updated, and change events fired as well.

Attributes can be set directly, or via a call to the backbone style set (useful if you wish to update multiple attributes at once):

person.set({firstName: 'Phil', lastName: 'Roberts'});
person.set('firstName', 'Phil');
person.firstName = 'Phil';

Possible options (when using state.set()):

  • silent {Boolean} - prevents triggering of any change events as a result of the set operation.
  • unset {Boolean} - unset the attributes keyed in the attributes object instead of setting them.

Note: when passing an object as the attributes argument, only that object's own enumerable properties (i.e. those that can be accessed with Object.keys(object)) are read and set. This behaviour is new as of v5.0.0, as prior version relied on for...in to access an object's properties, both owned by that object and those inherited through the prototypal chain.

unset state.unset(attribute|attributes[], [options])

Clear the named attribute or an array of named attributes from the state object. Fires a "change" event and a "change:attributeName" event unless silent is passed as an option.

If the attribute being unset is required and has a default value as defined in either props or session, it will be set to that value, otherwise it will be undefined.

// unset a single attribute
person.unset('firstName')
// unset multiple attributes
person.unset(['firstName', 'lastName'])

clear state.clear([options])

Clear all the attributes from the state object, by calling the unset function for each attribute, with the options provided.

person.clear()

toggle state.toggle('a')

Shortcut to toggle boolean properties, or cycle through array of specified property's values (see values option section and example below). When you reach the last available value from given array, toggle will go back to the beginning and use first one.

Fires "change" events, as you would expect from set().

var Person = AmpersandState.extend({
    props: {
        active: 'boolean',
        color: {
            type: 'string',
            values: ['red', 'green', 'blue']
        }
    }
});

var me = new Person({ active: true, color: 'green' });

me.toggle('active');
console.log(me.active) //=> false

me.toggle('color');
console.log(me.color) //=> 'blue'

me.toggle('color');
console.log(me.color) //=> 'red'

previousAttributes state.previousAttributes()

Return a copy of the object's previous attributes (the state before the last "change" event). Useful for getting a diff between versions of a state, or getting back to a valid state after an error occurs.

hasChanged state.hasChanged([attribute])

Determine if the state has been modified since the last "change" event. If an attribute name is passed, determine if that one attribute has changed.

note: that this will only be true if checked inside a handler while the various change events are firing. Once the change events are done this will always return false. This has nothing to do with determining whether a property has changed since the last time it was saved to the server.

changedAttributes state.changedAttributes([objectToDiff])

Return an object containing all the attributes that have changed, or false if there are no changed attributes. Useful for determining what parts of a view need to be updated and/or what attributes need to be persisted to the server. Unset attributes will be set to undefined. You can also pass an attributes object to diff against the state, determining if there would be a change.

note: that if passing an attributes object to diff against, only changes to properties defined on the model will be detected. This means that changes to children or collections will not be returned as changes by this method.

note: that this will only return values if checked inside a handler while the various change events are firing. Once the change events are done this will always be return an empty object. This has nothing to do with determining which properties have been changed since the last time it was saved to the server.

toJSON state.toJSON()

Return a shallow copy of the state's attributes for JSON stringification. This can be used for persistence, serialization, or augmentation, before being sent to the server. The name of this method is a bit confusing, as it doesn't actually return a JSON string — but I'm afraid that it's the way that the JavaScript API for JSON.stringify works.

Calls serialize to determine which values to return in the object. Will be called implicitly by JSON.stringify.

var me = new Person({ firstName: 'Phil', lastName: 'Roberts' });

me.toJSON() //=> { firstName: 'Phil', lastName: 'Roberts' }

//JSON.stringify implicitly calls toJSON:
JSON.stringify(me) //=> "{\"firstName\":\"Phil\",\"lastName\":\"Roberts\"}"

getAttributes state.getAttributes([options, raw])

Returns a shallow copy of the state's attributes while only including the types (props, session, derived) specified by the options parameter. The desired keys should be set to true on options (props, session, derived) if attributes of that type should be returned by getAttributes.

The second parameter, raw, is a boolean that specifies whether returned values should be the raw value or should instead use the getter associated with its data type. If you are using getAttributes to pass data to a template, most of the time you will not want to use the raw parameter, since you will want to take advantage of any built-in and custom data types on your state instance.

var Person = AmpersandState.extend({
  props: {
      firstName: 'string',
      lastName: 'string'
  },
  session: {
    lastSeen: 'date',
    active: 'boolean'
  },
  derived: {
    fullName: {
      deps: ['firstName', 'lastName'],
      fn: function () {
        return this.firstName + ' ' + this.lastName;
      }
    }
  }
});

var me = new Person({ firstName: 'Luke', lastName: 'Karrys', active: true, lastSeen: 1428430444479 });

me.getAttributes({derived: true}) //=> { fullName: 'Luke Karrys' }

me.getAttributes({session: true}) //=> { active: true, lastSeen: Tue Apr 07 2015 11:14:04 GMT-0700 (MST) }
me.getAttributes({session: true}, true) //=> { active: true, lastSeen: 1428430444479 }

me.getAttributes({
  props: true,
  session: true,
  derived: true
}) //=> { firstName: 'Luke', lastName: 'Karrys', active: true, lastSeen: Tue Apr 07 2015 11:14:04 GMT-0700 (MST), fullName: 'Luke Karrys' }
Suggest an edit

ampersand-model

ampersand-model

latest v8.0.0

An extension to ampersand-state that adds methods and properties for working with a RESTful API.

ampersand-model is an extension built on ampersand-state to provide methods and properties that you'll often want when modeling data you get from an API.

For further explanation see the learn ampersand-state guide.

Installing

npm install ampersand-model

Observing

Ampersand gets its event system from Backbone using the backbone-events-standalone module on npm.

For more, read all about how events work in ampersand.

API Reference

The module exports just one item, the ampersand-model constructor. It has a method called extend that works as follows:

extend AmpersandModel.extend({ })

To create a Model class of your own, you extend AmpersandModel and provide instance properties and options for your class. Typically here you will pass any properties (props, session, and derived) of your model class, and any instance methods to be attached to instances of your class.

extend correctly sets up the prototype chain, so that subclasses created with extend can be further extended as many times as you like.

As with AmpersandState, definitions like props, session, derived etc will be merged with superclass definitions.

var Person = AmpersandModel.extend({
    props: {
        firstName: 'string',
        lastName: 'string'
    },
    session: {
        signedIn: ['boolean', true, false],
    },
    derived: {
        fullName: {
            deps: ['firstName', 'lastName'],
            fn: function () {
                return this.firstName + ' ' + this.lastName;
            }
        }
    }
});

constructor/initialize new ExtendedAmpersandModel([attrs], [options])

This works exactly like state with a minor addition: If you pass collection as part of options it'll be stored for reference.

As with AmpersandState, if you have defined an initialize function for your subclass of State, it will be invoked at creation time.

var me = new Person({
    firstName: 'Phil',
    lastName: 'Roberts'
});

me.firstName //=> Phil

Available options:

  • [parse] {Boolean} - whether to call the class's parse function with the initial attributes. Defaults to false.
  • [parent] {AmpersandState} - pass a reference to a model's parent to store on the model.
  • [collection] {Collection} - pass a reference to the collection the model is in. Defaults to undefined.

save model.save([attributes], [options])

Save a model to your database (or alternative persistence layer) by delegating to ampersand-sync. Returns a xhr object if validation is successful and false otherwise. The attributes hash (as in set) should contain the attributes you'd like to change — keys that aren't mentioned won't be altered — but, a complete representation of the resource will be sent to the server. As with set, you may pass individual keys and values instead of a hash. If the model has a validate method, and validation fails, the model will not be saved. If the model isNew, the save will be a "create" (HTTP POST). If the model already exists on the server, the save will be an "update" (HTTP PUT).

If you only want the changed attributes to be sent to the server, call model.save(attrs, {patch: true}). You'll get an HTTP PATCH request to the server with just the passed-in attributes.

Calling save with new attributes will cause a "change" event immediately, a "request" event as the Ajax request begins to go to the server, and a "sync" event after the server has acknowledged the successful change. Pass {wait: true} if you'd like to wait for the server before setting the new attributes on the model.

var book = new Backbone.Model({
  title: "The Rough Riders",
  author: "Theodore Roosevelt"
});

book.save();
//=> triggers a `POST` via ampersand-sync with { "title": "The Rough Riders", "author": "Theodore Roosevelt" }

book.save({author: "Teddy"});
//=> triggers a `PUT` via ampersand-sync with { "title": "The Rough Riders", "author": "Teddy" }

save accepts success and error callbacks in the options hash, which will be passed the arguments (model, response, options). If a server-side validation fails, return a non-200 HTTP response code, along with an error response in text or JSON.

fetch model.fetch([options])

Resets the model's state from the server by delegating a GET to ampersand-sync. Returns a xhr. Useful if the model has yet to be populated with data, or you want to ensure you have the latest server state. A "change" event will be triggered if the retrieved state from the server differs from the current attributes. Accepts success and error callbacks in the options hash, which are both passed (model, response, options) as arguments.

var me = new Person({id: 123});
me.fetch();

destroy model.destroy([options])

Destroys the model on the server by delegating a HTTP DELETE request to ampersand-sync. Returns the xhr object, or false if the model isNew. Accepts success and error callbacks in the options hash, which are both passed (model, response, options) as arguments.

Triggers:

  • a "destroy" event on the model, which will bubble up through any collections which contain it.
  • a "request" event as it begins the Ajax request to the server
  • a "sync" event, after the server has successfully acknowledged the model's deletion.

Pass {wait: true} if you'd like to wait for the server to respond before removing the model from the collection.

var task = new Task({id: 123});
task.destroy({
    success: function () {
        alert('Task destroyed!');
    },
    error: function () {
        alert('There was an error destroying the task');
    },
});

sync model.sync(method, model, [options])

Uses ampersand-sync to persist the state of a model to the server. Usually you won't call this directly, you'd use save or destroy instead, but it can be overriden for custom behaviour.

Configuring

ajaxConfig model.ajaxConfig or model.ajaxConfig()

ampersand-sync will call ajaxConfig on your model before it makes the request to the server, and will merge in any options you return to the request. When extending your own model, set an ajaxConfig function to modify the request before it goes to the server.

ajaxConfig can either be an object, or a function that returns an object, with the following options:

  • useXDR [boolean]: (applies to IE9 only with cross domain requests): signifies that this is a cross-domain request and that IE should use its XDomainRequest object. This is required if you're making cross-domain requests and want to support IE9). Note that XDR doesn't support headers/withCredentials.
  • headers [object]: any extra headers to send with the request.
  • xhrFields [object]: any fields to set directly on the XHR request object, most typically:
    • withCredentials [boolean]: whether to send cross domain requests with authorization headers/cookies. Useful if you're making cross sub-domain requests with a root-domain auth cookie.
  • beforeSend [function]: beforeSend will be called before the request is made, and will be passed the raw xhr object if you wish to modify it directly before it's sent.
var Person = AmpersandModel.extend({
    urlRoot: 'http://otherdomain.example.com/people',

    ajaxConfig: function () {
        return {
            headers: {
                'Access-Token': this.accessToken
            },
            xhrFields: {
                'withCredentials': true
            }
        };
    }
});

var me = new Person({ id: 123 });
me.fetch();

url model.url or model.url()

The relative url that the model should use to edit the resource on the server. By default, url is constructed by sniffing for the model's urlRoot or the model's collection url, if present, then appending the idAttribute if the model has not yet been saved. However, if the model does not follow normal REST endpoint conventions, you may overwrite it. In such a case, url may be absolute.

// overwrite `url()` example
var Person = AmpersandModel.extend({
    props: {
        id: 'number',
        name: 'string'
    },
    url: function() {
        var base = _.result(this, "urlRoot");
        if (this.isNew()) return base;
        return base + '/' + someCustomActionOnServerId(this.getId());
    },
    urlRoot: function() {
        return '/api/' + me.apiVersion + '/persons';
    }
});

var bob = new Person({id: 1234, name: 'bob'});
console.log(bob.urlRoot()); //=> /api/v1/persons
console.log(bob.url()); //=> /api/v1/persons/some/CustomId-bob-1234

urlRoot model.urlRoot or model.urlRoot()

The base url to use for fetching this model. This is useful if the model is not in a collection and you still want to set a fixed "root" but have a dynamic model.url(). Can also be a function.

If your model is in a collection that has a url you won't need this, because the model will try to build the URL from its collection.

var Person = AmpersandModel.extend({
    props: {
        id: 'string',
        name: 'string'
    },
    urlRoot: '/api/persons'
});

var bob = new Person({id: "1234"});

console.log(bob.url()); //=> "/api/persons/1234"
Suggest an edit

ampersand-collection

ampersand-collection

latest v2.0.0

A way to store/manage objects or models.

A way to store/manage objects or models.

Unlike other tools this makes no assumptions about how it's going to be used or what type of models it is going to contain. This makes it a very flexible/useful tool for modeling all kinds of stuff.

It does not require underscore or jQuery, but instead makes it easy to extend with those methods if you'd like.

Installation

npm i ampersand-collection

API Reference

extend AmpersandCollection.extend([attributes])

Create a collection class of your own by extending AmpersandCollection, providing the required instance properties to be attached instances of your class.

Typically you will specify a model constructor (if you are storing ampersand-state or ampersand-model objects).

model collection.model

Override this property to specify the model class that the collection contains. If defined, you can pass raw attributes objects (and arrays) to add and reset, and the attributes will be converted into a model of the proper type.

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

A collection can also contain polymorphic models by overriding this property with a function that returns a model.

Please note that if you do this, you'll also want to override the isModel method with your own, and describe the logic used to determine whether an object is already an instantiated model or not.

var Library = AmpersandCollection.extend({

  model: function(attrs, options) {
    if (condition) {
      return new PublicDocument(attrs, options);
    } else {
      return new PrivateDocument(attrs, options);
    }
  },

  isModel: function (model) {
    return model instanceof PublicDocument || model instanceof PrivateDocument;
  }

});

constructor/initialize new AmpersandCollection([models [, options]])

When creating an AmpersandCollection, you may choose to pass in the initial array of models. The collection's comparator may be included as an option. If you define an initialize function, it will be invoked when the collection is created, with models and options as arguments. There are a couple of options that, if provided, are attached to the collection directly: model, comparator and parent.

var people = new AmpersandCollection([{ name: 'phil' }, { name: 'bob' }, { name: 'jane' }], {
    model: Person
});

mainIndex collection.mainIndex

Specify which property the collection should use as the main index (and unique identifier) for the models/objects it holds. This is the property that get uses to retrieve models, and what add, set, and remove uses to determine whether a collection already contains a model or not.

If you specify a model property in the collection, and the model specifies an idAttribute, the collection will use that as the mainIndex unless you explicitly set it to something else.

If no mainIndex or model is specified "id" is used as the default mainIndex.

This means, that most of the time you don't need to set mainIndex and things will still Just Work™. If you wish to index on a derived property, your derived fn must be a pure function, and will be bound to the object passed into the collection on .add()/.remove()/.set() etc.

You may set it explicitly while extending AmpersandCollection like so:

var People = AmpersandCollection.extend({
    mainIndex: '_id'
});

indexes collections.indexes

Specify an optional array of keys to serve as additional indexes for the models in your collection (in addition to mainIndex). This allows you to quickly retrieve models by specifying the key to use with get.

Note that get will only ever return a single model, so the values of these indexes should be unique across the models in the collection:

var People = AmpersandCollection.extend({
    mainIndex: '_id',

    indexes: ['otherId']
});

var people = new People([
    { _id: 1, otherId: 'a', name: 'Phil' },
    { _id: 2, otherId: 'b', name: 'Julie' },
    { _id: 3, otherId: 'c', name: 'Henrik' },
    { _id: 4, otherId: 'd', name: 'Jenn' }
]);

people.get(1) //=> { _id: 1, otherId: 'a', name: 'Phil' }

people.get('b', 'otherId') //=> { _id: 2, otherId: 'b', name: 'Julie' },

length collection.length

Returns the length of the underlying array.

isCollection/instanceof collection.isCollection

Because of module deps in npm and browserify, sometimes it’s possible to end up in a situation where the same collection constructor wasn't used to build a collection object. As a result, instanceof checks will fail.

To deal with this (because sometimes this is a legitimate scenario), collection simply creates a read-only isCollection property on all collection objects. You can use it to check whether or a not a given object is, in fact, a collection object—no matter what its constructor was.

add collection.add(modelOrObject, [options])

Add a model (or an array of models) to the collection, firing an "add" event. If a model property is defined, you may also pass raw attributes objects, and have them be vivified as instances of the model. Returns the added models (or preexisting models, if already contained).

Options:

  • Pass {at: index} to splice the model into the collection at the specified index.
  • If you're adding models to the collection that it already contains, they'll be ignored, unless you pass {merge: true}, in which case their attributes will be merged into the corresponding models, firing any appropriate "change" events.
var ships = new AmpersandCollection();

ships.on("add", function(ship) {
  console.log("Ahoy " + ship.name + "!");
});

ships.add([
  {name: "Flying Dutchman"},
  {name: "Black Pearl"}
]);

//logs:
//- "Ahoy Flying Dutchman!"
//- "Ahoy Black Pearl!"

Note that adding the same model (a model with the same id) to a collection more than once is a no-op.

serialize collection.serialize()

Serialize the collection into a plain javascript array, ready for sending to the server (typically called via toJSON). Also calls serialize() on each model in the collection.

toJSON collection.toJSON()

Returns a plain javascript array of the models in the collection (which are also serialized), ready for sending to the server. The name of this method is a bit confusing, as it doesn't actually return a JSON string — but I'm afraid that it's the way that the JavaScript API for JSON.stringify() works.

var collection = new AmpersandCollection([
    {name: "Tim", age: 5},
    {name: "Ida", age: 26},
    {name: "Rob", age: 55}
]);

console.log(JSON.stringify(collection));
//=> "[{\"name\":\"Tim\",\"age\":5},{\"name\":\"Ida\",\"age\":26},{\"name\":\"Rob\",\"age\":55}]"

parse collection.parse(data, [options])

The parse method gets called if the {parse: true} option is passed when calling collection.set method. By default, parse simply returns the data it was passed, but can be overwritten through .extend to provide any additional parsing logic to extract the array of data that should be stored in the collection. This is most commonly used when processing data coming back from an ajax request. The response from an API may look like this:

{
  "limit": 100,
  "offset": 0,
  "data": [
    {"name": "larry"},
    {"name": "curly"},
    {"name": "moe"}
  ]
}

To extract data you'd define a parse method on the collection as follows, to return the array of data to be stored.

var MyCollection = Collection.extend({
    parse: function (response) {
        return response.data;
    }
});

If you're using ampersand-rest-collection's fetch() method, the parse method will be called with the response by default. Also, the options object passed to set() gets passed through as a second argument to allow for conditional parsing logic.

set collection.set(models, [options])

The set method performs a "smart" update of the collection with the passed list of models:

  • If a model in the list isn't in the collection, it will be added.
  • If a model in the list is in the collection already, its attributes will be merged.
  • If the collection contains any models that aren't in the list, they'll be removed.

All of the appropriate "add", "remove", and "change" events are fired as this happens. If you'd like to customize the behavior, you can disable it with options: {add: false}, {remove: false}, or {merge: false}.

Returns the touched models in the collection.

var vanHalen = new AmpersandCollection([eddie, alex, stone, roth]);

vanHalen.set([eddie, alex, stone, hagar]);

// Fires a "remove" event for roth, and an "add" event for "hagar".
// Updates any of stone, alex, and eddie's attributes that may have
// changed over the years.

get collection.get(query, [indexName])

Retrieve a model from the collection by index.

If called without indexName (collection.get(123)), retrieves the model by its mainIndex attribute.

Alternatively, specify an indexName to retrieve a model by any of the other listed indexes.

var People = AmpersandCollection.extend({
    mainIndex: '_id',

    indexes: ['otherId']
});

var people = new People.add([
    { _id: 1, otherId: 'a', name: 'Phil' },
    { _id: 2, otherId: 'b', name: 'Julie' },
    { _id: 3, otherId: 'c', name: 'Henrik' },
    { _id: 4, otherId: 'd', name: 'Jenn' }
]);

people.get(1) //=> { _id: 1, otherId: 'a', name: 'Phil' }

people.get('b', 'otherId') //=> { _id: 2, otherId: 'b', name: 'Julie' },

at collection.at(index)

Get a model from a collection, specified by index. Useful if your collection is sorted.

If your collection isn't sorted, at() will still retrieve models in insertion order; e.g., collection.at(0) returns the first model in the collection.

remove collection.remove(models, [options])

Remove a model (or an array of models) from the collection, and returns them. Fires a "remove" event, which you can use the option { silent: true } to suppress. The model's index before removal is available to listeners as options.index.

The models object/array can be references to actual models, or just a list of ids to remove.

reset collection.reset(models, [options])

Adding and removing models one at a time is all well and good, but sometimes there are so many models to change that you'd rather just update the collection in bulk. Use reset() to replace a collection with a new list of models (or attribute hashes), triggering a single "reset" event at the end. For convenience, within a "reset" event, the list of any previous models is available as options.previousModels.

Returns the newly-set models.

Calling collection.reset() without passing any models as arguments will empty the entire collection.

sort collection.sort([options])

Force a collection to re-sort itself. Triggers a "sort" event on the collection.

You don't need to call this under normal circumstances, as a collection with a comparator will sort itself whenever a model is added. To prevent this when adding a model, pass a {sort: false} option to add().

models collection.models

Raw access to the JavaScript array of models inside of the collection. Usually you'll want to use get, at, or the proxied array methods to access model objects, but occasionally a direct reference to the array is desired.

comparator

The comparator option lets you define how models in a collection are sorted. There's a few ways to declare comparator:

  • Passing false prevents sorting
  • Passing string sorts the collection by a specific model attribute
  • Passing function will use native array sort function; which you can define with either 1 argument (each model one by one), or multiple arguments (which lets you write custom compare functions with next 2 models as arguments).

proxied ES5 array methods (9)

The base AmpersandCollection proxies some basic ES5 methods to the underlying model array. Further documentation of these methods is available at MDN

  • indexOf
  • lastIndexOf
  • every
  • some
  • forEach
  • each (alias for forEach)
  • map
  • filter
  • reduce
  • reduceRight

Unlike Backbone collections, it does not include Underscore and all of its array methods. But if you want more functions than those built into modern browsers, you can mixin ampersand-collection-underscore-mixin to get them.

var people = People([
    { name: 'Phil', hatColor: 'red' },
    { name: 'Jenn', hatColor: 'green' },
    { name: 'Henrik', hatColor: 'blue' },
    { name: 'Julie', hatColor: 'yellow' }
]);

people.map(function (person) { return person.name; }) //=> ['Phil', 'Jenn', 'Henrik', 'Julie']

people.filter(function (person) {
    return person.name[0] === 'J';
}) //=> ['Jenn', 'Julie']
Suggest an edit

ampersand-rest-collection

ampersand-rest-collection

latest v6.0.0

ampersand-collection with REST and Lodash mixins.

Extends ampersand-collection with REST and Lodash mixins.

This makes ampersand-collection work and act a lot like Backbone.Collection, if youre planning on hitting a REST-ful API this is probably what you want to use.

Install

npm install ampersand-rest-collection

API Reference

The ampersand-rest-collection is simply an ampersand-collection extended with two mixins: ampersand-collection-rest-mixin and ampersand-collection-lodash-mixin.

var Collection = require("ampersand-collection");
var lodashMixin = require("ampersand-collection-lodash-mixin");
var restMixins = require("ampersand-collection-rest-mixin");

module.exports = Collection.extend(lodashMixin, restMixins);

ajaxConfig AmpersandRestCollection.extend({ ajaxConfig: function () { ... } })

ampersand-sync will call ajaxConfig on your collection before it makes the request to the server, and will merge in any options you return to the request. When extending your own collection, set an ajaxConfig function to modify the request before it goes to the server.

ajaxConfig can either be an object, or a function that returns an object, with the following options:

  • useXDR [boolean]: (applies to IE9 only with cross domain requests): signifies that this is a cross-domain request and that IE should use it's XDomainRequest object. This is required if you're making cross-domain requests and want to support IE9). Note that XDR doesn't support headers/withCredentials.
  • headers [object]: any extra headers to send with the request.
  • xhrFields [object]: any fields to set directly on the XHR request object, most typically:
    • withCredentials [boolean]: whether to send cross domain requests with authorization headers/cookies. Useful if you're making cross sub-domain requests with a root-domain auth cookie.
  • beforeSend [function]: beforeSend will be called before the request is made, and will be passed the raw xhr object if you wish to modify it directly before it's sent.
var MyCollection = AmpersandRestCollection.extend({
    url: 'http://otherdomain.example.com/stuff',

    ajaxConfig: function () {
        return {
            headers: {
                'Access-Token': this.accessToken
            },
            xhrFields: {
                withCredentials: true
            }
        };
    }
});

var collection = new MyCollection()
collection.fetch();

fetch collection.fetch([options])

Fetch the default set of models for the collection from the server, setting them on the collection when they arrive. If the collection already contains data, by default, the operation of set will add new models from the server, merge the attributes of existing ones, and remove any which aren't in the response. This behaviour can be modified with the reset, add, remove, merge options.

Options:

  • success {Function} [optional] - callback to be called if the request was successful, will be passed (collection, response, options) as arguments.
  • error {Function} [optional] - callback to be called if the request was not successful, will be passed (collection, response, options) as arguments.
  • reset {Boolean} [optional] - call reset instead of set with the models returned from the server, defaults to false.
  • add {Boolean} [optional] - (assuming reset is false), {add: false} prevents the call to set from adding new models retrieved from the server that aren't in the local collection. Defaults to false
  • remove {Boolean} [optional] - (assuming reset is false), {remove: false} prevents the call to set from removing models that are in the local collection but aren't returned by the server. Defaults to false
  • merge {Boolean} [optional] - (assuming reset is false), {merge: false} prevents the call to set from updating models in the local collection which have changed on the server. Defaults to false

You can also pass any options that xhr expects to modify the query. For example: to fetch a specific page of a paginated collection: collection.fetch({ data: { page: 3 } })

getOrFetch collection.getOrFetch('id', [options], callback)

Convenience method. Gets a model from the server or from the collection if a model with that id already exists.

By default it will only fetch and add the model with the id you pass in.

collection.getOrFetch('42', function (err, model) {
    if (err) {
        console.log('handle');
    } else {
        // `model` here is a fully inflated model
        // It gets added to the collection automatically.
        // If the collection was empty before, it's got 1
        // now.
    }
});

If you pass {all: true} it will fetch the entire collection (by calling its fetch method) and then do a get to attempt to pull out the model by the id you specified.

collection.getOrFetch('42', {all: true}, function (err, model) {
    if (err) {
        console.log('handle');
    } else {
        // `model` here is a fully inflated model
        // It gets added to the collection automatically.
    }
});

fetchById collection.fetchById('id', callback)

Fetches and adds a model by id to the collection. This is what getOrFetch uses if it doesn't have a model already.

collection.fetchById('42', function (err, model) {
    // returns inflated, added model with a `null` error
    // or an error object.
});

create collection.create(model, [options])

Convenience to create a new instance of a model within a collection. Equivalent to instantiating a model with a hash of attributes, saving the model to the server, and adding the model to the set after being successfully created. Returns the new model. If client-side validation failed, the model will be unsaved, with validation errors. In order for this to work, you should set the model property of the collection. The create method can accept either an attributes hash or an existing, unsaved model object.

Creating a model will cause an immediate "add" event to be triggered on the collection, a "request" event as the new model is sent to the server, as well as a "sync" event, once the server has responded with the successful creation of the model. Pass {wait: true} if you'd like to wait for the server before adding the new model to the collection.

var Library = AmpersandRestCollection.extend({
  model: Book
});

var library = new Library;

var othello = library.create({
  title: "Othello",
  author: "William Shakespeare"
});

sync model.sync(method, collection, [options])

Simple delegation to ampersand-sync to persist the collection to the server. Can be overridden for custom behaviour.

lodash methods (42)

The ampersand-collection-lodash-mixin proxies the collection methods in lodash onto the underlying models array for the collection. For example:

books.each(function(book) {
  book.publish();
});

var titles = books.map(function(book) {
  return book.get("title");
});

var publishedBooks = books.filter(function(book) {
  return book.get("published") === true;
});

var alphabetical = books.sortBy(function(book) {
  return book.author.get("name").toLowerCase();
});

The full list of proxied methods is:

Suggest an edit

ampersand-view

ampersand-view

latest v10.0.1

A smart base view for Backbone apps, to make it easy to bind collections and properties to the DOM.

Lead Maintainer: Christopher Dieringer

A set of common helpers and conventions for using as a base view for ampersand.js apps.

What does it do?

  1. Gives you a proven pattern for managing/binding the contents of an element.
  2. Simple declarative property/template bindings without needing to include a template engine that does it for you. This keeps your logic out of your templates and lets you use a string of HTML as a fully dynamic template or just a simple function that returns an HTML string.
  3. The view's base element is replaced (or created) during a render. So, rather than having to specify tag type and attributes in javascript in the view definition you can just include that in your template like everything else.
  4. A way to render a collection of models within an element in the view, each with their own view, preserving order, and doing proper cleanup when the main view is removed.
  5. A simple way to render sub-views that get cleaned up when the parent view is removed.

install

npm install ampersand-view

API Reference

Note that this is a fork of Backbone's view so most of the public methods/properties here still exist: http://backbonejs.org/#View.AmpersandView extends AmpersandState so it can have it's own props values for example and can be bound directly to the template without a backing model object.

extend AmpersandView.extend([properties])

Get started with views by creating a custom view class. Ampersand views have a sane default render function, which you don't necessarily have to override, but you probably will wish to specify a template, your declarative event handlers and your view bindings.

var PersonRowView = AmpersandView.extend({
    template: "<li> <span data-hook='name'></span> <span data-hook='age'></span> <a data-hook='edit'>edit</a> </li>",

    events: {
        "click [data-hook=edit]": "edit"
    },

    bindings: {
        "model.name": {
            type: 'text',
            hook: 'name'
        },

        "model.age": {
            type: 'text',
            hook: 'age'
        }
    },

    edit: function () {
        //...
    }
});

template AmpersandView.extend({ template: "<div><input></div>" })

The .template is a property for the view prototype. It should either be a string of HTML or a function that returns a string of HTML or a DOM element. It isn't required, but it is used as a default for calling renderWithTemplate.

The important thing to note is that the returned string/HTML should not have more than one root element. This is because the view code assumes that it has one and only one root element that becomes the .el property of the instantiated view.

For more information about creating, and compiling templates, read the templating guide.

autoRender AmpersandView.extend({ autoRender: true })

The .autoRender property lets you optionally specify that the view should just automatically render with all the defaults. This requires that you at minimum specify a template string or function.

By setting autoRender: true the view will simply call .renderWithTemplate for you (after your initialize method if present). So for simple views, if you've got a few bindings and a template your whole view could just be really declarative like this:

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


module.exports = AmpersandView.extend({
    autoRender: true,
    template: '<div><span id="username"></span></div>',
    bindings: {
        name: '#username'
    }
});

Note: if you are using a template function (and not a string) the template function will get called with a context argument that looks like this, giving you access to .model, .collection and any other props you have defined on the view from the template.

this.renderWithTemplate(this, this.template);

events AmpersandView.extend({ events: { /* ...events hash... */ } })

The events hash allows you to specify declarative callbacks for DOM events within the view. This is much clearer and less complex than calling el.addEventListener('click', ...) everywhere.

  • Events are written in the format {"event selector": "callback"}.
  • The callback may either be the name of a method on the view, or an actual function.
  • Omitting the selector causes the event to be bound to the view's root element (this.el).
  • The events property may also be defined as a function that returns an events hash, to make it easier to programmatically define your events, as well as inherit them from parent views.

Using the events hash has a number of benefits over manually binding events during the render call:

  • All attached callbacks are bound to the view before being handed off to the event handler, so when the callbacks are invoked, this continues to refer to the view object.
  • All event handlers are delegated to the view's root el, meaning elements changed when the view is updated don't need to be unbound and rebound.
  • All events handlers are cleanly removed when the view is removed.
var DocumentView = AmpersandView.extend({

  events: {
    //bind to a double click on the root element
    "dblclick"                : "open",

    //bind to a click on an element with both 'icon' and 'doc' classes
    "click .icon.doc"         : "select",

    "contextmenu .icon.doc"   : "showMenu",
    "click .show_notes"       : "toggleNotes",
    "click .title .lock"      : "editAccessLevel",
    "mouseover .title .date"  : "showTooltip"
  },

  open: function() {
    window.open(this.model.viewer_url);
  },

  select: function() {
    this.model.selected = true;
  },

  //...

});

Note that the events definition is not merged with the superclass definition. If you want to merge events from a superclass, you have to do it explicitly:

var SuperheroRowView = PersonRowView.extend({
  events: _.extend({}, PersonRowView.prototype.events, {
    'click [data-hook=edit-secret-identitiy]': 'editSecretIdentity'
  })
});

bindings

The bindings hash gives you a declarative way of specifying which elements in your view should be updated when the view's model is changed.

For a full reference of available binding types see the ampersand-dom-bindings section.

For example, with a model like this:

var Person = AmpersandModel.extend({
    props: {
        name: 'string',
        age: 'number',
        avatarURL: 'string'
    },
    session: {
        selected: 'boolean'
    }
});

and a template like this:

<!-- templates.person -->
<li>
  <img data-hook="avatar">
  <span data-hook="name"></span>
  age: <span data-hook="age"></span>
</li>

you might have a binding hash in your view like this:

var PersonView = AmpersandView.extend({
    templates: templates.person,

    bindings: {
        'model.name': {
            type: 'text',
            hook: 'name'
        },

        'model.age': '[data-hook=age]', //shorthand of the above

        'model.avatarURL': {
            type: 'attribute',
            name: 'src',
            hook: 'avatar'
        },

        //no selector, selects the root element
        'model.selected': {
            type: 'booleanClass',
            name: 'active' //class to toggle
        }
    }
});

Note that the bindings definition is not merged with the superclass definition. If you want to merge bindings from a superclass, you have to do it explicitly:

var SuperheroRowView = PersonRowView.extend({
  bindings: _.extend({}, PersonRowView.prototype.bindings, {
    'model.secretIdentity': '[data-hook=secret-identity]'
  })
});

el view.el

All rendered views have a single DOM node which they manage, which is acessible from the .el property on the view. Allowing you to insert it into the DOM from the parent context.

var view = new PersonView({ model: me });
view.render();

document.querySelector('#viewContainer').appendChild(view.el);

constructor new AmpersandView([options])

The default AmpersandView constructor accepts an optional options object, and:

  • Attaches the following options directly to the instantiated view, overriding the defaults: model, collection, el.
  • Sets up event bindings defined in the events hash.
  • Sets up the model bindings defined in the bindings hash.
  • Initializes any subviews defined in the subviews hash.
  • Calls initialize passing it the options hash.
  • Renders the view, if autoRender is true and a template is defined.

Typical use-cases for the options hash:

  • To initialize a view with an el already in the DOM, pass it as an option: new AmpersandView({ el: existingElement }).
  • To perform extra work when initializing a new view, override the initialize function in the extend call, rather than modifying the constructor, it's easier.

initialize new AmpersandView([options])

Called by the default view constructor after the view is initialized. Overwrite initialize in your views to perform some extra work when the view is initialized. Initially it's a noop:

var MyView = AmpersandView.extend({
    initialize: function (options) {
        console.log("The options are:", options);
    }
});

var view = new MyView({ foo: 'bar' });
//=> logs 'The options are: {foo: "bar"}'

If you want to extend the initialize function of a superclass instead of redefining it completely, you can explicitly call the initialize of the superclass at the right time:

var SuperheroRowView = PersonRowView.extend({
  initialize: function () {
    PersonRowView.prototype.initialize.apply(this, arguments);
    doSomeOtherStuffHere();
  })
});

render view.render()

Render is a part of the Ampersand View conventions. You can override the default view method when extending AmpersandView if you wish, but as part of the conventions, calling render should:

  • Create a this.el property if the view doesn't already have one, and populate it with your view template
  • or if the view already has a this.el attribute, render should either populate it with your view template, or create a new element and replace the existing this.el if it's in the DOM tree.
  • Not be a problem if it's called more than once.

The default render looks like this:

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

If you want to extend the render function of a superclass instead of redefining it completely, you can explicitly call the render of the superclass at the right time:

var SuperheroRowView = PersonRowView.extend({
  render: function () {
    PersonRowView.prototype.render.apply(this, arguments);
    doSomeOtherStuffHere();
  })
});

ampersand-view triggers a 'render' event for your convenience, too, if you want to set listeners for it. The 'render' and 'remove' events emitted by this module are merely convenience events, as you may listen solely to change:rendered in order to capture the render/remove events in just one listener.

renderCollection view.renderCollection(collection, ItemView, containerEl, [viewOptions])

  • collection {Backbone Collection} The instantiated collection we wish to render.
  • ItemView {View Constructor | Function} The view constructor that will be instantiated for each model in the collection or a function that will return an instance of a given constructor. options object is passed as a first argument to a function, which can be used to access options.model and determine which view should be instantiated. This view will be used with a reference to the model and collection and the item view's render method will be called with an object containing a reference to the containerElement as follows: .render({containerEl: << element >>}).
  • containerEl {Element | String} This can either be an actual DOM element or a CSS selector string such as .container. If a string is passed ampersand-view runs this.query('YOUR STRING') to try to grab the element that should contain the collection of views. If you don't supply a containerEl it will default to this.el.
  • viewOptions {Object} [optional] Additional options
    • viewOptions {Object} Options object that will get passed to the initialize method of the individual item views.
    • filter {Function} [optional] Function that will be used to determine if a model should be rendered in this collection view. It will get called with a model and you simply return true or false.
    • reverse {Boolean} [optional] Convenience for reversing order in which the items are rendered.

This method will maintain this collection within that container element. Including proper handling of add, remove, sort, reset, etc.

Also, when the parent view gets .remove()'ed any event handlers registered by the individual item views will be properly removed as well.

Each item view will only be .render()'ed once (unless you change that within the item view itself).

The collection view instance will be returned from the function.

Example:

// some views for individual items in the collection
var ItemView = AmpersandView.extend({ ... });
var AlternativeItemView = AmpersandView.extend({ ... });

// the main view
var MainView = AmpersandView.extend({
    template: '<section class="page"><ul class="itemContainer"></ul></section>',
    render: function (opts) {
        // render our template as usual
        this.renderWithTemplate(this);

        // call renderCollection with these arguments:
        // 1. collection
        // 2. which view to use for each item in the list
        // 3. which element within this view to use as the container
        // 4. options object (not required):
        //      {
        //          // function used to determine if model should be included
        //          filter: function (model) {},
        //          // boolean to specify reverse rendering order
        //          reverse: false,
        //          // view options object (just gets passed to item view's `initialize` method)
        //          viewOptions: {}
        //      }
        // returns the collection view instance
        var collectionView = this.renderCollection(this.collection, ItemView, this.el.querySelector('.itemContainer'), opts);
        return this;
    }
});

// alternative main view
var AlternativeMainView = AmpersandView.extend({
    template: '<section class="sidebar"><ul class="itemContainer"></ul></section>',
    render: function (opts) {
        this.renderWithTemplate(this);
        this.renderCollection(this.collection, function (options) {
            if (options.model.isAlternative) {
                return new AlternativeItemView(options);
            }

            return new ItemView(options);
        }, this.el.querySelector('.itemContainer'), opts);
        return this;
    }
});

renderWithTemplate view.renderWithTemplate([context], [template])

  • context {Object | null} [optional] The context that will be passed to the template function, usually it will be passed the view itself, so that .model, .collection etc are available.
  • template {Function | String} [optional] A function that returns HTML or a string of HTML.

This is shortcut for the default rendering you're going to do in most every render method, which is: use the template property of the view to replace this.el of the view and re-register all handlers from the event hash and any other binding as described above.

var view = AmpersandView.extend({
    template: '<li><a></a></li>',
    bindings: {
        'name': 'a'
    },
    events: {
        'click a': 'handleLinkClick'
    },
    render: function () {
        // this does everything
        // 1. renders template
        // 2. registers delegated click handler
        // 3. inserts and binds the 'name' property
        //    of the view's `this.model` to the <a> tag.
        this.renderWithTemplate();
    }
});

query view.query('.classname')

Runs a querySelector scoped within the view's current element (view.el), returning the first matching element in the dom-tree.

notes:

  • It will also match agains the root element.
  • It will return the root element if you pass '' as the selector.
  • If no match is found it returns undefined
var view = AmpersandView.extend({
    template: '<li><img class="avatar" src=""></li>',
    render: function () {
        this.renderWithTemplate(this);

        // cache an element for easy reference by other methods
        this.imgEl = this.query(".avatar");

        return this;
    }
});

queryByHook view.queryByHook('hookname')

A convenience method for retrieving an element from the current view by it's data-hook attribute. Using this approach is a nice way to separate javascript view hooks/bindings from class/id selectors that are being used by CSS.

notes:

  • It also works if you're using multiple space-separated hooks. So something like <img data-hook="avatar user-image"/> would still match for queryByHook('avatar').
  • It simply uses .query() under the hood. So .queryByHook('avatar') is equivalent to .query('[data-hook~=avatar]')
  • It will also match to root elements.
  • If no match is found it returns undefined.
var view = AmpersandView.extend({
    template: '<li><img class='avatar-rounded' data-hook="avatar" src=""></li>',
    render: function () {
        this.renderWithTemplate(this);

        // cache an element for easy reference by other methods
        this.imgEl = this.queryByHook('avatar');

        return this;
    }
});

queryAll view.queryAll('.classname')

Runs a querySelectorAll scoped within the view's current element (view.el), returning an array of all matching elements in the dom-tree.

notes:

  • It will also include the root element if it matches the selector.
  • It returns a real Array not a DOM collection.

queryAllByHook view.queryAllByHook('hookname')

Uses queryAll method with a given data-hook attribute to retrieve all matching elements scoped within the view's current element (view.el), returning an array of all matching elements in the dom-tree or an empty array if no results has been found.

cacheElements view.cacheElements(hash)

A shortcut for adding reference to specific elements within your view for access later. This is avoids excessive DOM queries and makes it easier to update your view if your template changes. It returns this.

In your render method. Use it like so:

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

  this.cacheElements({
    pages: '#pages',
    chat: '#teamChat',
    nav: 'nav#views ul',
    me: '#me',
    cheatSheet: '#cheatSheet',
    omniBox: '[data-hook=omnibox]'
  });

  return this;
}

Then later you can access elements by reference like so: this.pages, or this.chat.

listenToAndRun view.listenToAndRun(object, eventsString, callback)

Shortcut for registering a listener for a model and also triggering it right away.

remove view.remove()

Removes a view from the DOM, and calls stopListening to remove any bound events that the view has listenTo'd. This method also triggers a remove event on the view, allowing for listeners (or the view itself) to listen to it and do some action, like cleanup some other resources being used. The view will trigger the remove event if remove() is overridden.

initialize : function() {
  this.listenTo(this,'remove',this.cleanup);
  // OR this, either statements will call 'cleanup' when `remove` is called
  this.once('remove',this.cleanup, this);
},

cleanup : function(){
  // do cleanup
}

registerSubview view.registerSubview(viewInstance)

  • viewInstance {Object} Any object with a "remove" method, typically an instantiated view. But doesn't have to be, it can be anything with a remove method. The remove method doesn't have to actually remove itself from the DOM (since the parent view is being removed anyway), it is generally just used for unregistering any handler that the subview sets up.

This method will:

  1. store a reference to the subview for cleanup when remove() is called.
  2. add a reference to itself at subview.parent
  3. return the subview

renderSubview view.renderSubview(viewInstance, containerEl)

  • viewInstance {Object} Any object with a .remove(), .render() and an .el property that is the DOM element for that view. Typically this is just an instantiated view.
  • containerEl {Element | String} This can either be an actual DOM element or a CSS selector string such as .container. If a string is passed ampersand-view runs this.query('YOUR STRING') to try to grab the element that should contain the sub view. If you don't supply a containerEl it will default to this.el.

This method is just sugar for the common use case of instantiating a view and putting in an element within the parent.

It will:

  1. fetch your container (if you gave it a selector string)
  2. register your subview so it gets cleaned up if parent is removed and so this.parent will be available when your subview's render method gets called
  3. call the subview's render() method
  4. append it to the container (or the parent view's el if no container given)
  5. return the subview
var view = AmpersandView.extend({
    template: '<li><div class="container"></div></li>',
    render: function () {
        this.renderWithTemplate();

        //...

        var model = this.model;
        this.renderSubview(new SubView({
            model: model
        }), '.container');

        //...

    }
});

subviews view.subviews

You can declare subviews that you want to render within a view, much like you would bindings. Useful for cases where the data you need for a subview may not be available on first render. Also, simplifies cases where you have lots of subviews.

When the parent view is removed the remove method of all subviews will be called as well.

You declare them as follows:

var AmpersandView = require('ampersand-view');
var CollectionRenderer = require('ampersand-collection-view');
var ViewSwitcher = require('ampersand-view-switcher');


module.exports = AmpersandView.extend({
    template: '<div><div></div><ul data-hook="collection-container"></ul></div>',
    subviews: {
        myStuff: {
            selector: '[data-hook=collection-container]',
            waitFor: 'model.stuffCollection',
            prepareView: function (el) {
                return new CollectionRenderer({
                    el: el,
                    collection: this.model.stuffCollection
                });
            }
        },
        tab: {
            hook: 'switcher',
            constructor: ViewSwitcher
        }
    }
});

subview declarations consist of:

  • selector {String} Selector that describes the element within the view that should hold the subview.
  • hook {String} Alternate method for specifying a container element using its data-hook attribute. Equivalent to selector: '[data-hook=some-hook]'.
  • constructor {ViewConstructor} Any view conventions compliant view constructor. It will be initialized with {el: [Element grabbed from selector], parent: [reference to parent view instance]}. So if you don't need to do any custom setup, you can just provide the constructor.
  • waitFor {String} String specifying they "key-path" (i.e. 'model.property') of the view that must be "truthy" before it should consider the subview ready.
  • prepareView {Function} Function that will be called once any waitFor condition is met. It will be called with the this context of the parent view and with the element that matches the selector as the argument. It should return an instantiated view instance.

delegateEvents view.delegateEvents([events])

Creates delegated DOM event handlers for view elements on this.el. If events is omitted, will use the events property on the view.

Generally you won't need to call delegateEvents yourself, if you define an event hash when extending AmpersandView, delegateEvents will be called for you when the view is initialize.

Events is a hash of {"event selector": "callback"}*

Will unbind existing events by calling undelegateEvents before binding new ones when called. Allowing you to switch events for different view contexts, or different views bound to the same element.

{
  'mousedown .title':  'edit',
  'click .button':     'save',
  'click .open':       function (e) { ... }
}

undelegateEvents view.undelegateEvents()

Clears all callbacks previously bound to the view with delegateEvents. You usually don't need to use this, but may wish to if you have multiple views attached to the same DOM element.

Suggest an edit

ampersand-view-switcher

ampersand-view-switcher

latest v2.1.0

A utility for swapping out views inside a container element.

Lead Maintainer: Kamil Ogórek

Purpose

This module does one thing: it helps you swap out views inside of an element. It's compatible with ampersand-view, backbone views and any view that has an .el, .render() and .remove()

What might you do with it?

  • build a page container for your app.
  • build a system for managing display of modals in your single page app.
  • animate a transition between showing any two views.

What it does

  • Takes an instantiated view and renders it in the container.
  • Removes the existing view from the container and calls remove on it.
  • Makes it easy to do custom stuff as views are added and removed.
  • Works either synchronously or asynchronously depending on whether you want to animate transitions between the views.
  • Makes no assumptions about what your views do or how they're structured except the following:
    • Views should have an .el property that is the root element of the view.
    • Views should have a .remove() method that cleans up and unbinds methods accordingly.
    • If your view has a .render() method it will get called before it's shown.
    • Beyond this, they could be any object.
  • IT DOES VERY LITTLE ELSE (and that is a feature)

Install

npm install ampersand-view-switcher

Example

Here's an example of how you might use the view switcher to handle page views within your ampersand app.

mainview.js

var AmpersandView = require('ampersand-view');
var ViewSwitcher = require('ampersand-view-switcher');
var templates = require('./templates');

module.exports = AmpersandView.extend({
    template: templates.body,
    render: function () {
        // render our template
        this.renderWithTemplate();

        // grab the element without our template based on its "data-hook" attribute
        this.pageContainer = this.queryByHook('page-container');

        // set up our page switcher for that element
        this.pageSwitcher = new ViewSwitcher(this.pageContainer, {
            // here we provide a few things we'd like to do each time
            // we switch pages in the app.
            show: function (view) {
                // set our document title
                document.title = view.pageTitle || 'my awesome app';
                // scroll to the top
                document.body.scrollTop = 0;
                // perhaps store a reference to our current page on our
                // app global for easy access from the browser console.
                app.currentPage = view;
            }
        });
    }
});

Or if you wanted to animate them you can do it asynchronously like so:

// set up our page switcher for that element
this.pageSwitcher = new ViewSwitcher(this.pageContainer, {
    // whether or not to wait for remove to be done before starting show
    waitForRemove: true,
    // here we provide a few things to do before the element gets
    // removed from the DOM.
    hide: function (oldView, cb) {
        // it's inserted and rendered for me so we'll add a class
        // that has a corresponding CSS transition.
        oldView.el.classList.add('animateOut');
        // give it time to finish (yes there are other ways to do this)
        setTimeout(cb, 1000);
    },
    // here we provide a few things we'd like to do each time
    // we switch pages in the app.
    show: function (newView) {
        // it's inserted and rendered for me
        document.title = newView.pageTitle || 'app name';
        document.body.scrollTop = 0;

        // store an additional reference, just because
        app.currentPage = newView;

        newView.el.classList.add('animateIn');
    }
});

API Reference

constructor new ViewSwitcher(element, [options])

  • element {Element} The DOM element that should contain the views.
  • options {Object} [optinal]
    • show {Function} [optional] A function that gets called when a view is being shown. It's passed the new view.
    • hide {Function} [optional] A function that gets called when a view is being removed. It's passed the old view and a callback. If you name 2 incoming arguments for example function (oldView, callback) { ... } the view switcher will wait for you to call the callback before it's considered ready. If you only use one like this: function (oldView) { ... } it won't wait for you to call a callback.
    • waitForRemove {Boolean} [default: false] Whether or not to wait until your hide animation callback gets called before starting your show animation.
    • prepend {Boolean} [default: false] Whether or not to prepend the view to the element. When this is false, the view is appended.
    • empty {Function} [optional] A function that gets called any time the view switcher is empty. Including when you instantiate it without giving it a view to start with.
    • view {View} [optional] A view instance to start with.
var switcher = new ViewSwitcher(document.querySelector('#pageContainer'));

var view = new MyView({ model: model });

switcher.set(view);

set switcher.set(viewInstance)

  • viewInstance {View} The new view to render.

The instantiated view switcher has this one main method. Simply call it with the new view you wish to show.

This is most likely going to be an instantiated ampersand-view or Backbone.View, but can be anything that has a .el property that represents that view's root element and .remove() method that cleans up after itself. In addition if your custom view object has a .render() method it will get called before the view is added to the DOM.

var switcher = new ViewSwitcher(document.querySelector('#pageContainer'));

var view = new MyView({ model: model });

switcher.set(view);

clear switcher.clear(callback)

  • callback {Function} [optional] An optional callback when removed. Useful if you're doing custom animations.

Removes the current view from the view switcher. Calls callback when done if one was provided.`

var switcher = new ViewSwitcher(document.querySelector('#pageContainer'));

var view = new MyView({ model: model });

switcher.set(view);

switcher.clear();
Suggest an edit

ampersand-router

ampersand-router

latest v4.0.0

Clientside router with fallbacks for browsers that don't support pushState. Mostly lifted from Backbone.js.

Clientside router with fallbacks for browsers that don't support pushState. Mostly lifted from Backbone.js.

Ampersand-router also adds a redirectTo method which is handy for doing "internal" redirects without breaking backbutton functionality in the browser.

install

npm install ampersand-router

API Reference

extend AmpersandRouter.extend(properties)

Get started by creating a custom router class. Define actions that are triggered when certain URL fragments are matched, and provide a routes hash that pairs routes to actions. Note that you'll want to avoid using a leading slash in your route definitions:

var AppRouter = AmpersandRouter.extend({

  routes: {
    "help":                 "help",    // #help
    "search/:query":        "search",  // #search/kiwis
    "search/:query/p:page": "search"   // #search/kiwis/p7
  },

  help: function() {
    //...
  },

  search: function(query, page) {
    //...
  }

});

routes router.routes

The routes hash maps URLs with parameters to functions on your router (or just direct function definitions, if you prefer), similar to the View's events hash. Routes can contain parameter parts, :param, which match a single URL component between slashes; and splat parts *splat, which can match any number of URL components. Part of a route can be made optional by surrounding it in parentheses (/:optional).

For example, a route of "search/:query/p:page" will match a fragment of #search/obama/p2, passing "obama" and "2" to the action.

A route of "file/*path" will match #file/nested/folder/file.txt, passing "nested/folder/file.txt" to the action.

A route of "docs/:section(/:subsection)" will match #docs/faq and #docs/faq/installing, passing "faq" to the action in the first case, and passing "faq" and "installing" to the action in the second.

Trailing slashes are treated as part of the URL, and (correctly) treated as a unique route when accessed. docs and docs/ will fire different callbacks. If you can't avoid generating both types of URLs, you can define a "docs(/)" matcher to capture both cases.

When the visitor presses the back button, or enters a URL, and a particular route is matched, the name of the action will be fired as an event, so that other objects can listen to the router, and be notified. In the following example, visiting #help/uploading will fire a route:help event from the router.

routes: {
  "help/:page":         "help",
  "download/*path":     "download",
  "folder/:name":       "openFolder",
  "folder/:name-:mode": "openFolder"
}

router.on("route:help", function(page) {
  ...
});

constructor / initialize new Router([options])

When creating a new router, you may pass its routes hash directly as the routes option, if you choose. All options will also be passed to your initialize function, if defined.

route router.route(route, name, [callback])

Manually create a route for the router, The route argument may be a routing string or regular expression. Each matching capture from the route or regular expression will be passed as an argument to the callback. The name argument will be triggered as a "route:name" event whenever the route is matched. If the callback argument is omitted router[name] will be used instead. Routes added later may override previously declared routes.

initialize: function(options) {

  // Matches #page/10, passing "10"
  this.route("page/:number", "page", function(number){ ... });

  // Matches /117-a/b/c/open, passing "117-a/b/c" to this.open
  this.route(/^(.*?)\/open$/, "open");

},

open: function(id) { ... }

navigate router.navigate(fragment, [options])

Whenever you reach a point in your application that you'd like to save as a URL, call navigate in order to update the URL. Route function will be called by default, but if you want to prevent it, you can set the trigger option to false. To update the URL without creating an entry in the browser's history, set the replace option to true.

openPage: function(pageNumber) {
  this.document.pages.at(pageNumber).open();
  this.navigate("page/" + pageNumber);
}

// Or ...

app.navigate("help/troubleshooting", {trigger: false});

// Or ...

app.navigate("help/troubleshooting", {replace: true});

reload router.reload()

Allows you to re-navigate to the same page. Re-runs the route handler for the current url.

redirectTo router.redirectTo(fragment)

Sometimes you want to be able to redirect to a different route in your application without adding an entry in the browser's history. RedirectTo is just a shorthand for calling navigate with both trigger and replace set to true.

var AppRouter = AmpersandRouter.extend({
    routes: {
        'login': 'login',
        'dashboard': 'dashboard'
    },

    dashboard: function () {
        if (!app.me.loggedIn) return this.redirectTo('login');

        // show dashboard page...
    }
});

execute router.execute(callback, args)

This method is called internally within the router, whenever a route matches and its corresponding callback is about to be executed. Override it to perform custom parsing or wrapping of your routes, for example, to parse query strings before handing them to your route callback, like so:

var Router = AmpersandRouter.extend({
  execute: function(callback, args) {
    args.push(parseQueryString(args.pop()));
    if (callback) callback.apply(this, args);
  }
});

history.start router.history.start([options])

AmpersandRouter automatically requires and instantiates a single ampersand-history object. AmpersandHistory serves as a global router (per frame) to handle hashchange events or pushState, match the appropriate route, and trigger callbacks. You shouldn't ever have to create one of these yourself since ampersand-router already contains one.

When all of your Routers have been created, and all of the routes are set up properly, call router.history.start() on one of your routers to begin monitoring hashchange events, and dispatching routes. Subsequent calls to history.start() will throw an error, and router.history.started() is a boolean value indicating whether it has already been called.

Supported options:

  • pushState {Boolean} - HTML5 pushState is turned on by default. However if you want to indicate that you don't want to use it in your application, you can add {pushState: false} to the options. Defaults to true
  • hashChange {Boolean} - If you'd like to use pushState, but have browsers that don't support it natively use full page refreshes instead, you can add {hashChange: false} to the options. Defaults to true
  • root {String} - If your application is not being served from the root url / of your domain, be sure to tell History where the root really is, as an option: router.history.start({root: "/public/search/"}). Defaults to /
  • silent {Boolean} - If the server has already rendered the entire page, and you don't want the initial route to trigger when starting History, pass silent: true. Defaults to false

When called, if a route succeeds with a match for the current URL, router.history.start() returns true. If no defined route matches the current URL, it returns false.

Suggest an edit

ampersand-events

ampersand-events

latest v2.0.2

Module that can be mixed into any object to provide eventing. Heavily based on Backbone Events.

A module that can be mixed into any object to make it able to trigger events and be listened to. This is heavily based on Backbone Events with a few modifications.

Install

npm install ampersand-events

Example usage

ampersand-events is simply an object of methods. So you can easily add those to any object using any of the following techniques.

Adding events to a constructor or "class"

var Events = require('ampersand-events');
var assign = require('lodash.assign');

// Create some constructor
var MyConstructor = function () {};

// Extend the prototype with the event methods and your own:
assign(MyConstructor.prototype, Events, {
    myOtherMethods: function () {}
});

// Now we can trigger and listen for events on
// any instances created with our constructor.
var myObj = new MyConstructor();

Using with plain objects

var Events = require('ampersand-events');

// Events has an `createEmitter` helper that you can pass
// any object to to add event methods to.
var myObj = {};
Events.createEmitter(myObj);

// it now has event methods
myObj.trigger('some event');

Using as an event bus / or pubsub channel

You can call Event.createEmitter() without any arguments to create an "empty" event emitter.

This can be really useful for creating what you might call an "event bus" or "pubsub" mechanism for things like application events, or whatnot.

Say you created a module like this:

var Events = require('ampersand-events');

module.exports = Events.createEmitter();

Any module in your app could now require this module and trigger events that other modules in your app could listen for.

API Reference

createEmitter Events.createEmitter([object])

Modifies any passed object to add event capabilities to it. If you don't pass any object, it simply creates and returns a new object with event capabilities.

  • object {Object} [Optional] Any object you want to add event capabilities to.

It simply adds the event methods to the passed in object.

var myObj = {};
Events.createEmitter(myObj);

myObj.on('customEvent', function () {
    // handle event
});
myObj.trigger('customEvent');

on eventObj.on(eventName, callback, [context])

(aliased as bind for backwards compatibility)

Bind a function to be called each time the eventName is triggered on that object.

  • eventName {String} The name of the event to listen for. Can also be "all" which will call your callback no matter what event is triggered.
  • callback {Function} The function to call when the event is triggered.
  • context {Object}[optional] If you provide an object here it will be the value of this inside your callback function.
myObj.on('anything', function () {
    console.log('I can handle anything!');
});

once eventObj.once(eventName, callback, [context])

Exactly like on but removes itself after getting called once no matter how many times the event is triggered.

off eventObj.off([eventName], [callback], [context])

(aliased as unbind for backwards compatibility)

Remove previously bound callback(s) from the object. If no context is specified all versions of callback no matter what context was given will be removed. If no callback was specified, all callbacks for that given eventName will be removed. If no eventName was specified callbacks for all events are removed.

  • eventName {String} The name of the event to remove. Pass null to "leave blank".
  • callback {Function} The function to remove or pass null to "leave blank".
  • context {Object} Remove all callbacks with this context.

These can be used in any combination as shown below:

// Removes the `onChange` callback
eventObj.off('change', onChange);

// Removes all callbacks listening for 'change' events
eventObj.off('change');

// Removes the `onChange` callback for any event
eventObj.off(null, onChange);

// Removes all callbacks for a given `context`
eventObj.off(null, null, context);

// Removes all callbacks no matter what
eventObj.off();

trigger eventObj.trigger(eventName, [argsToPassOn])

Triggers all the callbacks for the given eventName.

  • eventName {String} Name of event, or space-delimited list of events.
  • argsToPassOn { ... } Any additional arguments will simply be used to call the callbacks that are listening for this event.
eventObj.on('change', function (payload) {
    // `payload` will be the `{some: 'object'}` instance
    // from below.
});
eventObj.trigger('change', {some: 'object'});

listenTo eventObj.listenTo(otherEventObj, eventName, callback)

Tell an eventObject to listen to eventName on another object. This is another form of on that makes it easier to create an object that listens to events from other objects and keep track of them. This makes it so we can easily listen to several different events from several different objects but make it simple to remove them all at once.

For example, if we use .listenTo to listen to model changes it cares about in an ampersand-view the view will know what callbacks it needs to unregister if it gets removed.

  • otherEventObj {Event object} The other object to listen to.
  • eventName {String} Event to listen to (can also be space delimited list of events).
  • callback {Function} The function to call when event occurs.

Note: the callback will always be called with the eventObj as the this value.

view.listenTo(model, 'change', view.render);
// when `view.render` is called `this` will be `view`

listenToOnce eventObj.listenToOnce(otherEventObj, eventName, callback)

Same as listenTo but it unregisters itself after getting called once no matter how many times the events is triggered.

listenToAndRun eventObj.listenToAndRun(otherEventObj, eventName, callback)

Same as listenTo but also immediately executes the registered callback once right away. This can be useful for things like methods that re-calculate totals, but that also need to be run once to calculate the initial value.

stopListening eventObj.stopListening([otherEventObj], [eventName], [callback])

Tells the eventObj to stop listening to events. Most commonly it's used without arguments to remove all callbacks an object registered before destroying the object. For example ampersand-view calls this.stopListening() as part of .remove().

However, you can also use stopListening with specific objects, events or callbacks.

  • otherEventObj {Event object} The other object being listened to.
  • eventName {String} A specific string or space-delimited list of events to stop listening to.
  • callback {Function} A specific callback to remove.

These can be used in any combination as shown below:

// Stops listening to everything
eventObj.stopListening();

// Stop listening to a specific object
eventObj.stopListening(object);

// Stop listening to all 'change' events from any model
eventObj.stopListening(null, 'change');

// Remove just a specific `callback`
eventObj.stopListening(null, null, callback);
Suggest an edit

ampersand-dom-bindings

ampersand-dom-bindings

latest v3.9.1

Takes binding declarations and returns key-tree-store of functions that can be used to apply those bindings.

Takes binding declarations as described below and returns key-tree-store of functions that can be used to apply those bindings to a DOM tree.

ampersand-view use this for declarative bindings.

The returned functions should be called with these arguments: The root element, the current value of the property, and a name for the binding types where that is relevant.

install

npm install ampersand-dom-bindings

Binding types

text

sets/maintains textContent of selected element. treats undefined, null, and NaN as ''

'model.key': {
    type: 'text',
    selector: '.someSelector' // or hook
}

class

sets and maintains single class as string that matches value of property

  • handles removing previous class if there was one
  • treats undefined, null, and NaN as '' (empty string).
'model.key': {
    type: 'class',
    selector: // or hook
}

attribute

sets the whole attribute to match value of property. treats undefined, null, and NaN as '' (empty string). name can also be an array to set multiple attributes to the same value.

'model.key': {
    type: 'attribute',
    selector: '#something', // or hook
    name: 'width'
}

value

sets the value of the element to match value of the property. works well for input, select, and textarea elements. treats undefined, null, and NaN as '' (empty string).

note: The binding will only be applied if the element is not currently in focus. This is done by checking to see if the element is the document.activeElement first. The reason it works this way is because if you've set up two-way data bindings you get a circular event: the input changes, which sets the bound model property, which in turn updates the value of the input. This might sound OK but results in the cursor always jumping to the end of the input/textarea. So if you're editing the middle of a bound text field, the cursor keeps jumping to the end. We avoid this by making sure it's not already in focus thus avoiding the bad loop.

'model.key': {
    type: 'value',
    selector: '#something', // or hook
}

booleanClass

add/removes class based on boolean interpretation of property name. name, yes, or no can also be an array of class names where all the values will be toggled. If you need the opposite effect, (false adds class, true removes class), specify invert: true.

'model.active': {
    type: 'booleanClass',
    selector: '#something', // or hook
    // to specify name of class to toggle (if different than key name)
    // you could either specify a name
    name: 'active'
    // or a yes/no case
    yes: 'active',
    no: 'not-active'
    // if you need inverse interpretation
    invert: true
}

booleanAttribute

toggles whole attribute on the element (think checked) based on boolean interpretation of property name. name can also be an array of attribute names where all the values will be toggled. If you need the opposite effect, (false adds attribute, true removes attribute), specify invert: true.

'model.isAwesome': {
    type: 'booleanAttribute',
    selector: '#something', // or hook
    // to specify name of attribute to toggle (if different than key name)
    // you could either specify a name
    name: 'checked'
    // or a yes/no case
    yes: 'data-is-awesome',
    no: 'data-is-not-awesome'
    // if you need inverse interpretation
    invert: true
}

toggle

toggles visibility (using display: none by default) of entire element based on boolean interpretation of property.

// simple show/hide of single element
'model.key': {
    type: 'toggle',
    selector: '#something' // or hook
}

// Inverse interpretation of value
'model.key': {
    type: 'toggle',
    invert: true,
    hook: 'some-element'
}

// toggle visibility property instead
'model.key': {
    type: 'toggle',
    selector: '#something', // or hook
    mode: 'visibility'
}

// show/hide where true/false show different things
'model.key': {
    type: 'toggle',
    yes: '#true_case',
    no: '#false_case'
}

switch

Toggles existence of multiple items based on value of property.

'model.activetab': {
    type: 'switch',
    cases: {
        'edit': '#edit_tab',
        'new': '#new_tab',
        'details': '#details_tab'
    }
}

switchClass

Toggles existence of a class on multiple elements based on value of property.

'model.key': {
    type: 'switchClass',
    name: 'is-active',
    cases: {
        'edit': '#edit_tab',
        'new': '#new_tab',
        'details': '#details_tab'
    }
}

switchAttribute

Sets attribute(s) on matching elements based on the value of a property matching the case.

'model.key': {
    type: 'switchAttribute',
    selector: 'a', // or hook
    name: 'href',  // name defaults to the property name (e.g. 'key' from 'model.key' in this example)
    cases: {
        value1: '/foo',
        value2: '/bar'
    }
}

You can also specify multiple attributes by using an object as the case value. The object keys are used instead of the name option.

'model.key': {
    type: 'switchAttribute',
    selector: 'a', // or hook
    cases: {
        value1: { href: '/foo', name: 'foo' },
        value2: { href: '/bar', name: 'bar' }
    }
}

innerHTML

renders innerHTML, can be a string or DOM, based on property value of model

'model.key': {
    type: 'innerHTML',
    selector: '#something' // or hook
}

custom functions

type can also be a function. It will be run for each matching el with the value and previousValue of the property. The function is bound to the view declaring the bindings, so this refers to the view.

'model.key': {
    type: function (el, value, previousValue) {
        // Do something custom to el
        // using value and/or previousValue
    },
    selector: '#something', // or hook
}

Handling multiple bindings for a given key

If given an array, then treat each contained item as separate binding

'model.key': [
    {
        type: 'booleanClass',
        selector: '#something', // or hook
        name: 'active' // (optional) name of class to toggle if different than key name
    },
    {
        type: 'attribute',
        selector: '#something', // or hook
        name: 'width'
    }
]

The attribute, booleanAttribute and booleanClass types also accept an array for the name property (and yes/no for booleanClass). All the values in the array will be set the same as if each were bound separately.

'model.key': {
    // Also works with booleanAttribute and booleanClass
    type: 'attribute',
    selector: '#avatar',
    // Both height and width will be bound to model.key
    name: ['height', 'width']
}

binding using data-hook attribute

We've started using this convention a lot, rather than using classes and IDs in JS to select elements within a view, we use the data-hook attribute. This lets designers edit templates without fear of breaking something by changing a class. It works wonderfully, but the only thing that sucks about that is the syntax of attribute selectors: [data-hook=some-hook] is a bit annoying to type a million types, and also in JS-land when coding and we see [ we always assume arrays.

So for each of these bindings you can either use selector or hook, so these two would be equivalent:

'model.key': {
    selector: '[data-hook=my-element]'
}

'model.key': {
    hook: 'my-element'
}

handling simplest cases: text

'model.key': '#something' // creates `text` binding for that selector and property

// `type` defaults to `text` so we can also do
'model.key': {
    hook: 'hook-name'
}

real life example

var View = require('ampersand-view');
var templates = require('../templates');


module.exports = View.extend({
    template: templates.includes.app,
    bindings: {
        'model.client_name': {
            hook: 'name'
        },
        'model.logo_uri': {
            type: 'attribute',
            name: 'src',
            hook: 'icon'
        }
    }
});

other benefits

Previously after having given views the ability to have their own properties (since view inherits from state) it was awkward to bind those to the DOM. Also, for binding things that were not just this.model the syntax had to change.

Now this is fairly simple/obvious:

module.exports = View.extend({
    template: templates.includes.app,
    props: {
        activetab: 'string',
        person: 'state',
        meeting: 'state'
    },
    bindings: {
        // for the property that's directly on the view
        'activetab': {
            type: 'switch',
            case: {
                'edit': '#edit_tab',
                'new': '#new_tab',
                'details': '#details_tab'
            }
        },
        // this one is for one model
        'person.full_name': '[data-hook=name]',
        // this one is for another model
        'meeting.subject': '[data-hook=subject]'
    }
});

firstMatchOnly

As an option you can add firstMatchOnly: true to the binding declaration. It will cause the selector matcher to grab only the first match.

Useful for cases when a view renders a collection with several elements with the same class/data-hook.

module.exports = View.extend({
  template: '<div><span data-hook="foo"></span><span data-hook="foo"></span>',
  props: {
    someText: 'string'
  },
  initialize: function(){
    this.someText = 'hello';
  },
  bindings: {
    'someText': {
      type: 'text',
      hook: 'foo',
      firstMatchOnly: true
    }
  }
});
// will render <div><span data-hook="foo">hello</span><span data-hook="foo"></span></div>