tabs.some(function(tab) {
     // if activeTab: 'x' is in both objects, set the active tab
     // and write this better..maybe use ids
     if(objectContains(params, tab.tabDef.params)) {
         tab.setActiveTab(merge(cloneDeep(tab.tabDef), { params: params }));
     }
 });
    before(function (done) {
        registerViewModels({
            adapter: adapterViewModel,
            test_input(node) {
                let value = ko.observable(),
                    error = ko.observable(true),
                    context = this;

                // using subscribe pattern
                if (this.data && this.data.subscribe) {
                    this.data.subscribe(data => {
                        value(data[node.id]);
                    });
                }

                return merge(node, {
                    context,
                    getValue() {
                        return value();
                    },
                    setValue(val) {
                        value(val);
                    },
                    error
                });
            },
            test_parent(node) {
                let mappedChildNodes = createViewModels.call(this, node.children || []);

                return merge(node, {
                    mappedChildNodes
                });
            },
            errors(node) {
                let dict = this.dictionary(),
                    aggregateErrors = function () {
                        return Object.keys(dict).filter(function (id) {
                            return (dict[id].error) ? dict[id].error() : false;
                        });
                    };
                return aggregateErrors;
            }
        });

        adapter = createViewModel(node);
        done();
    });
        dataSourceEndpointArray.forEach((e) => {
            let endpoint = e;
            if (endpoint.uri) {
                console.warn('dataSourceEndpoint expects URI in "target". Please update your JSON to reflect the new syntax');
                endpoint = merge(endpoint, {
                    target: endpoint
                });
                delete endpoint.uri;
            }

            createViewModel.call(context, {
                type: 'action',
                actionType: 'ajax',
                options: endpoint
            }).action({
                callback: function (error, results) {
                    let resultsByKey,
                        keyMapArray = endpoint.keyMap || [{}],
                        newDataObject = {};

                    count += 1;

                    if (!Array.isArray(keyMapArray)) {
                        keyMapArray = [keyMapArray];
                    }

                    if (!error) {
                        keyMapArray.forEach((keyMap) => {
                            resultsByKey = keyMap.resultsKey ? get(results, keyMap.resultsKey) : results;
                            // optional: keyMap.dataKey path to extend dataObject on
                            if (keyMap.dataKey) {
                                newDataObject[keyMap.dataKey] = resultsByKey;
                            } else if (keyMap.storeKey) {
                                noticeboard.setValue(keyMap.storeKey, resultsByKey);
                            } else {
                                newDataObject = resultsByKey;
                            }
                            extend(dataObject, newDataObject);
                        });
                    }

                    if (count === dataSourceEndpointArray.length) {
                        updateData(dataObject);
                        if (!mappedChildNodes().length) {
                            mappedChildNodes(createViewModels.call(context, node.children || []));
                        }
                    }
                }
            });
        });
export default function globalNavigation(node) {
    const routes = ko.observableArray(node.routes),
        navLinks = navigation.navLinks,
        activeLink = navigation.activeLink;

    function walkGetTypes(nodes) {
        return (nodes || [])
            .reduce((types, childNode) => types.concat([childNode.type])
            .concat(walkGetTypes(childNode.children)), []);
    }

    routes().forEach((route) => {
        navigation.addNav(route, (routeInfo) => {
            const name = route.get.replace('{path}', routeInfo.path ? `_${routeInfo.path.replace('/', '_')}` : '');
            dataservice.ajax({ uri: name }, (err, metadata) => {
                if (err) {
                    return;
                }
                const types = _.uniq(walkGetTypes(Array.isArray(metadata) ? metadata : [metadata]))
                    .filter(type =>
                        type && getRegisteredTypes().indexOf(type) === -1
                    );

                // console.log('Missing types:', types);

                layout.content(metadata);
            });
        });
    });

    navigation.init(node.initial || 0);

    routes.subscribe((oldRoutes) => {
        oldRoutes.forEach((routeOptions) => {
            navigation.removeNav(routeOptions.text);
        });
    }, null, 'beforeChange');

    return merge(node, {
        navLinks,
        activeLink,
        dispose: function () {
            routes([]);
        }
    });
}
/**
 * Route action to re-route to another page
 *
 * @module route
 *
 * @param {object} node
 *  The configuration object for the route action
 * @param {string} node.type='action'
 *  The type of the node is action
 * @param {string} node.actionType='route'
 *  The actionType of the node is route
 * @param {string} node.text
 *  The text to display on the button
 * @param {object} node.options
 *  The options pertaining to the route action
 * @param {string} node.options.target
 *  The uri to route to
 * @param {object} node.options.data
 *  The data to send along when routing
 * @param {object} node.options.params
 *  Key-value pairs to merge pass along as data when routing
 * @param {string} node.options.paramsKey
 *  The key of the data for the parameters
 *
 * @example
 * {
 *     "type": "action",
 *     "actionType": "route",
 *     "text": "Add User",
 *     "options": {
 *         "target": "add-user"
 *     }
 *  }
 */
