milo

Data

declaration
 Data 

milo.registry.facets.get('Data')
Facet to give access to DOM data

var Data = _.createSubclass(ComponentFacet, 'Data');

Data facet instance methods

  • start - start Data facet
  • get - get DOM data from DOM tree
  • set - set DOM data to DOM tree
  • path - get reference to Data facet by path
_.extendProto(Data, {
    start: Data$start,
    getState: Data$getState,
    setState: Data$setState,

    get: Data$get,
    set: Data$set,
    del: Data$del,
    splice: Data$splice,
    len: Data$len,
    path: Data$path,
    getPath: Data$getPath,
    getKey: Data$getKey,

    _get: Data$_get,
    _set: Data$_set,
    _del: Data$_del,
    _splice: Data$_splice,
    _len: Data$_len,

    _setScalarValue: Data$_setScalarValue,
    _getScalarValue: Data$_getScalarValue,
    _bubbleUpDataChange: Data$_bubbleUpDataChange,
    _queueDataChange: Data$_queueDataChange,
    _postDataChanges: Data$_postDataChanges,
    _prepareMessageSource: _prepareMessageSource
});

facetsRegistry.add(Data);

module.exports = Data;

ModelPath methods added to Data prototype

['push', 'pop', 'unshift', 'shift'].forEach(function(methodName) {
    var method = Model.Path.prototype[methodName];
    _.defineProperty(Data.prototype, methodName, method);
});



// these methods will be wrapped to support "*" pattern subscriptions
var proxyDataSourceMethods = {
        // value: 'value',
        trigger: 'trigger'
    };

Data$start

function
 Data$start() 

Data facet instance method
Starts Data facet
Called by component after component is initialized.

function Data$start() {
    // change messenger methods to work with "*" subscriptions (like Model class)
    pathUtils.wrapMessengerMethods.call(this);

    ComponentFacet.prototype.start.apply(this, arguments);

    // get/set methods to set data of element
    this.elData = getElementDataAccess(this.owner.el);

    this._dataChangesQueue = [];

    this._prepareMessageSource();

    // store facet data path
    this._path = '.' + this.owner.name;

    // current value
    this._value = this.get();

    // prepare internal and external messengers
    // this._prepareMessengers();

    // subscribe to DOM event and accessors' messages
    this.onSync('', onOwnDataChange);

    // message to mark the end of batch on the current level
    this.onSync('datachangesfinished', onDataChangesFinished);

    // changes in scope children with Data facet
    this.onSync('childdata', onChildData);

    // to enable reactive connections
    this.onSync('changedata', changeDataHandler);
}

Data facet instance method
Create and connect internal and external messengers of Data facet.
External messenger's methods are proxied on the Data facet and they allows "*" subscriptions.

// function _prepareMessengers() {
    // Data facet will post all its changes on internal messenger
    // var internalMessenger = new Messenger(this);

    // message source to connect internal messenger to external
    // var internalMessengerSource = new MessengerMessageSource(this, undefined, new ModelMsgAPI, internalMessenger);

    // external messenger to which all model users will subscribe,
    // that will allow "*" subscriptions and support "changedata" message api.
    // var externalMessenger = new Messenger(this, Messenger.defaultMethods, internalMessengerSource);

//     _.defineProperties(this, {
//         _messenger: externalMessenger,
//         _internalMessenger: internalMessenger
//     });
// }

onDataChangesFinished

function
 onDataChangesFinished() 

Option name Type Description
msg String
[data] Object

Subscriber to datachangesfinished event.
Calls the method to post changes batch and bubbles up the message

function onDataChangesFinished(msg, data) {
    this._postDataChanges(data.inTransaction);
    var parentData = this.scopeParent();
    if (parentData) parentData.postMessage('datachangesfinished', data);
}

Data$set

function
 Data$set() 

Option name Type Description
value Object, String, Number

value to be set. If the value if scalar, it will be set on component's element, if the value is object - on DOM tree inside component

return Object, String, Number

Data facet instance method
Sets data in DOM hierarchy recursively.
Returns the object with the data actually set (can be different, if components matching some properties are missing).

function Data$set(value) {
    var inTransaction = getTransactionFlag(Data$set);

    try {
        return executeHook.call(this, 'set', arguments);
    } catch (e) {
        if (e != noHook) throw e;
    }

    setTransactionFlag(this._set, inTransaction);

    var oldValue = this._value
        , newValue = this._set(value);

    // this message triggers onOwnDataChange, as well as actuall DOM change
    // so the parent gets notified
    var msg = { path: '', type: 'changed',
                newValue: newValue, oldValue: oldValue };
    setTransactionFlag(msg, inTransaction);
    this.postMessage('', msg);

    return newValue;
}


