Skip to content

AtomicJS Documentation

Tyree Jackson edited this page Jul 5, 2017 · 16 revisions
<style>.indent { padding-left: 5em; }</style>

Introduction

AtomicJS is designed to support building applications using the Model-View-View Adapter-Controller (MVVAC) design pattern. Applications are built on AtomicJS by creating controllers and view adapters in JavaScript, and models in JSON. The default view adapter engine provided with AtomicJS will bind view adapters to views created using plain HTML5. A unit testing adapter engine will be provided at some future date. AtomicJS also provides a set of observer classes which provide wrappers that can be setup around models so that changes to the model can be observed and reacted to. This is combined with the view adapter engine to provide 2 way data binding across view adapter controls. All dependencies in AtomicJS are injected via constructor functions. Concerns are well separated and thanks to the View-View Adapter relationship, mixing of logic and structure is avoided. There is no need to add expressions, conditionals or other logic into the views (HTML). The result is a principled but simple approach to building applications.

Concepts

The following concepts are promoted/utilized when building an application using AtomicJS.

Model - View - View Adapter - Controller (MVVAC)

MVVAC differs from classic MVC only in that views are abstracted away from consumers by way of the view adapter. This view adapter is simply the adapter pattern applied between the view and it's consumers. By using this pattern, JavaScript developers need not target HTML directly in their code. This allows for a separation of roles between user experience design and development. While this separation is supported, it is not required that the roles be fulfilled by different individuals. This also allows for the views to be injected and potentially replaced with mocks or other implementations as needed. View adapters are built from view adapter definitions written in JavaScript by application developers. These definitions are processed and assembled into view adapter instances by the AtomicJS view adapter engine. A set of adapter attachments are injected into the AtomicJS engine that target and connect the view adapters to the technology that the views are built in. By default, AtomicJS provides adapter attachments for supporting HTML 5 based views. Adapter attachments for supporting unit testing are expected to be added at a later date. By isolating the integration of the view adapters with the underlying views to be a function of the AtomicJS library, application developers can focus more on their application logic and less on JS/HTML binding logic.

View Adapter Definitions and Controls

A view adapter definition defines the structure of the view that it is expected to be bound to. A control is an instance of another view adapter that serves as a component of the parent view adapter and that is either defined as a part of the parent view adapter structure or as an independent external view adapter itself that is to be "docked" to the parent. A view adapter definition consists of the following properties:

  • controls - child view adapter definitions embedded within the parent view adapter definition; each control definition may also have one or more initializer properties;
  • events - list of events defined by the view adapter
  • members - list of methods defined by the view adapter

Each component control of a view adapter is listed by key under the controls structure. Controls themselves may contain nested controls under their own subset definition structure. Some controls may be defined as a repeater type control, in which case they will contain a repeat structure in place of a controls structure.

Events are a simple array containing a list of event names to be defined by the view adapter. Consumers of the view adapter may listen to these events by accessing the event name as a key of the on property of the view adapter instance.

Members is a hash map that contain functions by key that are to be grafted onto the view adapter instances created from the adapter definition. These functions are bound to the view adapter instance which is accessible via the this keyword.

Note that the instance of the root view adapter defined within a view adapter definition class is accessible via a scoped parameter defined in the class' function signature (generally named viewAdapter, controlViewAdapter or appViewAdapter by recommended convention).

Service proxying

Typically, applications built on AtomicJS will be web applications that need to consume one or more back-end services. In order to support construction of web applications that are fully separated from those back-end services, the construction of service proxies and mock service proxies are encouraged in order to allow for front-end design and development to proceed prior to any back-end development. This allows for shorter sprints when working on the web application. The objects and other parameter arguments and return values that are exchanged between the controller and the proxies can be tested and approved prior to starting design on the server side components. Additionally, the definitions of the objects involved in those exchanges provide the blue prints for the back-end services and their contracts.

2-Way Data Binding

The view adapter engine supports binding models to view adapters by way of an intermediary known as an observer. Models are wrapped by an observer and then the observer instance is "bound" to the view adapters in a hierarchal fashion. When changes are sent through the observer to the model, the observer notifies bound listeners of the changes. During the binding, the observer monitors which properties the listener is getting values from and binds the listeners to each observed property. This check is re-performed each time a listener is updated due to a change in a property value.

