source: Dev/branches/rest-dojo-ui/client/dojox/mvc/_DataBindingMixin.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

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