↑Instantiating an object
Since AFrame tries to be somewhat AOP with its use of Plugins, and often times those Plugins depend on knowing
when the plugged object is initialized, it is recommended to use a class' create function instead
of the 'new' operator to do object instantiation. Most AFrame.AObject based classes do very little in
their constructors, and do their initialization in the 'init' function. This allows us to create an object and all of its plugins, then
have the plugins take action whenever their plugged object is initialized.
All AFrame.AObject based items have a CID. A CID is a Client IDentifier that is used to uniquely identify objects within
the system. CIDs can be assigned on object creation, if a CID is not given, one is assigned automatically.
↑Example of object creation
// Simple example, simply create an object, no configuration given to the object
var object = AFrame.SomeAObject.create();
// Complex example, configuration, plugins, plugins with configuration
var object = AFrame.SomeAObject.create( {
configConfig1: val1,
configConfig2: val2,
plugins: [ [ AFrame.SomePlugin, {
pluginConfig1: val1,
pluginConfig2: val2
} ] ]
} );
object.someOperation();
What this does under the hood is create an instance of AFrame.SomeAObject and an instance of AFrame.SomePlugin. AFrame.SomePlugin is bound to AFrame.SomeObject.
AFrame.SomePlugin finally has its init function called.
↑Defining a Class
Defining a class in AFrame is a very straight forward process.
// Define a class that has no superclass
var Class = AFrame.Class( {
someFunctionality: function() {
// do something
}
} );
// Define a class that uses AFrame.AObject as a superclass.
var Class = AFrame.Class( AFrame.AObject, {
someFunctionality: function() {
// do something
}
} );
↑Using Observables within AFrame.AObjects
An Observable is the way events are done. Observables are very similar to DOM Events in that
each object has a set of events that it can trigger. Objects that are concerned with a particular event register a callback to be
called whenever the event is triggered. Observables allow for each event to have zero or many listeners, meaning the developer does not have
to manually keep track of who to notify when a particular event happens. This completely decouples the triggering object from any
objects that care about it.
↑Example of Binding to an AObject's Observable
/**
* Assume anObject is an AFrame.AObject based object.
* Every AFrame.AObject based object triggers an onInit
* event when its init function is called.
*/
var onObjectInit = function() {
// called whenever anObject.init is called.
};
anObject.bindEvent( 'onInit', onObjectInit );
anObject.init(); // calls onObjectInit function
Model Related Classes
↑Using DataContainers
A DataContainer is AFrame's basic unit of storage. A DataContainer
allows many listeners to be notified when a field changes. This is important in an MVC system where one
item of data could have several distinct Views.
↑Basic DataContainer Usage
var dataObject = {
firstName: 'Shane',
lastName: 'Tomlinson'
};
var dataContainer = AFrame.DataContainer( dataObject );
dataContainer.bindField( 'firstName', function( eventObject ) {
alert( 'new name: ' + eventObject.value );
} );
dataContainer.set( 'firstName', 'Charlotte' );
↑Using Schemas to Define the Data's Structure
A Schema acts as a template to define the structure of a piece of data.
Though schemas define the structure of data, they can also be used to validate, clean up or just plain transmogrify data.
These capabilities are especially useful when either retreiving data from or sending data to some sort of persistence layer.
Schemas can even be nested to create complex data structures. A Schema, combined with a DataContainer,
make up what is traditionally thought of as a Model (more on Models in the next section).
↑An Example Schema
/**
* A simple schema for a note.
* the integer and text types are self explanatory, iso8601 means ISO8601
* formatted date. Dates using the iso8601 type format will be
* automatically converted to Javascript Date objects.
*
* def means "default value"
*/
var noteSchemaConfig = {
id: { type: 'integer' },
title: { type: 'text', def: 'Note Title' },
contents: { type: 'text' },
date: { type: 'iso8601' },
edit_date: { type: 'iso8601' }
};
var noteSchema = AFrame.Schema.create( {
schema: noteSchemaConfig
} );
↑Using DataContainers and Schemas Together = Model
↑Example Making a Model from a Schema and DataContainer
// When creating a model, an explicit Schema does not need made, one will
// automatically be created from the schema configuration object (using
// noteSchemaConfig from above).
var model = AFrame.Model.create( {
schema: noteSchemaConfig,
data: {
id: '1',
title: 'Get some milk',
contents: 'Go to the supermarket and grab some milk.',
date: '2010-12-10T18:09Z',
edit_date: '2010-12-10T18:23Z'
extra_field: 'this field does not get through'
}
} );
/**
* Here:
* model.id is the integer 1
* model.date is a Javascript Date
* model.edit_date is a Javascript Date
* extra_field does not exist
*/
// update a field. prevVal will be 'Get some milk'
var prevVal = model.set( 'title', 'Get some milk and eggs' );
// This is setting the date in error, the prevVal will have a FieldValidityState
// with its typeMismatch field set to true. This will NOT actually set the value.
prevVal = model.set( 'edit_date', '1' );
// Check the overall model for validity. Returns true if all valid, an object of
// of FieldValidityStates otherwise
var isValid = model.checkValidity();
↑Collections
AFrame provides two collections, CollectionHash and CollectionArray.
Both collections are very similar to their native Javascript counterparts, but by using accessor functions to insert or delete data, it is possible to
use Observables to notify multiple views of changes to the collection. Any sort of data can be stored
in a collection, but all items inserted into the hash or array are given a CID. If an object is inserted that has a CID field, the object's CID will be used.
By decoupling CIDs from an object's id field, it allows for instances of inserting objects that do not yet have ids - this is often the case when creating a
new object that needs saved to a backend database, where the database assigns the object's id field.
↑Example of Using a CollectionHash
/* hash is set up as an AFrame.CollectionHash.
noteModel is the DataContainer from the above example. */
var cid = hash.insert( noteModel );
/* some other operations */
var note = hash.get( cid );
console.log( note.title ); // prints 'Get some milk'
/* some other operations */
this.hash.remove( cid );
View/Display Related Classes
↑A Basic Display
Displays are similar to Views, but unlike a traditional View, it is not limited to displaying data related
to a particular model. A Display is any class that relates to putting "stuff" on the screen. Currently, Displays depend on the
jQuery library, jQuery is used to do DOM manipulation. All Displays must have a target specified, a target is
considered that Display's root node.
↑Ultra Basic Display
<button id="submitForm">Submit</button>
---------
var buttonSelector = '#submitForm';
/* buttonSelector is a selector used to specify the root node of
the target. */
var button = AFrame.Display.create( {
target: buttonSelector
} );
/* When binding to a DOM event, must define the target, which
can be any jQuery element or selector. If a selector is given,
the target is looked for as a descendant of the display's
target. */
button.bindClick( $( buttonSelector ), function( event ) {
/* take care of the click, the event's default action is
already prevented. */
} );
/* Any DOM event can be bound to. */
button.bindDOMEvent( $( buttonSelector ), 'mouseenter', function( event ) {
// Do a button highlight or some other such thing.
} );
↑Using Fields
A Field is the basic display unit for a form. Fields can be used for
either input or output. If the browser is HTML5 compatible, the
HTML5
spec is followed with regards to validation. The current field state is returned using field.getValidityState() which returns
a FieldValidityState.
↑Example of Field Usage
<input type="number" id="numberInput" />
---------
var field = AFrame.Field.create( {
target: $( '#numberInput' )
} );
/* Set the value of the field, it is now displaying 3.1415 */
field.set(3.1415);
/* Check the validity of the field */
var isValid = field.checkValidity();
/* The field is cleared, displays nothing */
field.clear();
field.set('invalid set');
/* This will return false */
isValid = field.checkValidity();
/* Get the validity state, as per the HTML5 spec */
var validityState = field.getValidityState();
↑Using Forms
A Form is a composite of Fields. A generic form is
not bound to any data, it is only a collection of form fields. A field factory function must
be specified in the form configuration, this allows for complete control over the construction of Fields.
On form initialization, the Form's descendents will be searched for elements with the "data-field"
attribute. Each element found will be passed to the field factory, the factory must return a
Field compatible object. A basic Form that is not bound to any particular
DataContainer or Model is useful primarily
to do input validation where data from the form is retreived and used through methods outside of the framework.
To bind a form to data, see the next section, Binding a Form to a DataContainer/Model.
↑Example of a Form Using the Default fieldFactory
<div id="nameForm">
<input type="text" data-field name="name" />
</div>
---------
/* Set up the form with the default field factory, form will look in #nameForm
* for elements with the "data-field" attribute. This will find one field
* in the above HTML.
*/
var form = AFrame.Form.create( {
target: $( '#nameForm' )
} );
// do some stuff, user enters data.
/* Check the validity of the form */
var isValid = form.checkValidity();
// do some other stuff.
form.clear();
↑Example of a Form Using a Specialized fieldFactory
/* Sets up a form with a specialty field factory */
var formWithSpecialtyField = AFrame.Form.create( {
target: $( '#nameForm' ),
formFieldFactory: function( element ) {
return AFrame.SpecializedField.create( {
target: element
} );
}
} );
↑Binding a Form to a DataContainer or Model
Basic forms are very useful in themselves, but frequently a form is bound to a particular set of data.
By binding a DataForm to a DataContainer,
the DataContainer is used as both the source and the receiver of information. Form fields will automatically have their values populated
with the values contained in the DataContainer. The user is then free to update the form as they please, but data within the DataContainer
is only updated when the DataForm's save function is called and the form successfully validates. This ensures that the user cannot pollute the data
within the DataContainer with invalid entries.
<div id="nameForm">
<input type="text" data-field name="name" />
<input type="text" data-field name="version" />
</div>
---------
/* Note that we do not use new when getting a DataContainer for an object.
* This is so that if an object already has a DataContainer associated with it,
* the original DataContainer will be returned.
*/
var libraryDataContainer = AFrame.DataContainer( {
name: 'AFrame',
version: '0.0.20'
} );
/* Set up the form to look under #nameForm for elements with the "data-field"
attribute. This will find two fields, each field will be tied to the
appropriate field in the libraryDataContainer */
var form = AFrame.DataForm.create( {
target: $( '#nameForm' ),
dataSource: libraryDataContainer
} );
/* do some stuff, user updates the fields with the library name and version
number. Note, throughout this period the libraryDataContainer is never
updated. */
/* Check the validity of the form, if we are valid, save the data back to
the dataContainer */
var isValid = form.checkValidity();
if( isValid ) {
/* if the form is valid, the input is saved back to
the libraryDataContainer */
form.save();
}
↑Displaying a List
A List is a way of displaying lists of data (circular definition). A List shares
the majority of its interface with a CollectionArray since lists are inherently
ordered (even if they are ULs). A list can be added to by calling insertRow with a
DOM element to use for the list item, or insert with a data object. If insert is used,
listElementFactory must be given in the configuration. The callback will be passed an index and the data being inserted and must
return an element which can be inserted into the list. The callback can create an element using a templating mechanism or by creating an
element itself.
↑Example of Using a List
<ul id="clientList">
</ul>
---------
var factory = function( data, index ) {
var listItem = $( '<li>' + data.name + ', ' + data.employer + '</li>' );
return listItem;
};
var list = AFrame.List.create( {
target: '#clientList',
listElementFactory: factory
} );
/* Creates a list item using the factory function at the end of the list */
list.insert( {
name: 'Shane Tomlinson',
employer: 'AFrame Foundary'
} );
/* Inserts a pre-made list item at the head of the list */
list.insertRow( $( '<li>Joe Smith, the Coffee Shop</li>' ), 0 );
---------
<ul id="clientList">
<li>Joe Smith, The Coffee Shop</li>
<li>Shane Tomlinson, AFrame Foundary</li>
</ul>
↑Binding a List to a Collection
Lists are useful by themselves but become much more powerful when paired with a Collection of some sort. Instead
of adding and removing from the list directly, it is usually preferable in MVC systems to have Views react to changes in the Models. If the List is
thought of as a view of a Collection model, any update to the Collection should be reflected in the List. This means whenever
data is added, removed, or modified within the Collection, the List should automatically updated. The
ListPluginBindToCollection is the first step in this. ListPluginBindToCollection
is a plugin on a list that is associated with a Collection. Any time an item is added or removed from the Collection, the plugin automatically updates the list.
↑Using a ListPluginBindToCollection
<ul id="clientList">
</ul>
---------
/* A List with the same results as the previous example is
the expected result */
/* First we need to set up the collection */
var collection = AFrame.CollectionArray.create();
var factory = function( data, index ) {
var listItem = $( '<li>' + data.name + ', ' + data.employer + '</li>' );
return listItem;
};
/* Sets up our list with the ListPluginBindToCollection. Notice the
ListPluginBindToCollection has a collection config parameter.
*/
var list = AFrame.List.create( {
target: '#clientList',
listElementFactory: factory,
plugins: [ [
AFrame.ListPluginBindToCollection, {
collection: collection
} ] ]
} );
collection.insert( {
name: 'Shane Tomlinson',
employer: 'AFrame Foundary'
} );
collection.insert( {
name: 'Joe Smith',
employer: 'The Coffee Shop'
}, 0 );
/* The same list as in the example above is shown */
---------
<ul id="clientList">
<li>Joe Smith, The Coffee Shop</li>
<li>Shane Tomlinson, AFrame Foundary</li>
</ul>
----------
collection.remove( 0 );
/* Joe Smith has been removed */
---------
<ul id="clientList">
<li>Shane Tomlinson, AFrame Foundary</li>
</ul>
↑Creating Forms in Lists Using ListPluginFormRow
Now there are Lists, and Forms, and Fields, but what about putting them all together? This is where a
ListPluginFormRow comes in. A ListPluginFormRow allows
the creation of a Form for each row in the List. To keep coupling low and flexibility high, the ListPluginFormRow
does not create the Forms itself, but relies on a formFactory function that is passed in as configuration. Whenever a row is added to the list, the
formFactory will be passed the row's root element as well as the row's data. The formFactory must then return an AFrame.Form compatible object.
The ListPluginFormRow adds extra functions to the base List object, these functions are validate, save, clear, and reset.
↑Example of a ListPluginFormRow tied with ListPluginCollection
<ul id="clientList">
</ul>
---------
/* the row element factory, can easily use a templating
* mechanism instead of direct creation */
var rowElementFactory = function( data, index ) {
var listItem = $( '<li><input type="text" data-field name="name" />,' +
'<input type="text" data-field name="employer" /></li>' );
return listItem;
};
/* Set up a collection to add data to */
var collection = AFrame.CollectionArray.create();
/* Sets up our list with the ListPluginBindToCollection. Notice the
ListPluginBindToCollection has a collection config parameter.
*/
var list = AFrame.List.create( {
target: '#clientList',
listElementFactory: rowElementFactory,
plugins: [ [ AFrame.ListPluginBindToCollection, {
collection: collection
} ], AFrame.ListPluginFormRow ]
} );
collection.insert( {
name: 'Shane Tomlinson',
employer: 'AFrame Foundary'
} );
--------------
/* At this point, the list contains one row with two form elements
that can be edited */
<ul id="clientList">
<li><input type="text" data-field name="name" value="Shane Tomlinson" />,<input type="text" data-field name="employer" value="AFrame Foundary" /></li>
</ul>
--------------
/* I, as the user, modify the employer name to AFrameJS,
no data in the collection has yet changed, but I
want to save the data back to the collection */
if( list.validate() ) {
list.save(); /* Data is now updated in the collection */
}