If the view adapter is a container, when an observer is bound, the view adapter binds to the observed model and forwards the binding to all of it's child controls. If the view adapter is a repeater, when an observer is bound, the view adapter binds to and iterates through the observed array and creates a cloned instance of it's item control template for each item in the array and binds the clone to the item. If the view adapter is an input type control, when an observer is bound, the view adapter 2 way binds itself to the observed model.

You can access a value from the observed model using standard object notation. If the value located at a particular path is either an object or an array, then the value returned will be wrapped in an observer. To access the natural value contained within the observer, simply call the return observer with no path specified. The following calls on an observed model are all valid examples:

function(observer)
{
    var model   =
    new observer
    ({
        someInt:    1,
        someObject:
        {
            someArray:
            [
                {
                    someOtherInt: 2
                }
            ]
        }
    });

    // returns an integer value 1
    var value   = model("someInt");

    // returns an integer value 2
    value       = model("someObject.someArray[0].someOtherInt");

    // returns an observed object { someOtherInt: 2 }
    value       = model("someObject.someArray[0]");

    // returns an object { someOtherInt: 2}
    value       = model("someObject.someArray[0]")();

    // returns an observed array [{ someOtherInt: 2 }]
    value       = model("someObject.someArray");

    // returns an array [{ someOtherInt: 2 }]
    value       = model("someObject.someArray")();

    // returns an observed object { someArray: [{ someOtherInt: 2 }] }
    value       = model("someObject");

    // returns an object { someArray: [{ someOtherInt: 2 }] }
    value       = model("someObject")();

    // returns an object { someInt: 1, someObject: { someArray: [{ someOtherInt: 2 }] } }
    value       = model();
}

View Adapter Definition Structure

A view adapter definition is defined as a JavaScript object containing one or more of three root properties. Those root properties are controls, events and members. Additionally, one or more initializers may be defined as root properties on inline child control definitions under the controls property.

Note that initializers cannot be defined for the root of a view adapter definition. Initializers are intended to be defined in parent definitions for child controls where they are declared regardless of whether or not the child control structure is fully defined inline or defined in a separate view adapter definition (via either the adapter or factory option properties). Any initialization the developer wants to include for the root of the view adapter definition can be performed inside of the construct method within the members structure.

Example:

!function()
{"use strict";root.define("acme.exampleViewAdapter",
function(reusableControlAdapter, anotherReusableControlFactory)    // <-- dependencies are injected here
{return function exampleViewAdapter(viewAdapter)                   // <-- viewAdapter is a reference to the root view adapter instance assembled from the adapterDefinition returned by this class
{
    var adapterDefinition   =
    {
        controls:
        {
            exampleInlineChildControl:
            {
                controls:   {},
                events:     [],
                members:    {},
                onclick:    function() { alert("The exampleInlineChildControl was clicked"); }
            },
            exampleAdapterChildControl:
            {
                adapter:    reusableControlAdapter
            },
            exampleChildControlFromFactory:
            {
                factory:    anotherReusableControlFactory
            }
        },
        events: ["customEvent1", "customEvent2"],
        members:
        {
            construct:
            function()
            {
                this.addEventListener("click", function(event){ alert("This view adapter was clicked."); });
            },
            customMethod1:
            function(parameter1)
            {
                alert("The customMethod1 method of the view adapter was called with " + parameter1);
            }
        }
    }

    return adapterDefinition;
}});}();

controls

Defined as a JavaScript object containing named controls that are defined as components of the view adapter. Each control may be defined inline to the parent view adapter definition or their definition may be obtained by AtomicJS by supplying an appropriate object on either an adapter or a factory property of the control's JavaScript object structure.

events

Defined as a JavaScript array object containing a list of event names exposed by the view adapter. Each named event is added by AtomicJS to the on property of the assembled view adapter as a pub/sub instance. Consumers may register to listen to each event by calling the listen method of each. Consumers may also stop listening to each event by calling the corresponding ignore method of each.

Example:

function eventHandler(parameter1)
{
}

// start listening to customEvent1 of the exampleViewAdapter
exampleViewAdapter.on.customEvent1.listen(eventHandler);

// stop listening to customEvent1 of the exampleViewAdapter
exampleViewAdapter.on.customEvent1.ignore(eventHandler);

members

