epik
epik is Epitome ver 2 for primish
, lodash
and jquery/zepto
, no depenency on MooTools.
Warning: this is a fully functional experiment. If you like primish, MooTools and Epitome, feel free to use or to contribute
epik is a small, modular and extensible MVC framework for modern web development, built in the spirit of AMD and bower components. It will also work with global exports (w/o a dependency loader) and it works under node.js. epik offers 90% of the functionality of Backbone but allows you to develop in the style of MooTools classes via primish. The views are either powered by jQuery or via a built-in rivets.js adapter for bi-directional binding in the style of AngularJS.
Q: Why would you use it? It allows you to quickly prototype and develop code that runs in the browser or in node.js, sharing components between client and server. It also makes it easy to work with for any ex MooTools developers who are being forced to use jquery in their work.
Q: Should I use it? Whereas epik is fully functional, tested and powerful, it has literally no adoption in the wild, therefore no community help. For the time being, only use it if you know what you are doing.
Getting started
To install epik in your project, you have several routes.
bower
$ bower install epik --save
A bower install will only bring down the following files:
lib/index.js
lib/model.js
lib/model-sync.js
lib/collection.js
lib/collection-sync.js
lib/agent.js
lib/router.js
lib/storage.js
lib/plugins/rivets-adapter.js
lib/epik-min.js
script tags
You can grab the minified concatenated version:
epik-min.js (27.5k) (pointing to master branch)
epik
uses the following packages as dependencies, not part of the build:
- lodash (utils)
- primish (classes)
- jquery (views)
- rivets.js (views)
Additionally, slicker
, MooTools Slick parser for the web, is bundled in the concatenated minified files but if you use it in development mode and reference all the files locally, you'd need to resolve it as well. Should be done automatically if you use bower/AMD, see below.
Make sure that the dependencies listed above - primish
, lodash
and view helpers jquery
and rivets.js
, if applicable, are loaded beforehand and use the global object epik
to reference components.
node.js
$ npm install epik --save
When using it under node.js, you need to reference relative paths for sub components. For example:
var epik = require('epik'),
model = require('epik/lib/model');
AMD configuration
You can use epik in a number of ways.
In development, you can let requirejs fetch all dependencies as needed. A typical require.config looks like this:
require.config({
paths: {
epik: '../bower_components/epik/lib',
'rivets-adapter': '../bower_components/epik/lib/plugins/rivets-adapter',
primish: '../bower_components/primish',
lodash: '../bower_components/lodash/dist/lodash',
slicker: '../bower_components/slicker/index',
rivets: '../bower_components/rivets/dist/rivets',
jquery: '../bower_components/jquery/jquery'
}
});
});
A sample requirejs-config.js
can be seen at this gist.
The above is applicable after a bower install epik --save
and will dynamically load any components as needed.
Obviously, you may have a different config for jquery
, lodash
and rivets
so reflect them as needed - epik will require
them via the root level ids of jquery
, lodash
and rivets
respectively.
If you prefer, you can use the builds of epik instead. There are two builds shipped - a minimum one, which includes only
the epik files (and slicker
) and a full one, which includes ALL dependencies, lodash
, rivets
and jquery
into the
build.
There are module ids set so you can use the RequireJS 2.1.9 feature bundles and define where to find the resolved modules.
An example config using the minified built epik would look like this:
require.config({
paths: {
epik: '../bower_components/epik/lib',
primish: '../bower_components/primish',
lodash: '../bower_components/lodash/dist/lodash',
rivets: '../bower_components/rivets/dist/rivets',
jquery: '../bower_components/jquery/jquery'
},
bundles: {
'epik/epik-min': [
'epik/index',
'epik/model',
'epik/model-sync',
'epik/collection',
'epik/collection-sync',
'epik/agent',
'epik/storage',
'epik/router',
'epik/view',
'epik/plugins/rivets-adapter',
'slicker'
]
}
});
Once that is setup, requests for epik/model
on an empty require module factory will get the minified built version and
prime the factory against the module IDs defined above. You should only see a single HTTP request in your console for
epik-min.js
. Notice slicker
is bundled already and the rivets adapter is with the id of epik/plugins/rivets-adapter
.
Usage in both cases remains the same.
require.config({ ... }});
define(function(require){
var primish = require('primish/primish'),
Model = require('epik/model-sync');
var User = primish({
extend: Model
});
// ...
});
If you use the FULL build from dist/build/epik-full-min.js
, you would also have to add to the bundles config to let
requirejs know it will resolve rivets.js, jquery, lodash and primish as well:
require.config({
paths: {
epik: '../bower_components/epik/lib',
primish: '../bower_components/primish',
lodash: '../bower_components/lodash/dist/lodash',
rivets: '../bower_components/rivets/dist/rivets',
jquery: '../bower_components/jquery/jquery'
},
bundles: {
'epik/epik-min': [
'epik/index',
'epik/model',
'epik/model-sync',
'epik/collection',
'epik/collection-sync',
'epik/agent',
'epik/storage',
'epik/router',
'epik/view',
'epik/plugins/rivets-adapter',
'slicker',
'jquery',
'lodash',
'rivets',
'primish/primish',
'primish/emitter',
'primish/options'
]
}
});
Keep in mind that the strings passed to the bundles config are module IDs, not expanded paths.
Overview
The framework has been written to use primish classes so usage implies some familiarity with how that works - or at the very least, knowledge of either MooTools Class or similar from prototype.js or DOJO.
The basic premise is, epik
itself returns primish
in the same namespace (or AMD module). A typical Class looks like
this:
// assume a non-AMD environment
var MyView = epik.primish({
// set super
extend: epik.view,
// local methods and overrides
// ..
render: function(){
this.$element.html(this.model.toJSON());
}
});
var instance = new MyView({
element: '#header',
template: 'some html',
model: modelInstance
});
You can use primish to create your own controllers or to extend / mixin the existing ones that epik provides.
Model
The epik Model implementation at its core is a primish class with custom data accessors that fires events. You can extend models or implement objects or other classes into your definitions.
The Model can fire the following events:
ready
- when instantiatedchange
- when any properties have changedchange:key
- when a particular propertykey
has changedempty
- when a model has been emptied of all propertiesdestroy
- when a model has beendestroyed
and all data removed.error
- when a model validator fails on a property set.error:key
- when a particular key validation error has occurred
The following methods are official API on all Model Classes:
constructor
Expects arguments: {Object} obj
, {Object} options
Returns: modelInstance
Events: ready
The obj
sets the internal data hash to a new derefrenced object. Special accessor properties, as defined in the epik.model.prototype.properties
, will run first and be applicable. See properties for more info.
The options
object is a setOptions
override and is being merged with the epik.model.prototype.options
when a new model is created. It typically contains various event handlers in the form of:
require(['epik/index', 'epik/model'], function(epik, Model){
var Person = epik.primish('person', {
extend: Model,
defaults: {
sex: 'male',
title: 'Mr.',
age: 0
}
});
var bob = new Person({
name: 'Bob',
age: 30
});
console.log(bob.toJSON());
console.log(bob._id); // 'person'
});
set
Expects arguments: mixed: {String} key
, {Mixed} value
- pair - or: {Object} obj
Returns: modelInstance
Events:
-
change: function(changedProperties) {}
-
change:key: function(valueForKey) {}
-
error: function(objectFailedValidation) {}
-
error:key: function(objectFailedValidation) {}
Allows changing of any individual model key or a set of key/value pairs, encapsulated in an object. Will fire a single change
event with all the changed properties as well as a specific change:key
event that passes just the value of the key as argument.
For typing of value, you can store anything at all (Primitives, Objects, Functions). Keep in mind that, when it comes to serialising the Model and sending it to the server, only Primitive types or ones with a sensible toString()
implementation will make sense.
get
Expects arguments mixed: {String} key
or {Array} keys
Returns: {mixed|Object}
Returns known values within the model for either a single key or an array of keys. For an array of keys, it will return an object with key
: value
mapping. Properties gotten are not implicitly de-refrenced so careful if you have stored an object - modifying the value of the get
will modify your model as well.
The following example illustrates why it's a bad idea to store deep model properties. Alternatively, you can use instance.toJSON()
and reference and modify properties off of that without them making it back into the model.
require(['epik/index', 'epik/model'], function(epik, Model){
var Person = epik.primish('person', {
extend: Model,
defaults: {
sex: 'male',
title: 'Mr.',
age: 0
}
});
var bob = new Person({
a: 'a',
b: 'b',
location: {
country: 'UK',
city: 'London'
}
});
var location = bob.get('location');
console.log(location.city); // London
location.city = 'Manchester';
console.log(bob.get('location').city); // Manchester. oh no!
// get around dereferencing
location = epik._.clone(bob.get('location'));
location.city = 'London';
console.log(bob.get('location').city); // Still Manchester.
// multiple property getters
console.log(bob.get(['a','b']));
});
toJSON
Expects arguments: none
Returns: {Object} data
Returns a de-referenced Object, containing all the known model keys and values.
unset
Expects arguments: mixed: {String} key1
.. {String} keyN
or {Array} keys
Returns: modelInstance
Removes keys from model, either a single one or an array of multiple keys. Should fire a change event for every property removed as well as a change
. If a property does not exist in the model, no change event will fire for it.
empty
Expects arguments: none
Returns: modelInstance
Events: empty
Empties the model of all data and fires a single change event with all keys as well as individual change:key
events.
destroy
Expects arguments: none
Returns: this
Events: destroy
Empties the model. No change event. Event is observed by Collections the model is a member of, where it triggers a remove()
validate
Expects arguments: {String} key, {*} value
Returns: {Boolean} validates
Internal method that gets run whenever a property is being set. Checks to see if there is a validator for the key
, is so, returns the result of the validator function, else, assumes it's allowed and returns true
Model properties*
There are several additional properties each model instance will have.
_attributes: {Object}
The attributes object is public (exposed to manipulation on the instance) and it holds the hash data for the model, based upon keys. It is de-referenced from the constructor object used when creating a model but should not be read directly (normally). Exported by model.toJSON()
. Avoid changing this directly as it won't fire any change events at all.
_collections: {Array}
An array that contains references to all instances of epik.collection that the model is currently a member of. Useful for iteration as well as utilised by collections that subscribe to events for models.
options: {Object}
A default options set, which can be on the prototype of the Model constructor.
defaults: {Object|Function}
An object with default Model Attributes to use when instantiating. Merged with Model object when populating model data via the constructor. Also accepts a function, which should return an object, performed in the constructor.
propertiesChanged: {Array}
An array of all property keys that reflect the last change
event. Available on all instances.
validationFailed: {Array}
An array of all error Objects with info on all validation failed properties after a set
. Available on all instances.
properties: {Object}
A collection of custom accessors that override default model.get
and model.set
methods. For example:
properties: {
dob: {
get: function() {
// scope is model
return new Date(this._attributes.dob);
},
set: function(value) {
// return a value to be set
return value instanceof Date ? +value : value;
}
},
id: {
get: function(){
// returns a property from instance instead of _attributes
return this.id;
},
set: function(id){
this.id = id;
// may want to fire events manually here.
}
},
price: {
set: function(value){
return this.formatCurrency(value);
}
}
}
In the examples above, any calls to model.set('dob', new Date(1985, 5, 15))
and model.get('dob')
are handled by custom functions as we want our model to deal with unix timestamps only but return Date instances. This is a pattern that allows you to use getters and setters for properties that are handled differently than normal ones. If the set
function returns a value, it will use the normal set
chain and act as a formatter/pre-processor, firing events etc. You don't have to use this and can do as in the id
example, where the value is simply redirected elsewhere.
Avoid setting them on prototypes that you extend from, better to have them on the instance from the constructor or another method as they are not de-referenced and are not being merged. If you need to extend them and keep the default id getter, you need to merge with model.prototype.properties
in your new model definitions. This may change in future versions.
var Person = primish({
properties: _.merge({
foo: {
get: function(){},
set: function(){}
}
}, epik.model.prototype.properties);
});
Model validators*
You can also include basic validators into your model. Validators are an object on the Model prototype that maps any expected key to a function that will return true
if the validation passes or a string
error message or false
on failure.
Here is an example:
require(['epik/index', 'epik/model'], function(epik, Model){
var Person = epik.primish('person', {
extend: Model,
validators: {
email: function(value) {
return (/(.+)@(.+){2,}\.(.+){2,}/).test(value) ? true : 'This looks like an invalid email address';
}
}
});
var userInstance = new Person({}, {
onError: function(allErrors) {
console.log('The following fields were rejected', allErrors);
},
'onError:email': function(errorObj) {
// can have a custom message, action or whatever.
console.log('Email rejected', errorObj.error);
}
});
userInstance.set('email', 'this will fail!');
});
The error
event is observed by collections and views and fires on all view and collection instances.
Model Sync
This is an example implementation of RESTful module that extends the base epik.model class and adds the ability to read, update and delete models with remote server. In terms of implementation, there are subtle differences. The API and methods are as per the normal Model, unless outlined below:
constructor (initialize)
Expects arguments: {Object} model
, {Object} options
model-sync extends the normal model by adding some extra properties, namely id
and a urlRoot
either as a property of the model or as an options property, which allow you to sync it. The constructor function first calls the parent model constructor and then sets up the XHR instance and methods via a custom implementation of agent, which supports XHR2. agent.js
is a local file that does not need to be included separately due to certain changes around CORS headers and primish that are not available upstream.
options.headers
{Object} is a way to pass headers to the Agent instance, such as the content-type
to application/json
(by default), etc.
sync
Expects optional arguments: {String} method
, {Object} model
, {Function} callback
_Events: success|failure
: function(responseObj) {}
Sync acts as a proxy/interface to the XHR instance in the model this.request
A method can be one of the following:
get, post, create, read, delete, update
If no method is supplied, a read
is performed.
The second argument model
is optional and should be a simple object. If it is not supplied, the default model.toJSON()
is used instead.
If a callback is supplied, it will be called when done - although it will still raise the success
or failure
events
As a whole, you should probably NOT use the sync directly but elect to use the API methods for each specific request task.
WARNING: epik is a REST framework. Please make sure you are returning a valid JSON string or 204 (no content) after all requests -
otherwise, the save events may not fire. Additionally, try to ensure application/json
content type of your response so that the response is converted to an Object when passed back. Failing to do so will return it raw as plain text or whatever content type you have supplied.
postProcessor
Expects arguments: {Object} response
Expected return: {Object} response
A method that you pass in your definition of Models for doing any post-processing of data returned
by sync from the server. For example:
postProcessor: function(response) {
// data comes back with decoration. split them first.
this.meta = response.meta;
return response.data;
}
save
Expects optional arguments: {String} key
, {String} value
or {Object} keyValues
Returns: this
Events: save
, success|failure
, also either create
or update
, dependent on if the model is new
The save should send the contents of the model to the server for storage. If it is a model that has not been saved before or fetched from the server, it will do so via create()
, else, it will use update()
instead.
If the optional key
=> value
pair is passed, it will set them on the model and then save the updated model.
preProcessor
Expects arguments: {Object} response
Expected return: {Object} response
A method that you can add to your definition of Models for doing any pre-processing of data before using CREATE
or UPDATE
via, .save
when syncing to a server. For example:
preProcessor: function(data) {
// remove local property 'meta' which the server does not like.
delete data.meta;
return data;
}
fetch
Expects arguments: none
Returns: this
Events: fetch
, read
It will request the server to return the model object for the current id via a .read()
. It will also change the status of the model (model.isNewModel
) to false, meaning .save()
will never use .create()
. The fetch event will fire once the response object has been returned. The response object is then merged with the current model via a .set
, it won't empty your data. To do so, you need to issue a .empty()
first.
CRUD
As a side note, the following methods are exported on the model instance:
create
- maps to POSTread
- maps to GETupdate
- maps to POSTdelete_
- maps to DELETE (note that due to IE8, use ofdelete
and dot notation throws so it has been renamed)
Collection
Collections are in essence, an Array-like Type with Models as members. By passing a model prototype, adding and removing of models works either based with simple JSPO data has or an actual Model instance. Collections observe and bubble all events that all of its model members emit, firing them on the collection instance. It also allows for filtering, mapping, sorting and many sugar methods, copied from the Array.prototype and applied on the Model.
Unlike Arrays, collections here also allow a powerful search/filter that can return a subset {Array} of Models from the collection that match CSS-like selectors for model attributes
Instances of collection should have a ._id
property of collection, unless you override that.
constructor
Expects arguments: {Array|Object} models|objects
(or a single model /object), {Object} options
Returns: this
Events: ready
The constructor method will accept a large variety of arguments. You can pass on either an Array of Models or an Array of Objects or a single Model or a single Object. You can also pass an empty Array and populate it later. Typical Collection prototype definition looks something like this:
var userModel = primish({extend: epik.model}),
usersCollection = primish({
extend: epik.collection,
model: userModel // or epik.model by default
});
var users = new usersCollection([{
id: 'bob'
}], {
onChange: function(model, props) {
console.log('model change', model, props);
},
onReady: function() {
console.log('the collection is ready');
}
});
For reference purposes, each Model that enters a collection needs to have a cid
- collection id. If the Model has an id
, that is preferred. Otherwise, a cid
will be generated. If the Model gets an id
later on, the cid
will not be changed.
Please note that Collections observe and bubble all model events. For instance, if a Model fires change
, the Collection instance will fire onChange
, passing the model as arguments[0]
and then keeping the rest of the arguments in their original order. For the purposes of implementing this, a decorated local copy of each Model's .fireEvent
method is created instead of the one from Class.Event prototype. Once a Model stops being a member of collections, the original fireEvent
is restored by deleting the local method on the Model instance.
set
Expects arguments: {Array|Object} model(s)
, {Boolean} quiet
Returns: this
Events: set: function() {}
Sets models into the collection 'sugar'. Accepts a single model or an array of models to add, passing through to .add
and also firing a set
event when done. Previously .reset
in Epitome.
add
Expects arguments: {Object|Model} model
, {Boolean} replace
Returns: model
Events: add: function(model, cid) {}
, reset
Previously addModel
in Epitome. Adding a Model to a Collection should always go through this method. It either appends the Model instance to the internal _models
Array or it creates a new Model and then appends it. It also starts observing the Model's events and emitting them to the Collection instance with an additional argument passed Model
. So, if you add a Model stored in bob
and then do bob.trigger('hai', 'there')
, the collection will also fire an event like this: this.trigger('hai', [bob, 'there']); Adding a Model also increases the
Collection.length` property.
When a model is added, the collection uses listenTo()
to subscribe to all methods from the model and bubble them locally. Previously, this used to overload the fireEvent
method in Epitome's Models but it no longer does.
Increments the Collection.length
property (if not replacing existing models).
remove
Expects arguments: {Mixed} model(s)
, {Boolean} quiet
Returns: this
Events: remove: function(model, cid) {}
, reset
This method allows you to remove a single model or an array of models from the collection in the same call. For each removed model, a remove
Event will fire (if quiet
is not true). When removing of all Models is done, the collection will also fire a reset
event, allowing you to re-render your views etc.
In addition to removing the Model from the Collection, it removes the reference to the Collection in the Model's _collections
Array and stops observing the Model's events.
Decrements the Collection.length
property.
at
Expects arguments: {Number} index
Returns: modelInstance
or undefined
Returns a particular model by reference to current oder, eg. collection.at(3)
will return collection._models[3]
. Index is 0-based.
getModelById
Expects arguments: {String} id
Returns: modelInstance
or null
Performs a search in the collection by the Model's id
via the standard model getter
. Returns found Model instance or null
if no match is found.
getModelByCID
Expects arguments: {String} cid
Returns: modelInstance
or null
Performs a search in the collection by the cid
property (Collection ID). Returns found Model instance or null
if no match is found.
toJSON
Expects arguments: none
Returns: modelsData
Returns an array of the results of the .toJSON()
method called on all Models instances in the collection. Resulting array is de-referenced from both the collection and models instances.
empty
Expects arguments: {Boolean} quiet
Returns: this
Events: remove
, set
, empty
Applies this.remove
to all Models of the collection. Fires empty
when done - though before that, a remove
and reset
will fire unless quiet
is set as true
, see remove
sort
Expects arguments: {String|Function} how
Returns: this
Events: sort
Sorting is quite flexible. It works a lot like Array.prototype.sort
. By default, you can sort based upon strings that represent keys in the Models. You can also stack up secondary, trinary etc sort keys in case the previous keys are equal. For example:
users.sort('name');
// descending order pseduo
users.sort('name:desc');
// by type and then birthdate in reverse order (oldest first)
users.sort('type,birthdate:desc');
Sorting also allows you to pass a function you define yourself as per the Array.prototype.sort interface. When done, it will fire a sort
event.
reverse
Expects arguments: none
Returns: this
Events: sort
Reverses sort the order of Models in the collection. Fires a sort
event, not reverse
find
Expects arguments: {String} expression
Returns: {Array} MatchingModelObjects
This is an experimental API and is subject to change without notice. Collection.find
is currently powered by the MooTools Slick.parse
engine. This means you can
search through your Collection for Models by attributes and #ids
like you would search in a CSS selector.
For example:
require([
'epik/collection'
], function(Collection) {
var collection = new Collection([{
name: 'Bob',
id: 2
}, {
name: 'Angry Bob',
id: 3
}]);
console.log(collection.find('[name]')); // where name is defined.
console.log(collection.find('[name=Bob]')); // where name is exactly Bob.
console.log(collection.find('[name*=Bob]')); // where name contains Bob.
console.log(collection.find('[name$=Bob]')); // where name ends on Bob.
console.log(collection.find('[name^=Bob]')); // where name starts with Bob.
console.log(collection.find('[name=Bob],[name^=Angry]')); // name Bob OR starting with Angry.
console.log(collection.find('[name=Bob][id]')); // name Bob AND to have an id
console.log(collection.find('#2[name=Bob],#3')); // (name Bob AND id==2) OR id==3
console.log(collection.find('[name=Bob][id=2]')); // name Bob AND id==2
});
Supported operators are =
(equals), !=
(not equal), *=
(contains), $=
(ends on), ^=
(starts with). Currently, you cannot reverse a condition by adding !
or not:
- in fact, pseudos are not supported yet. Find is just sugar and for more complicated stuff, you can either extend it or use filter
instead.
A sugar 'feature' has been added that allows you to quickly select deeper object properties by treating any parent keys as tags. For instance:
require([
'epik/collection'
], function(Collection) {
var collection = new Collection([{
name: 'Bob',
permissions: {
edit: 'true'
}
}, {
name: 'Angry Bob',
permissions: {
edit: 'false'
}
}]);
console.log(collection.find('permissions[edit]')); // all where there is an edit property
console.log(collection.find('permissions[edit=true]')); // all where there edit is true
});
However, this is more of a convenience than convention. Since it does not do type checking, it is difficult to pass what type of a value you are after in a string. In the example above, edit
needs to be exactly the string true
. For complex selectors and non-string data, you should use your own .filter
methods.
It also won't allow you to do complex CSS-like selections as you cannot combine 'tag' with properties. This means you cannot do permissions[edit][name=Bob]
as the search context changes to the permissions property. This kind of structure is possibly an anti-pattern anyway, try to keep your models flat and avoid nested objects where possible.
findOne
Expects arguments: {String} expression
Returns: {Model} First matching Model instance or null
Useful for getting a single Model via the .find
, this method will return the first matched Model or null if none found.
var bob = collection.findOne('[name=bob]');
// if found, set
bob && bob.set('name','Robert');
Array helpers
The following Array methods are also available directly on the Collection instances:
- forEach
- each (alias for forEach)
- invoke
- filter
- map
- some
- indexOf
- contains
- getRandom
- getLast
where
Expects arguments: {Object} attributes
Returns: {Model} First matching Model instance or null
Useful for getting a single Model via the lodash _.find
API, this method will return the first matched Model that has
the exact same properties as the attributes map passed in or null/undefined if none found. CASE SENSITIVE.
var bob = collection.where({name: 'bob'});
// if found, set
bob && bob.set('name','Robert');
Collection properties*
_models
Each Collection instance has an Array property called _models
that contains all referenced Model instances. Even though it is not a real private property, it is recommended you do not alter it from outside of the API.
length
Tries to always reference the length of _models
, unless you have directly modified _models
without using the API.
model
Each Collection prototype has that property that references a Model prototype constructor. When data is being received in raw format (so, simple Objects), Models are being created by instantiating the stored constructor object in this.model
.
id
Due to serialisation and the ability to use storage to retrieve a collection later, each collection has an id, derived either from the options object or generated at random.
Collection Sync
The Sync collection is just a tiny layer on top of the normal collection. It extends
the default Collection class and adds an agent instance that can retrieve an array of Model data from a server and add / update the Collection after.
constructor (initialize)
Expects arguments: {Array|Object} models|objects
(or a single model /object), {Object} options
Returns: this
Events: ready
In terms of differences with the original prototype, the options
, needs just one extra key: urlRoot
, which should contain the absolute or relative URL to the end-point that can return the Model data.
fetch
Expects optional arguments: {Boolean} refresh
, {Object} queryParams
Returns: this
Events: fetch
When called, it will asynchronously try to go and fetch Model data. When data arrives, Models are reconciled with the Models in the collection already by id
. If they exist already, a set()
is called that will merge new data into the Model instance and fire change
events as appropriate. If the optional refresh
argument is set to true, the current collection will be emptied first via empty.
Returns the instance 'now' but because it is async, applying anything to the collection before the fetch
event has fired may have unexpected results.
The queryParams
object, which is also optional, allows you to pass on any GET
arguments to the baseUrl
. If your default endpoint looks like this:
/comments/2/
and you call collection.fetch(false, {page: 2})
, it will actually get /comments/2/?page=2
.
Keep in mind that agent will serialize the response as per the content type, just like in Model Sync. If it's application/Json
, the decoder will kick in and set the response into the instance.
postProcessor
Expects arguments: {Array|Mixed} response
Expected return: {Array} response
A method that you can extend in your definition of Epitome.Collection.Sync for doing any pre-processing of data returned by sync from the server. For example:
var Users = primish({
extend: epik.collection,
model: UserModel,
postProcessor: function(response){
// data comes back with decoration. split them first.
// { meta: { something: 'here' }, models: [] }
this.meta = response.meta;
return response.models;
}
});
View
The view is very un-assuming. It can work with either DOM library (jquery, jquery-lite, zepto) and a templating engine like handlebars or lodash.template. It can also work with a template binding engine like rivets.js. A combination of both is also possible.
constructor
Expects arguments: {Object} options
Returns: viewInstance
Events: ready
The default view is a pretty loose binding around a HTMLElement, it does not try to do much by default. It essentially binds an element to either a Model or a Collection, listening and propagating events that they fire in order to be able to react to them. It has glue to pass DOM events delegating into the element to the instance as events or by calling methods on the instance, when possible.
The expectation is that a render
method will be defined that uses the data to output it in the browser. How the render can be called is up to you, eg. on change
or reset
events.
A single argument in the shape of an options
Object is passed to the constructor of the View. It is expected to have special 'mutator'-like properties and key properties that it stores for future use.
Significant keys to the options passed in are:
element
- a String id or an element to bind events to and referencemodel
- optional Model instance structure to bind to. Exchangeable withcollection
collection
- optional Collection instance to bind to. Exchangeable withmodel
template
- a String of raw HTML that defines the raw template to use in output.events
- an Object with MooTools style event bindings to apply to theelement
, delegated or not. values are implied event handlers on the instanceonEventHandlers
- code that reacts to various events that the instance fires.
Epik views do not support the tag
options of Backbone, you need to figure the elements on your own.
By default, if you have a method called
attachEvents
, it will automatically get called from the constructor function. If you are extendingview
, thenthis.parent('constructor', options);
will have the same effect.
require([
'epik/index',
'epik/view',
'epik/model'
], function(epik, View, Model) {
'use strict';
var primish = epik.primish,
tpl = 'I am template <a href="#" class="task-rename"><%=name%> <%=status%></a><br/><button class="done">completed</button>';
var testView = primish({
extend: View,
options: {
events: {
'click a.task-rename': 'renameTask',
'click button.done': 'reset'
}
},
render: function(){
this.empty();
this.$element.html(this.template(this.model.toJSON()));
this.parent('render');
return this;
},
reset: function(){
this.model.set('status', 'done');
this.render();
},
renameTask: function(event){
event && event.preventDefault && event.preventDefault();
this.model.set('name', 'Changed name');
}
});
var testInstance = new testView({
model: new Model({name: 'View fun', status: 'pending'}),
element: '#main',
template: tpl,
onReady: function(){
this.render();
},
'onModel:change': function(){
this.model.set('name', new Date().getTime());
this.render();
}
});
});
render
Expects arguments: unknown
Returns: viewInstance
Events: render
It is essential that this method is defined in your View prototype declaration. It does not assume to do anything by default, you need to define how the output takes place and how your data is being used. For convenience, it has access to either this.model
or this.collection
as the source of data that can be be passed to the template method. It is expected that at the bottom of your definition, this.parent('render')
is called in order for the render
event to fire, though you can manually do a this.trigger('render')
instead, if you want.
setElement
Expects arguments: {Object|String} element
, optional {Object} events
Returns: viewInstance
A public method that allows you to change or set an element that powers a view. If called the first time, it will get the Element (through Sizzle / jQuery()
) and save a reference in this.element
to the raw object, as well as this.$element
to the wrapped jQuery object. If an events object is passed, it will bind the events. If called a second time, it will unbind all events on the old element, change the element reference and rebind any new events.
template
Expects arguments: {Object} data
, optional {String} template
Returns: compiled template or function.
A simple sandbox function where you can either use the lodash templating engine or call an external engine like Mustache, Handlebars, Hogan etc. The second argument is optional and if not supplied, it will revert to this.options.template
instead.
An example override to make it work with Mustache would be:
var myView = epik.primish({
extends: epik.view,
template: function(data, template) {
template = template || this.options.template;
return Mustache.render(template, data);
},
render: function() {
this.$element.html(this.template(this.model.toJSON(), 'Hello {{name}}'));
}
});
You can change the View prototype to always have Mustache in your views. For example, via AMD/RequireJS, you could do a
small module that deals with the prototyping of the default View constructor. Say, epitome-view-mustache.js
define(['epik/view'], function(View){
// for everyone to use mustache in every view instance when .template()
View.prototype.template = function(data, template) {
// refactor this to work with any other template engine in your constructor
template = template || this.options.template;
return Mustache.render(template, data);
}
return View;
});
empty
Expects arguments: {Boolean} soft
Returns: viewInstance
Events: empty
By default, it will empty the element through making innerHTML
an empty string, calling GC on all child nodes. If the soft
argument is true, will apply this.$element.empty()
, which is a jQuery method that removes all child nodes without destroying them.
dispose
Expects arguments: none
Returns: viewInstance
.
Events: dispose
Will detach this.$element
from the DOM. It can be injected again later on.
destroy
Expects arguments: none
Returns: viewInstance
.
Events: dispose
Removes and destroys this.$element
from the DOM and from memory. You need to use setElement to add a new one if you want to re-render.
View rivets-adapter
There is an adapter for rivets.js provided, which does the following customisations:
- prefix is
ep-
for your data bindings - the adapter suffix is
#
Example view implementation:
require([
'epik/index',
'epik/view',
'epik/model',
'epik/plugins/rivets-adapter'
], function(epik, View, Model, rivets) {
var primish = epik.primish;
var MyView = primish({
// mixin the rivets class
implement: [rivets],
extend: View,
options: {
template: ''
},
constructor: function(options){
this.parent('constructor', options);
this.element.innerHTML = this.options.template;
this.bindRivets(this.element, {
person: this.model
});
},
destroy: function(){
this.unbindRivets();
this.parent('destroy');
}
});
var person = new Model({
name: 'Bob'
});
// new view with bi-directional binding between model and dom.
var myView = new MyView({
element: document.getElementById('main'),
model: person,
template: 'Name: <span ep-text="person#name"></span><br/><input ep-value="person#name"/>'
});
setTimeout(function(){
person.set('name', 'Robert');
}, 1500);
});
Any change of the model will fire events which the adapter is listening for and will automatically update the view
for the relevant bound nodes. Conversely, changes from the DOM via conventional onChange
events will be exported
to the model's .set()
method (subject to validation rules).
Collections are similarly implemented. Notice the use of ep-
as opposed to rv-
and the #
call to pass through
the epik adapter (the rivets default PJSO one is .
and it can still be used)
The full spectrum of Rivets.js API will work as expected.
bindRivets
Sugar that passes an object to be bound to this.element
. Optionally, you can pass a different element as the first
argument. Creates a this.boundRivets
property on the object, containing reference to the current rivet view context instance
unbindRivets
Used as a destructor to unbind existing events from this.boundRivets
syncRivets
A method that calls rivets.sync()
on the bound view to force manual processing, like $digest
router
The Router prime is a hashbang controller, useful for single page applications. Currently, it works with window.onhashchange
and a setInterval
polyfill for older browsers. It may change to push/pop state soon.
constructor
Expects arguments: {Object} options
Returns: routerInstance
Events: ready
, before
, after
, mixed, undefined
, error
, route:add
, route:remove
As this is quite involved and can act as a Controller for your app, here's a practical example that defines a few routes and event handlers within the epik.router prime instantiation:
App.router = new epik.router({
// routes definition will proxy the events
routes: {
'' : 'index',
'#!help' : 'help',
'#!test1/:query/:id?' : 'test1',
'#!test2/:query/*' : 'test2',
'#!error' : 'dummyerror'
},
// router init
onReady: function(){
console.log('init');
},
// before route method, fires before the event handler once a match has been found
onBefore: function(routeId){
console.log('before', routeId)
},
// specific pseudos for :before
'onIndex:before': function() {
console.log('we are about to go to the index route');
},
// specific pseudos for after
'onIndex:after': function() {
console.log('navigated already to index route, update breadcrumb?');
},
// after route method has fired, post-route event.
onAfter: function(route){
console.info('after', route)
},
// routes events callbacks are functions that call parts of your app
// index
onIndex: function() {
console.log('index')
},
onHelp: function() {
console.log('help');
console.log(this.route, this.req, this.param, this.query)
},
onTest1: function(query, id) {
console.info('test1', query, id);
console.log(this.route, this.req, this.param, this.query)
},
onTest2: function(query) {
console.info('test2', query);
console.log(this.route, this.req, this.param, this.query)
},
// no route event was found, though route was defined
onError: function(error){
console.error(error);
// recover by going default route
this.navigate('');
},
onUndefined: function() {
console.log('this is an undefined route');
},
'onRoute:remove': function(route) {
alert(route + ' was removed by popular demand');
},
'onRoute:add': function(constructorObject) {
console.log(constructorObject.id + ' was added as a new route');
}
});
addRoute
Expects arguments: {Object} route
Returns: routerInstance
Events: route:add
Example late adding of route to your instance (after instantiation):
App.router.addRoute({
route: '#!dynamicRoute',
id: 'dynamic',
events: {
onDynamic: function() {
alert('you found the blowfish');
if (confirm('remove this route?'))
this.removeRoute('#!dynamicRoute');
}
}
});
removeRoute
Expects arguments: {String} route
Returns: routerInstance
Events: route:remove
Removes a route by the route identifier string.
For more examples of Router, have a look inside the dist/example
folder, it's powered by a router instance.
Contributing
Whilst this is being ported, you can help. Fork the repo or ask for commit access.
$ git clone git@github.com:DimitarChristoff/epik.git
...
$ cd epik/
$ npm i
...
$ bower install
...
$ cd test/
$ buster-static
...
Examples - run a grunt express server with socket.io etc. Install grunt if you don't have it, then run from root of the repo.
$ npm install -g grunt-cli
$ grunt
$ grunt build
$ grunt requirejs
The web server is on port 8000 - visit http://locahost:8000/example/ to view live examples
A nice example of a practical web app running mocked FX currency pairs is here: https://github.com/DimitarChristoff/cp. It has been written to test the speed of bindings via the rivets adapter and should work fine with as many as 200+ currency pairs running and streaming via socket.io under chrome, all at no more than 20-30% CPU use.
You can view it in action http://fragged.org:8000/ - if the grunt server is up.
A TodoMVC implementation can be seen here