source: Dev/trunk/src/client/dojox/mvc/_DataBindingMixin.js @ 529

Last change on this file since 529 was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 14.7 KB
Line 
1define([
2        "dojo/_base/kernel",
3        "dojo/_base/lang",
4        "dojo/_base/array",
5        "dojo/_base/declare",
6        "dojo/Stateful",
7        "dijit/registry"
8], function(kernel, lang, array, declare, Stateful, registry){
9
10        kernel.deprecated("dojox.mvc._DataBindingMixin", "Use dojox/mvc/at for data binding.");
11
12        // Note: This should be a plain Object, not a Class.
13        // But no need to change it since it's deprecated.
14        return declare("dojox.mvc._DataBindingMixin", null, {
15                // summary:
16                //              Deprecated.  Use dojox/mvc/at for data binding.
17                //              Provides the ability for dijits or custom view components to become
18                //              data binding aware.
19                //
20                // description:
21                //              Data binding awareness enables dijits or other view layer
22                //              components to bind to locations within a client-side data model,
23                //              which is commonly an instance of the dojox/mvc/StatefulModel class. A
24                //              bind is a bi-directional update mechanism which is capable of
25                //              synchronizing value changes between the bound dijit or other view
26                //              component and the specified location within the data model, as well
27                //              as changes to other properties such as "valid", "required",
28                //              "readOnly" etc.
29                //
30                //              The data binding is commonly specified declaratively via the "ref"
31                //              property in the "data-dojo-props" attribute value.
32                //
33                //              Consider the following simple example:
34                //
35                //              |       <script>
36                //              |               var model;
37                //              |               require(["dijit/StatefulModel", "dojo/parser"], function(StatefulModel, parser){
38                //              |                       model = new StatefulModel({ data : {
39                //              |                               hello : "Hello World"
40                //              |                       }});
41                //              |                       parser.parse();
42                //              |               });
43                //              |       </script>
44                //              |
45                //              |       <input id="hello1" data-dojo-type="dijit/form/TextBox"
46                //              |               data-dojo-props="ref: model.hello"></input>
47                //              |
48                //              |       <input id="hello2" data-dojo-type="dijit/form/TextBox"
49                //              |               data-dojo-props="ref: model.hello"></input>
50                //
51                //              In the above example, both dijit/form/TextBox instances (with IDs
52                //              "hello1" and "hello2" respectively) are bound to the same reference
53                //              location in the data model i.e. "hello" via the "ref" expression
54                //              "model.hello". Both will have an initial value of "Hello World".
55                //              Thereafter, a change in the value of either of the two textboxes
56                //              will cause an update of the value in the data model at location
57                //              "hello" which will in turn cause a matching update of the value in
58                //              the other textbox.
59                // tags:
60                //              deprecated
61       
62                // ref: [deprecated] String||dojox/mvc/StatefulModel
63                //              The value of the data binding expression passed declaratively by
64                //              the developer. This usually references a location within an
65                //              existing datamodel and may be a relative reference based on the
66                //              parent / container data binding (dot-separated string).
67                ref: null,
68
69/*=====
70                // binding: [readOnly] dojox/mvc/StatefulModel
71                //              The read only value of the resolved data binding for this widget.
72                //              This may be a result of resolving various relative refs along
73                //              the parent axis.
74                binding: null,
75=====*/
76
77                //////////////////////// PUBLIC METHODS ////////////////////////
78       
79                isValid: function(){
80                        // summary:
81                        //              Returns the validity of the data binding.
82                        // returns:
83                        //              Boolean
84                        //              The validity associated with the data binding.
85                        // description:
86                        //              This function is meant to provide an API bridge to the dijit API.
87                        //              Validity of data-bound dijits is a function of multiple concerns:
88                        //
89                        //              - The validity of the value as ascertained by the data binding
90                        //                and constraints specified in the data model (usually semantic).
91                        //              - The validity of the value as ascertained by the widget itself
92                        //                based on widget constraints (usually syntactic).
93                        //
94                        //              In order for dijits to function correctly in data-bound
95                        //              environments, it is imperative that their isValid() functions
96                        //              assess the model validity of the data binding via the
97                        //              this.inherited(arguments) hierarchy and declare any values
98                        //              failing the test as invalid.
99                        var valid = this.get("valid");
100                        return typeof valid != "undefined" ? valid : this.get("binding") ? this.get("binding").get("valid") : true;
101                },
102
103                //////////////////////// LIFECYCLE METHODS ////////////////////////
104
105                _dbstartup: function(){
106                        // summary:
107                        //              Tie data binding initialization into the widget lifecycle, at
108                        //              widget startup.
109                        // tags:
110                        //              private
111                        if(this._databound){
112                                return;
113                        }
114                        this._unwatchArray(this._viewWatchHandles);
115                        // add 2 new view watches, active only after widget has started up
116                        this._viewWatchHandles = [
117                                // 1. data binding refs
118                                this.watch("ref", function(name, old, current){
119                                        if(this._databound && old !== current){
120                                                this._setupBinding();
121                                        }
122                                }),
123                                // 2. widget values
124                                this.watch("value", function(name, old, current){
125                                        if(this._databound){
126                                                var binding = this.get("binding");
127                                                if(binding){
128                                                        // dont set value if the valueOf current and old match.
129                                                        if(!((current && old) && (old.valueOf() === current.valueOf()))){
130                                                                binding.set("value", current);
131                                                        }
132                                                }
133                                        }
134                                })
135                        ];
136                        this._beingBound = true;
137                        this._setupBinding();
138                        delete this._beingBound;
139                        this._databound = true;
140                },
141
142                //////////////////////// PRIVATE METHODS ////////////////////////
143
144                _setupBinding: function(parentBinding){
145                        // summary:
146                        //              Calculate and set the dojo/Stateful data binding for the
147                        //              associated dijit or custom view component.
148                        // parentBinding:
149                        //              The binding of this widget/view component's data-bound parent,
150                        //              if available.
151                        // description:
152                        //              The declarative data binding reference may be specified in two
153                        //              ways via markup:
154                        //
155                        //              - For older style documents (non validating), controls may use
156                        //                the "ref" attribute to specify the data binding reference
157                        //                (String).
158                        //              - For validating documents using the new Dojo parser, controls
159                        //                may specify the data binding reference (String) as the "ref"
160                        //                property specified in the data-dojo-props attribute.
161                        //
162                        //              Once the ref value is obtained using either of the above means,
163                        //              the binding is set up for this control and its required, readOnly
164                        //              etc. properties are refreshed.
165                        //              The data binding may be specified as a direct reference to the
166                        //              dojo/Stateful model node or as a string relative to its DOM
167                        //              parent or another widget.
168                        //              There are three ways in which the data binding node reference is
169                        //              calculated when specified as a string:
170                        //
171                        //              - If an explicit parent widget is specified, the binding is
172                        //                calculated relative to the parent widget's data binding.
173                        //              - For any dijits that specify a data binding reference,
174                        //                we walk up their DOM hierarchy to obtain the first container
175                        //                dijit that has a data binding set up and use the reference String
176                        //                as a property name relative to the parent's data binding context.
177                        //              - If no such parent is found i.e. for the outermost container
178                        //                dijits that specify a data binding reference, the binding is
179                        //                calculated by treating the reference String as an expression and
180                        //                evaluating it to obtain the dojo/Stateful node in the datamodel.
181                        //
182                        //              This method calls console.warn in these two conditions:
183                        //
184                        //              - The ref is an expression i.e. outermost bound dijit, but the
185                        //                expression evaluation fails.
186                        //              - The calculated binding turns out to not be an instance of a
187                        //                dojo/Stateful node.
188                        // tags:
189                        //              private
190
191                        if(!this.ref){
192                                return; // nothing to do here
193                        }
194                        var ref = this.ref, pw, pb, binding;
195                        // Now compute the model node to bind to
196                        if(ref && lang.isFunction(ref.toPlainObject)){ // programmatic instantiation or direct ref
197                                binding = ref;
198                        }else if(/^\s*expr\s*:\s*/.test(ref)){ // declarative: refs as dot-separated expressions
199                                ref = ref.replace(/^\s*expr\s*:\s*/, "");
200                                binding = lang.getObject(ref);
201                        }else if(/^\s*rel\s*:\s*/.test(ref)){ // declarative: refs relative to parent binding, dot-separated
202                                ref = ref.replace(/^\s*rel\s*:\s*/, "");
203                                parentBinding = parentBinding || this._getParentBindingFromDOM();
204                                if(parentBinding){
205                                        binding = lang.getObject("" + ref, false, parentBinding);
206                                }
207                        }else if(/^\s*widget\s*:\s*/.test(ref)){ // declarative: refs relative to another dijits binding, dot-separated
208                                ref = ref.replace(/^\s*widget\s*:\s*/, "");
209                                var tokens = ref.split(".");
210                                if(tokens.length == 1){
211                                        binding = registry.byId(ref).get("binding");
212                                }else{
213                                        pb = registry.byId(tokens.shift()).get("binding");
214                                        binding = lang.getObject(tokens.join("."), false, pb);
215                                }
216                        }else{ // defaults: outermost refs are expressions, nested are relative to parents
217                                parentBinding = parentBinding || this._getParentBindingFromDOM();
218                                if(parentBinding){
219                                        binding = lang.getObject("" + ref, false, parentBinding);
220                                }else{
221                                        try{
222                                                var b = lang.getObject("" + ref) || {};
223                                                if(lang.isFunction(b.set) && lang.isFunction(b.watch)){
224                                                        binding = b;
225                                                }                                               
226                                        }catch(err){
227                                                if(ref.indexOf("${") == -1){ // Ignore templated refs such as in repeat body
228                                                        console.warn("dojox/mvc/_DataBindingMixin: '" + this.domNode +
229                                                                "' widget with illegal ref not evaluating to a dojo/Stateful node: '" + ref + "'");
230                                                }
231                                        }
232                                }
233                        }
234                        if(binding){
235                                if(lang.isFunction(binding.toPlainObject)){
236                                        this.binding = binding;
237                                        if(this[this._relTargetProp || "target"] !== binding){
238                                                this.set(this._relTargetProp || "target", binding);
239                                        }
240                                        this._updateBinding("binding", null, binding);
241                                }else{
242                                        console.warn("dojox/mvc/_DataBindingMixin: '" + this.domNode +
243                                                "' widget with illegal ref not evaluating to a dojo/Stateful node: '" + ref + "'");
244                                }
245                        }
246                },
247
248                _isEqual: function(one, other){
249                        // test for equality
250                        return one === other ||
251                                // test for NaN === NaN
252                                isNaN(one) && typeof one === 'number' &&
253                                isNaN(other) && typeof other === 'number';
254                },
255
256                _updateBinding: function(name, old, current){
257                        // summary:
258                        //              Set the data binding to the supplied value, which must be a
259                        //              dojo/Stateful node of a data model.
260                        // name:
261                        //              The name of the binding property (always "binding").
262                        // old:
263                        //              The old dojo/Stateful binding node of the data model.
264                        // current:
265                        //              The new dojo/Stateful binding node of the data model.
266                        // description:
267                        //              Applies the specified data binding to the attached widget.
268                        //              Loses any prior watch registrations on the previously active
269                        //              bind, registers the new one, updates data binds of any contained
270                        //              widgets and also refreshes all associated properties (valid,
271                        //              required etc.)
272                        // tags:
273                        //              private
274       
275                        // remove all existing watches (if there are any, there will be 5)
276                        this._unwatchArray(this._modelWatchHandles);
277                        // add 5 new model watches
278                        var binding = this.get("binding");
279                        if(binding && lang.isFunction(binding.watch)){
280                                var pThis = this;
281                                this._modelWatchHandles = [
282                                        // 1. value - no default
283                                        binding.watch("value", function (name, old, current){
284                                                if(pThis._isEqual(old, current)){return;}
285                                                if(pThis._isEqual(pThis.get('value'), current)){return;}
286                                                pThis.set("value", current);
287                                        }),
288                                        // 2. valid - default "true"
289                                        binding.watch("valid", function (name, old, current){
290                                                pThis._updateProperty(name, old, current, true);
291                                                if(current !== pThis.get(name)){
292                                                        if(pThis.validate && lang.isFunction(pThis.validate)){
293                                                                pThis.validate();
294                                                        }
295                                                }
296                                        }),
297                                        // 3. required - default "false"
298                                        binding.watch("required", function (name, old, current){
299                                                pThis._updateProperty(name, old, current, false, name, current);
300                                        }),
301                                        // 4. readOnly - default "false"
302                                        binding.watch("readOnly", function (name, old, current){
303                                                pThis._updateProperty(name, old, current, false, name, current);
304                                        }),
305                                        // 5. relevant - default "true"
306                                        binding.watch("relevant", function (name, old, current){
307                                                pThis._updateProperty(name, old, current, false, "disabled", !current);
308                                        })
309                                ];
310                                var val = binding.get("value");
311                                if(val != null){
312                                        this.set("value", val);
313                                }
314                        }
315                        this._updateChildBindings();
316                },
317       
318                _updateProperty: function(name, old, current, defaultValue, setPropName, setPropValue){
319                        // summary:
320                        //              Update a binding property of the bound widget.
321                        // name:
322                        //              The binding property name.
323                        // old:
324                        //              The old value of the binding property.
325                        // current:
326                        //              The new or current value of the binding property.
327                        // defaultValue:
328                        //              The optional value to be applied as the current value of the
329                        //              binding property if the current value is null.
330                        // setPropName:
331                        //              The optional name of a stateful property to set on the bound
332                        //              widget.
333                        // setPropValue:
334                        //              The value, if an optional name is provided, for the stateful
335                        //              property of the bound widget.
336                        // tags:
337                        //              private
338                        if(old === current){
339                                return;
340                        }
341                        if(current === null && defaultValue !== undefined){
342                                current = defaultValue;
343                        }
344                        if(current !== this.get("binding").get(name)){
345                                this.get("binding").set(name, current);
346                        }
347                        if(setPropName){
348                                this.set(setPropName, setPropValue);
349                        }
350                },
351
352                _updateChildBindings: function(parentBind){
353                        // summary:
354                        //              Update this widget's value based on the current binding and
355                        //              set up the bindings of all contained widgets so as to refresh
356                        //              any relative binding references.
357                        //              findWidgets does not return children of widgets so need to also
358                        //              update children of widgets which are not bound but may hold widgets which are.
359                        // parentBind:
360                        //              The binding on the parent of a widget whose children may have bindings
361                        //              which need to be updated.
362                        // tags:
363                        //              private
364                        var binding = this.get("binding") || parentBind;
365                        if(binding && !this._beingBound){
366                                array.forEach(registry.findWidgets(this.domNode), function(widget){
367                                        if(widget.ref && widget._setupBinding){
368                                                widget._setupBinding(binding);
369                                        }else{ 
370                                                widget._updateChildBindings(binding);
371                                        }
372                                });
373                        }
374                },
375
376                _getParentBindingFromDOM: function(){
377                        // summary:
378                        //              Get the parent binding by traversing the DOM ancestors to find
379                        //              the first enclosing data-bound widget.
380                        // returns:
381                        //              The parent binding, if one exists along the DOM parent axis.
382                        // tags:
383                        //              private
384                        var pn = this.domNode.parentNode, pw, pb;
385                        while(pn){
386                                pw = registry.getEnclosingWidget(pn);
387                                if(pw){
388                                        pb = pw.get("binding");
389                                        if(pb && lang.isFunction(pb.toPlainObject)){
390                                                break;
391                                        }
392                                }
393                                pn = pw ? pw.domNode.parentNode : null;
394                        }
395                        return pb;
396                },
397
398                _unwatchArray: function(watchHandles){
399                        // summary:
400                        //              Given an array of watch handles, unwatch all.
401                        // watchHandles:
402                        //              The array of watch handles.
403                        // tags:
404                        //              private
405                        array.forEach(watchHandles, function(h){ h.unwatch(); });
406                }
407        });
408});
Note: See TracBrowser for help on using the repository browser.