Skip to content
xzer edited this page Oct 28, 2015 · 4 revisions

bootstrap

To start a Asta4js page, we need invoke the init method of a global object reference "Aj". Additionally, calling init at document ready is recommended.

$(function(){
  Aj.init(function($scope){
    ...
  });
});

All the real logic should be placed inside the passed callback function in which we can get a scope reference which affords developers two abilities:

  1. Attach a observable variable reference

    Aj.init(function($scope){
      $scope.data = {};
    });

    In the above, we declared a reference "data" which can be observed.

  2. Attach monitor handler on the observable references of current scope. There are two type of monitor handler, snippet binding handler or observing handler. The details will be explained later, a simple sample as following shows how we bind the $scope.data to a DOM element by css selector:

    $scope.snippet("body")
          .bind($scope.data, "#value-preview");

snippet binding

As shown at above example, we need to declare a snippet which can be used to bind data to DOM. Commonly, a snippet represents a DOM node retrieved by the given css selector and all the declared DOM binding css selector will be limited under the current snippet node.

binding meta

As shown at above example, we call the bind method of snippet to declare the relation between javascript model and DOM element, the first argument of bind is the observe target variable reference, and the second argument is called as binding meta which exactly tells the framework how to do binding.

To show the complete image of binding meta, the following example is a bit of complicated, but you can find the simplified version later after this example.

To bind the $scope.data.value to the DOM element "input[name=input1]"

$scope.snippet("body")
      .bind($scope.data, {
        value: {
           // the binding target
          _selector: "[name=input1]",
          // how to render value to the target DOM
          _render: function(target, newValue, oldValue){
            target.val(newValue);
          },
          // respond the DOM value change to assign new value the $scope.data.value
          _register_dom_change : function(target, changeHandler, bindContext){
            target.keyup(function(){
              var v = $(this).val();
              changeHandler(v, bindContext);
            });
            return function(){
              changeHandler(target.val(), bindContext);
            };
          }
        },
      });

As the comments in the above, basically, we use "_selector" to declare the target DOM element and "_render" to implement the real rendering logic, if we want 2-way binding, "_register_dom_change" can be utilized to monitor the DOM value change, the returned function of "_register_dom_change" will be used to forcely assign current DOM's value to the corresponding model by framework(currently, the returned function will never be invoked, but it will be invoked in furture).

Notice that only the direct child properties of current scope can be bound directly. For the recursive properties under the binding target, recursive javascript object can be performed.

$scope.snippet("body")
      .bind($scope.data, {
        user: {
          name : {
            _selector : "#name",
          },
          age : {
            _selector : "#age"
          }
        },
      });

In above example, the "_render" and "_register_dom_change" are absent. Absent "_render" cause the default implementation of "target.text(newValue)" to be invoked when bound target value is changed. If "_register_dom_change" is absent, observing value change of DOM element will not be performed.

  • simplified meta

    The raw format of binding meta allows developers to customize arbitrary rendering/binding logic, but it is exactly not convenient, thus the simplified declaration can be used for most common cases.

    $scope.snippet("body")
          .bind($scope.data, {
            value: [
              Aj.form({name: "input1"}),
              "#data-preview"
            ]
          });

    Rather than a plain object, an array can be used to bind certain data in multiple ways. Aj.form() is a convenient api to help us declare 2-way binding for html form elements, details can be found at 2 Way Binding. Aslo a plain string causes the target value to be bound to the inner text of target element by treating the string as a css selector, of course, in 1-way, from model to DOM.

attribute operation

Not only the inner text or value can be bound to model, but aslo the DOM's attribution can be bound too. The raw format for binding attribution operation is as following:

$scope.snippet("body")
      .bind($scope.data, {
        borderSize:{
          _selector: "#data-preivew",
          _attr_op: "[border=]"
        }
      });

Obviousely, the attribution operation binding can only be 1-way, from model to DOM. The possible operations are as following:

  1. attribution value assign(by $.attr())

    [border=] : set the value of "border" of current DOM to the value of current model

  2. style property value assign

    [style:width=] : set value of "width" of current DOM's style to the value of current model

  3. css class switch

    [class:(blue|red)?] : switch to the matched class in the given list by the value of current model, if absent, all the class in the given list will be removed from the target DOM.

  4. css class on/off

    • [class:current?] : add/remove the given class from current DOM by treating the value of current model as boolean.
    • [class:not(current)?] : treat the bound model as opponent boolean value.
  5. property assign(by $.prop())

    • [checked?] : set the given property of current DOM by treating the value of current model as boolean.
    • [:not(checked)?]: treat the bound model as opponent boolean value.

    See the difference between $.attr() and $.prop()

  • simplified attribute operation meta

    As convenience, the _attr_op can be combined to _selector by "@>" separator.

    $scope.snippet("body")
          .bind($scope.data, {
            border: "#target-table@>[border=]",
            width: "#target-table@>[style:width=]",
            color: "#target-table@>[class:(blue|red)?]",
            height: "#target-table@>[class:current?]",
            checked: "#check-box@>[checked?]",
            enabled:  [
              "#check-box@>[:not(disabled)?]",
              "#check-box@>[class:not(hidden)?]"
            ]
          });

array binding

List data can be bound to the DOM element simply. The raw format for array binding is as following:

$scope.snippet("body")
      .bind($scope.data.list, {
        _duplicator: "li.row",
        _item : {
          name: "#name"
        }
      });

The "_duplicator" suggests which DOM node we should map our array's items to, which cause the target DOM node to be duplicated times as the array's length.