function route(options) {
    let data = unwrap(options.data || (this && this.data)),
        target = unwrap(options.target),
        params;

    if (options.params && options.paramsKey) {
        data = merge(data, options[options.paramsKey]);
    }

    params = options.params ? renderParams(options.params, data) : undefined;

    if (options.renderTarget) {
        target = mustache.render(target, data);
    }

    setRoute(target, params);
}
/**
 * Redirect action to redirect the current page to another
 *
 * @module redirect
 *
 * @param {object} node
 *  The configuration object for the redirect action
 * @param {string} node.type='action'
 *  The type of the node is action
 * @param {string} node.actionType='redirect'
 *  The actionType of the node is redirect
 * @param {string} node.text
 *  The text to display on the button
 * @param {object} node.options
 *  The options pertaining to the redirect action
 * @param {string} node.options.target
 *  The url to redirect to
 * @param {object} node.options.data
 *  The data to mustache render the target url with
 * @param {object} node.options.params
 *  Key-value pairs to merge pass along as data when redirecting
 * @param {string} node.options.paramsKey
 *  The key of the data for the parameters
 *
 * @example
 * {
 *     "type": "action",
 *     "actionType": "redirect",
 *     "text": "Redirect",
 *     "options": {
 *         "target": "https://www.google.com"
 *     }
 * }
 */
function redirect(options) {
    if (!options.target) {
        console.error('Must provide target!');
        return;
    }

    let data = unwrap(options.data || (this && this.data)),
        url;

    if (options.params && options.paramsKey) {
        data = merge(data, options[options.paramsKey]);
    }

    url = mustache.render(options.target, data);

    window.location.replace(url);
}
 function getOptions() {
     return merge({
         theme: 'pjson',
         onResult: onResult,
         onAdd: onAdd,
         onDelete: onDelete,
         preventDuplicates: true,
         minChars: 0,
         caching: false,
         searchDelay: 0,
         animateDropdown: false,
         allowTabOut: true,
         noResultsText: null,
         searchingText: null,
         disabled: ko.unwrap(bindings.tokeninputDisable),
         prePopulate: valueAccessor().filter(result => (value() || []).includes(result.id))
     }, tokeninputOptions);
 }
 tabObj.setActiveTab = function () {
     // merge the params from route with current query as not to overwrite it
     // also merge with the tabObj.tabDef.params so that the tab initializes correctly
     // for the objectContains check below, if the tabDef.params is part of the query it will use that tab as the initial tab
     // so we need to make sure, that if we're setting a tabRoute, that the params of the last tab are overwriten with the params in the current tab
     // so you dont get another route accidentally
     createViewModel.call(context, {
         type: 'action',
         actionType: 'route',
         options: {
             target: tab.route.target,
             params: merge(
                 getCurrent().query,
                 tabObj.tabDef.params,
                 tab.route.params
             )
         }
     }).action();
 }
