Source: ui.js

/** @module ui */

import h from 'virtual-dom/h';
import {fromJS, Map, List, Iterable} from 'immutable';


/**
 * Accepts a Nux vDOM object and returns a VirtualNode. The vDOM object's 'children' are recursively converted
 * to VirtualNodes. The 'props' for any given are modified such that any custom events are assigned callbacks
 * which dispatch the associated actions. The Nux default event handlers are also added to the 'props' object
 * for a given node. This is where all the magic happens, folks.
 *
 * @author Mark Nutter <marknutter@gmail.com>
 *
 * @param {Store} store       A redux store
 * @param {Object} ui         The Nux vDOM object to be recursively converted into a VirtualNode
 * @param {Array} [pathArray] The location of the provided vDOM object within another vDOM object (if applicable)
 * @return {Store}            Redux store where app state is maintained
 */
export function renderUI (store, ui, pathArray = List()) {

  // ui objects come as a key/value pair so extract the key as the tag name and value as the node data

  const tagName = ui.findKey(() => { return true });
  const node = ui.get(tagName);

  // keep track of our current location in the ui vDOM object
  const currentPathArray = pathArray.size === 0 ? pathArray.push(tagName) : pathArray;
  let children = List(),
      props = node.get('props') || Map();

  let childNodes = node.filterNot((val, key) => { return key === 'props' });

  // recurse through this node's children and render their UI as hyperscript
  if (childNodes) {
    children = childNodes.map((childVal, childTagName) => {
      if (childTagName === '$text') {
        return new List([childVal]);
      } else {
        return renderUI(store, (new Map()).set(childTagName, childVal), currentPathArray.concat([childTagName]));
      }
    }).toList();
  }

  // add an event handler to inputs that updates their 'val' prop node when changes are detected
  let registeredKeyEvents = {};
  if (tagName.indexOf('input') > -1) {
    props = props.set('ev-keyup', (e) => {
      e.preventDefault();
        store.dispatch({
          type: '_UPDATE_INPUT_VALUE',
          val: e.target.value,
          pathArray: ['ui'].concat(currentPathArray.toJS())
        });
      registeredKeyEvents['ev-keyup'] ? registeredKeyEvents['ev-keyup'](e) : false;
      registeredKeyEvents[`ev-keyup-${e.keyCode}`] ? registeredKeyEvents[`ev-keyup-${e.keyCode}`](e) : false;
    });
  }

  // for any custom events detected, add callbacks that dispatch provided actions
  if (props.get('events')) {
    props = props.get('events').reduce((oldProps, val, key) => {
      if (key.indexOf('ev-keyup') > -1) {
        registeredKeyEvents[key] = (e) => {
          fireDispatch(store, val, e);
          createAction(store, val, e);
        };
        return oldProps;
      } else {
        return oldProps.set(key, (e) => {
          e.preventDefault();
          fireDispatch(store, val, e);
          createAction(store, val, e);
        });
      }
    }, props).delete('events');
  }

  // combine tag, props, and children into an array of plain javascript objects and return hyperscript VirtualNode
  return h.apply(this, [tagName, props.toJS(), children.toJS()]);
};

function fireDispatch(store, val, event) {
  if (val.get('dispatch')) {
    store.dispatch(val.get('dispatch').merge(Map({event})).toJS());
  }
}

function createAction(store, val, event) {
  if (val.get('action')) {
    // if the action is an object, pass that object along as the argument to the action creator
    if (typeof val.getIn(['action', 'type']) === 'string') {
      let actionCreator = store.getActionCreator(val.getIn(['action', 'type']))
      let action = val.get('action').merge(Map({event})).toJS();
      store.dispatch(actionCreator(action));
    }
    // if the action is just the action name, then fire it without any arguments
    else if(typeof val.get('action') === 'string') {
      let actionCreator = store.getActionCreator(val.get('action'));
      let action = Map({type: val.get('action'), event}).toJS();
      store.dispatch(actionCreator(action));
    }
  }
}