391 lines
14 KiB
JavaScript
391 lines
14 KiB
JavaScript
//>>built
|
|
define("dojox/mvc/_DataBindingMixin", [
|
|
"dojo/_base/lang",
|
|
"dojo/_base/array",
|
|
"dojo/_base/declare",
|
|
"dojo/Stateful",
|
|
"dijit/registry"
|
|
], function(lang, array, declare, Stateful, registry){
|
|
/*=====
|
|
registry = dijit.registry;
|
|
=====*/
|
|
|
|
return declare("dojox.mvc._DataBindingMixin", null, {
|
|
// summary:
|
|
// Provides the ability for dijits or custom view components to become
|
|
// data binding aware.
|
|
//
|
|
// description:
|
|
// Data binding awareness enables dijits or other view layer
|
|
// components to bind to locations within a client-side data model,
|
|
// which is commonly an instance of the dojox.mvc.StatefulModel class. A
|
|
// bind is a bi-directional update mechanism which is capable of
|
|
// synchronizing value changes between the bound dijit or other view
|
|
// component and the specified location within the data model, as well
|
|
// as changes to other properties such as "valid", "required",
|
|
// "readOnly" etc.
|
|
//
|
|
// The data binding is commonly specified declaratively via the "ref"
|
|
// property in the "data-dojo-props" attribute value.
|
|
//
|
|
// Consider the following simple example:
|
|
//
|
|
// | <script>
|
|
// | var model;
|
|
// | require(["dijit/StatefulModel", "dojo/parser"], function(StatefulModel, parser){
|
|
// | model = new StatefulModel({ data : {
|
|
// | hello : "Hello World"
|
|
// | }});
|
|
// | parser.parse();
|
|
// | });
|
|
// | </script>
|
|
// |
|
|
// | <input id="hello1" data-dojo-type="dijit.form.TextBox"
|
|
// | data-dojo-props="ref: model.hello"></input>
|
|
// |
|
|
// | <input id="hello2" data-dojo-type="dijit.form.TextBox"
|
|
// | data-dojo-props="ref: model.hello"></input>
|
|
//
|
|
// In the above example, both dijit.form.TextBox instances (with IDs
|
|
// "hello1" and "hello2" respectively) are bound to the same reference
|
|
// location in the data model i.e. "hello" via the "ref" expression
|
|
// "model.hello". Both will have an initial value of "Hello World".
|
|
// Thereafter, a change in the value of either of the two textboxes
|
|
// will cause an update of the value in the data model at location
|
|
// "hello" which will in turn cause a matching update of the value in
|
|
// the other textbox.
|
|
|
|
// ref: String||dojox.mvc.StatefulModel
|
|
// The value of the data binding expression passed declaratively by
|
|
// the developer. This usually references a location within an
|
|
// existing datamodel and may be a relative reference based on the
|
|
// parent / container data binding (dot-separated string).
|
|
ref: null,
|
|
|
|
/*=====
|
|
// binding: [readOnly] dojox.mvc.StatefulModel
|
|
// The read only value of the resolved data binding for this widget.
|
|
// This may be a result of resolving various relative refs along
|
|
// the parent axis.
|
|
binding: null,
|
|
=====*/
|
|
|
|
//////////////////////// PUBLIC METHODS ////////////////////////
|
|
|
|
isValid: function(){
|
|
// summary:
|
|
// Returns the validity of the data binding.
|
|
// returns:
|
|
// Boolean
|
|
// The validity associated with the data binding.
|
|
// description:
|
|
// This function is meant to provide an API bridge to the dijit API.
|
|
// Validity of data-bound dijits is a function of multiple concerns:
|
|
// - The validity of the value as ascertained by the data binding
|
|
// and constraints specified in the data model (usually semantic).
|
|
// - The validity of the value as ascertained by the widget itself
|
|
// based on widget constraints (usually syntactic).
|
|
// In order for dijits to function correctly in data-bound
|
|
// environments, it is imperative that their isValid() functions
|
|
// assess the model validity of the data binding via the
|
|
// this.inherited(arguments) hierarchy and declare any values
|
|
// failing the test as invalid.
|
|
return this.get("binding") ? this.get("binding").get("valid") : true;
|
|
},
|
|
|
|
//////////////////////// LIFECYCLE METHODS ////////////////////////
|
|
|
|
_dbstartup: function(){
|
|
// summary:
|
|
// Tie data binding initialization into the widget lifecycle, at
|
|
// widget startup.
|
|
// tags:
|
|
// private
|
|
if(this._databound){
|
|
return;
|
|
}
|
|
this._unwatchArray(this._viewWatchHandles);
|
|
// add 2 new view watches, active only after widget has started up
|
|
this._viewWatchHandles = [
|
|
// 1. data binding refs
|
|
this.watch("ref", function(name, old, current){
|
|
if(this._databound){
|
|
this._setupBinding();
|
|
}
|
|
}),
|
|
// 2. widget values
|
|
this.watch("value", function(name, old, current){
|
|
if(this._databound){
|
|
var binding = this.get("binding");
|
|
if(binding){
|
|
// dont set value if the valueOf current and old match.
|
|
if(!((current && old) && (old.valueOf() === current.valueOf()))){
|
|
binding.set("value", current);
|
|
}
|
|
}
|
|
}
|
|
})
|
|
];
|
|
this._beingBound = true;
|
|
this._setupBinding();
|
|
delete this._beingBound;
|
|
this._databound = true;
|
|
},
|
|
|
|
//////////////////////// PRIVATE METHODS ////////////////////////
|
|
|
|
_setupBinding: function(parentBinding){
|
|
// summary:
|
|
// Calculate and set the dojo.Stateful data binding for the
|
|
// associated dijit or custom view component.
|
|
// parentBinding:
|
|
// The binding of this widget/view component's data-bound parent,
|
|
// if available.
|
|
// description:
|
|
// The declarative data binding reference may be specified in two
|
|
// ways via markup:
|
|
// - For older style documents (non validating), controls may use
|
|
// the "ref" attribute to specify the data binding reference
|
|
// (String).
|
|
// - For validating documents using the new Dojo parser, controls
|
|
// may specify the data binding reference (String) as the "ref"
|
|
// property specified in the data-dojo-props attribute.
|
|
// Once the ref value is obtained using either of the above means,
|
|
// the binding is set up for this control and its required, readOnly
|
|
// etc. properties are refreshed.
|
|
// The data binding may be specified as a direct reference to the
|
|
// dojo.Stateful model node or as a string relative to its DOM
|
|
// parent or another widget.
|
|
// There are three ways in which the data binding node reference is
|
|
// calculated when specified as a string:
|
|
// - If an explicit parent widget is specified, the binding is
|
|
// calculated relative to the parent widget's data binding.
|
|
// - For any dijits that specify a data binding reference,
|
|
// we walk up their DOM hierarchy to obtain the first container
|
|
// dijit that has a data binding set up and use the reference String
|
|
// as a property name relative to the parent's data binding context.
|
|
// - If no such parent is found i.e. for the outermost container
|
|
// dijits that specify a data binding reference, the binding is
|
|
// calculated by treating the reference String as an expression and
|
|
// evaluating it to obtain the dojo.Stateful node in the datamodel.
|
|
// This method throws an Error in these two conditions:
|
|
// - The ref is an expression i.e. outermost bound dijit, but the
|
|
// expression evaluation fails.
|
|
// - The calculated binding turns out to not be an instance of a
|
|
// dojo.Stateful node.
|
|
// tags:
|
|
// private
|
|
if(!this.ref){
|
|
return; // nothing to do here
|
|
}
|
|
var ref = this.ref, pw, pb, binding;
|
|
// Now compute the model node to bind to
|
|
if(ref && lang.isFunction(ref.toPlainObject)){ // programmatic instantiation or direct ref
|
|
binding = ref;
|
|
}else if(/^\s*expr\s*:\s*/.test(ref)){ // declarative: refs as dot-separated expressions
|
|
ref = ref.replace(/^\s*expr\s*:\s*/, "");
|
|
binding = lang.getObject(ref);
|
|
}else if(/^\s*rel\s*:\s*/.test(ref)){ // declarative: refs relative to parent binding, dot-separated
|
|
ref = ref.replace(/^\s*rel\s*:\s*/, "");
|
|
parentBinding = parentBinding || this._getParentBindingFromDOM();
|
|
if(parentBinding){
|
|
binding = lang.getObject("" + ref, false, parentBinding);
|
|
}
|
|
}else if(/^\s*widget\s*:\s*/.test(ref)){ // declarative: refs relative to another dijits binding, dot-separated
|
|
ref = ref.replace(/^\s*widget\s*:\s*/, "");
|
|
var tokens = ref.split(".");
|
|
if(tokens.length == 1){
|
|
binding = registry.byId(ref).get("binding");
|
|
}else{
|
|
pb = registry.byId(tokens.shift()).get("binding");
|
|
binding = lang.getObject(tokens.join("."), false, pb);
|
|
}
|
|
}else{ // defaults: outermost refs are expressions, nested are relative to parents
|
|
parentBinding = parentBinding || this._getParentBindingFromDOM();
|
|
if(parentBinding){
|
|
binding = lang.getObject("" + ref, false, parentBinding);
|
|
}else{
|
|
try{
|
|
if(lang.getObject(ref) instanceof Stateful){
|
|
binding = lang.getObject(ref);
|
|
}
|
|
}catch(err){
|
|
if(ref.indexOf("${") == -1){ // Ignore templated refs such as in repeat body
|
|
throw new Error("dojox.mvc._DataBindingMixin: '" + this.domNode +
|
|
"' widget with illegal ref expression: '" + ref + "'");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if(binding){
|
|
if(lang.isFunction(binding.toPlainObject)){
|
|
this.binding = binding;
|
|
this._updateBinding("binding", null, binding);
|
|
}else{
|
|
throw new Error("dojox.mvc._DataBindingMixin: '" + this.domNode +
|
|
"' widget with illegal ref not evaluating to a dojo.Stateful node: '" + ref + "'");
|
|
}
|
|
}
|
|
},
|
|
|
|
_isEqual: function(one, other){
|
|
// test for equality
|
|
return one === other ||
|
|
// test for NaN === NaN
|
|
isNaN(one) && typeof one === 'number' &&
|
|
isNaN(other) && typeof other === 'number';
|
|
},
|
|
|
|
_updateBinding: function(name, old, current){
|
|
// summary:
|
|
// Set the data binding to the supplied value, which must be a
|
|
// dojo.Stateful node of a data model.
|
|
// name:
|
|
// The name of the binding property (always "binding").
|
|
// old:
|
|
// The old dojo.Stateful binding node of the data model.
|
|
// current:
|
|
// The new dojo.Stateful binding node of the data model.
|
|
// description:
|
|
// Applies the specified data binding to the attached widget.
|
|
// Loses any prior watch registrations on the previously active
|
|
// bind, registers the new one, updates data binds of any contained
|
|
// widgets and also refreshes all associated properties (valid,
|
|
// required etc.)
|
|
// tags:
|
|
// private
|
|
|
|
// remove all existing watches (if there are any, there will be 5)
|
|
this._unwatchArray(this._modelWatchHandles);
|
|
// add 5 new model watches
|
|
var binding = this.get("binding");
|
|
if(binding && lang.isFunction(binding.watch)){
|
|
var pThis = this;
|
|
this._modelWatchHandles = [
|
|
// 1. value - no default
|
|
binding.watch("value", function (name, old, current){
|
|
if(pThis._isEqual(old, current)){return;}
|
|
if(pThis._isEqual(pThis.get('value'), current)){return;}
|
|
pThis.set("value", current);
|
|
}),
|
|
// 2. valid - default "true"
|
|
binding.watch("valid", function (name, old, current){
|
|
pThis._updateProperty(name, old, current, true);
|
|
if(current !== pThis.get(name)){
|
|
if(pThis.validate && lang.isFunction(pThis.validate)){
|
|
pThis.validate();
|
|
}
|
|
}
|
|
}),
|
|
// 3. required - default "false"
|
|
binding.watch("required", function (name, old, current){
|
|
pThis._updateProperty(name, old, current, false, name, current);
|
|
}),
|
|
// 4. readOnly - default "false"
|
|
binding.watch("readOnly", function (name, old, current){
|
|
pThis._updateProperty(name, old, current, false, name, current);
|
|
}),
|
|
// 5. relevant - default "true"
|
|
binding.watch("relevant", function (name, old, current){
|
|
pThis._updateProperty(name, old, current, false, "disabled", !current);
|
|
})
|
|
];
|
|
var val = binding.get("value");
|
|
if(val != null){
|
|
this.set("value", val);
|
|
}
|
|
}
|
|
this._updateChildBindings();
|
|
},
|
|
|
|
_updateProperty: function(name, old, current, defaultValue, setPropName, setPropValue){
|
|
// summary:
|
|
// Update a binding property of the bound widget.
|
|
// name:
|
|
// The binding property name.
|
|
// old:
|
|
// The old value of the binding property.
|
|
// current:
|
|
// The new or current value of the binding property.
|
|
// defaultValue:
|
|
// The optional value to be applied as the current value of the
|
|
// binding property if the current value is null.
|
|
// setPropName:
|
|
// The optional name of a stateful property to set on the bound
|
|
// widget.
|
|
// setPropValue:
|
|
// The value, if an optional name is provided, for the stateful
|
|
// property of the bound widget.
|
|
// tags:
|
|
// private
|
|
if(old === current){
|
|
return;
|
|
}
|
|
if(current === null && defaultValue !== undefined){
|
|
current = defaultValue;
|
|
}
|
|
if(current !== this.get("binding").get(name)){
|
|
this.get("binding").set(name, current);
|
|
}
|
|
if(setPropName){
|
|
this.set(setPropName, setPropValue);
|
|
}
|
|
},
|
|
_updateChildBindings: function(parentBind){
|
|
// summary:
|
|
// Update this widget's value based on the current binding and
|
|
// set up the bindings of all contained widgets so as to refresh
|
|
// any relative binding references.
|
|
// findWidgets does not return children of widgets so need to also
|
|
// update children of widgets which are not bound but may hold widgets which are.
|
|
// parentBind:
|
|
// The binding on the parent of a widget whose children may have bindings
|
|
// which need to be updated.
|
|
// tags:
|
|
// private
|
|
var binding = this.get("binding") || parentBind;
|
|
if(binding && !this._beingBound){
|
|
array.forEach(registry.findWidgets(this.domNode), function(widget){
|
|
if(widget.ref && widget._setupBinding){
|
|
widget._setupBinding(binding);
|
|
}else{
|
|
widget._updateChildBindings(binding);
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
_getParentBindingFromDOM: function(){
|
|
// summary:
|
|
// Get the parent binding by traversing the DOM ancestors to find
|
|
// the first enclosing data-bound widget.
|
|
// returns:
|
|
// The parent binding, if one exists along the DOM parent axis.
|
|
// tags:
|
|
// private
|
|
var pn = this.domNode.parentNode, pw, pb;
|
|
while(pn){
|
|
pw = registry.getEnclosingWidget(pn);
|
|
if(pw){
|
|
pb = pw.get("binding");
|
|
if(pb && lang.isFunction(pb.toPlainObject)){
|
|
break;
|
|
}
|
|
}
|
|
pn = pw ? pw.domNode.parentNode : null;
|
|
}
|
|
return pb;
|
|
},
|
|
|
|
_unwatchArray: function(watchHandles){
|
|
// summary:
|
|
// Given an array of watch handles, unwatch all.
|
|
// watchHandles:
|
|
// The array of watch handles.
|
|
// tags:
|
|
// private
|
|
array.forEach(watchHandles, function(h){ h.unwatch(); });
|
|
}
|
|
});
|
|
});
|