/**
 * Grid component to display a grid of data
 *
 * @module grid
 *
 * @param {object} node
 *  The configuration object for the grid
 * @param {object} node.data
 *  Initial data to populate the grid with
 * @param {string} node.id
 *  The id of the grid
 * @param {string} node.classes
 *  The classes to apply to the grid
 * @param {string} node.gridHeaderClasses
 *  The classes to apply to the grid header
 * @param {array} node.gridHeader
 *  An array of PJSON components to use as the grid header
 * @param {object} node.dataSourceEndpoint
 *  Configuration object for the grid's data source
 * @param {object} node.dataSourceEndpoint.target
 *  Configuration object for the target of the grid's data source
 * @param {string} node.dataSourceEndpoint.target.uri
 *  The uri endpoint for the grid's data source
 * @param {object} node.dataSourceEndpoint.target.dataMapFunctions
 *  An object of functions to run on the data
 * @param {string} node.dataSourceEndpoint.target.dataMapFunctions.before
 *  Function to run before the data is added to the grid?
 * @param {string} node.dataSourceEndpoint.target.dataMapFunctions.after
 *  Function to run after the data is added to the grid?
 * @param {object|array} node.dataSourceEndpoint.keyMap
 *  A mapper object or array of mapper objects to map keys
 * @param {object} node.pagination
 *  An object to specify pagination for the grid
 * @param {number} node.pagination.start=0
 *  The number of which page to start the grid at
 * @param {number} node.pagination.limit=15
 *  The max number of grid items to show on each page
 * @param {array} node.columns
 *  An array of objects to build the columns
 * @param {object} node.selection
 *  A PJSON action to use when a row is selected
 * @param {object} node.options
 *  The options pertaining to the grid
 * @param {boolean} node.options.infinite
 *  Boolean to specify whether to show infinite items on the grid
 * @param {boolean} node.options.fixedHeader
 *  Boolean to specify if the grid header should be fixed or not
 * @param {object} node.options.footer
 *  Configuration object for the grid footer
 * @param {boolean} node.options.footer.hideOnDone
 *  Boolean to hide the footer once the grid is loaded or not
 * @param {string} node.options.footer.loadingText
 *  A string to show while the grid is loading
 * @param {string} node.options.footer.doneText
 *  A string to show when the grid has finished loading.
 * @param {object} node.options.hasChildren
 *  Configuration object for a grid row's child
 * @param {string} node.options.hasChildren.showIcon
 *  The class to apply to the show child button
 * @param {string} node.options.hasChildren.hideIcon
 *  The class to apply to the hide child button
 * @param {string} node.options.hasChildren.template
 *  The template to apply to the child row
 * @param {boolean} node.options.hasChildren.onRowSelect
 *  Boolean to determine whether to show/hide the child on selecting the row or via a button
 * @param {boolean} node.options.hasChildren.accordion
 *  Boolean to determine if only one child should be shown at a time
 * @param {boolean} node.options.clientSearch
 *  Boolean on whether to search/sort client side
 * @param {boolean|expression} node.options.gridDisplay
 *  Boolean or expression to render the grid programmatically
 * @param {string} node.options.scrollElement
 *  Element to scroll grid on, defaults to scrolling on window
 *
 * @example
 * {
 *      "type": "grid",
 *      "id": "my_grid_id",
 *      "classes": "grid-container",
 *      "gridHeaderClasses": "grid-header",
 *      "options": {
 *          "infinite": true,
 *          "fixedHeader": true,
 *          "footer": {
 *              "hideOnDone": true,
 *              "loadingText": "Loading...",
 *              "doneText": "Loaded all rows."
 *          },
 *          "hasChildren": {
 *              "showIcon": "icon-open",
 *              "hideIcon": "icon-close",
 *              "template": "grid_child_template",
 *              "onRowSelect": true,
 *              "accordion": true,
 *          },
 *      }
 *      "dataSourceEndpoint": {
 *         "target": {
 *              "uri": "endpoint"
 *         },
 *         "keyMap": {
 *             "resultsKey": "data"
 *         }
 *      },
 *      "pagination": {
 *          "start": 0,
 *          "limit": 30
 *      },
 *      "columns": [
 *          {
 *              "data": "colData",
 *              "title": "Column Data Title"
 *          }
 *      ]
 * }
 */
