3.0 beta 2

Properties

ActiveWidgets objects represent the application view model and object properties encapsulate the view state. Properties can be calculated (i.e. hold a function instead of a static value) and depend on other properties. A calculated property usually holds a compiled template function and represents an HTML fragment, which together form the component markup. When modified, properties trigger change events and allow automatic granular updates of the generated HTML (reactive templates).

Properties may hold other objects and collections. All together they make a component tree, which represents your application UI. Changes in the component tree properties and composition are automatically reflected in the generated markup and re-rendered by the browser.

In order to simplify asynchronous loading and processing, all properties can accept a deferred/promise instead of an actual value. You don't have to write code to extract the value and handle error conditions - all that is done internally by the property accessor method. The actual assignment happens when the value becomes available, and the screen is automatically updated.

Getters and setters

ActiveWidgets implements properties using a single accessor method (like jQuery) instead of separate getter and setter methods.

// assign a string to the 'text' property
button.text('Hello!'); 

// retrieve value of a property 'text'
var s = button.text();

Called with no arguments the accessor method returns property value (getter). Calling with one argument invokes property assignment (setter). Property accessor methods are chainable when used as setters (return this).

button
    .text('ok')
    .width(100)
    .height(30);

Instead of calling property accessor method directly, you can use property name with get() and set() methods.

button.set('text', 'Hello!'); 

var s = button.get('text');

You can also use config object with the set() method to assign multiple values in one call.

button.set({
    text: 'ok',
    width: 100,
    height: 30
});

Calling set(config) is equivalent to calling set(key, value) for each key-value pair in the config object.

Creating properties

Calling set() creates a new property if a property with the requested name does not exist on the given object or its prototype chain. After the property accessor method is generated you can start calling it directly or continue using get()/set().

var obj = new AX.Object();

// create a new property and assign initial value
obj.set('myProperty', 123);

// now myProperty() method exists and returns 123
var x = obj.myProperty();

The right place to create new properties is a setup function used to initialize the prototype of a new class. This way the accessor methods will be generated once and available through the prototype, which is more efficient than doing it for each object instance.

// define a new class
var MyClass = new SuperClass.extend(function(){

    // create new properties
    this.set({
        property1: 123,
        property2: 'abc'
    });
});

var myObject = new MyClass({
    property2: 'def', // correct, property2 already defined
    property3: 'xyz'  // also works, but less efficient, creates property3 on the fly
});

When you call an object constructor with a config object - the constructor invokes set() method, which will silently create missing properties.

// create an object with a new property
var obj = new AX.Object({
    myProperty: 123
});

Reacting to property changes

Assigning a new value to the property triggers change event if the new value is not equal to the current one. The triggered event name will be the same as the name of the property.

// create an object with a new property
var obj = new AX.Object({
    myProperty: 123
});

// attach an event handler
obj.on('myProperty', function(){
    alert(this.myProperty());
});

// assignment triggers 'myProperty' event
obj.myProperty(456);

Processing values before assignment

Often it is necessary to transform or process the value before assigning it to some property. For example, assigning a template string to the component html property will get it automatically parsed and compiled into the template function.

var obj = new AX.Component({
    message: 'Hello!',
    html: '<b>{{@message}}</b>' // compiled into template function using AX.template()
});

Or assigning an i18n key to the button text property will get it replaced with the corresponding text in the selected language.

var button = new AX.Button({
    text: 'app.captions.button1' // replaced with the text in user's language via AX.i18n()
});

This kind of transformations is achieved with converter function, which is an optional third argument when defining a new property via set() method.

// re-define 'height' property using the converter function,
// called before each assignment to expand the assigned value

button.set('height', 0, function(value){

    switch(value) {
        case 'small': return '20px';
        case 'medium': return '30px';
        case 'large': return '40px';
    };

    // return the same value if no transformation applied
    return value; 
});

// before assignment 'small' will be replaced with '20px'
button.height('small');

Handling deferreds/promises

If the result of an asynchronous operation is returned via deferred/promise, you can assign it directly to the property of an ActiveWidgets component. The property accessor detects the presence of the then method and attaches success and failure callbacks to extract the result and handle error conditions automatically.

// load html content via jQuery AJAX
var content = $.get('pages/panel1.html');

var panel = new AX.Panel({
    id: 'panel1',
    html: content  // assign the asynchronous result directly into the 'html' property
});

The same approach works for the data/template combination.

// load JSON data and html template (asynchronously)
var product = $.getJSON('services/product', params),
    template = $.get('templates/product.html');

var panel = new AX.Panel({
    id: 'panel1',
    data: product,  // deferred JSON data
    html: template  // deferred html template
});

In this example the panel initially will be rendered empty. After the results of the asynchronous calls become available the panel will automatically repaint itself with the new content.

Calculated properties

It is possible to assign a function to a property. In this case the property value will be calculated dynamically based on other properties and/or bound data.

obj.set('state', function(){
    if (this.selected()){return 'selected'}
    if (this.disabled()){return 'disabled'}
    if (this.pressed()){return 'pressed'}
    if (this.hover()){return 'hover'}
    return '';
});

Most often the calculated property function is a compiled html template.

var tpl = AX.template('{{@name}}: {{@description}}');

var obj = new AX.Component({
    name: 'Javascript',
    description: 'Programming language',
    html: tpl
});

Those properties are defined with AX.template as converter function, so you don't have to explicitly compile the template before assigning to html, innerHTML, outerHTML.

var obj = new AX.Component({
    html: '{{@name}}: {{@description}}' // compiled automatically by AX.template() converter
});