Defined as a JavaScript object containing custom functions to graft onto the assembled view adapter instance. Functions defined here will be bound to the view adapter which can be accessed via the this keyword.

Note, if one of the custom functions is named construct then this function will be executed and removed from the view adapter after it has been fully assembled but before it is returned to its consumer.

Inline Child Control Definition Structure - (repeater controls)

A child control defined inline within a View Adapter Definition can be defined with the same properties controls, events and members as the root View Adapter Definition. In addition, a special type of repeater control may be defined with a repeat property instead of a controls property.

repeat

Defined as an alternative JavaScript object to a controls object, containing one or more templates that are to be repeated within the repeater control for each item in a list bound to the repeater control. Each template declaration is any valid child control declaration and may be a simple field, a container child control declared with the standard controls, events and members structure properties like any other container control, or another repeater control containing another repeat declaration. Additionally, each template declaration is required to provide a getKey property defining a function that accepts an observer object of an item that the clone will be bound to and returns a unique key to be used for the id of the cloned template control. Also, each template may optionally define a skipItem property defining a function that accepts an observer object of an item that the clone will be bound to an returns a boolean indicating whether or not to skip the cloning the template for that item.

Example:

exampleList:
{
    repeat:
    {
        exampleListTemplate:
        {
            getKey:     function(item){ return "exampleItem-"+item().id; },
            skipItem:   function(item){ return item().isDeleted; }
            controls:
            {
                exampleLabel:   { bind: "label" },
                exampleValue:   { bind: "value" }
            }
        }
    },
    bind:   "listOfExampleItems"
}

getKey - function(item){}

This property is required on repeater item template declarations and must define a function that accepts an observer wrapped item that the clone is to be bound to and returns a unique key to be used for the id of the cloned template control for the item specified.

skipItem - function(item){}

This property is optional on repeater item template declarations. When present, it should define a function that accepts an observer wrapped item that the clone is to be bound to and returns a boolean indicating whether or not to skip cloning the template for the item specified.

Initializers - Properties

The following list of initializers are properties that can be defined with in a control portion of a view adapter definition. Defining any of these properties will initialize the corresponding property on its control instance within the view adapter instance.

hidden - boolean

Setting this property to true will hide the control on the view adapter when it is constructed.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        exampleControl: { hidden: true }
    }
};

isDataRoot - boolean

Setting this property indicates that it expects to have an observed model directly bound to it instead of participating in the data binding of a parent view adapter. Normally, when an observed model has been bound to a view adapter, if the view adapter is a container or repeater, then the binding is shared with all of the view adapter's child controls. Set this property to true when you expect a specific control to receive its own observed model to bind to.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        exampleControl: { isDataRoot: true }
    }
};

bind - multiple options

There are multiple options that can be specified for the bind initializer. They are as follows:

bind - string

If a string is provided for the bind initializer, then it specifies which property to read and write to on the observed model for the value of this control. As the value of the property changes in the model, the value on this control is also updated. Likewise, as the value on this control changes, the value of this property in the model is also updated.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        exampleControl: { bind: "exampleProperty" }
    }
};

bind - function(item){}

If specified, a function defined on this property will be called with an observed model whenever one has been bound to the control. The item parameter will contain an observer which can be used to return a custom formatted value to use as the bound value for the control. Any property paths accessed in the item will be monitored by the observer and any changes to the values in the observed model of those paths will cause this function to be re-executed.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        // This function will be called whenever the firstName or lastName property changes in the model
        nameLabel: { bind: function(item) { return item("firstName") + " " + item("lastName"); } }
    }
};

bind - object {}

There are multiple options that can be specified for the object based bind initializer. They are as follows:

items - multiple options

There are multiple options that can be specified for the items property of the object based bind initializer. They are as follows:

items - string

This property specifies which property of the observed model that the control should obtain its source data from. Typically this option is used when the property of the observed model contains an array of primitive values.

Example:

var observedModel           = 
new observer
({
    lookupData: ['item1', 'item2', 'item3']
});

var viewAdapterDefinition   =
{
    controls:
    {
        lookupList: { bind: { items: "lookupData" } }
    }
};
items - object

This property contains several properties that specify how to bind a control option list to an observed model. Typically this option is used when the property of the observed model contains an array of object values.

to - string

