Source: src/main/javascript/oracle/oj/ojknockout-validation/ValueBinding.js

Oracle® JavaScript Extension Toolkit (JET)
1.1.2

E65298-01

/**
 * Copyright (c) 2014, Oracle and/or its affiliates.
 * All rights reserved.
 */

/*jslint browser: true, devel: true*/

// private to prevent creating a JSDoc page for this class.  The only thing we wish 
// to JSDoc is the invalidComponentTracker, which we're putting in EditableValue's output.
/**
 * An extension to oj.ComponentBinding, properties exposed on this binding are available 
 * to jet components that extend from oj.editableValue. 
 * 
 * @private
 * @constructor oj.ValueBinding 
 * @see oj.ComponentBinding
 * @see oj.editableValue
 * @since 0.6
 */
oj.ValueBinding = function(){};


/** 
 * <p>When this attribute is bound to an observable, the framework pushes an object of type {@link oj.InvalidComponentTracker}
 * onto the observable. The object itself tracks the validity of a group of editable components.
 * 
 * <p>When this attribute is present, the binding registers a listener for the <a href="#optionChange">optionChange</a>
 * event. This event is fired by JET editable components whenever its validity changes (i.e. when 
 * <a href="#messagesShown">messagesShown</a> or <a href="#messagesHidden">messagesHidden</a> 
 * options change). When the event is fired, the listener determines the current validity of the 
 * component and updates the tracker.
 * 
 * <p>
 * The observable bound to this attribute is often used with multiple component binding declarations 
 * as shown in the example below.
 * </p>
 * 
 * <p>
 * This attribute is only exposed via the <code class="prettyprint">ojComponent</code> binding, and 
 * is not a component option.
 * </p>
 * 
 * @example <caption>Track validity of multiple components using a single observable bound to the <code class="prettyprint">invalidComponentTracker</code> attribute:</caption>
 * <input id="username" type="text" name="username" required 
 *   data-bind="ojComponent: {component: 'ojInputText', value: userName, 
 *                            invalidComponentTracker: tracker}">
 * 
 * <input id="password" type="password" name="password" required 
 *   data-bind="ojComponent: {component: 'ojInputPassword', value: password, 
 *                            invalidComponentTracker: tracker}"/>
 *   
 * // ViewModel that defines the tracker observable
 * <script>
 * function MemberViewModel() 
 * { 
 *   var self = this;
 *   
 *   self.tracker = ko.observable();
 *   
 *   self.userName = ko.observable();   
 *   self.password = ko.observable();
 * }
 * </script>
 * 
 * @example <caption>Use tracker property <code class="prettyprint">invalid</code> to disable button:</caption>
 * // button is disabled if there are components currently showing errors
 * <button type="button" data-bind="ojComponent: {component: 'ojButton', label: 'Submit',
 *                                                disabled: tracker()['invalidShown']}"></button>
 * 
 * @ojbindingonly
 * @member
 * @name invalidComponentTracker
 * @memberof oj.editableValue
 * @instance
 * @type {oj.InvalidComponentTracker}
 * @default <code class="prettyprint">null</code>
 * @since 0.7
 */
/** prevent preceding jsdoc from applying to following line of code */

oj.ValueBinding._ATTRIBUTE_INVALID_COMPONENT_TRACKER = 'invalidComponentTracker';

// An listener is added for this event to listen to changes to the 'messagesHidden' or 
// 'messagesShown' options. The listener updates the InvalidComponentTracker.
oj.ValueBinding._EVENT_OPTIONCHANGE = "ojoptionchange";

// Options we are interested in listening to changes for.
oj.ValueBinding._OPTION_MESSAGES_SHOWN = 'messagesShown';
oj.ValueBinding._OPTION_MESSAGES_HIDDEN = 'messagesHidden';

// options that are managed primarily to detect changes for tracker to be notified.
oj.ValueBinding._OPTION_DISABLED = 'disabled';
oj.ValueBinding._OPTION_READONLY = 'readOnly';


// callback called when managed attribute is being updated
oj.ValueBinding._update = function(name, value, element, component, valueAccessor)
{
  var options = valueAccessor.call(), updateProps = {},
      ictObs = options[oj.ValueBinding._ATTRIBUTE_INVALID_COMPONENT_TRACKER], 
      icTracker;
      
  if (name === oj.ValueBinding._OPTION_DISABLED || name === oj.ValueBinding._OPTION_READONLY)
  {
    icTracker = ictObs && ictObs.peek() || null; // don't add extra subscriptions
    // when either of these options are updated
    if (icTracker !== null && ko.isWriteableObservable(ictObs))
    {
      if (icTracker._update.call(icTracker, component, name, value))
      {
        // if _update mutates state
        ictObs.valueHasMutated();
      }
    }
    updateProps[name] = value;
    return updateProps;
  }
};

// init callback for managed attributes. When managing options like disabled, readOnly
// this method is required to return values.
oj.ValueBinding._init = function(name, value)
{
  var initProps = {};

  initProps[name] = value;
  return initProps;
};

/**
 * Called after component binding creates the component. 
 * @param {string} property 
 * @param {Element} element the element to which binding applied the componnet
 * @param {Function=} component the widget bridge 
 * @param {Object=} valueAccessor 
 * @private
 */
