diff --git a/src/scripts/lib/clay-config.js b/src/scripts/lib/clay-config.js index eef7bad..02e9260 100644 --- a/src/scripts/lib/clay-config.js +++ b/src/scripts/lib/clay-config.js @@ -30,90 +30,11 @@ var componentStore = require('./component-registry'); function ClayConfig(settings, config, $rootContainer) { var self = this; - self.test = Math.random(); - var _settings = _.copyObj(settings); var _items = []; var _itemsById = {}; var _itemsByAppKey = {}; - - self.EVENTS = { - /** - * Called before framework has initialized. This is when you would attach your - * custom components. - * @const - */ - BEFORE_BUILD: 'BEFORE_BUILD', - - /** - * Called after the config has been parsed and all components have their initial value - * set - * @const - */ - AFTER_BUILD: 'AFTER_BUILD' - }; - utils.updateProperties(self.EVENTS, {writable: false}); - - /** - * @param {string} key - * @returns {ClayItem} - */ - self.getItemByAppKey = function(key) { - return _itemsByAppKey[key]; - }; - - /** - * @param {string} key - * @returns {ClayItem} - */ - self.getItemById = function(key) { - return _itemsById[key]; - }; - - /** - * @param {string} key - * @returns {[ClayItem]} - */ - self.getItemsByType = function(type) { - return _items.filter(function(item) { - return item.config.type === type; - }); - }; - - /** - * @returns {object} - */ - self.getSettings = function() { - _.eachObj(_itemsByAppKey, function(appKey, item) { - _settings[appKey] = item.get(); - }); - return _settings; - }; - - /** - * Register a component to Clay. This must be called prior to .build(); - * @param {{}} component - the clay component to register - * @param {string} component.name - the name of the component - * @param {string} component.template - HTML template to use for the component - * @param {{}} component.manipulator - methods to attach to the component - * @param {function} component.manipulator.set - set manipulator method - * @param {function} component.manipulator.get - get manipulator method - * @param {{}} component.defaults - template defaults - * @param {function} [component.initialize] - method to scaffold the component - */ - self.registerComponent = function(component) { - componentStore[component.name] = component; - }; - - self.build = function() { - self.trigger(self.EVENTS.BEFORE_BUILD); - // initialize the config - _addItems(config, $rootContainer); - self.trigger(self.EVENTS.AFTER_BUILD); - }; - - // attach event methods - ClayEvents.call(self, $rootContainer); + var _isBuilt = false; /** * Add item(s) to the config @@ -153,6 +74,116 @@ function ClayConfig(settings, config, $rootContainer) { } }; + /** + * + * @param {string} fnName + * @private + */ + var _checkBuilt = function(fnName) { + if (!_isBuilt) { + throw new Error( + 'ClayConfig not built. build() must be run before ' + + 'you can run ' + fnName + '()' + ); + } + return true; + }; + + self.EVENTS = { + /** + * Called before framework has initialized. This is when you would attach your + * custom components. + * @const + */ + BEFORE_BUILD: 'BEFORE_BUILD', + + /** + * Called after the config has been parsed and all components have their initial + * value set + * @const + */ + AFTER_BUILD: 'AFTER_BUILD' + }; + utils.updateProperties(self.EVENTS, {writable: false}); + + /** + * @returns {ClayItem} + */ + self.getAllItems = function() { + _checkBuilt('getAllItems'); + return _items; + }; + + /** + * @param {string} key + * @returns {ClayItem} + */ + self.getItemByAppKey = function(key) { + _checkBuilt('getItemByAppKey'); + return _itemsByAppKey[key]; + }; + + /** + * @param {string} key + * @returns {ClayItem} + */ + self.getItemById = function(key) { + _checkBuilt('getItemById'); + return _itemsById[key]; + }; + + /** + * @param {string} key + * @returns {[ClayItem]} + */ + self.getItemsByType = function(type) { + _checkBuilt('getItemsByType'); + return _items.filter(function(item) { + return item.config.type === type; + }); + }; + + /** + * @returns {object} + */ + self.getSettings = function() { + _checkBuilt('getSettings'); + _.eachObj(_itemsByAppKey, function(appKey, item) { + _settings[appKey] = item.get(); + }); + return _settings; + }; + + /** + * Register a component to Clay. This must be called prior to .build(); + * @param {{}} component - the clay component to register + * @param {string} component.name - the name of the component + * @param {string} component.template - HTML template to use for the component + * @param {{}} component.manipulator - methods to attach to the component + * @param {function} component.manipulator.set - set manipulator method + * @param {function} component.manipulator.get - get manipulator method + * @param {{}} component.defaults - template defaults + * @param {function} [component.initialize] - method to scaffold the component + */ + self.registerComponent = function(component) { + componentStore[component.name] = component; + }; + + /** + * Build the config page. This must be run before any of the get methods can be run + * @returns {ClayConfig} + */ + self.build = function() { + self.trigger(self.EVENTS.BEFORE_BUILD); + _addItems(config, $rootContainer); + _isBuilt = true; + self.trigger(self.EVENTS.AFTER_BUILD); + return self; + }; + + // attach event methods + ClayEvents.call(self, $rootContainer); + // prevent external modifications of properties utils.updateProperties(self, { writable: false, configurable: false }); diff --git a/src/scripts/lib/clay-item.js b/src/scripts/lib/clay-item.js index 08c2808..fdd2ac8 100644 --- a/src/scripts/lib/clay-item.js +++ b/src/scripts/lib/clay-item.js @@ -15,6 +15,12 @@ function ClayItem(config) { var self = this; var _itemType = componentRegistry[config.type]; + + if (!_itemType) { + throw new Error('the component: ' + config.type + ' is not registered. ' + + 'Make sure to register it with ClayConfig.registerComponent()'); + } + var _templateData = _.extend({}, _itemType.defaults, config); /** @type {string|null} */ diff --git a/test/fixture.js b/test/fixture.js index d270f9f..65c9362 100644 --- a/test/fixture.js +++ b/test/fixture.js @@ -1,19 +1,32 @@ 'use strict'; var _ = require('../src/scripts/vendor/minified/minified')._; +var $ = require('../src/scripts/vendor/minified/minified').$; +var HTML = require('../src/scripts/vendor/minified/minified').HTML; var ClayItem = require('../src/scripts/lib/clay-item'); +var ClayConfig = require('../src/scripts/lib/clay-config'); var idCounter = 0; +var componentRegistry = require('../src/scripts/lib/component-registry'); + +// add some components to the registry to test +componentRegistry.text = require('../src/scripts/components/text'); +componentRegistry.input = require('../src/scripts/components/input'); +componentRegistry.toggle = require('../src/scripts/components/toggle'); +componentRegistry.footer = require('../src/scripts/components/footer'); +componentRegistry.select = require('../src/scripts/components/select'); + /** - * @param {string} type - * @param {{}} [config] + * @param {string|{}} config * @returns {{}} */ -function configItem(type, config) { +module.exports.configItem = function(config) { + if (typeof config === 'string') { + config = { type: config }; + } var basic = { - type: type, - label: type + '-label', + label: config.type + '-label', appKey: 'appKey-' + idCounter, id: 'id-' + idCounter }; @@ -21,17 +34,39 @@ function configItem(type, config) { idCounter++; return _.extend({}, basic, config); -} +}; /** - * - * @param {string} type - * @param {{}} [config] + * @param {string|{}} [config] * @returns {ClayItem} */ -function clayItem(type, config) { - return new ClayItem(configItem(type, config)); -} +module.exports.clayItem = function(config) { + return new ClayItem(module.exports.configItem(config)); +}; + +/** + * @param {[]} types + * @returns {*} + */ +module.exports.config = function(types) { + return types.map(function(item) { + return Array.isArray(item) ? + {type: 'section', items: module.exports.config(item)} : + module.exports.configItem(item); + }); +}; + +/** + * @param {[]} types + * @param {boolean} [noBuild=false] - don't run the build method on the result + * @param {{}} [settings] - settings to pass to constructor + * @returns {ClayConfig} + */ +module.exports.clayConfig = function(types, noBuild, settings) { + var clayConfig = new ClayConfig( + settings || {}, + module.exports.config(types), $(HTML('
')) + ); + return noBuild ? clayConfig : clayConfig.build(); +}; -module.exports.configItem = configItem; -module.exports.clayItem = clayItem; diff --git a/test/spec/lib/clay-config.js b/test/spec/lib/clay-config.js new file mode 100644 index 0000000..daa33e7 --- /dev/null +++ b/test/spec/lib/clay-config.js @@ -0,0 +1,156 @@ +'use strict'; + +var assert = require('chai').assert; +var sinon = require('sinon'); +var textComponent = require('../../../src/scripts/components/text'); +var componentRegistry = require('../../../src/scripts/lib/component-registry'); +var checkReadOnly = require('../../test-utils').checkReadOnly; +var fixtures = require('../../fixture'); + +describe('ClayConfig', function() { + it('defines read-only properties', function() { + var properties = [ + 'EVENTS', + 'getItemByAppKey', + 'getItemById', + 'getItemsByType', + 'getSettings', + 'registerComponent', + 'build', + 'on', + 'one', + 'off', + 'trigger' + ]; + var clayConfig = fixtures.clayConfig(['input']); + checkReadOnly(clayConfig, properties); + }); + + describe('throws when trying to run methods before being built', function() { + [ + 'getItemByAppKey', + 'getItemById', + 'getItemsByType', + 'getSettings' + ].forEach(function(method) { + it('.' + method + '()', function() { + var clayConfig = fixtures.clayConfig(['input', 'text'], true); + assert.throws(clayConfig[method], new RegExp(method)); + }); + }); + }); + + describe('.getAllItems()', function() { + it('returns an array of all the items', function() { + var config = fixtures.config(['input', 'text', ['input']]); + var clayConfig = fixtures.clayConfig(config); + var allItems = clayConfig.getAllItems(); + assert.strictEqual(allItems.length, 3); + assert.deepEqual(allItems[0].config, config[0]); + assert.deepEqual(allItems[1].config, config[1]); + assert.deepEqual(allItems[2].config, config[2].items[0]); + }); + }); + + describe('.getItemByAppKey()', function() { + it('it returns the correct item', function() { + var config = fixtures.config([ + {type: 'input', appKey: 'test-app-key'}, + {type: 'input', appKey: undefined} + ]); + var clayConfig = fixtures.clayConfig(config); + assert.deepEqual(clayConfig.getItemByAppKey('test-app-key').config, config[0]); + }); + }); + + describe('.getItemById()', function() { + it('it returns the correct item', function() { + var config = fixtures.config([ + {type: 'input', id: 'test-id'}, + {type: 'input', id: undefined} + ]); + var clayConfig = fixtures.clayConfig(config); + assert.deepEqual(clayConfig.getItemById('test-id').config, config[0]); + }); + }); + + describe('.getItemsByType()', function() { + it('it returns the correct items', function() { + var config = fixtures.config(['input', 'text', 'input']); + var clayConfig = fixtures.clayConfig(config); + assert.deepEqual(clayConfig.getItemsByType('input')[0].config, config[0]); + assert.deepEqual(clayConfig.getItemsByType('input')[1].config, config[2]); + }); + }); + + describe('.getSettings()', function() { + it('returns the correct settings', function() { + var clayConfig = fixtures.clayConfig( + [ + {type: 'input', appKey: 'test1', value: 'not this'}, + {type: 'select', appKey: 'test2', options: [ + {label: 'label-1', value: 'val-1'}, + {label: 'label-2', value: 'val-2'} + ]}, + {type: 'toggle', appKey: 'test3'} + ], + false, + { + test1: 'val-1' // set one of the values via settings + } + ); + + clayConfig.getItemByAppKey('test2').set('val-2'); + clayConfig.getItemByAppKey('test3').set(true); + + assert.deepEqual(clayConfig.getSettings(), { + test1: 'val-1', + test2: 'val-2', + test3: true + }); + }); + }); + + describe('.registerComponent()', function() { + it('adds the component to the registry', function(done) { + delete componentRegistry.text; + assert.typeOf(componentRegistry.text, 'undefined'); + var clayConfig = fixtures.clayConfig(['text'], true); + + clayConfig.on(clayConfig.EVENTS.BEFORE_BUILD, function() { + clayConfig.registerComponent(textComponent); + }); + + clayConfig.on(clayConfig.EVENTS.AFTER_BUILD, function() { + assert.strictEqual(this.getAllItems()[0].config.type, 'text'); + done(); + }); + + clayConfig.build(); + }); + }); + + describe('.build()', function() { + it('dispatches the BEFORE_BUILD event at the right time', function(done) { + var clayConfig = fixtures.clayConfig(['input', 'text', 'input'], true); + clayConfig.on(clayConfig.EVENTS.BEFORE_BUILD, function() { + + // this should throw because the config has not been built yet + assert.throws(clayConfig.getAllItems); + done(); + }); + clayConfig.build(); + }); + + it('dispatches the AFTER_BUILD event at the right time', function(done) { + var clayConfig = fixtures.clayConfig(['input', 'text', 'input'], true); + clayConfig.on(clayConfig.EVENTS.AFTER_BUILD, function() { + + // this should not throw because the config has been built + assert.doesNotThrow(clayConfig.getAllItems); + done(); + }); + clayConfig.build(); + }); + }); +}); diff --git a/test/spec/lib/clay-item.js b/test/spec/lib/clay-item.js index a72929b..b70a532 100644 --- a/test/spec/lib/clay-item.js +++ b/test/spec/lib/clay-item.js @@ -2,31 +2,12 @@ var assert = require('chai').assert; var sinon = require('sinon'); +var checkReadOnly = require('../../test-utils').checkReadOnly; var ClayItem = require('../../../src/scripts/lib/clay-item'); var clayItemFixture = require('../../fixture').clayItem; var configItemFixture = require('../../fixture').configItem; var componentRegistry = require('../../../src/scripts/lib/component-registry'); -// add some components to the registry to test -componentRegistry.text = require('../../../src/scripts/components/text'); -componentRegistry.input = require('../../../src/scripts/components/input'); -componentRegistry.toggle = require('../../../src/scripts/components/toggle'); -componentRegistry.footer = require('../../../src/scripts/components/footer'); -componentRegistry.select = require('../../../src/scripts/components/select'); - -/** - * @param {Object} object - * @param {Array} properties - */ -function checkReadOnly(object, properties) { - properties.forEach(function(property) { - assert.strictEqual( - Object.getOwnPropertyDescriptor(object, property).writable, - false - ); - }); -} - describe('ClayItem', function() { it('defines read-only properties', function() { var properties = [ @@ -53,6 +34,13 @@ describe('ClayItem', function() { }); }); + it('throws if a component is not in the registry', function() { + var config = configItemFixture('fake'); + /* eslint-disable no-new */ + assert.throws(function() { new ClayItem(config); }, /fake/); + /* eslint-enable no-new */ + }); + describe('.id', function() { it('sets id if config has id', function() { var config = configItemFixture('input'); @@ -61,7 +49,7 @@ describe('ClayItem', function() { }); it('sets id to null if there is no id in the config', function() { - var clayItem = clayItemFixture('input', {id: undefined}); + var clayItem = clayItemFixture({type: 'input', id: undefined}); assert.strictEqual(clayItem.id, null); }); }); @@ -74,7 +62,7 @@ describe('ClayItem', function() { }); it('sets appKey to null if there is no appKey in the config', function() { - var clayItem = clayItemFixture('input', {appKey: undefined}); + var clayItem = clayItemFixture({type: 'input', appKey: undefined}); assert.strictEqual(clayItem.appKey, null); }); }); diff --git a/test/test-utils.js b/test/test-utils.js new file mode 100644 index 0000000..c25914e --- /dev/null +++ b/test/test-utils.js @@ -0,0 +1,16 @@ +'use strict'; + +var assert = require('chai').assert; + +/** + * @param {Object} object + * @param {Array} properties + */ +module.exports.checkReadOnly = function(object, properties) { + properties.forEach(function(property) { + assert.strictEqual( + Object.getOwnPropertyDescriptor(object, property).writable, + false + ); + }); +};