This property specifies which property of the observed model that the control should obtain its source data from.

text - string

This property specifies which property of each object in the source data array should be bound to obtain the friendly display value of the object.

value - string

This property specifies which property of each object in the source data array should be bound to obtain the raw value of the object.

Example:

var observedModel           =
new observer
({
    lookupData: 
    [
        { id: 1, description: 'item1' },
        { id: 2, description: 'item2' },
        { id: 3, description: 'item3' }
    ]
});

var viewAdapterDefinition   =
{
    controls:
    {
        lookupList:
        {
            bind: { items: { to: "lookupData", text: "description", value: "id" } }
        }
    }
};
value - multiple options

There are multiple options that can be specified for the value property of the object based bind initializer. They are as follows:

value - string

If a string is provided for the value initializer, then it specifies which property to read and write to on the observed model for the value of this control. As the value of the property changes in the model, the value on this control is also updated. Likewise, as the value on this control changes, the value of this property in the model is also updated.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        exampleControl: { bind: { value: "exampleProperty" } }
    }
};
value - function(item){}

If specified, a function defined on this property will be called with an observed model whenever one has been bound to the control. The item parameter will contain an observer which can be used to return a custom formatted value to use as the bound value for the control. Any property paths accessed in the item will be monitored by the observer and any changes to the values in the observed model of those paths will cause this function to be re-executed.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        // This function will be called whenever the firstName or lastName property changes in the model
        nameLabel: { bind: { value: function(item) { return item("firstName") + " " + item("lastName"); } } }
    }
};
value - object

This property contains several properties that specify how to bind a control value to an observed model. Typically this option is used when the property of the observed model contains an array of object values.

to - multiple options

There are multiple options that can be specified for the value property of the object based bind initializer. They are as follows:

to - string

If a string is provided for the value initializer, then it specifies which property to read and write to on the observed model for the value of this control. As the value of the property changes in the model, the value on this control is also updated. Likewise, as the value on this control changes, the value of this property in the model is also updated.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        exampleControl: { bind: { value: { to: "exampleProperty" } } }
    }
};
to - function(item){}

If specified, a function defined on this property will be called with an observed model whenever one has been bound to the control. The item parameter will contain an observer which can be used to return a custom formatted value to use as the bound value for the control. Any property paths accessed in the item will be monitored by the observer and any changes to the values in the observed model of those paths will cause this function to be re-executed.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        // This function will be called whenever the firstName or lastName property changes in the model
        nameLabel: { bind: { value: { to: function(item) { return item("firstName") + " " + item("lastName"); } } } }
    }
};
root - string

This initializer property specifies a property on the observed model to react to not just the direct property itself but also to changes to the values of any nested properties of the property.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        exampleControl: { bind: { value: { to: function(item){return new Date(); }, root: "sampleData" } } }
    }
};
updateon - array [string]

By default, when a input type control is bound to an observed model, the control listens to the change event of the control on the view. When an array of strings is specified on this property, then the control listens to the events named in the values of this array instead of the change event.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        exampleControl:
        {
            bind:
            {
                value:
                {
                    to:         "exampleProperty",
                    updateon:   ["keyup", "keydown", "change"]
                }
            }
        }
    }
};
onupdate - function (){} requires to property to not be specified

Any function specified to this property will be called whenever the parent control's bound property of the observed model is changed.

var viewAdapterDefinition   =
{
    controls:
    {
        container:
        {
            bind:       "sampleData",
            controls:
            {
                exampleControl:
                {
                    bind:
                    {
                        value:
                        {
                            // do something when the sampleData property of the observed model is updated
                            onupdate: function(item){  }
                        }
                    }
                }
            }
        }
    }
};

Initializers - Special Events

These special events currently can only be assigned from the control definition. A function can be supplied for each initializer in this category.

onbind - function(value){}

This event is fired whenever a new observed model has been bound to the control. The value parameter argument will contain the property of the observed model specified by the bindTo initializer property if specified, or the observed model itself.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        exampleControl:
        {
            onbind: function(value){ this.href(value().linkTo); }
        }
    }
};

onboundedupdate - function(value){}

This event is fired whenever one of the properties being observed by the control of the bound observed model has been updated. The value parameter argument will contain the property of the observed model specified by the bindTo initializer property if specified, or the observed model itself.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        exampleControl:
        {
            onboundedupdate: function(value){ this.parent.toggleClass("displayExpandedData", value().containsExpandedData); }
        }
    }
};

