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.
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.
npm install -g ampersand
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.
ampersand gen {{type}}
Type can be form, view, model or collection.
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
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.
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 hapiindent: indent sizeview: default templaterouter: default templatemodel: default templatepage: default templatecollection: default templateclientfolder: name for the 'client' folderviewfolder: name for the 'views' folderpagefolder: name for the 'pages' foldermodelfolder: name for the 'models' folderformsfolder: name for the 'forms' foldercollectionfolder: name for the collection folder - grouped with 'models' by defaultmakecollection: whether to create collection when making a modelapproot: 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'{
"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"
}
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.
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!
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.
});
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.
npm install ampersand-app
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.
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});
}
});
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.
latest v5.0.3
An observable, extensible state object with derived watchable properties.
Lead Maintainer: Philip Roberts
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.
npm install ampersand-state --save
AmpersandState.extend({ })To create a State class of your own, you extend AmpersandState and provide instance properties and options for your class. Typically, this is when you'll define the 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):
// don't do this!
class Foo extends AmpersandView
constructor: (options)->
@special = options.special
super
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.state.idAttributeThe 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"
state.getId()Gets the state's ID, per idAttribute configuration. Should always be how ID is determined by other code.
state.namespaceAttributeThe 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'.
state.getNamespace()Get namespace of state per namespaceAttribute configuration. Should always be how namespace is determined by other code.
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'.
state.getType()Get type of state per typeAttribute configuration. Should always be how type is determined by other code.
AmpersandState.extend({ extraProperties: 'allow' })Determines 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".');
state.collectionA 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
state.cidA special property of states, the cid (or 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 still needs a unique id (for rendering in the UI, and so on).
var userA = new User();
console.log(userA.cid) //=> "state-1"
var userB = new User();
console.log(userB.cid) //=> "state-2"
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.
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');
state.isValid()Check if the state is currently valid. It does this by calling the state's validate method (if you've provided one).
AmpersandState.extend({ datatypes: myCustomTypes })ampersand-state defines several built-in datatypes:
stringnumberbooleanarrayobjectdatestateanyOf these, object, array, and any allow for a lot of extra flexibility.
However, sometimes it's useful to define your own custom datatypes. Doing so allows you to use them in the props below, along with all their features (like required, default, etc).
Setting type is required. A typeError will be thrown if it's missing or has not been chosen (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.
| FUNCTION | RETURNS | DESCRIPTION |
|---|---|---|
set : function(newVal){} |
{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){} |
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){} |
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(){} |
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 define 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};
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:
string, number, boolean, array, object, date, or any. (Example: name: 'string'.) dataTypes, if the class defines any.[dataType, required, default]{ type: 'string', required: true, default: '' , values: [], allowNull: false, setOnce: false }default: 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).required is true:default, it will start with that value, and revert to it after a call to unset(propertyName)unset(propertyName) throw an errorvalues array is passed, then you'll be able to change the property to one of those values only.setOnce is true, then you'll be able to set property only once.default, and you don't set the value initially, the property will be permanently set to the default value.test function is passed, then a negative validation test will be executed every time this property is about to be set. false to tell State to go ahead and set the value. 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;
}
},
}
});
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 rename 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
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 []; }
}
}
});
NOTE: 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.
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'
}
);
AmpersandState.extend({ derived: { derivedProperties }})Derived properties (also known as computed properties) are properties of the state object that depend on other properties to determine their value. They may depend on properties defined in props, session, or even derived—as well as the same from state props or children.
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 every time 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.
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/collection. Children'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! 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.
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 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.)
state.serialize([options])Serialize the state object into a plain object, ready for sending to the server (typically called via toJSON).
By default, this returns only properties defined in props, omitting properties in session and derived. To also serialize session or derived attributes, you can pass 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.
state.get(attribute); state[attribute]; state.firstNameGet the current value of an attribute from the state object. Attributes can be accessed directly, or a call to the Backbone style get.
// these are all equivalent
person.get('firstName');
person['firstName'];
person.firstName
Get will retrieve props, session, or derived properties all in the same way.
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 for as well. For example: "change:firstName" and "change:content". If the update affects any derived properties, their values will be updated and fire "change" events 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. (Prior version relied on for...in to access an object's properties, both owned by that object and those inherited through the prototypal chain.)
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'])
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()
state.toggle('a')Shortcut to toggle boolean properties, or to cycle through array of specified property's values. (See the values option 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'
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.
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: 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.
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: When 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: This will only return values if checked inside a handler for "change" events (i.e. while the events are firing). Once the change events are done, this will always 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.
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\"}"
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 dataType. 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 dataTypes 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' }
latest v8.0.1
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.
npm install ampersand-model
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.
The module exports just one item, the ampersand-model constructor. It has a method called extend that works as follows:
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;
}
}
}
});
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.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 AmpersandModel({
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.
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();
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:
"destroy" event on the model, which will bubble up through any collections which contain it."request" event as it begins the Ajax request to the server"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');
},
});
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.
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();
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
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"
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.
npm i ampersand-collection
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).
collection.modelOverride 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;
}
});
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
});
collection.mainIndexSpecify 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'
});
collections.indexesSpecify 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' },
collection.lengthReturns the length of the underlying array.
collection.isCollectionBecause 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.
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:
{at: index} to splice the model into the collection at the specified index.{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.
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.
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}]"
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.
collection.set(models, [options])The set method performs a "smart" update of the collection with the passed list of models:
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.
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' },
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.
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.
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.
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().
collection.modelsRaw 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.
The comparator option lets you define how models in a collection are sorted. There's a few ways to declare comparator:
false prevents sortingstring sorts the collection by a specific model attributefunction 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).The base AmpersandCollection proxies some basic ES5 methods to the underlying model array. Further documentation of these methods is available at MDN
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']
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.
npm install ampersand-rest-collection
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);
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();
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 falseremove {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 falsemerge {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 falseYou 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 } })
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.
}
});
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.
});
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"
});
model.sync(method, collection, [options])Simple delegation to ampersand-sync to persist the collection to the server. Can be overridden for custom behaviour.
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:
latest v10.0.3
A smart base view for Backbone apps, to make it easy to bind collections and properties to the DOM.
Lead Maintainer: Rick Butler
A set of common helpers and conventions for using as a base view for ampersand.js apps.
What does it do?
npm install ampersand-view
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.
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 () {
//...
}
});
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.
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);
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.
{"event selector": "callback"}.selector causes the event to be bound to the view's root element (this.el).Using the events hash has a number of benefits over manually binding events during the render call:
this continues to refer to the view object.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'
})
});
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]'
})
});
view.elAll 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);
new AmpersandView([options])The default AmpersandView constructor accepts an optional options object, and:
model, collection, el.events hash.bindings hash.subviews hash.initialize passing it the options hash.autoRender is true and a template is defined.Typical use-cases for the options hash:
el already in the DOM, pass it as an option: new AmpersandView({ el: existingElement }).initialize function in the extend call, rather than modifying the constructor, it's easier.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();
})
});
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:
this.el property if the view doesn't already have one, and populate it with your view templatethis.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.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.
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 >>})..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 optionsviewOptions {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.
// 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;
}
});
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();
}
});
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:
'' as the selector.undefinedvar 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;
}
});
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:
<img data-hook="avatar user-image"/> would still match for queryByHook('avatar')..query() under the hood. So .queryByHook('avatar') is equivalent to .query('[data-hook~=avatar]')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;
}
});
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:
Array not a DOM collection.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.
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.
view.listenToAndRun(object, eventsString, callback)Shortcut for registering a listener for a model and also triggering it right away.
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
}
view.registerSubview(viewInstance)This method will:
remove() is called.subview.parentview.renderSubview(viewInstance, containerEl).remove(), .render() and an .el property that is the DOM element for that view. Typically this is just an instantiated view..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:
this.parent will be available when your subview's render method gets calledrender() methodvar 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');
//...
}
});
view.subviewsYou 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:
data-hook attribute. Equivalent to selector: '[data-hook=some-hook]'.{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 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.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) { ... }
}
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.
latest v3.0.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?
What it does
remove on it..el property that is the root element of the view..remove() method that cleans up and unbinds methods accordingly..render() method it will get called before it's shown.npm install ampersand-view-switcher
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({
el: 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({
el: 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');
}
});
new ViewSwitcher([options])options {Object} [optional]el {Element} The DOM element that should contain the views.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);
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({el: document.querySelector('#pageContainer')});
var view = new MyView({ model: model });
switcher.set(view);
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({el: document.querySelector('#pageContainer')});
var view = new MyView({ model: model });
switcher.set(view);
switcher.clear();
latest v5.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.
npm install ampersand-router
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) {
//...
}
});
router.routesThe 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) {
...
});
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.
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) { ... }
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});
router.reload()Allows you to re-navigate to the same page. Re-runs the route handler for the current url.
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...
}
});
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);
}
});
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: false} to the options. Defaults to true{hashChange: false} to the options. Defaults to true/ 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: true. Defaults to falseWhen 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.
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.
npm install ampersand-events
ampersand-events is simply an object of methods. So you can easily add those to any object using any of the following techniques.
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();
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');
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.
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');
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!');
});
eventObj.once(eventName, callback, [context])Exactly like on but removes itself after getting called once no matter how many times the event is triggered.
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();
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'});
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`
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.
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.
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);
latest v3.9.2
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.
npm install ampersand-dom-bindings
sets/maintains textContent of selected element. treats undefined, null, and NaN as ''
'model.key': {
type: 'text',
selector: '.someSelector' // or hook
}
sets and maintains single class as string that matches value of property
undefined, null, and NaN as '' (empty string).'model.key': {
type: 'class',
selector: // or hook
}
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'
}
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
}
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
}
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
}
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'
}
Toggles existence of multiple items based on value of property.
'model.activetab': {
type: 'switch',
cases: {
'edit': '#edit_tab',
'new': '#new_tab',
'details': '#details_tab'
}
}
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'
}
}
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' }
}
}
renders innerHTML, can be a string or DOM, based on property value of model
'model.key': {
type: 'innerHTML',
selector: '#something' // or hook
}
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
}
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']
}
data-hook attributeWe'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'
}
'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'
}
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'
}
}
});
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',
cases: {
'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]'
}
});
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>