export default function (node) {
    const data = node.data,
        options = node.options,
        columns = node.columns,
        context = this,
        pagination = node.pagination,
        endpoint = node.dataSourceEndpoint,
        rows = ko.observableArray(),
        skip = ko.observable(pagination.start || 0),
        limit = ko.observable(pagination.limit || 15),
        search = ko.observable(''),
        filters = ko.observable({}),
        caseInsensitive = ko.observable(true),
        clientSearch = options.clientSearch,
        selectedItem = ko.observable({}),
        gridContext = {
            search,
            filters,
            rows,
            skip,
            clientSearch,
            caseInsensitive,
            getValue: this.getValue,
            parentContext: context
        },
        loaderNoText = '',
        loaderLoading = 'Loading more...',
        loaderDone = 'Loaded all rows',
        loader = {
            text: ko.observable(loaderNoText),
            done: false,
            inProgress: ko.observable(false)
        },
        subs = [];

    let query,
        queryCallback,
        gridHeaderItems = node.gridHeader || [];

    function setupQuery() {
        query = createViewModel({
            type: 'action',
            actionType: 'ajax',
            options: endpoint
        });
    }

    function setupGetResponse() {
        queryCallback = {
            callback: function (err, results) {
                if (!err) {
                    const key = get(endpoint, 'keyMap.resultsKey'),
                        resultData = key && results ? results[key] : results;
                    rows.push(...resultData);
                    skip(results.skip);
                    loader.inProgress(false);
                    loader.done = results.skip >= results.total;
                    if (loader.done) {
                        loader.text(loaderDone);
                    } else {
                        loader.text(loaderNoText);
                    }
                } else {
                    console.error(`Error in grid query callback: ${err.message || ''}`);
                }
            }
        };
    }

    function sendQuery(isFilter) {
        if (isFilter || (!loader.done && !loader.inProgress())) {
            if (isFilter) {
                skip(0);
                rows.removeAll();
            }
            query.options.target.data = {
                skip: skip(),
                limit: limit()
            };
            // data is in target.data, so it will be sent as is
            if (!clientSearch) {
                query.options.target.data.search = search();
                query.options.target.data.filters = filters();
            }
            loader.inProgress(true);
            loader.text(loaderLoading);
            query.action(queryCallback);

            // TODO: call a resetfilter function to update skip/row observs in here
        }
    }

    function setupGridHeader() {
        gridHeaderItems = gridHeaderItems.map(item => createViewModel.call(gridContext, item));
        // TODO: update to createViewModels after Erica updates mf
        search.extend({ rateLimit: 1000 });
        if (!clientSearch) {
            search.subscribe(() => sendQuery(true));
            filters.subscribe(() => sendQuery(true));
        }
    }

    function setupRefresh() {
        subs.push(receive(`${node.id}.refresh`, () => {
            sendQuery(true);
        }));
    }

    function setupSelection() {
        if (node.selection) {
            selectedItem.subscribe((item) => {
                const action = _.cloneDeep(node.selection),
                    selectionCtx = _.cloneDeep(context);
                selectionCtx.data = item;
                createViewModel.call(selectionCtx, action).action();
            });
        }
    }

    function setupData() {
        if (endpoint) {
            setupQuery();
            setupGetResponse();
            sendQuery();
            setupRefresh();
        } else if (data) {
            rows(data);
        }
    }

    function addRow(row) {
        rows.push(...row);
    }

    // Set up a receiver to push rows to the grid.
    subs.push(receive(`${node.id}.add`, (row) => {
        addRow(row);
    }));


    setupData();
    setupSelection();
    setupGridHeader();

    return merge(node, {
        rows,
        columns,
        sendQuery,
        loader,
        options,
        gridHeaderItems,
        search,
        filters,
        caseInsensitive,
        selectedItem,
        dispose: function () {
            subs.forEach((sub) => {
                sub.dispose();
            });
        }
    });
}
 // when tabs are selected update the tab route
 // do not update tabroute on initialization, just replace
 function setTabRoute(tabDef) {
     var currentRoute = getCurrent().route + (getCurrent().path ? '/' + getCurrent().path : '' ),
         query = getCurrent().query;
     setRoute(tabDef.target || currentRoute, merge(query,tabDef.params), false, initialized);
     initialized = true;
 }
    export default function (node, metadata) {
        var context = this,
            createViewModel = createViewModelUnbound.bind(context), //ensures context is passed
            options = node.options || {},
            mappedChildNodes,
            activeTabRegion = observable({}),
            initialized = false,
            subs = [],
            query = getCurrent().query,
            tabs = [],
            initialActiveTab;

        // when tabs are selected update the tab route
        // do not update tabroute on initialization, just replace
        function setTabRoute(tabDef) {
            var currentRoute = getCurrent().route + (getCurrent().path ? '/' + getCurrent().path : '' ),
                query = getCurrent().query;
            setRoute(tabDef.target || currentRoute, merge(query,tabDef.params), false, initialized);
            initialized = true;
        }

        // zip the children tabs with the tab defs
        node.children.forEach(function (tab, index) {
            var tabTemplate = observable(), // can by dynamic because of ajax tabs
                tabDef = typeof node.headers[index] === 'string' ? { text: node.headers[index] } : node.headers[index],
                tabName = tabDef.computed ? computed(function () {
                    return formatText(tabDef.text, context.getValue);
                }) : tabDef.text,
                tabObj = {
                    tabName: tabName,
                    tabDef: tabDef,
                    tabTemplate: tabTemplate,
                    keepCache: tab.cache || node.cache
                },
                tabViewModel;

           // sets the active tab region to the tab template
           // sets newRoute if defined, else sets tabRoute
           tabObj.setActiveTab = function (newRoute) {
               if (options.validations && options.validations[tabDef.text]) {
                   notify(options.validations[tabDef.text], {
                       successCallback: function() {
                           activeTabRegion(tabTemplate());
                           if (options.setRoute !== false) {
                               setTabRoute(newRoute || tabDef);
                           }
                       }
                   });
               } else {
                    activeTabRegion(tabTemplate());
                    if (options.setRoute !== false) {
                        setTabRoute(newRoute || tabDef);
                    }
               }
           };

           // tab is active when the active region is the template
           tabObj.isActive = computed(function () {
                return activeTabRegion() === tabTemplate();
            });

            // create tabViewModel from type if defined
            // else call createViewModel
            if (tabTypes[tab.type]) {
                tabViewModel = tabTypes[tab.type].call(context, tabObj, tab)
            } else {
                tabViewModel = createViewModel(tab)
                tabTemplate(template('metadata_item_template', tabViewModel));
            }

            extend(tabObj, tabViewModel);

            // visible expression binding using context' getValue
            if(has(tabDef.visible)) {
                tabObj.visible = is(tabDef.visible, 'boolean') ?
                    tabDef.visible
                    : computed(function() {
                        return evaluate(tabDef.visible, context.getValue || function (id) {
                            console.error('Trying to evaluate a binding when getValue isnt specified on the context', tabObj);
                        });
                    });
            } else {
                tabObj.visible = true;
            }

            tabs.push(tabObj);

            // sets the active tab if defined in the query or tabDef
            if(tabDef.isActive || tabDef.params && query && objectContains(query, tabDef.params)) {
                initialActiveTab = tabObj;
            }
        });

        if (initialActiveTab) {
            initialActiveTab.setActiveTab();
        } else if (!activeTabRegion().template) {
            // initialize to first tab if we havent routed to a specific tab

            // will set first visible tab to active tab
            let initialTab = tabs.filter((tab) => {
                return unwrap(tab.visible);
            })[0];

            initialTab && initialTab.setActiveTab();
        }

        // receive events to set active tab
        if(node.id) {
            subs.push(receive(node.id +'.setActiveTab', function (params) {
                tabs.some(function(tab) {
                    // if activeTab: 'x' is in both objects, set the active tab
                    // and write this better..maybe use ids
                    if(objectContains(params, tab.tabDef.params)) {
                        tab.setActiveTab(merge(cloneDeep(tab.tabDef), { params: params }));
                    }
                });
            }));

            subs.push(receive(node.id+'.setNextTab', function () {
                var currentIndex =  0,
                    newIndex;

                tabs.some(function(tab, index) {
                    if (activeTabRegion() == tab.tabTemplate()) {
                        currentIndex = index;
                    }
                })

                while(newIndex === undefined) {
                    if (!tabs[(currentIndex+1) % tabs.length].isChild) {
                        newIndex = currentIndex +1;
                    } else {
                        currentIndex++
                    }
                }

                tabs[newIndex].setActiveTab();
            }));
        }

        return merge(node, {
            tabs: tabs,
            mappedChildNodes: tabs,
            activeTabRegion: activeTabRegion,
            context: this,
            dispose: function () {
                subs.forEach(function (sub) {
                    sub.dispose();
                });

                tabs.forEach(function (tab) {
                    var tabViewModels = tab.tabTemplate() && tab.tabTemplate().template.data;
                    // why is tab viewmodels an array, anyway?
                    if (tabViewModels && !Array.isArray(tabViewModels)) {
                        tabViewModels = [tabViewModels];
                    }
                    (tabViewModels || []).forEach(function(vm) {
                        vm && vm.dispose && vm.dispose();
                    });
                });
            }
        });
    };