function Data$_set(value) {
    var inTransaction = getTransactionFlag(Data$_set);

    var valueSet;
    if (value !== null && typeof value == 'object') {
        if (Array.isArray(value)) {
            valueSet = [];

            var listFacet = this.owner.list;
            if (listFacet){
                var listLength = listFacet.count()
                    , newItemsCount = value.length - listLength;
                if (newItemsCount >= 3) {
                    listFacet._addItems(newItemsCount);
                    listFacet._updateDataPaths(listLength, listFacet.count());
                }

                value.forEach(function(childValue, index) {
                    setChildData.call(this, valueSet, childValue, index, '[$$]');
                }, this);

                var listCount = listFacet.count()
                    , removeCount = listCount - value.length;

                while (removeCount-- > 0)
                    listFacet._removeItem(value.length);
            } else
                logger.warn('Data: setting array data without List facet');
        } else {
            valueSet = {};
            _.eachKey(value, function(childValue, key) {
                setChildData.call(this, valueSet, childValue, key, '.$$');
            }, this);
        }
    } else
        valueSet = this._setScalarValue(value);

    this._value = valueSet;

    return valueSet;


    function setChildData(valueSet, childValue, key, pathSyntax) {
        var childPath = pathSyntax.replace('$$', key);
        var childDataFacet = this.path(childPath, typeof childValue != 'undefined');
        if (childDataFacet) {
            setTransactionFlag(childDataFacet.set, inTransaction);
            valueSet[key] = childDataFacet.set(childValue);
        }
    }
}

Data$del

function
 Data$del() 

Data facet instance method
Deletes component from view and scope, only in case it has Item facet on it

function Data$del() {
    var inTransaction = getTransactionFlag(Data$del);

    try {
        var result = executeHook.call(this, 'del');
        postTransactionFinished.call(this, inTransaction);
        return result;
    } catch (e) {
        if (e != noHook) throw e;
    }

    var oldValue = this._value;

    setTransactionFlag(this._del, inTransaction);
    this._del();

    // this message triggers onOwnDataChange, as well as actuall DOM change
    // so the parent gets notified
    var msg = { path: '', type: 'deleted', oldValue: oldValue };
    setTransactionFlag(msg, inTransaction);
    this.postMessage('', msg);
}


function Data$_del() {
    var inTransaction = getTransactionFlag(Data$_del);
    setTransactionFlag(this._set, inTransaction);
    this._set();
}

Data$get

function
 Data$get() 

Option name Type Description
deepGet Boolean

true by default

return Object

Data facet instance method
Get structured data from DOM hierarchy recursively
Returns DOM data

function Data$get(deepGet) {
    try {
        return executeHook.call(this, 'get', arguments);
    } catch (e) {
        if (e != noHook) throw e;
    }

    return this._get(deepGet);
}

function Data$_get(deepGet) {
    if (deepGet === false) // a hack to enable getting shallow state
        return;

    var comp = this.owner
        , scopeData;

    if (comp.list) {
        scopeData = [];
        comp.list.each(function(listItem, index) {
            scopeData[index] = listItem.data.get();
        });

        if (comp.container)
            comp.container.scope._each(function(scopeItem, name) {
                if (! comp.list.contains(scopeItem) && scopeItem.data)
                    scopeData[name] = scopeItem.data.get();
            });
    } else if (comp.container) {
        scopeData = {};
        comp.container.scope._each(function(scopeItem, name) {
            if (scopeItem.data)
                scopeData[name] = scopeItem.data.get();
        });
    } else
        scopeData = this._getScalarValue();

    this._value = scopeData;

    return scopeData;
}

Data$splice

function
 Data$splice() 

Option name Type Description
spliceIndex Integer

index to delete/insert at

spliceHowMany Integer

number of items to delete

arguments List

optional items to insert

return Array

Data facet instance method
Splices List items. Requires List facet to be present on component. Works in the same way as array splice.
Returns data retrieved from removed items

function Data$splice(spliceIndex, spliceHowMany) { //, ... arguments
    var inTransaction = getTransactionFlag(Data$splice);
    var result;

    try {
        result = executeHook.call(this, 'splice', arguments);
        postTransactionFinished.call(this, inTransaction);
        return result;
    } catch (e) {
        if (e != noHook) throw e;
    }

    setTransactionFlag(this._splice, inTransaction);
    result = this._splice.apply(this, arguments);

    if (!result) return;

    var msg = { path: '', type: 'splice',
                index: result.spliceIndex,
                removed: result.removed,
                addedCount: result.addedCount,
                newValue: this._value };
    setTransactionFlag(msg, inTransaction);
    this.postMessage('', msg);

    return result.removed;
}