oj.ValueBinding._afterCreate = function(property, element, component, valueAccessor)
{
  var initProps = {}, optionsSet = valueAccessor.call(), isICTOptionSet;

  if (property ===  oj.ValueBinding._ATTRIBUTE_INVALID_COMPONENT_TRACKER)
  {
    isICTOptionSet = optionsSet[property] ? true : false;

    if (isICTOptionSet)
    {
      // register a writeback for invalidComponentTracker property by registering an event listener
      //  for the optionChange event.
      oj.ValueBinding._registerInvalidComponentTrackerWriteback(property, optionsSet, element, component);
    }
  }

  return initProps;
};

/**
 * Called right before component is destroyed.
 *  
 * @param {Element} element
 * @private
 */
oj.ValueBinding._beforeDestroy = function(property, element, component, valueAccessor)
{
  var jelem = $(element), options = valueAccessor.call(), icTracker,
      ictObs = options[property];

  if (property ===  oj.ValueBinding._ATTRIBUTE_INVALID_COMPONENT_TRACKER)
  {
    if (jelem)
    {
      jelem.off(oj.ValueBinding._EVENT_OPTIONCHANGE, oj.ValueBinding._updateInvalidComponentTracker);
      if (ictObs && ko.isWriteableObservable(ictObs))
      {
        icTracker = ictObs.peek(); 
        // remove component from tracker
        if (icTracker._remove.call(icTracker, component))
        {
          // if _remove mutates state, then components need to react to it.
          // example a button that binds to properties on invalidComponentTracker.
          ictObs.valueHasMutated();
        }
      }
    }
  }
};

/**
 * Listener for the optionChange event, it updates the invalidComponentTracker associated to the 
 * component that triggered the event.
 * 
 * @param {jQuery.event=} event
 * @private
 */
oj.ValueBinding._updateInvalidComponentTracker = function(event)
{
  var ictObs = event.data.tracker, icTracker,
      component = event.data.component, payload = arguments[1], option = payload['option'], 
      msgs = payload['value'];
  
  if (option === oj.ValueBinding._OPTION_MESSAGES_SHOWN || 
      option === oj.ValueBinding._OPTION_MESSAGES_HIDDEN)
  {
    if (ictObs && ko.isWriteableObservable(ictObs))
    {
      icTracker = ictObs.peek();
      if (icTracker && icTracker._update.call(icTracker, component, option, msgs))
      {
        // if _update mutates state
        ictObs.valueHasMutated();
      }
    }
  }
};


/**
 * Register a default callback for the 'optionChange' event. The callback writes the component and 
 * its validity to the invalidComponentTracker observable.
 * @param {string} property
 * @param {Object} options original options set on element 
 * @param {Element} element
 * @param {Function=} component
 * @private
 */
oj.ValueBinding._registerInvalidComponentTrackerWriteback = function(property, options, element, component)
{
  var ictObs = options[property], messagesShown, messagesHidden, eventData,
      icTracker, jElem = $(element);

  // Create new intsance of InvalidComponentTracker if the observable is not set.
  if (ko.isObservable(ictObs))
  {
    icTracker = ictObs.peek();
    // push new instance of oj.InvalidComponentTracker onto observable if none present.
    if (icTracker == null) // null or undefined
    {
      icTracker = new oj.InvalidComponentTracker();
      ictObs(icTracker);
    }
  }
  else
  {
    // tracker object is not an observable. 
    throw new Error('Binding attribute ' + oj.ValueBinding._ATTRIBUTE_INVALID_COMPONENT_TRACKER +
            ' should be bound to a ko observable.');
  }

  if (icTracker !== null)
  {
    // update icTracker inital state using component's latest option values
    if (ko.isWriteableObservable(ictObs))
    {
      messagesShown = component.call(component, "option", oj.ValueBinding._OPTION_MESSAGES_SHOWN);
      messagesHidden = component.call(component, "option", oj.ValueBinding._OPTION_MESSAGES_HIDDEN);

      icTracker._update.call(icTracker, component,
              oj.ValueBinding._OPTION_MESSAGES_SHOWN, messagesShown);
      icTracker._update.call(icTracker, component,
              oj.ValueBinding._OPTION_MESSAGES_HIDDEN, messagesHidden);
      ictObs.valueHasMutated();
    }

    // register listener for optionChange event for future changes to messages* options
    eventData = {tracker: ictObs, component: component};
    jElem.on(oj.ValueBinding._EVENT_OPTIONCHANGE, eventData,
            oj.ValueBinding._updateInvalidComponentTracker);
  }
};

/**
 * editableValue Behavior Definition and Injection
 */
oj.ComponentBinding.getDefaultInstance().setupManagedAttributes(
{
  'for': 'editableValue',
  'attributes': [oj.ValueBinding._ATTRIBUTE_INVALID_COMPONENT_TRACKER, 
                 oj.ValueBinding._OPTION_DISABLED,
                  oj.ValueBinding._OPTION_READONLY],
  'init': oj.ValueBinding._init,
  'update': oj.ValueBinding._update,
  'afterCreate': oj.ValueBinding._afterCreate,
  'beforeDestroy': oj.ValueBinding._beforeDestroy
});