/* TODO:
In PJSON, we used readonly, errors, etc. We need a way to do that outside of adapter
i.e. plugin to adapter context with other components
*/

/** Adapter: a viewless component which keeps track of child nodes and the data for the nodes
 * @module adapter
 *
 * @param {object} node
 *  The configuration object for the module
 * @param {string} node.type='adapter'
 *  The type of the node is adapter
 * @param {string} node.id
 *  The id for the module
 * @param {boolean} [node.lazy=false]
 *  If the child nodes need to be lazily loaded
 * (e.g. delay creation of children viewmodels until data returns)
 * @param {boolean} [node.persist=false]
 *  If data object should be persisted from one fetch data call to the next (upon refresh)
 * @param {object|Object[]} [node.dataSourceEndpoint]
 *  An object defining the endpoint(s) that makes the ajax calls
 * @param {string} node.dataSourceEndpoint.uri
 *   The uri for the endpoint
 * @param {string} [node.dataSourceEndpoint.url]
 *  The url for the endpoint
 * @param {array|object} [node.dataSourceEndpoint.keyMap]
 *  A mapper object or array of mapper objects to map keys
 * @param {string} [node.dataSourceEndpoint.keyMap.resultsKey]
 *  Map the results from the ajax call with this key
 * @param {string} [node.dataSourceEndpoint.keyMap.dataKey]
 *  Extend the data object with this key
 * @param {string} [node.dataSourceEndpoint.keyMap.storeKey]
 *  Place the resultsByKey inside of the store with this key
 * @param {object} [node.dataSourceEndpoint.options]
 *  Options for the ajax call
 * @param {array} node.children
 *  The json configuration for children nodes which will be mapped
 * to view models and kept track of from the adapter
 * @param {array} [node.plugins]
 *  The json configuration for plugins which will be accessible
 * from getValue function, based upon type
 *
 * @property {array} mappedChildNodes the mapped children nodes
 * @property {observable} data the data retrieved from dataSourceEndpoint and tracked from children
 * @property {object} contextPlugins an object that contains the plugins which have
 * been added to the adapter context
 * @property {context} the context for the adapter (which can be utilized in a custom template)
 * @property {function} dispose the dispose function for all internal subs
 *
 * @example
 * {
 *      "type": "adapter",
 *      "id": "ADAPTER_ID",
 *      "dataSourceEndpoint": [
 *          {
 *              "uri": "endpoint/uri",
 *              "options": {
 *                  "type": "PUT"
 *              },
 *              "keyMap": {
 *                  "dataKey": "a",
 *                  "resultsKey": "b"
 *              }
 *          }
 *      ],
 *      "children": [
 *          // children json configuration goes here
 *      ]
 * }
 */
