milo

List

declaration
 List 

milo.registry.facets.get('List')
Facet enabling list functionality

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

_.extendProto(List, {
    init: List$init,
    start: List$start,
    destroy: List$destroy,

    require: ['Container', 'Dom', 'Data'],
    _itemPreviousComponent: _itemPreviousComponent,

    item: List$item,
    count: List$count,
    contains: List$contains,
    addItem: List$addItem,
    addItems: List$addItems,
    replaceItem: List$replaceItem,
    moveItem: List$moveItem,
    removeItem: List$removeItem,
    extractItem: List$extractItem,
    each: List$each,
    map: List$map,
    _setItem: List$_setItem,
    _removeItem: List$_removeItem,
    _addItem: List$_addItem,
    _addItems: List$_addItems,
    _createCacheTemplate: List$_createCacheTemplate,
    _updateDataPaths: List$_updateDataPaths
});

facetsRegistry.add(List);

module.exports = List;

List$init

function
 List$init() 

Facet instance method
Initialized List facet instance and sets up item properties.

function List$init() {
    ComponentFacet.prototype.init.apply(this, arguments);
    var self = this;

    _.defineProperties(this, {
        _listItems: [],
        _listItemsHash: {}
    });
    _.defineProperty(this, 'itemSample', null, _.WRIT);
}

List$start

function
 List$start() 

Facet instance method
Starts the List facet instance, finds child with Item facet.

function List$start() {
    // Fired by __binder__ when all children of component are bound
    this.owner.on('childrenbound', onChildrenBound);
}


function onChildrenBound() {
    // get items already in the list
    var children = this.dom.children()
        , items = this.list._listItems
        , itemsHash = this.list._listItemsHash;

    if (children) children.forEach(function(childEl) {
        var comp = Component.getComponent(childEl);
        if (comp && comp.item) {
            items.push(comp);
            itemsHash[comp.name] = comp;
            comp.item.list = this.list;
        }
    }, this);

    if (items.length) {
        var foundItem = items[0];
        items.splice(0, 1);
        delete itemsHash[foundItem.name];
        items.forEach(function(item, index) {
            item.item.setIndex(index);
        });
    }

    // Component must have one child with an Item facet
    if (! foundItem) throw new Error('No child component has Item facet');

    this.list.itemSample = foundItem;

    // After keeping a reference to the item sample, it must be hidden and removed from scope.  The item sample will
    // remain in the DOM and as such is marked with a CSS class allowing other code to ignore this element if required.
    foundItem.dom.hide();
    foundItem.remove(true);
    foundItem.dom.addCssClasses(LIST_SAMPLE_CSS_CLASS);

    // remove references to components from sample item
    foundItem.walkScopeTree(function(comp) {
        delete comp.el[miloConfig.componentRef];
    });

    this.list._createCacheTemplate();
}


function List$_createCacheTemplate() {
    if (!this.itemSample) return false;

    var itemSample = this.itemSample;

    // create item template to insert many items at once
    var itemElCopy = itemSample.el.cloneNode(true);
    itemElCopy.classList.remove(LIST_SAMPLE_CSS_CLASS);

    var attr = itemSample.componentInfo.attr;
    var attrCopy = _.clone(attr);
    attr.compName = '{{= it.componentName() }}';
    attr.el = itemElCopy;
    attr.decorate();

    var itemsTemplateStr =
          '{{ var i = it.count; while(i--) { }}'
        + itemElCopy.outerHTML
        + '{{ } }}';

    this.itemsTemplate = doT.compile(itemsTemplateStr);
}

List$item

function
 List$item() 

Option name Type Description
index Integer

The index of the child item to get.

return Component

The component found

Facet instance method
Retrieve a particular child item by index

function List$item(index) {
    return this._listItems[index];
}

List$count

function
 List$count() 

Facet instance method
Gets the total number of child items

function List$count() {
    return this._listItems.length;
}


function List$_setItem(index, component) {
    this._listItems.splice(index, 0, component);
    this._listItemsHash[component.name] = component;
    component.item.list = this;
    component.item.setIndex(+index);
}

List$contains

function
 List$contains() 

Option name Type Description
component Component

The component to look for.

return Boolean

Facet instance method
Returns true if a particular child item exists in the list

function List$contains(component) {
    return this._listItemsHash[component.name] == component;
}

List$addItem

function
 List$addItem() 

Option name Type Description
index Integer

The index to add at

return Component

The newly created component

Facet instance method
Adds a new child component at a particular index and returns the new component.
This method uses data facet, so notification will be emitted on data facet.

function List$addItem(index, itemData) {
    index = isNaN(+index) ? this.count() : +index;
    this.owner.data.splice(index, 0, itemData || {});
    return this.item(index);
}

List$_addItem

function
 List$_addItem() 

Option name Type Description
index Integer

The index to add at

return Component

The newly created component

Facet instance method
Adds a new child component at a particular index and returns the new component

function List$_addItem(index) {
    if (this.item(index))
        throw Error('attempt to create item with ID of existing item');

    // Copy component
    var component = Component.copy(this.itemSample, true);
    var prevComponent = this._itemPreviousComponent(index);

    if (!prevComponent.el.parentNode)
        return logger.warn('list item sample was removed from DOM, probably caused by wrong data. Reset list data with array');

    // Add it to the DOM
    prevComponent.dom.insertAfter(component.el);

    // Add to list items
    this._setItem(index, component);

    // Show the list item component
    component.el.style.display = '';
    component.el.classList.remove(LIST_SAMPLE_CSS_CLASS);

    _updateItemsIndexes.call(this, index + 1);

    return component;
}


