SOURCE CODE: Uize.Widget.mChildBindings (view docs)

/*______________
|       ______  |   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.Widget.mChildBindings 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: 95
  docCompleteness: 10
*/

/*?
  Introduction
    The =Uize.Widget.mChildBindings= mixin implements features to provide a declarative approach to binding the state properties of a widget to those of its children.

    *DEVELOPERS:* `Ben Ilegbodu`, original code contributed by `Zazzle Inc.`
*/

Uize.module ({
  name:'Uize.Widget.mChildBindings',
  builder:function () {
    'use strict';

    var
      /*** Variables for Scruncher Optimization ***/
        _undefined,
        _Uize = Uize,
        _forEach = _Uize.forEach,
        _isString = _Uize.isString,
        _isArray = _Uize.isArray,
        _isPlainObject = _Uize.isPlainObject,
        _resolveTransformer = _Uize.resolveTransformer,
        _pairUp = _Uize.pairUp,

      /*** Variables for Performance Optimzation ***/
        _bindingFormatRegExp = /^([<\->]+)?(\w+)(\.(.+))?$/
    ;

    function _syncWidgets(_sourceWidget, _sourceProperty, _destinationWidget, _destinationProperty, _valueTransformer) {
      var _sourceValue = _sourceWidget.get(_sourceProperty);
      _destinationWidget.set(
        _destinationProperty,
        _valueTransformer
          ? _valueTransformer(_sourceValue, _sourceWidget, _destinationWidget)
          : _sourceValue
      );
    }

    return function (_class) {
      _class.declare ({
        alphastructor:function () {
          var
            m = this,
            _addedChildren = m.addedChildren
          ;

          /* NOTE: Format of mChildBindings_bindings:
            {
              childA:{
                'propertyA/childPropertyA':function() {
                  // set up bindings
                }
              }
            }
          */

          _forEach(
            m.Class.mChildBindings_bindings,
            function(_bindingsForChild, _childName) {
              _addedChildren.whenever(
                _childName,
                function() {
                  for (var _key in _bindingsForChild)
                    _bindingsForChild[_key](m)
                  ;
                }
              );
            }
          );
        },

        staticMethods:{
          childBindings:function(_bindings) {
            var _childBindings = this.mChildBindings_bindings;

            _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]
                    }
                  ;

                  if (_isPlainObject(_binding) && _binding.child) {
                    var
                      _childName = _binding.child,
                      _childPropertyName = _binding.property || _propertyName,
                      _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),

                      _directionIsToChild = _direction.indexOf('->') == (_directionLength - 2),  // parent -> child
                      _directionIsFromChild = !_direction.indexOf('<-'),  // child -> parent
                      _directionIsTwoWay = _direction.indexOf('<->'),
                      _widgetChangedPropertyEventName = 'Changed.' + _propertyName,
                      _childWidgetChangedPropertyEventName = 'Changed.' + _childPropertyName
                    ;

                    // Construct function to be called once the child is added. It will actually create the bindings
                    (_childBindings[_childName] = _childBindings[_childName] || {})[_propertyName + '/' + _childPropertyName] = function(m) {
                      var
                        _childWidget = m.children[_childName],
                        _syncToChildEvent,
                        _syncFromChildEvent
                      ;

                      function _syncToChild() { _syncWidgets(m, _propertyName, _childWidget, _childPropertyName, _valueTransformerAtoB) }
                      function _syncFromChild() { _syncWidgets(_childWidget, _childPropertyName, m, _propertyName, _valueTransformerBtoA) }

                      if (_directionIsToChild) {
                        // First set child widget to have same value as widget
                        // We don't want to do this if the binding is bi-drectional and the widget is undefined.
                        // In that case we'd rather the child widget be the driver
                        (_directionIsTwoWay || m.get(_propertyName) !== _undefined)
                          && _syncToChild()
                        ;

                        // Then wire Changed.* handler on widget to update child widget
                        m.wire(_syncToChildEvent = _pairUp(_widgetChangedPropertyEventName, _syncToChild));
                      }
                      if (_directionIsFromChild) {
                        // First set widget to have same value as child
                        _syncFromChild();

                        // Then wire Changed.* handler on child to update widget
                        _childWidget.wire(_syncFromChildEvent = _pairUp(_childWidgetChangedPropertyEventName, _syncFromChild));
                      }

                      // Finally wire up unwire if/when the child is removed
                      m.addedChildren.whenever(
                        '!' + _childName,
                        function() {
                          if (_childWidget) {
                            // unwire parent -> child
                            _syncToChildEvent && m.unwire(_syncToChildEvent);

                            // unwire child -> parent (even though child is removed, it is not necessarilly destroyed)
                            _syncFromChildEvent && _childWidget && _childWidget.unwire(_syncFromChildEvent);

                            // clear out our reference to the removed child widget to not potentially hang on memory that can be disposed
                            _childWidget = undefined;
                          }
                        }
                      );
                    };
                  }
                }

                _isArray(_bindingForProperty)
                  ? _forEach(_bindingForProperty, _processBinding)
                  : _processBinding(_bindingForProperty)
                ;
              }
            );
            /*?
              Static Methods
                Uize.Widget.mChildBindings.childBindings
                  .

                  SYNTAX
                  .........................................
                  MyWidgetClass.childBindings (bindingsOBJ);
                  .........................................

                  VERBOSE EXAMPLE
                  ......................................................
                  MyNamespace.MyWidgetClass = Uize.Widget.mChildBindings.subclass ({
                    childBindings:{
                      size:{
                        child:'sizeWidget',
                        property:'value',
                        direction:'<->' // bi-directional changes (default),
                        valueAdapter:{
                          aToB:function(value) { return value * value },
                          bToA:function(value) { return Math.sqrt(value) }
                        }
                      },
                      value:[ // parent-to-many-children
                        {
                          child:'valueWidget',
                          direction:'->',
                          valueAdapter:{
                            aToB:'value * value' // via Uize.resolveValueTransformer
                          }
                        },
                        {
                          child:'valueWidget2',
                          direction:'<-',
                          valueAdapter:{
                            bToA:'Math.sqrt(value)' // via Uize.resolveValueTransformer
                          }
                        }
                      ]
                    }
                  });
                  ......................................................

                  SHORT-HAND EXAMPLE
                  ......................................................
                  MyNamespace.MyWidgetClass = Uize.Widget.mChildBindings.subclass ({
                    childBindings:{
                      size:'<->sizeWidget.value',  // bi-directional changes with "value" state proprety in "sizeWidget" child
                      value:[ // parent-to-many-children
                        '->valueWidget', // changes to child only
                        '<-valueWidget2' // changes from child only
                      ],
                      values:'valuesWidget' // bi-directional changes with same-named state proprety in "valuesWidget" child
                    }
                  });
                  ......................................................
            */
          }
        },

        staticProperties:{
          mChildBindings_bindings:{}
        }
      });
    };
  }
});