define([ "dojo/_base/lang", "dojo/_base/config", "dojo/_base/array", "dojo/has" ], function(lang, config, array, has){ var mvc = lang.getObject("obno.mvc", true); /*===== mvc = {}; =====*/ /*===== obno.mvc.sync.converter = { // summary: // Class/object containing the converter functions used when the data goes between data binding source (e.g. data model or controller) to data binding origin (e.g. widget). format: function(value, constraints){ // summary: // The converter function used when the data comes from data binding source (e.g. data model or controller) to data binding origin (e.g. widget). // value: Anything // The data. // constraints: Object // The options for data conversion, which is: mixin({}, dataBindingTarget.constraints, dataBindingOrigin.constraints). }, parse: function(value, constraints){ // summary: // The converter function used when the data comes from data binding origin (e.g. widget) to data binding source (e.g. data model or controller). // value: Anything // The data. // constraints: Object // The options for data conversion, which is: mixin({}, dataBindingTarget.constraints, dataBindingOrigin.constraints). } }; obno.mvc.sync.options = { // summary: // Data binding options. // bindDirection: Number // The data binding bindDirection, choose from: obno.mvc.Bind.from, obno.mvc.Bind.to or obno.mvc.Bind.both. bindDirection: obno/mvc.both, // converter: obno/mvc/sync.converter // Class/object containing the converter functions used when the data goes between data binding source (e.g. data model or controller) to data binding origin (e.g. widget). converter: null }; =====*/ has.add("mvc-bindings-log-api", (config["mvc"] || {}).debugBindings); var sync; if(has("mvc-bindings-log-api")){ function getLogContent(/*dojo/Stateful*/ source, /*String*/ sourceProp, /*dojo/Stateful*/ target, /*String*/ targetProp){ return [ [target._setIdAttr || !target.declaredClass ? target : target.declaredClass, targetProp].join(":"), [source._setIdAttr || !source.declaredClass ? source : source.declaredClass, sourceProp].join(":") ]; } } function equals(/*Anything*/ dst, /*Anything*/ src){ // summary: // Returns if the given two values are equal. return dst === src || typeof dst == "number" && isNaN(dst) && typeof src == "number" && isNaN(src) || lang.isFunction((dst || {}).getTime) && lang.isFunction((src || {}).getTime) && dst.getTime() == src.getTime() || (lang.isFunction((dst || {}).equals) ? dst.equals(src) : lang.isFunction((src || {}).equals) ? src.equals(dst) : false); } function copy(/*Function*/ convertFunc, /*Object?*/ constraints, /*dojo/Stateful*/ source, /*String*/ sourceProp, /*dojo/Stateful*/ target, /*String*/ targetProp, /*Anything*/ old, /*Anything*/ current, /*Object?*/ excludes){ // summary: // Watch for change in property in dojo/Stateful object. // description: // Called when targetProp property in target is changed. (This is mainly used as a callback function of dojo/Stateful.watch()) // When older value and newer value are different, copies the newer value to sourceProp property in source. // convertFunc: Function // The data converter function. // constraints: Object? // The data converter options. // source: dojo/Stateful // The dojo/Stateful of copy source. // sourceProp: String // The property of copy source, specified in data binding. May be wildcarded. // target: dojo/Stateful // The dojo/Stateful of copy target. // targetProp: String // The property of copy target, being changed. For wildcard-based data binding, this is used as the property to be copied. // old: Anything // The older property value. // current: Anything // The newer property value. // excludes: Object? // The list of properties that should be excluded from wildcarded data binding. // Bail if there is no change in value, // or property name is wildcarded and the property to be copied is not in source property list (and source property list is defined), // or property name is wildcarded and the property to be copied is in explicit "excludes" list if(sync.equals(current, old) || sourceProp == "*" && array.indexOf(source.get("properties") || [targetProp], targetProp) < 0 || sourceProp == "*" && targetProp in (excludes || {})){ return; } var prop = sourceProp == "*" ? targetProp : sourceProp; if(has("mvc-bindings-log-api")){ var logContent = getLogContent(source, prop, target, targetProp); } try{ current = convertFunc ? convertFunc(current, constraints) : current; }catch(e){ if(has("mvc-bindings-log-api")){ console.log("Copy from" + logContent.join(" to ") + " was not done as an error is thrown in the converter."); } return; } if(has("mvc-bindings-log-api")){ console.log(logContent.reverse().join(" is being copied from: ") + " (Value: " + current + " from " + old + ")"); } // Copy the new value to source lang.isFunction(source.set) ? source.set(prop, current) : (source[prop] = current); } var directions = { // from: Number // Data binding goes from the source to the target from: 1, // to: Number // Data binding goes from the target to the source to: 2, // both: Number // Data binding goes in both directions (obno/mvc/Bind.from | obno/mvc/Bind.to) both: 3 }, undef; sync = function(/*dojo/Stateful*/ source, /*String*/ sourceProp, /*dojo/Stateful*/ target, /*String*/ targetProp, /*obno/mvc/sync.options*/ options){ // summary: // Synchronize two dojo/Stateful properties. // description: // Synchronize two dojo/Stateful properties. // source: dojo/Stateful // Source dojo/Stateful to be synchronized. // sourceProp: String // The property name in source to be synchronized. // target: dojo/Stateful // Target dojo/Stateful to be synchronized. // targetProp: String // The property name in target to be synchronized. // options: obno/mvc/sync.options // Data binding options. // returns: // The handle of data binding synchronization. var converter = (options || {}).converter, converterInstance, formatFunc, parseFunc; if(converter){ converterInstance = {source: source, target: target}; formatFunc = converter.format && lang.hitch(converterInstance, converter.format); parseFunc = converter.parse && lang.hitch(converterInstance, converter.parse); } var _watchHandles = [], excludes = [], list, constraints = lang.mixin({}, source.constraints, target.constraints), bindDirection = (options || {}).bindDirection || mvc.both; if(has("mvc-bindings-log-api")){ var logContent = getLogContent(source, sourceProp, target, targetProp); } if(targetProp == "*"){ if(sourceProp != "*"){ throw new Error("Unmatched wildcard is specified between source and target."); } list = target.get("properties"); if(!list){ list = []; for(var s in target){ if(target.hasOwnProperty(s) && s != "_watchCallbacks"){ list.push(s); } } } excludes = target.get("excludes"); }else{ list = [sourceProp]; } if(bindDirection & mvc.from){ // Start synchronization from source to target (e.g. from model to widget). For wildcard mode (sourceProp == targetProp == "*"), the 1st argument of watch() is omitted if(lang.isFunction(source.set) && lang.isFunction(source.watch)){ _watchHandles.push(source.watch.apply(source, ((sourceProp != "*") ? [sourceProp] : []).concat([function(name, old, current){ copy(formatFunc, constraints, target, targetProp, source, name, old, current, excludes); }]))); }else if(has("mvc-bindings-log-api")){ console.log(logContent.reverse().join(" is not a stateful property. Its change is not reflected to ") + "."); } // Initial copy from source to target (e.g. from model to widget) array.forEach(list, function(prop){ // In "all properties synchronization" case, copy is not done for properties in "exclude" list if(targetProp != "*" || !(prop in (excludes || {}))){ var value = lang.isFunction(source.get) ? source.get(prop) : source[prop]; copy(formatFunc, constraints, target, targetProp == "*" ? prop : targetProp, source, prop, undef, value); } }); } if(bindDirection & mvc.to){ if(!(bindDirection & mvc.from)){ // Initial copy from source to target (e.g. from model to widget) array.forEach(list, function(prop){ // In "all properties synchronization" case, copy is not done for properties in "exclude" list if(targetProp != "*" || !(prop in (excludes || {}))){ // Initial copy from target to source (e.g. from widget to model), only done for one-way binding from widget to model var value = lang.isFunction(target.get) ? target.get(targetProp) : target[targetProp]; copy(parseFunc, constraints, source, prop, target, targetProp == "*" ? prop : targetProp, undef, value); } }); } // Start synchronization from target to source (e.g. from widget to model). For wildcard mode (sourceProp == targetProp == "*"), the 1st argument of watch() is omitted if(lang.isFunction(target.set) && lang.isFunction(target.watch)){ _watchHandles.push(target.watch.apply(target, ((targetProp != "*") ? [targetProp] : []).concat([function(name, old, current){ copy(parseFunc, constraints, source, sourceProp, target, name, old, current, excludes); }]))); }else if(has("mvc-bindings-log-api")){ console.log(logContent.join(" is not a stateful property. Its change is not reflected to ") + "."); } } if(has("mvc-bindings-log-api")){ console.log(logContent.join(" is bound to: ")); } var handle = {}; handle.unwatch = handle.remove = function(){ for(var h = null; h = _watchHandles.pop();){ h.unwatch(); if(has("mvc-bindings-log-api")){ console.log(logContent.join(" is unbound from: ")); } } }; return handle; // dojo/handle }; lang.mixin(mvc, directions); // lang.setObject() thing is for back-compat, remove it in 2.0 return lang.setObject("obno.mvc.sync", lang.mixin(sync, {equals: equals}, directions)); });