export default function adapterViewModel(node) {
    const dictionary = observable({}), // dictionary of nodes with an id
        data = observable({}), // data of dictionary contents
        context = {
            metadata: node.children,
            parentContext: this,
            getValue: getValue,
            dictionary: dictionary,
            data: data,
            id: node.id
        },
        mappedChildNodes = observableArray(),
        subs = [],
        plugins = node.plugins ? createViewModels.call(context, node.plugins) : [],
        contextPlugins = {};

    let dataSyncSubscription,
        updated = false;

    plugins.forEach((plugin) => {
        contextPlugins[plugin.type] = plugin;
    });

    // recursive function which parses through nodes and adds nodes with an id to dictionary
    function createDictionary(nodes) {
        const dict = dictionary.peek();
        nodes.forEach((n) => {
            // add node to dictionary if it isnt there yet
            if (n.id && !dict[n.id]) {
                dict[n.id] = n;
                updated = true;
            }
            // add children to dictionary if getValue function is not exposed
            if (!n.getValue) {
                createDictionary(unwrap(n.mappedChildNodes) || []);
            }
        });
    }

    // keep the data current if the node value changed with dataSyncDescription
    function syncDataDictionary() {
        dataSyncSubscription = computed(() => {
            const dict = dictionary();
            Object.keys(dict).forEach((id) => {
                if (dict[id].rendered) {
                    if (dict[id].rendered() && dict[id].getValue) {
                        data()[id] = dict[id].getValue();
                    } else if (!dict[id].rendered()) {
                        if (dict[id].trackIfHidden) {
                            data()[id] = dict[id].getValue();
                        } else {
                            delete data()[id];
                        }
                    }
                }
            });
        });
    }

    // pause dataSyncDescription and update the data
    function updateData(newData) {
        dataSyncSubscription && dataSyncSubscription.dispose();
        data(newData);
        syncDataDictionary();
    }

    // fetches the data from dataSourceEndpoint(s)
    function fetchData() {
        const dataSourceEndpointArray = Array.isArray(node.dataSourceEndpoint)
            ? node.dataSourceEndpoint : [node.dataSourceEndpoint],
            dataObject = node.persist ? data() : {};
        let count = 0;

        dataSourceEndpointArray.forEach((e) => {
            let endpoint = e;
            if (endpoint.uri) {
                console.warn('dataSourceEndpoint expects URI in "target". Please update your JSON to reflect the new syntax');
                endpoint = merge(endpoint, {
                    target: endpoint
                });
                delete endpoint.uri;
            }

            createViewModel.call(context, {
                type: 'action',
                actionType: 'ajax',
                options: endpoint
            }).action({
                callback: function (error, results) {
                    let resultsByKey,
                        keyMapArray = endpoint.keyMap || [{}],
                        newDataObject = {};

                    count += 1;

                    if (!Array.isArray(keyMapArray)) {
                        keyMapArray = [keyMapArray];
                    }

                    if (!error) {
                        keyMapArray.forEach((keyMap) => {
                            resultsByKey = keyMap.resultsKey ? get(results, keyMap.resultsKey) : results;
                            // optional: keyMap.dataKey path to extend dataObject on
                            if (keyMap.dataKey) {
                                newDataObject[keyMap.dataKey] = resultsByKey;
                            } else if (keyMap.storeKey) {
                                noticeboard.setValue(keyMap.storeKey, resultsByKey);
                            } else {
                                newDataObject = resultsByKey;
                            }
                            extend(dataObject, newDataObject);
                        });
                    }

                    if (count === dataSourceEndpointArray.length) {
                        updateData(dataObject);
                        if (!mappedChildNodes().length) {
                            mappedChildNodes(createViewModels.call(context, node.children || []));
                        }
                    }
                }
            });
        });
    }

    function getValue(id) {
        const dictNode = dictionary()[id],
            dataValue = (data() || {})[id];

        // the node has been defined so get the value from the node
        if (dictNode && dictNode.getValue) { return dictNode.getValue(); }

        // data has been defined for the node but the node doesnt exist yet
        if (dataValue) { return dataValue; }

        if (contextPlugins && contextPlugins[id]) {
            return contextPlugins[id]();
        }
        return context.parentContext.getValue(id);
    }

    if (node.keepContextData) {
        data(unwrap(this.data) || {});
    }

    if (!node.lazy) {
        mappedChildNodes(createViewModels.call(context, node.children || []));
    }

    // update dictionary if mappedChildNodes of a node updates
    computed(() => {
        updated = false;
        createDictionary(mappedChildNodes());
        if (updated) {
            dictionary.valueHasMutated();
        }
    });

    // initialize the data subscription
    syncDataDictionary();

    // get initial data
    if (node.dataSourceEndpoint) {
        fetchData();
    }

    // listen for 'refresh' event
    subs.push(receive(`${node.id}.refresh`, (options) => {
        // console.log('-->', node);
        if (node.dataSourceEndpoint) {
            fetchData(options);
        } else {
            Object.keys(dictionary()).forEach((key) => {
                dictionary()[key].setValue && dictionary()[key].setValue('');
            });
        }
    }));

    return merge(node, {
        mappedChildNodes: mappedChildNodes,
        data: data,
        contextPlugins: contextPlugins,
        context: context,
        dispose: function () {
            subs.forEach((sub) => {
                sub.dispose();
            });
        }
    });
}