/*______________ | ______ | U I Z E J A V A S C R I P T F R A M E W O R K | / / | --------------------------------------------------- | / O / | MODULE : Uize.Class.mChildObjectBindings Mixin | / / / | | / / / /| | ONLINE : http://uize.com | /____/ /__/_| | COPYRIGHT : (c)2014-2016 UIZE | /___ | LICENSE : Available under MIT License or GNU General Public License |_______________| http://uize.com/license.html */ /* Module Meta Data type: Mixin importance: 5 codeCompleteness: 100 docCompleteness: 100 */ /*? Introduction The =Uize.Class.mChildObjectBindings= mixin implements features to provide a declarative approach to binding the state properties of a class instance to those of its child objects. *DEVELOPERS:* `Ben Ilegbodu`, original code contributed by `Zazzle Inc.` */ Uize.module ({ name:'Uize.Class.mChildObjectBindings', builder:function () { 'use strict'; var /*** Variables for Scruncher Optimization ***/ _undefined, _Uize = Uize, _forEach = _Uize.forEach, _isString = _Uize.isString, _isArray = _Uize.isArray, _resolveTransformer = _Uize.resolveTransformer, _pairUp = _Uize.pairUp, _returnTrue = _Uize.returnTrue, /*** Variables for Performance Optimzation ***/ _bindingFormatRegExp = /^\s*([<\->]+)?\s*(\w*?)\s*(\.\s*(.+?))?\s*(\:\s*(.+?))?\s*$/ ; function _getSourceValue(_sourceObject, _sourceProperty, _destinationObject, _valueTransformer) { var _sourceValue = _sourceObject.get(_sourceProperty); return _valueTransformer ? _valueTransformer(_sourceValue, _sourceObject, _destinationObject) // passing _sourceObject & _destinationObject so function transformers can have the references in case they are needed : _sourceValue ; } function _syncObjects(_sourceObject, _sourceProperty, _destinationObject, _destinationProperty, _valueTransformer, _whenCondition) { if (this.isMet(_whenCondition)) { var _sourceValue = _getSourceValue(_sourceObject, _sourceProperty, _destinationObject, _valueTransformer); _sourceValue != _destinationObject.get(_destinationProperty) && _destinationObject.set(_destinationProperty, _sourceValue) ; } } return function (_class) { _class.declare ({ staticMethods:{ childObjectBindings:function(_properties) { var _bindingsFunctionName = _properties.declaration, _childObjectsInstancePropertyName = _properties.instanceProperty, _addedChildObjectsPropertyName = _properties.addedInstanceProperty, _childObjectsStatePropertyName = _properties.stateProperty, _bindingWiringsStaticDataName = 'mChildObjectBindings_' + _bindingsFunctionName + '_wirings', _childObjectsToChildStaticDataName = 'mChildObjectBindings_' + _bindingsFunctionName + '_toChild' ; function _getChildObjectsToSet(m, _includePropertyFunc) { var _toChildInfo = m.Class[_childObjectsToChildStaticDataName], _childObjects = m[_childObjectsInstancePropertyName], _childObjectsToSet = {} ; for (var _childName in _toChildInfo) { var _toChildInfoForChild = _toChildInfo[_childName], _childObjectToSet = _childObjectsToSet[_childName] = _childObjectsToSet[_childName] || {} ; for (var _childPropertyName in _toChildInfoForChild) { var _toChildInfoForChildProperty = _toChildInfoForChild[_childPropertyName]; for (var _instancePropertyName in _toChildInfoForChildProperty) { var _info = _toChildInfoForChildProperty[_instancePropertyName]; if (_includePropertyFunc(_instancePropertyName, _info) && m.isMet(_info._whenCondition)) { var _child = _childName ? _childObjects[_childName] : m, _instancePropertyValue = _getSourceValue( m, _instancePropertyName, _child, _info._valueTransformerAtoB ) ; // An optimization to not include the property value if it actually won't // cause a change if (!_child || _instancePropertyValue != _child.get(_childPropertyName)) _childObjectToSet[_childPropertyName] = _instancePropertyValue; } } } } return _childObjectsToSet; } this.declare({ staticProperties:_pairUp( _bindingWiringsStaticDataName, {}, _childObjectsToChildStaticDataName, {} ), staticMethods:_pairUp( _bindingsFunctionName, function(_bindings) { var mClass = this, _bindingWirings = mClass[_bindingWiringsStaticDataName], _toChildInfo = mClass[_childObjectsToChildStaticDataName], _sharedPropertiesOnChangedHandlerLookup = {}, _sharedPropertiesOnChangeHandler = function(_changedState) { var m = this, _childObjects = m[_childObjectsInstancePropertyName], _changedProperties = _Uize.lookup(_Uize.keys(_changedState)), _childObjectsToSet = _getChildObjectsToSet( m, // make sure property is in the list of changed properties function(_instancePropertyName) { return _instancePropertyName in _changedProperties } ) ; for (var _childName in _childObjectsToSet) { var _childObject = _childName ? _childObjects[_childName] : m, _childStateToSet = _childObjectsToSet[_childName] ; _childObject && !_Uize.isEmpty(_childStateToSet) && _childObject.set(_childStateToSet) ; } } ; _forEach( _bindings, function(_bindingForProperty, _propertyName) { function _processBinding(_binding) { var _formatMatch = _isString(_binding) && _binding.match(_bindingFormatRegExp); if (_formatMatch) // canonicalize string _binding = { child:_formatMatch[2], property:_formatMatch[4], direction:_formatMatch[1], when:_formatMatch[6] } ; var _childName = _binding.child || '', _childPropertyName = _binding.property || _propertyName ; if (_childName || (_propertyName != _childPropertyName)) { // no point binding the same property to itself for an instance (!childName) var _direction = _binding.direction || '<->', // bi-directional is the default _directionLength = _direction.length, _valueAdapter = _binding.valueAdapter, _valueTransformerAtoB = _valueAdapter && _valueAdapter.aToB && _resolveTransformer(_valueAdapter.aToB), _valueTransformerBtoA = _valueAdapter && _valueAdapter.bToA && _resolveTransformer(_valueAdapter.bToA), _whenCondition = _binding.when || _returnTrue, _directionIsToChild = _direction.indexOf('->') == (_directionLength - 2), // parent -> child _directionIsFromChild = !_direction.indexOf('<-'), // child -> parent _directionIsTwoWay = !_direction.indexOf('<->'), _childObjectChangedPropertyEventName = 'Changed.' + _childPropertyName, _toChildInfoForChild = _toChildInfo[_childName] = _toChildInfo[_childName] || {}, _toChildInfoForChildProperty = _toChildInfoForChild[_childPropertyName] = _toChildInfoForChild[_childPropertyName] || {} ; if (_directionIsToChild) { // Instead of wiring Changed.* event on each instance, we'll just augment the state property definition // to include a shared onChange handler in order to update the child when the instance changes. // And by using a shared onChange we can update *all* of the bound properties for the child at once instead // of one by one. if (!(_propertyName in _sharedPropertiesOnChangedHandlerLookup)) { // register the state property only once the first time we encounter it _sharedPropertiesOnChangedHandlerLookup[_propertyName] = 1; mClass.stateProperties( _pairUp( _propertyName, { name:_propertyName, onChange:_sharedPropertiesOnChangeHandler } ) ); } // Save the value transformation for the child's property from the instance's property so that we can have the child // instantiated w/ the data (provided the direction is to-child) _toChildInfoForChildProperty[_propertyName] = { _directionIsTwoWay:_directionIsTwoWay, _valueTransformerAtoB:_valueTransformerAtoB, _whenCondition:_whenCondition }; } // Construct function to be called once the child is added. It will actually create the bindings (_bindingWirings[_childName] = _bindingWirings[_childName] || {})[_propertyName + '/' + _childPropertyName] = function(m) { var _childObject = _childName ? m[_childObjectsInstancePropertyName][_childName] : m, _syncFromChildEvent, _whenConditionWheneverWiring ; function _syncToChild() { _syncObjects.call(m, m, _propertyName, _childObject, _childPropertyName, _valueTransformerAtoB, _whenCondition) } function _syncFromChild() { _syncObjects.call(m, _childObject, _childPropertyName, m, _propertyName, _valueTransformerBtoA, _whenCondition) } if (_directionIsToChild) { // First set child object to have same value as instance. We still need this here despite setting // the child objects' initial data when they are added in the event that a child is added, removed, // and added again. In that case the child objects special state property won't have the data we need. // We don't want to do this if the binding is bi-drectional and the instance is undefined. // In that case we'd rather the child object be the driver (!_directionIsTwoWay || m.get(_propertyName) !== _undefined) && _syncToChild() ; // Sync to child event is handled by adding onChange handler for state property } if (_directionIsFromChild) { // First set instance to have same value as child if it's one-way (or the instance's property was undefined for bi-directional) (!_directionIsTwoWay || m.get(_propertyName) == _undefined) && _syncFromChild(); // Then wire Changed.* handler on child to update instance _childObject.wire(_syncFromChildEvent = _pairUp(_childObjectChangedPropertyEventName, _syncFromChild)); } // if a when condition is specified, we need to register a whenever condition handler to sync when the condition // becomes true if (_whenCondition != _returnTrue) _whenConditionWheneverWiring = m.whenever( _whenCondition, function() { if (_directionIsToChild && (!_directionIsTwoWay || m.get(_propertyName) !== _undefined)) _syncToChild(); else if (_directionIsFromChild) _syncFromChild(); } ) ; // Finally, wire up unwire if/when the child is removed _childName && m[_addedChildObjectsPropertyName].whenever( '!' + _childName, function() { if (_childObject) { // NOTE: can't unwire onChange (it just checks to see if child exists) // unwire child -> parent (even though child is removed, it is not necessarilly destroyed) _syncFromChildEvent && _childObject && _childObject.unwire(_syncFromChildEvent); // unwire whenever condition handler _whenConditionWheneverWiring && m.unwire(_whenConditionWheneverWiring); // clear out our reference to the removed child object to not potentially hang on memory that can be disposed _childObject = undefined; } } ) ; }; } } _isArray(_bindingForProperty) ? _forEach(_bindingForProperty, _processBinding) : _processBinding(_bindingForProperty) ; } ); } ), alphastructor:function () { var m = this, _addedChildObjects = m[_addedChildObjectsPropertyName] ; /* NOTE: Format of _bindingWiringsStaticDataName: { childA:{ 'propertyA/childPropertyA':function() { // set up bindings } } } */ _forEach( m.Class[_bindingWiringsStaticDataName], function(_bindingWiringsForChild, _childName) { var _applyBinding = function() { for (var _key in _bindingWiringsForChild) _bindingWiringsForChild[_key](m) ; } ; _childName ? _addedChildObjects.whenever(_childName, _applyBinding) : _applyBinding() ; } ); }, omegastructor:function() { var m = this; // set initial values for children on state proprety so that when the children are // constructed they'll have those values ready at construction, which should eliminate // an initial changed event. Optimization! m.set( _childObjectsStatePropertyName, _getChildObjectsToSet( m, // We don't want to do this if the binding is bi-drectional and the instance is undefined. // In that case we'd rather the child object be the driver. // Also make sure that the when condition is met, otherwise we don't want to sync the instance's state // with that of the child. function(_instancePropertyName, _info) { return !_info._directionIsTwoWay || m.get(_instancePropertyName) !== _undefined } ) ); } }); /*? Static Methods Uize.Class.mChildObjectBindings.childObjectBindings Lets you conveniently declare the type of child objects binding to declare on the class. SYNTAX ......................................... MyClass.mChildObjectBindings (childObjectBindingsPropertiesOBJ); ......................................... The sole =childObjectBindingsPropertiesOBJ= parameter supports the following properties... - =declaration= - the name of the actual child object bindings declaration =function= to create (such as ='childBindings'= for =Uize.Widget.mChildBindings=) - =instanceProperty= - the name of the instance property that contains the references to the child objects (such as ='children'= for =Uize.Widget.mChildBindings=) - =addedInstanceProperty= - the name of the =Uize.Class= instance property that contains state about which child objects have been added (such as ='addedChildren' for =Uize.Widget.mChildBindings=) - =stateProperty= - the name of the special state property that allows for setting state of child objects (such as ='children'= for =Uize.Widget.mChildBindings=) */ } } }); }; } });