Fork me on GitHub

AFrameJS

- Javascript MVC Library

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 */
    }

To build your own copy of AFrame and docs

  1. Apache Ant is required. Go get it. Ant allows for concurrent development in both Unix/Linux and Windows flavored OSes.
  2. Python must be installed for the YUI document generator. Please see the YUI Doc homepage for more details
  3. A modified version of Carlo Zottmann's Dana-Theme is being used to generate the documents, this requires the Markdown.
  4. Download the AFrame source from GitHub or pull a copy using Git
  5. The sample per_user.properties.sample must be personalized and copied to per_user.properties
  6. run "ant all" to do a full build.

Build options

  1. "ant all" does a full build.
  2. "ant compress" concatinates and compresses javascript
  3. "ant docs" builds the docs
  4. "and jslint" runs a javascript linter to check for errors
  5. "ant clean" cleans up any messes

Other Info

I can be written at set117 at yahoo period com.