var noHook = {};
function executeHook(methodName, args) {
    var hook = this.config[methodName];
    switch (typeof hook) {
        case 'function':
            return hook.apply(this.owner, args);

        case 'string':
            return this.owner[hook].apply(this.owner, args);

        default:
            throw noHook;
    }
}


function Data$_splice(spliceIndex, spliceHowMany) { //, ... arguments
    var inTransaction = getTransactionFlag(Data$_splice);

    var listFacet = this.owner.list;
    if (! listFacet)
        return logger.warn('Data: cannot use splice method without List facet');

    var removed = [];

    var listLength = listFacet.count();
    arguments[0] = spliceIndex =
        modelUtils.normalizeSpliceIndex(spliceIndex, listLength);

    if (spliceHowMany > 0 && listLength > 0) {
        for (var i = spliceIndex; i < spliceIndex + spliceHowMany; i++) {
            var item = listFacet.item(spliceIndex);
            if (item) {
                var itemData = item.data.get();
                listFacet._removeItem(spliceIndex);
            } else
                logger.warn('Data: no item for index', i);

            removed.push(itemData);
        }

        listFacet._updateDataPaths(spliceIndex, listFacet.count());
    }

    var added = [];

    var argsLen = arguments.length
        , addItems = argsLen > 2
        , addedCount = argsLen - 2;
    if (addItems) {
        listFacet._addItems(addedCount, spliceIndex);
        for (var i = 2, j = spliceIndex; i < argsLen; i++, j++) {
            item = listFacet.item(j);
            if (item) {
                setTransactionFlag(item.data.set, inTransaction);
                itemData = item.data.set(arguments[i]);
            } else
                logger.warn('Data: no item for index', j);

            added.push(itemData);
        }

        // change paths of items that were added and items after them
        listFacet._updateDataPaths(spliceIndex, listFacet.count());
    }

    // if (Array.isArray(this._value)) {
    //     _.prependArray(added, [spliceIndex, spliceHowMany]);
    //     Array.prototype.splice.apply(this._value, added);
    // } else
        this._value = this.get();

    return {
        spliceIndex: spliceIndex,
        removed: removed,
        addedCount: addItems ? addedCount : 0
    };
}


function Data$len() {
    try {
        return executeHook.call(this, 'len');
    } catch (e) {
        if (e != noHook) throw e;
    }
    
    return this._len();
}


function Data$_len() {
    if (this.owner.list) return this.owner.list.count();
    else logger.error('Data: len called without list facet');
}

Data$path

function
 Data$path() 

Option name Type Description
accessPath String

data access path

Data facet instance method
Returns data facet of a child component (by scopes) corresponding to the path

function Data$path(accessPath, createItem) {
    // createItem = true; // this hack seems to be no longer needed...

    if (! accessPath)
        return this;

    var parsedPath = pathUtils.parseAccessPath(accessPath);
    var currentComponent = this.owner;

    for (var i = 0, len = parsedPath.length; i < len; i++) {
        var pathNode = parsedPath[i]
            , nodeKey = pathUtils.getPathNodeKey(pathNode);
        if (pathNode.syntax == 'array' && currentComponent.list) {
            var itemComponent = currentComponent.list.item(nodeKey);
            if (! itemComponent && createItem !== false) {
                itemComponent = currentComponent.list._addItem(nodeKey);
                itemComponent.data._path = pathNode.property;
            }
            currentComponent = itemComponent;
        } else if (currentComponent.container)
            currentComponent = currentComponent.container.scope[nodeKey];

        var currentDataFacet = currentComponent && currentComponent.data;
        if (! currentDataFacet)
            break;
    }

    return currentDataFacet;
}

Data$getPath

function
 Data$getPath() 

Data facet instance method
Returns path to access this data facet from parent (using path method)

function Data$getPath() {
    return this._path;
}

Data$getKey

function
 Data$getKey() 

Data facet instance method
Returns key to access the value related to this data facet on the value related to parent data facet.
If component has List facet, returns index

function Data$getKey() {
    var path = this._path;
    return path[0] == '['
            ? +path.slice(1, -1) // remove "[" and "]"
            : path.slice(1); // remove leading "."
}

Data$getState

function
 Data$getState() 

Option name Type Description
deepState, Boolean

true by default

return Object

Data facet instance method
Called by Component.prototype.getState to get facet's state
Returns DOM data

function Data$getState(deepState) {
    return { state: this.get(deepState) };
}

Data$setState

function
 Data$setState() 

Option name Type Description
state Object

data to set on facet's model

Data facet instance method
Called by Component.prototype.setState to set facet's state
Simply sets model data

function Data$setState(state) {
    return this.set(state.state);
}