Data-driven events and validation for your javascript applications
About
JSchema.Binding is a lightweight framework for managing complex, data-driven UIs. It allows for binding UI callbacks to data Models (or classes) and Records (or instances), and for manipulating those records' data. It also provides JSON schema validation over record data using JSV internally to check changes in real time. In a nutshell, it basically serves to keep your UI fresh, synchronised, valid and responsive. Everything else is up to you.
If you already use JSON schema with your application, think of JSchema.Binding as adding state to your validation.
For simple data manipulation tasks or applications without the need for clientside data validation, Binding is a more than adequate event engine and data handling layer all on its own - just pass false
insted of a schema when creating new Models.
Binding performs no ajax operations and is not a full MVC framework, but could easily be used as a strong foundation to build one. If you are after something more heavyweight, try the excellent Backbone.js (which indeed influences some of Binding's design).
Please note: you should clone this repository with git clone --recursive
.
Features
- Powerful event engine
JSchema.Binding's event engine combines namespaced events to allow for fine-grained update callbacks with namespace wildcards to allow even more control. JSchema.EventHandler also provides a robust layer for implementing events on any other non-DOM javascript objects in your applications.- Event namespacing
Allows responding to changes in object subproperties with infinite granularity- A modification to the object
addEvent('change', ...);
- A deletion within the object
addEvent('change.delete', ...);
- An update to propertyA
addEvent('change.update.propertyA', ...);
- An update to propertyA of objectB
addEvent('change.update.objectB.propertyA', ...);
- A modification to the object
- Event bubbling
Changes to deeply nested object properties bubble up to their parents with the correct parameters being passed back to bound callbacks. So a change to objectA.propertyB would first fire callbacks with the values for propertyB, and then again with values for objectA. Bubbling can be aborted by returningfalse
in callbacks, just like with DOM events. This allows you stop bubbling back up the callback namespace chain early once you have processed everything relevant to a change. - Callback wildcards
Allows binding events with even finer control, for example:- An update to propertyA
addEvent('change.update.propertyA', ...);
- propertyA being initialised
addEvent('change.create.propertyA', ...);
- Any type of modification to propertyA
addEvent('change.?.propertyA', ...);
- An update to the record's propertyA, or to any propertyA within any subobjects of the record
addEvent('change.update.*.propertyA', ...);
- An update to propertyA
- Event marshalling
Allows events to be pooled, combined and fired as a single logical 'change'. - Callback data consistency
Record state is predictable in all callbacks - firing is deferred until the state of the object has completed updating.
- Event namespacing
- Change handling
Records can be checked for modifications to allow intelligent serverside data pushing, and uploaded propertysets can be refined to only those modified. Snapshots of data can be taken at any point in time and compared with one another or checked for changes easily. - JSON schema validation
Naturally, all changes to data objects are validated against your schema in real-time and can provide feedback of any changes and errors straight to your UI or other application code.- Enhanced errors
JSV's standard error objects are augmented with attributes for the current value, - Error bubbling
Using the same event mechanism as with change events, errors bubble up to their parent properties. The array of schema error data at each point in the chain contains all errors relevant for that object and all its child properties.
- Enhanced errors
- Record composition
Record instances can be seamlessly inserted into each other providing for composite data relationships and sharing of data between records. JSchema allows you to deal directly with the data whilst ensuring that changes to attributes from a parent record immediately propagate to attributes shared with their children, and from child records back up to their parents. - ID tracking
When configured to do so, record IDs are automatically tracked and record instances can be retrieved from your models viagetRecordById()
.
Compatibility
JSchema.Binding has minimal coupling to any library and uses jQuery only for utility methods. Separate branches exist for compatibility with other libraries, and lines of code dependency are clearly denoted by the comment LIBCOMPAT
.
Branches:
git checkout mootools
(:TODO:)
Usage
The first thing you'll want to do with a Binding instance is create a model (think 'class') for it. To do this, you simply call JSchema.Binding.Create(schema, options)
:
Schema
The JSON schema document used to validate this record, as a javascript object.
Options
A map of options for the new record class.
idField
Setting this property enables you to manage your record objects by primary key or other dentifying information, using the Record methodgetId()
to retrieve object IDs, and Model methodgetRecordById()
to retrieve record instances by those IDs.doCreateEvents
If true, callbacks bound to any create events will fire when instantiating the record. Defaults to false.clearIdOnClone
If true, records cloned from others will have their IDs reset. Defaults to false. Do not enable this if your schema prohibits records without an ID field!validateCreation
If true, records should be validated when they are initialised for the first time. Defaults to false.
Once you have your model ready, you can bind events to it and begin creating instances:
var person = JSchema.Binding.Create({ ... });
person.prototype.addEvent('change.update.name', function(newName) { alert('My name is now ' + newName); });
var Jimmy = new person({ ... });
Jimmy.set({name : 'Denny'}); // "My name is now Denny"
You can add events to particular instances too, if you wish:
Jimmy.addEvent('change.update.name', function() { alert('Thank god! Jimmy changed his name back.'); });
Jimmy.set({name : 'Jimmy'});
// "Thank god! Jimmy changed his name back."
// "My name is now Jimmy"
In normal usage, you'll probably first want to pull down your JSON schema files and record data from an external source. You would typically do something like the following:
// create a data model for validation
var schema = jQuery.getJSON(/* some URL containing a validation schema... */);
var Model = JSchema.Binding.Create(schema, {
idField : 'record_id',
clearIdOnClone : true
});
// bind some events to these records
Model.addEvent('change', function(record, prevData) {
// update something...
});
// ...
// load and create a record
var data = jQuery.getJSON(/* some URL containing a record... */);
var something = new Model(data);
// updating the record triggers events
something.set({
someProperty : 'some value'
});
// change callback triggered, something updated!
// later in our program, we can check if the record needs saving...
if (something.isDirty()) {
// and update by posting the entire record back...
jQuery.post(/* some URL for updating the record */, something.getAll());
// ...or just the changes.
jQuery.post(/* some URL for updating the record */, something.getChangedAttributes());
// note that we would call something.changesPropagated() upon a successful POST call being completed
}
My APIs, Let Me Show You Them
Events
JSchema.Binding records recognise the following events:
error
fires in response to an error updating the data record. Receives as parameters the record instance and an array of error objects from JSV, augmented with some of Binding's own:{ "uri": "URI of the instance that failed to validate", "schemaUri": "URI of the schema instance that reported the error", "attribute": "The attribute of the schema instance that failed to validate", "message": "Error message", "details": // The value of the schema attribute that failed to validate // JSchema.Binding properties "recordProperty" : // the dot-notated index of the property which failed to validate "current" : // the current, (assumedly) valid value of the errored property "invalid" : // the value of the passed property which invalidated the record }
Error callbacks can also be bound to specific properties within the object using dot notation. When bound, these callbacks will recieve the record instance, invalid value that broke the record, dot-notated index of the invalid property and the relevant error object from JSV. One callback is fired for each error object passed to the top-level error callback.
change
fires in response to a change in the record. Receives the current record instance and previous data bundle as parameters.change.update
fires in response to an update of the record, or an update of a particular value on the record. Receives as parameters the current record instance, previous value, new value, dot-notated index of the property modified and name of the event fired.
This callback, as well aschange.create
andchange.delete
, can also be bound to any number of subnamespaces, denoting indexes into the data record and its subproperties.change.create
fires in response to a property initialisation within the record, in the same manner aschange.update
. The previous value will be undefined in this case.change.delete
fires in response to a property being deleted within the record, in the same manner aschange.update
. The new value will be undefined in this case.
Methods
Global Methods
These methods can be found on the global JSchema
object.
registerSchema(schema, uri)
Allows registering a schema definition with JSV, in order for other schemas to be able to reference it by its URI. If the uri is ommitted, theid
field of the schema itself will be used instead.getSchema(uri)
Allows retrieving a schema previously registered withregisterSchema()
.isRecord(thing[, model])
Determine whether the passed argument is a JSchema.Binding data record. If themodel
parameter is provided, the method also checks whether the record is an instace of the given model.isModel(thing)
Determine whether the passed argument is a JSchema.Binding data model
Model Methods
These methods are available to record Model instances.
getRecordById(id)
When theidField
option is provided, records are automatically referenced in their corresponding models. This method can be used to retrieve them by those IDs.getInstanceCount(includeNew = false)
Retrieve the number of active Records of this Model. By default, only saved records (those with IDs) are returned. To return all objects, pass true.getAllInstances(includeNew = false)
Get a map of all active records, indexed by ID. If true is passed, unsaved records will be returned as well under the indexes 'new#0', 'new#1' etc
Record Methods
These methods are available to all individual Record instances.
Event Handling (JSchema.EventHandler
)
addEvent(eventName, callbackFn, [context])
Bind a callback to an event on an object. Optional third parameter specifies the 'this' argument of the callback function. Also available to Model instances.addEvents(events)
Bind a number of callbacks to various events all at once. Callbacks will be bound to events matching the key names of the passed object. Also available to Model instances.removeEvent([eventName, [callback]])
Unregister previously bound callback events. You can remove all events by passing no arguments, a whole callback set by passing the callback name, and a specific callback by passing the callback name and bound function. Also available to Model instances.holdEvents()
Begins event marshalling: data modification will not execute event callbacks, but instead keep a cache of all callbacks called while in this state. CallingfireHeldEvents()
will execute all callbacks fired while in this state. Usually (as with the below event methods) used internally by JSchema.Binding, but useful elsewhere as well.eventQueued(eventName)
Checks whether an event has been fired whilst marshalling.unfireEvent(eventName)
Remove an event called whilst marshalling from the called event cache to prevent it from firing when marshalled events are applied.abortHeldEvents()
Stop holding events from firing and clear out the held event cache.fireHeldEvents()
Merge and fire all events accumulated during the last hold phase.fireEvent(eventName, ...)
Fire an arbitrarily named event, passing all arguments following the event name to matching callback functions. Callbacks matching all namespaces of the event will be called upward in turn, unlessfalse
is returned from one of the callbacks to abort the bubbling.fireEventUntilDepth(eventName, depthToStopAt = 0, ...)
Fire event callbacks matching this event, but only up to a certain namespace depth.
Data Manipulation
set()
Sets data on a record. Note that all data manipulation operations can be performed withset()
, they exist mostly for convenience. Accepts two paramter formats:object
,bool
Merges this object's values in with the record's. To unset values, setundefined
in their place.
The second parameter controls whether (true) or not (false) to suppress event firing.string
,mixed
,bool
Sets the attribute at this index (specified by dot notation).
Param 2 is the value to set, param 3 controls whether (true) or not (false) to suppress event firing.
setId(newId)
Sets the record's Id, which can be any scalar value.idField
must be configured in options for this method to work.unset(attribute, suppressEvent)
Unsets one of the record's attributes. Accepts 2 parameters: the property to erase (dot notation) and a boolean to allow suppressing event firing.clear(suppressEvent)
Clear all data from the record. You may wish to override this method to reset the record's data to a clean state if your schema prohibits an empty record.push(attribute, value, suppressEvent)
Helper for array data. Allows you to append to arrays using dot notation to locate the array in the record. Accepts the attribute index, value to append and the usual flag to suppress events.pop(attribute, suppressEvent)
Helper for array data. Removes the last element of the specified array attribute.clone(cloneEvents)
Creates a duplicate of the record. Iftrue
is passed, the original record's instance events are copied as well. If the record'sidField
andclearIdOnClone
options are set, this may also clear the new record's id attribute.
Data Validation
validate(newData)
Manually perform validation of some data against the record. The supplied data will be merged in to the record's current attributes and checked for validity.pauseValidation()
Pause all validation when setting data on the object.resumeValidation()
Resume validation after pausing it to forcibly update the record to an invalid state.setSchema(schema)
Changes the schema used to validate the object.
Data Reading
isNew()
Checks whether the record is new. Only works if theidField
option has been set.has(attribute)
Checks whether an attribute has been set. The attribute is specified in dot notation.get(attribute)
Retrieve a specific data member. The value index in the record is passed in dot notation.getId()
Return the record's ID. Only works ifidField
has been set.getAttributes()
/getAll()
Retrieve a copy of the complete data record from the Binding.getSubrecord(attribute)
Works as withget()
, except that only subattributes which are instances of other JSchema records will be returned. Used for pulling child record objects back out of their parents after injection.
Change Handling
saveState(key)
Records the attributes of the model as they are now into an internal cache by the namekey
. This can then be used with other change handling methods to query the state of the object between two distinct known times.eraseState(key)
Deletes a state previous saved withsaveState()
.revertToState(key)
Reverts a record to one of its previously saved states. Note that this does not remove the state or perform any kind of 'undo stack' operation, all prior saved states will persist. Any changes to the record will fire as usual.getPrevious(attribute, since)
Retrieve a particular attribute from before the last change using dot notation, or from a particular point in time (previously stored bysaveState()
) ifsince
is specified.getPreviousAttributes(since)
Gets the full record from before the last change, or from a particular point in time ifsince
is passed.hasChanged(attribute, since)
Check whether the record has changed since last edit or a saved state. If an attribute is specified, checks this property for changes. You may pass(null, 'statename')
to determine whether the entire record has changed since a previous saved state.getChangedAttributes(includePrev, old, now)
By default, returns an object containing all properties modified in the last edit action. IfincludePrev = true
, each value will instead be a 2 element array of the old and new values.
old
andnow
can be used to check changes between other sets of record attributes - for example, to check changes between the current record and an old state, usegetChangedAttributes(false, myRecord.getPreviousAttributes('oldState'))
.isDirty()
Allows client code to flag to this record that clientside changes to it have been dealt with in some way (propagated to server etc). This method queries whether the record needs saving.changesPropagated()
Flag that changes have been dealt with and reset the status ofisDirty()
.
Utility Methods
These methods are internal to JSchema most of the time, but they're there to use if you wish.
JSchema.pathToSchemaUri(attr, backwards, schema, model)
Provides translation between the dot-notation syntax used by JSchema and the internal URI format of any JSONSchema. Whenbackwards
is true, the translation is from a schema URI to JSchema dot-notated string.model
is an internal argument and can optionally be provided to cache the schema's fragment identifier character somewhere for subsequent runs.JSchema.extendAndUnset()
Exactly the same as jQuery'sextend()
, except that it always expects variable length arguments and allows you to unset existing values in the first object by setting properties toundefined
in subsequent objects. This method both augments the first object passed and returns it.JSchema.dotSearchObject(target, attr, returnParent, createSubobjects, topLevelSchema)
Manages dot notation handling of object properties. This method is capable of performing various tasks on objects depending on the arguments passed in:- To simply retrieve properties from a javascript object, pass the object in as
target
and the attribute to read as a string. The method returns the property orundefined
if it doesn't exist. - For editing of objects, pass
returnParent = true
. The method then returns a 3-element array consisting of a reference to the object's parent element (or the original target object if reading a top-level property), index of the target object within the parent and dot-notated string targeting the parent element in the object's hierarchy. These three values can be used together to manipulate objects in any other way - deleting & changing values, popping from arrays or calling any other methods on subobjects with attached logic. - Setting deeply nested data can be automated by setting both
returnParent
andcreateSubObjects
totrue
. When activated, rather than returningundefined
when a value is not found, the function will recurse downward and add objects into the target at all passed indexes. Upon reaching the final index, the newly created parent object and other values are returned as before.
If you are setting data within a data record, you may also pass atopLevelSchema
to the function. This JSONSchema can be used to determine the correct parent datatypes of new values, creating arrays instead of objects as appropriate.
- To simply retrieve properties from a javascript object, pass the object in as
JSchema.isEqual(a, b)
Compare any two variables for equality (probably) as efficiently as possible. This method is pretty much taken straight from underscore.js.
TODO
Bugs
- Fix revertToState() not clearing properties from the current object which aren't set in the reverting state (probably needs to get a diff of all elements, instead of stopping at top-level difference)
- Archive old attributes when event deferring is enabled
- (symptom?) while marshalling, subsequent edits to the same property only show as the final difference afterwards
- Arrays
- changing individual array elements directly doesnt fire events
- when objects within arrays are set but unmodified, a change is detected on the object
- fix trailing star wildcards skipping bottom-level events when bubbling property changes
- * wildcards appear to be working incorrectly with multiple levels
Incomplete features
- Record composition:
- Fire events from changes in child records
- fire
clear()
events properly - Add error callback value passing for dependencies
- remove
clearIdOnClone
option or addstoreInstances
option to select between these behaviours - set cache threshold for ID tracking to limit record storage
Additions
- Retrieve & set default values from schema when creating records
- events should fire from undefined => defaults
- validate readonly properties
- validate URI format
- Allow creating separate environments with JSV
- Expose more useful JSV methods & options
- allow setting validateReferences & enforceReferences to false in JSV when loading
- do mootools branch
Review
- fire events in correct order internally in databinding to reduce sort time when firing
- ensure all callback contexts are being carried through to execution
- refactor some duplicate EventHandler code for reuse
Known Issues
- Holding events masks the return values of
fireEvent()
,fireHeldEvents()
etc and code will be unable to determine whether callbacks have been fired (these functions always return true while marshalling to keep any errors in your own code from being raised prematurely, if anyone has any ideas on how to handle this I'd be very interested to hear them!) - If no error callback is registered (which you should not really do anyway), invalid value setting while holding events with
holdEvents()
will trigger errors internally even if callingabortHeldEvents()
. It will also prematurely trigger errors multiple times beforefireHeldEvents()
is called due to the same limitation.
License
This software is provided under an MIT open source license, read the 'LICENSE.txt' file for details.