onunbind - function(){}

This event is fired whenever the bound observed model has been unbounded from the control.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        exampleControl:
        {
            onunbind: function(){ this.removeClass("bounded"); }
        }
    }
};

Initializers - Regular Events

A function can be supplied for each initializer in this category. The function will be registered as a listener to the corresponding event on the control. Functions can also be added programmatically via JavaScript by calling the addEventListener method on the control view adapter with the event name minus the on prefix. The following example demonstrates two ways to listen to a control's click event.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        clickMeButton:
        {
            onclick:
            function(event)
            {
                alert("I was clicked");
            }
        }
    },
    members:
    {
        construct:
        function()
        {
            this.controls.clickMeButton.addEventListener
            (
                "click",
                function(event)
                {
                    alert("I was clicked");
                }
            );
        }
    }
};

onenter - function(event){}

This event is fired whenever a user presses the enter key in an input type control.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        exampleControl:
        {
            onenter: function(event){ viewAdapter.on.saveData(); }
        }
    }
};

onescape - function(event){}

This event is fired whenever a user presses the escape key in an input type control.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        exampleControl:
        {
            onescape: function(event){ viewAdapter.on.cancelEdit(); }
        }
    }
};

onblur - function(event){}

This event is fired whenever a control loses focus.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        exampleControl:
        {
            onblur: function(event){ alert("I lost focus!"); }
        }
    }
};

onchange - function(event){}

This event is fired whenever the value of the control has changed.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        exampleControl:
        {
            onchange: function(event){ alert("My value changed to " + this.value()); }
        }
    }
};

onclick

This event is fired whenever a user clicks on the control.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        exampleControl:
        {
            onclick: function(event){ alert("I was clicked!"); }
        }
    }
};

oncontextmenu

This event is fired whenever a user right-clicks on the control.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        exampleControl:
        {
            oncontextmenu: function(event){ alert("I was right-clicked!"); }
        }
    }
};

oncopy

This event is fired whenever the user copies the content of a control or the control itself.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        exampleControl:
        {
            oncopy: function(event){ alert("'" + this.value() + "' was copied to the clipboard."); }
        }
    }
};

oncut

This event is fired whenever the user cuts the content of a control or the control itself.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        exampleControl:
        {
            oncopy: function(event){ alert("My content was cut to the clipboard."); }
        }
    }
};

ondblclick

ondrag

ondrageend

ondragenter

ondragleave

ondragover

ondragstart

ondrop

onfocus

onfocusin

onfocusout

oninput

onkeydown

onkeypress

onkeyup

onmousedown

onmouseenter

onmouseleave

onmousemove

onmouseover

onmouseout

onmouseup

onpaste

onsearch

onselect

onshow

ontouchcancel

ontouchend

ontouchmove

ontouchstart

onwheel

Assembled View Adapter Structure

Methods

Methods listed here are defined directly on the assembled view adapters prior to grafting any custom members on the adapter.

addClass(className)

This method adds the argument specified for the className parameter to the list of classes assigned to the control if it is not already present in the list.

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        todoLabel:
        {
            ondblclick: function(event){ this.parent.addClass("editing"); }
        }
    }
};

addClassFor(className, milliseconds)

Example:

var viewAdapterDefinition   =
{
    controls:
    {
        todoLabel:
        {
            onboundupdate: function(event){ this.parent.addClassFor("display", 2000); }
        }
    }
};

addEventListener(eventName, listener, withCapture, notifyEarly)

appendControl(childControl)

attribute(attributeName, [value])

bindSourceData(sourceData)

bindData(data)

bindTo([value])

bindSource([value])

bindSourceValue([value])

bindSourceText([value])

blur()

click()

focus()

hasClass(className)

href([value])

hide()

hideFor(milliseconds)

removeClass(className)

removeClassFor(className, milliseconds)

removeControl(childControl)

removeEventListener(eventName, listener, withCapture)

show()

showFor(milliseconds)

scrollIntoView()

toggleClass(className, [condition])

toggleEdit([condition])

toggleDisplay([condition])

unbindData()

unbindSourceData()

value([value])

Methods [input types only]

select()

Clone this wiki locally