The "_item" demonstrates the meta of how the array's single item should be bound as a common binding meta.

value transforming

By default, what is passed to "_render" function is the original value of binding target, since in most cases we will use the default rendering implementation, we would want to have a chance to transform the rendering value, which can be performed by "_tranform" extension.

Futher more, we can do type conversion before call the passed change handler which is usually the default variable assignment implementation by framework when we use "_register_dom_change" to monitor DOM change, but since in most cases we will use the default "_register_dom_change" impelmentation of framework(see 2 Way Binding), "_transform" can also be used to convert the value before being assigned to binding target model.

$scope.snippet("body")
      .bind($scope.data, {
        value: {
          _selector: "[name=input1]",
          _render: function(target, newValue, oldValue){...},
          _register_dom_change : function(target, changeHandler, bindContext){...}
          _transform: {
            _get_value: function(v){
              return (v).toString(10);
            },
            _set_value: function(v){
              return parseInt(v, 10);
            }
          }
        },
      });

Notice that the "_get_value" means how we should get value from the javascript model and "_set_value" means how we should assign the value to the javascript model, it is all about the model/variable, not the DOM.

For 1-way binding cases, we do not need "_set_value", so the "_transform" can be simplified to a function which will be treated as "_get_value" by default.

$scope.snippet("body")
      .bind($scope.data, {
        age: {
          _selector: "#age",
          _transform: function(v){
            // we can become younger
            return v - 10;
          }
        },
      });

virtual property and watch

  • virtual property

    virtual property is a property that is not existing at the original binding model but can be bound as an exsiting property.

    $scope.snippet("body").bind($scope.data, {
      content : Aj.form(),
      "@errMsg": [
        {
          _virtual: true,
          _selector: "#errMsg"
        },
        {
          _virtual: true,
          _value_ref: "#sendBtn"
        }
      ]
    }).bind("#sendBtn", "click", function(){
      var errMsgRef = Aj.getValueRef(this);
      var errMsg = validate($scope.data);
      errMsgRef.setValue(errMsg);
      if(!errMsg){
        sendToServer($scope.data);
      }
    });

    A virtual property can be accessed by valur reference which can be rendered to an existing DOM and can be retrieved later by Aj.getValueRef(). Notice that even we set value to the "@errMsg" property, the assigned value will not be stored to the target model.

    The "@" mark in the name of virtual property is not necessary, but we can use some special charactors such as "@" mark to emphasize it is a not existing virtual property.

  • watch

    In some situation, we would want to monitor multiple viarables to calculate a rendering value, which can be performed by "_watch" extension which makes use of virtual property mechanism.

    $scope.snippet("body").bind($scope.data, {
      year : Aj.form(),
      month :Aj.form(),
      day : Aj.form(),
      "date-str": {
        _selector: ".x-date-str",
        _watch: {
          _fields: ["year", "month", "day", "@:data2.babe"],
          _cal: function(y, m, d, b){
            return y + "-" + m + "-" + d + ":" + b;
          }
        }
      }
    }).bind($scope.data2,{
      babe: Aj.form()
    });

    The "_watch" suggests that "date-str" is a virtual property under $scope.data.

    The "_fields" tells which fields we want to watch. Item of simple string means the watch target path is from the same level or deeper level of the current place where the virtual property is declared. A string starting with "@:" means that the watch target path should be started from the scope root which is the $scope in the example.

    The "_cal" defines a calculation function for how to calculate the rendering value of decalred virtual property.

    For normal variable observing without rendering/binding, Observe would be a better choice

meta merge

The declared binding meta can merge an extra object into itself by "_merge" declaration.

var someMeta = {
  content: "#content",
  list: {
    _item: {
       name: "#name"
    }
  }
}
$scope.snippet("body")
      .bind($scope.data, {
        _merge: someMeta,
        content: "#content-show",
        list: {
          _duplicator: "li",
          _item: {
            name: Aj.form(),
            age: Aj.form()
          }
        }
      });

The above sample shows how to merge a existing meta to the current binding meta and the following source is exactly equal to the above example:

$scope.snippet("body")
      .bind($scope.data, {
        content: ["#content-show", "#content"]
        list: {
          _duplicator: "li",
          _item: {
            name: [Aj.form(), "#name"]
            age: Aj.form()
          }
        }
      });

Utilizing meta merge can help developers to separate the binding meta logically better.

debug

The meta is called back by framework so that it is difficult to find out what is wrong when it does not works as expected. There are two options to help developers to combat with bugs.

  • _omit_target_not_found

    By default, if there is not target elements found by the given "_selector", Asta4js will output an error message to the console, this error message can be disabled by set "_omit_target_not_found" to true.

    $scope.snippet("body")
          .bind($scope.data, {
            age:{
              _selector: "#age",
              _omit_target_not_found: true
            }
          });

    If absent or set to false, the output should be something like following:

    could not find target of selector: #age Object {_selector: "#age", _meta_type: "_value", _target_path: "value", _meta_id: "aj-1-1432534770405", _meta_trace_id: "aj-18-1432534770405"…}
    
  • _debug

    Another helpful option is "_debug" which requires to be configured a debug id string used to idendify the output information. If "_debug" is specified, the related information

    $scope.snippet("body")
          .bind($scope.data, {
            age:{
              _selector: "#age",
              _debug: "debug age binding"
            }
          });

    The output would be something like following:

    debug info: age binding
    current meta: Object {_selector: "#age", _debug: "age binding", _meta_type: "_value", _target_path: "value", _meta_id: "aj-1-1432534770405"…} 
    (on change)calling args: [undefined, undefined, BindContext]