function _updateItemsIndexes(fromIndex, toIndex) {
    fromIndex = fromIndex || 0;
    toIndex = toIndex || this.count();
    for (var i = fromIndex; i < toIndex; i++) {
        var component = this._listItems[i];
        if (component)
            component.item.setIndex(i);
        else
            logger.warn('List: no item at position', i);
    }
}


function List$addItems(count, index) { // ,... items data
    var itemsData = _.slice(arguments, 2);
    if (itemsData.length < count)
        itemsData.concat(_.repeat(count - itemsData.length, {}));
    var spliceArgs = [index, 0].concat(itemsData);
    var dataFacet = this.owner.data;
    dataFacet.splice.apply(dataFacet, spliceArgs);
}

List$_addItems

function
 List$_addItems() 

Option name Type Description
count Integer

number of items to add

[index] Integer

optional index of item after which to add

List facet instance method
Adds a given number of items using template rendering rather than adding elements one by one

function List$_addItems(count, index) {
    check(count, Match.Integer);
    if (count < 0)
        throw new Error('can\'t add negative number of items');

    if (count === 0) return;

    var itemsHTML = this.itemsTemplate({
        componentName: componentName,
        count: count
    });

    var wrapEl = document.createElement(this.owner.el.tagName);
    wrapEl.innerHTML = itemsHTML;

    miloBinder(wrapEl, this.owner.container.scope);
    var children = domUtils.children(wrapEl);

    if (count != children.length)
        logger.error('number of items added is different from requested');

    if (children && children.length) {
        var listLength = this.count();
        var spliceIndex = index < 0
                            ? 0
                            : typeof index == 'undefined' || index > listLength
                                ? listLength
                                : index;

        var prevComponent = spliceIndex === 0
                                ? this.itemSample
                                : this._listItems[spliceIndex - 1];

        var frag = document.createDocumentFragment()
            , newComponents = [];

        children.forEach(function(el, i) {
            var component = Component.getComponent(el);
            if (! component)
                return logger.error('List: element in new items is not a component');
            newComponents.push(component);
            this._setItem(spliceIndex++, component);
            frag.appendChild(el);
            el.style.display = '';
        }, this);

        _updateItemsIndexes.call(this, spliceIndex);

        if (!prevComponent.el.parentNode)
            return logger.warn('list item sample was removed from DOM, probably caused by wrong data. Reset list data with array');

        // Add it to the DOM
        prevComponent.dom.insertAfter(frag);

        _.deferMethod(newComponents, 'forEach', function(comp) {
            comp.broadcast('stateready');
        });
    }
}


/**
 * List facet instance method
 * @param {Integer} index The index of the item to remove
 * @return {Array[Object]} The spliced data
 */
function List$removeItem(index) {
    return this.owner.data.splice(index, 1);
}


/**
 * List facet instance method
 * @param {Integer} index The index of the item to extract
 * @return {Component} The extracted item
 */
function List$extractItem(index) {
    var itemComp = this._removeItem(index, false);
    this._updateDataPaths(index, this.count());
    return itemComp;
}


/**
 * List facet instance method
 * Removes item, returns the removed item that is destroyed by default.
 *
 * @param  {Number} index item index
 * @param  {Boolean} doDestroyItem optional false to prevent item destruction, true by default
 * @return {Component}
 */
function List$_removeItem(index, doDestroyItem) {
    var comp = this.item(index);

    if (! comp)
        return logger.warn('attempt to remove list item with id that does not exist');

    this._listItems[index] = undefined;
    delete this._listItemsHash[comp.name];
    if (doDestroyItem !== false) comp.destroy();
    else {
        comp.remove();
        comp.dom.remove();
    }

    this._listItems.splice(index, 1);
    _updateItemsIndexes.call(this, index);

    return comp;
}


function List$replaceItem(index, newItem){
    var oldItem = this.item(index);
    oldItem.dom.insertAfter(newItem.el);
    this._removeItem(index);
    this._setItem(index, newItem);
}


function List$moveItem(fromIndex, toIndex) {
    var componentToMove = this.extractItem(fromIndex);
    var toComponent = this.item(toIndex);

    componentToMove.insertInto(this.owner.el, toComponent.el);

    this._setItem(toIndex, componentToMove);
    _updateItemsIndexes.call(this, 0);
}


// Returns the previous item component given an index
function _itemPreviousComponent(index) {
    while (index >= 0 && ! this._listItems[index])
        index--;

    return index >= 0
                ? this._listItems[index]
                : this.itemSample;
}


// toIndex is not included
// no range checking is made
function List$_updateDataPaths(fromIndex, toIndex) {
    for (var i = fromIndex; i < toIndex; i++) {
        var item = this.item(i);
        if (item)
            item.data._path = '[' + i + ']';
        else
            logger.warn('Data: no item for index', i);
    }
}


/**
 * Facet instance method
 * Similar to forEach method of Array, iterates each of the child items.
 * @param {Function} callback An iterator function to be called on each child item.
 * @param {Any} [thisArg]  Context to set `this`.
 */
function List$each(callback, thisArg) {
    this._listItems.forEach(function(item, index) {
        if (item) callback.apply(this, arguments); // passes item, index to callback
        else logger.warn('List$each: item', index, 'is undefined');
    }, thisArg || this);
}


function List$map(callback, thisArg) {
    return this._listItems.map(function(item, index) {
        if (item) return callback.apply(this, arguments); // passes item, index to callback
        else logger.warn('List$map: item', index, 'is undefined');
    }, thisArg || this);
}


/**
 * Facet instance method
 * Destroys the list
 */
function List$destroy() {
    if (this.itemSample) this.itemSample.destroy(true);
    ComponentFacet.prototype.destroy.apply(this, arguments);
}