diff --git a/src/backbone.js b/src/backbone.js index ddee989..12b6434 100644 --- a/src/backbone.js +++ b/src/backbone.js @@ -61,7 +61,6 @@ define([ Backbone.classMembers({ SHARED : { - IMMEDIATE_TRACKER : 'immediateTracker', CHANGES_TRACKER : 'changesTracker', PATIENT_INFO : 'patientInfo' } diff --git a/src/base-with-proxy.js b/src/base-with-proxy.js deleted file mode 100644 index 4885912..0000000 --- a/src/base-with-proxy.js +++ /dev/null @@ -1,134 +0,0 @@ -/** _ - * _ _ _| |_ - * | | | |_ _| - * | |___ _ _ | | |_| - * | '_ \| | | || | | | - * | | | || |_| || | | | - * |_| |_|\___,_||_| |_| - * - * (c) Huli Inc - */ - -/** - * Base component with proxy - */ -define([ - 'jquery', - 'ju-components/base', - 'ju-components/base-with-proxy/payload-handler' - ], - function( - $, - BaseComponent, - PayloadHandler - ) { - - 'use strict'; - - var BaseWithProxy = BaseComponent.extend({ - init : function() { - - this.setOptions({ - proxy : null // must be instance, not class - }); - - this._super.apply(this, arguments); - - this.payloadHandler = PayloadHandler.getInst(); - }, - /** - * Setups the component and triggers the provided proxy load - */ - setup : function() { - // var $insertionPoint = arguments.length > 0 ? arguments[0] : null; - - this._super.apply(this, arguments); - - // Pass the rest of the parameters to fetchDataWithParams - // Transform arguments into an array - var params = Array.prototype.slice.call(arguments, 1); - - // @TODO: calling this method in the setup method instead of the load, - // will cause that the data cannot be fetched at the same time as the components resources - this.fetchDataWithParams.apply(this, params); - }, - /** - * Retrieves the data from the server - */ - fetchDataWithParams : function() { - var self = this, - waitForPromise; - - // Enable the spinner before we start fetching data - self.toggleSpinnerVisibility(true); - - // @TODO: prevent fetching when no arguments were passed? - // Store the promise locally - self.fetchDataWithParamsPromise = this.getComponentPayload.apply(this, arguments); - - // Append to the end of the - if (this.isRootComponent && self.setupCompletedPromise) { - waitForPromise = - self.setupCompletedPromise - .then(function() { - // Wait until all the components tree is rendered (setup is completed) to load the data into them - return self.fetchDataWithParamsPromise; - }); - } else { - // This component is part of the component tree but is not the root. - // Hence we assume that the component tree is already rendered in the screen - // (setup methods call is completed) - // - // @TODO: Check What happens when a base with proxy is coded inside a base with proxy that already loads it's data - waitForPromise = self.fetchDataWithParamsPromise; - } - - waitForPromise - .then($.proxy(self.processPayloadData, self)) - // Catch any error in the chain - ['catch']($.proxy(self.errorFetchingData, self)) - // Hide the spinner - .then($.proxy(self.toggleSpinnerVisibility, self, false)); - }, - /** - * Function meant to be overwritten, receives parameters for the current component - * and returns a Promise that will resolve to the data to be set into the component - * @return Promise [description] - */ - getComponentPayload : function() { - var self = this, - data = arguments; - - var dataPromise = new Promise(function(resolve, reject) { - // Children of base component proxy - // ju-components/base-with-proxy/proxy - if (self.opts.proxy != null && self.opts.proxy.getPayload) { - self.opts.proxy.getPayload(data, resolve, reject); - } else { - Logger.error('BaseWithProxy: no proxy setup or getPayload method is not defined'); - } - }); - return dataPromise; - }, - /** - * Process the data given by the getComponent Payload method - */ - processPayloadData : function(/* response */) { - return this.payloadHandler.processPayloadData.apply(this, arguments); - }, - /** - * There is an error trying to fecth data for this component - */ - errorFetchingData : function() { - Logger.error('BaseWithProxy: error fetching data', arguments); - } - }); - - BaseWithProxy.classMembers({ - - }); - - // Exporting module - return BaseWithProxy; - -}); diff --git a/src/base-with-proxy/base-with-save.js b/src/base-with-proxy/base-with-save.js deleted file mode 100644 index 0fe400a..0000000 --- a/src/base-with-proxy/base-with-save.js +++ /dev/null @@ -1,135 +0,0 @@ -/** _ - * _ _ _| |_ - * | | | |_ _| - * | |___ _ _ | | |_| - * | '_ \| | | || | | | - * | | | || |_| || | | | - * |_| |_|\___,_||_| |_| - * - * (c) Huli Inc - */ - -/** - * Base component with proxy - */ -define([ - 'jquery', - 'ju-components/base-with-proxy', - 'ju-components/base-with-proxy/save-strategy/all', - 'ju-components/base-with-proxy/save-strategy/changed', - 'ju-components/base-with-proxy/save-strategy/immediate' - ], - function( - $, - BaseComponentWithProxy, - SaveAllStrategy, - SaveChangedStrategy, - SaveImmediateStrategy - ) { - - 'use strict'; - - var BaseWithSave = BaseComponentWithProxy.extend({ - init : function() { - - this.setOptions({ - saveStrategy : BaseWithSave.STRATEGY.ALL, - }); - - this._super.apply(this, arguments); - - switch (this.opts.saveStrategy) { - case BaseWithSave.STRATEGY.CHANGED: - this.payloadHandler = new SaveChangedStrategy(); - break; - case BaseWithSave.STRATEGY.IMMEDIATE: - this.payloadHandler = new SaveImmediateStrategy(); - break; - case BaseWithSave.STRATEGY.ALL: - case BaseWithSave.STRATEGY.NONE: - this.payloadHandler = new SaveAllStrategy(); - } - }, - /** - * Extra setup for the base with save component - */ - setup : function() { - if (this.payloadHandler.setup) { - this.payloadHandler.setup.apply(this, arguments); - } - this._super.apply(this, arguments); - }, - /** - * Triggers a save action in the current component - * including the children component's data - * - * @return Promise Save promise - */ - saveData : function() { - var promise = this.payloadHandler.saveData.apply(this, arguments); - return promise; - }, - /** - * Saves the data to the server using the proxy - * Overwrite this method in the child classes - * - * @param object dataToSave data to be saved - * @return Promise A save promise - */ - submitDataToServer : function(dataToSave) { // jshint ignore:line - }, - /** - * Handles the save success event triggered from the payloadHandler by - * bypassing the event to the higher level. - */ - onSaveSuccess : function(response) { - this.trigger(BaseWithSave.EV.SAVE_SUCCESS, response); - }, - /** - * Returns a rejected promise so it's handled afterwars - */ - getInvalidFieldsPromise : function(errors) { - return Promise.reject(errors); - }, - /** - * Validates that the current components does not contain any errors. - * This happens everytime, whether the save strategy is CHANGED or ALL. - */ - validateAll : function() { - - var childrenRecursiveOpts = - { - callOnChildren : function(comp) { - // We only call the validate method on the children if the validate method - // does not exists in the current component. - // The validate method is recursive by nature, this is why we prevent - // duplicate recursive calls - return comp.validate === undefined; - }, - flatten : true - }; - - var childrenErrors = this.callRecursively(childrenRecursiveOpts, 'validate'), - errors = $.grep(childrenErrors,function(n) { return n; }); //ComponentUtils.flatten(childrenErrors); - - return errors; - } - }); - - BaseWithSave.classMembers({ - EV : { - SAVE_SUCCESS : 'saveSuccess', - SAVE_ERROR : 'saveError', - CANCELLED : 'onCancel' - }, - STRATEGY : { - ALL : 'all', - CHANGED : 'changed', - NONE : 'none', - IMMEDIATE : 'immediate', - } - }); - - return BaseWithSave; - -}); diff --git a/src/base-with-proxy/payload-handler.js b/src/base-with-proxy/payload-handler.js deleted file mode 100644 index 33276e1..0000000 --- a/src/base-with-proxy/payload-handler.js +++ /dev/null @@ -1,65 +0,0 @@ -/** _ - * _ _ _| |_ - * | | | |_ _| - * | |___ _ _ | | |_| - * | '_ \| | | || | | | - * | | | || |_| || | | | - * |_| |_|\___,_||_| |_| - * - * (c) Huli Inc - */ - -/** - * Payload Handler - * @abstract - */ -define([ - 'jquery', - 'ju-shared/observable-class' - ], - function( - $, - ObservableClass - ) { - - 'use strict'; - - var PayloadHandler = ObservableClass.extend({ - /** - * Process the data given by the get component payload method - * @abstract - */ - processPayloadData : function() { - var response = arguments.length > 0 ? arguments[0] : null, - args = Array.prototype.slice.call(arguments, 1); - - // If the response is explicitly null or undefined - // then we return since there is nothing to process - if (response === null || response === undefined) { - log('PayloadHandler: No data to process...'); - return; - } - // At this point, the server returned data but we need - // to check that has the right format - if (response.data && response.data.response_data) { - var setSuccesfully = this.setData.apply(this, $.merge([response.data.response_data], args)); - - // Notify data set succesfully from root component - if (setSuccesfully) { - this.fireEventAndNotify(PayloadHandler.EV.DATA_READY); - } - } else { - Logger.error('Component: response from server does not have the expected format'); - } - } - }); - - PayloadHandler.classMembers({ - EV : { - DATA_READY : 'dataReady' - } - }); - - return PayloadHandler; - -}); diff --git a/src/base-with-proxy/save-strategy/all.js b/src/base-with-proxy/save-strategy/all.js deleted file mode 100644 index f381f10..0000000 --- a/src/base-with-proxy/save-strategy/all.js +++ /dev/null @@ -1,75 +0,0 @@ -/** _ - * _ _ _| |_ - * | | | |_ _| - * | |___ _ _ | | |_| - * | '_ \| | | || | | | - * | | | || |_| || | | | - * |_| |_|\___,_||_| |_| - * - * (c) Huli Inc - */ - -/** - * Save strategy : All - */ -define([ - 'jquery', - 'ju-components/base-with-proxy/payload-handler', - 'ju-components/helper/changes-tracker' - ], - function( - $, - PayloadHandler, - ChangesTracker - ) { - 'use strict'; - - /************************************************** - * - * IMPORTANT : All the methods here called in the scope of the base with save instance - * - **************************************************/ - var SaveAllStrategy = PayloadHandler.extend({ - setup : function() { - /* - At this point we know that the notification center has already been set - */ - var changesTracker = ChangesTracker.getInst(this.backbone); - // Subscribe to events - changesTracker.track(); - }, - /** - * Triggers a save action in the current component - * including the children component's data - * - * @return Promise Save promise - */ - saveData : function() { - // Allow force all data in this model - var data, - savePromise, - self = this; - - var errors = this.validateAll(); - if (!errors || errors.length === 0) { - // If there is'nt any errors in the components tree - // then submit - data = this.getData(); - - savePromise = this.submitDataToServer(data); - - savePromise.then(function(response) { - self.onSaveSuccess(response); - }); - } else { - savePromise = this.getInvalidFieldsPromise(errors); - } - - return savePromise; - } - }); - - // Exporting module - return SaveAllStrategy; - -}); diff --git a/src/base-with-proxy/save-strategy/changed.js b/src/base-with-proxy/save-strategy/changed.js deleted file mode 100644 index 2342d9f..0000000 --- a/src/base-with-proxy/save-strategy/changed.js +++ /dev/null @@ -1,105 +0,0 @@ -/** _ - * _ _ _| |_ - * | | | |_ _| - * | |___ _ _ | | |_| - * | '_ \| | | || | | | - * | | | || |_| || | | | - * |_| |_|\___,_||_| |_| - * - * (c) Huli Inc - */ - -/** - * Save strategy : Changed - */ -define([ - 'jquery', - 'ju-components/base-with-proxy/payload-handler', - 'ju-components/helper/changes-tracker' - ], - function( - $, - PayloadHandler, - ChangesTracker - ) { - 'use strict'; - - /************************************************** - * - * IMPORTANT : All the methods here called in the scope of the base with save instance - * - **************************************************/ - var SaveChangedStrategy = PayloadHandler.extend({ - setup : function() { - /* - At this point we know that the notification center has already been set - */ - var changesTracker = ChangesTracker.getInst(this.backbone); - // Subscribe to events - changesTracker.track(); - }, - /** - * Triggers a save action in the current component - * including the children component's data - * - * @return Promise Save promise - */ - saveData : function(onSuccess) { - // Allow force all data in this model - var data, - savePromise, - changedChildren = null, - self = this; - - var errors = this.validateAll(); - - if (!errors || errors.length === 0) { - - // Select strategy to collect then unsaved data - var changesTracker = ChangesTracker.getInst(this.backbone), - changesBag = changesTracker.getChangesBag(this), - changedChildren = changesBag.getChangedChildrenInstances(); - - data = this.getData(changedChildren); - - savePromise = this.submitDataToServer(data, onSuccess); - - savePromise.then(function(response) { - // Commits the set of changes - changesBag.commit(); - - self.onSaveSuccess(response); - }); - - } else { - savePromise = this.getInvalidFieldsPromise(errors); - } - - return savePromise; - }, - /** - * Mark components with data as changed in the changes tracker - * - */ - markComponentsWithData : function(data) { - var changesTracker = ChangesTracker.getInst(this.backbone); - - if (!data || !changesTracker || !this.getComponents()) { - return; - } - - for (var key in data) { - if (data.hasOwnProperty(key)) { - var childComp = this.c(key); - if (childComp) { - changesTracker.modifiedHandler(childComp); - } - } - } - } - }); - - // Exporting module - return SaveChangedStrategy; - -}); diff --git a/src/base-with-proxy/save-strategy/immediate.js b/src/base-with-proxy/save-strategy/immediate.js deleted file mode 100644 index 14a15ce..0000000 --- a/src/base-with-proxy/save-strategy/immediate.js +++ /dev/null @@ -1,61 +0,0 @@ -/** _ - * _ _ _| |_ - * | | | |_ _| - * | |___ _ _ | | |_| - * | '_ \| | | || | | | - * | | | || |_| || | | | - * |_| |_|\___,_||_| |_| - * - * (c) Huli Inc - */ - -/** - * Save strategy : Immediate - */ -define([ - 'jquery', - 'ju-components/base-with-proxy/payload-handler', - 'ju-components/helper/changes-tracker' - ], - function( - $, - PayloadHandler, - ChangesTracker - ) { - 'use strict'; - - /************************************************** - * - * IMPORTANT : All the methods here called in the scope of the base with save instance - * - **************************************************/ - var SaveImmediateStrategy = PayloadHandler.extend({ - setup : function() { - if (this.opts.tracker) { - this.immediateTracker = this.opts.tracker.getInst(this.backbone); - } else { - Logger.error('Dashboard: a ImmediateTracker is needed to configure this component strategy'); - } - - // Subscribe to events - this.immediateTracker.track(); - - // listeing changed event - this.immediateTracker.on(ChangesTracker.EV.COMPONENT_CHANGED, $.proxy(this.saveData, this)); - }, - - saveData : function(triggeredEvent, dataToSave) { - var self = this, - savePromise = this.submitDataToServer(triggeredEvent, dataToSave); - - savePromise.then(function(response) { - self.onSaveSuccess(triggeredEvent, response); - }); - - return savePromise; - }, - }); - - // Exporting module - return SaveImmediateStrategy; -}); diff --git a/src/base.js b/src/base.js index 45af8ad..22d067b 100644 --- a/src/base.js +++ b/src/base.js @@ -17,8 +17,9 @@ define([ 'ju-shared/observable-class', 'ju-shared/util', 'ju-components/factory/base', - 'ju-components/resource-collector', - 'ju-components/resource/resource-manager', + 'ju-components/helper/options', + 'ju-components/resource/collector', + 'ju-components/resource/manager', 'ju-components/util', 'ju-components/backbone', 'ju-components/factory/single-instance' @@ -28,6 +29,7 @@ define([ ObservableClass, Util, FactoryBase, + OptionsHelper, ResourceCollector, ResourceManager, ComponentUtil, @@ -190,7 +192,9 @@ define([ /** * Stores the options for this */ - this.prepareOptions(opts); + this._initOptsManager(); + this.optsManager.prepareOptions(opts); + this.opts = this.optsManager.getOptions(); // Reference to the spinner this.spinnerComp = null; @@ -422,33 +426,20 @@ define([ */ setupCompleted : function() { }, /** - * Include options in the opts collector - * Set default options and override parents options - * Keeps the flow from child to parent (no untraceable flows with before and after super) - * - Child options always have precedence over parent options - * - Parent options should never overwrite children options - * @warn ALWAYS call setOptions BEFORE _super - * @param {object} class level options to queue + * Initialize options manager + * @see OptionsHelper */ - setOptions : function() { - this.optsCollector = this.optsCollector || []; - // Optimized array prepend - var i, argLen, len = this.optsCollector.length + arguments.length, - optsArr = new Array(len); - - for (i = 0, argLen = arguments.length; i < argLen; i++) { - optsArr[i] = arguments[i]; - } - - for (i = 0; i < len; i++) { - optsArr.push(this.optsCollector[i]); - - } - this.optsCollector = optsArr; + _initOptsManager : function() { + this.optsManager = this.optsManager || new OptionsHelper(); }, - prepareOptions : function(forcedOpts) { - // Merge options collector - this.opts = $.extend.apply($, [true, {}].concat(this.optsCollector).concat([forcedOpts])); + /** + * Store options in manager + * @decorator + * @see OptionsHelper + */ + setOptions : function() { + this._initOptsManager(); + this.optsManager.setOptions.apply(this.optsManager, arguments); }, /** * Merge selectors into the selector dictionary (collector) used by _findLocalElems diff --git a/src/blocks/base-ui.js b/src/blocks/base-ui.js index 5c881c5..ed7a6ce 100644 --- a/src/blocks/base-ui.js +++ b/src/blocks/base-ui.js @@ -332,7 +332,7 @@ define([ // isEmpty can be undefined // ..but promise AND isEmpty can't be processed if (typeof childEmptinessPromise === 'undefined' && childEmptinessUndefined) { - Logger.error('base-ui : checkLocalEmptiness : unkown empty state or promise', childrenEmptiness); + Logger.error('base-ui : checkLocalEmptiness : unkown empty state or promise', childResult); } // Verify first if there is any unresolved promise diff --git a/src/flows/fetch/handler.js b/src/flows/fetch/handler.js new file mode 100644 index 0000000..5901496 --- /dev/null +++ b/src/flows/fetch/handler.js @@ -0,0 +1,215 @@ +/** _ + * _ _ _| |_ + * | | | |_ _| + * | |___ _ _ | | |_| + * | '_ \| | | || | | | + * | | | || |_| || | | | + * |_| |_|\___,_||_| |_| + * + * (c) Huli Inc + */ + +define([ + 'ju-components/handler', + 'ju-components/flows/fetch/payload', + 'ju-components/flows/fetch/response' + ], + function( + BaseHandler, + PayloadHandler, + ResponseHandler + ) { + 'use strict'; + + /** + * Adds a proxy to a base component lifecycle + * + * On base component setup: + * setupPayloadHandler, _super, fetchData + * + * @param {object} opts + * { + * component, + * proxy + * } + */ + var FetchHandler = BaseHandler.extend({ + init : function() { + this.setOptions({ + proxy : null + }); + + this._super.apply(this, arguments); + + // Define fetchHandler in component + this.component.fetchHandler = this; + }, + setup : function() { + // Payload handler + this.setupPayloadHandler(); + + // Response handler + this.setupResponseHandler(); + }, + /** + * Initialize payload handler and run setup + */ + setupPayloadHandler : function() { + this.payloadHandler = new PayloadHandler({ component : this.component }); + }, + /** + * Initialize response handler and run setup + */ + setupResponseHandler : function() { + this.responseHandler = new ResponseHandler({ component : this.component }); + }, + /** + * Decorator for fetchData + * This is also the main action of a proxy-handler + */ + process : function() { + this.fetchData(arguments); + }, + /** + * Triggers the provided proxy main fetch + * @param {array|arguments} args component setup params + */ + fetchData : function(args) { + // Pass the rest of the parameters to fetchDataWithParams + // Transform arguments into an array + var params = Array.prototype.slice.call(args, 1); + + // @TODO: calling this method in the setup method instead of the load, + // will cause that the data cannot be fetched at the same time as the components resources + this.fetchDataWithParams.apply(this, params); + }, + /** + * Fetching data process + * @param {array|arguments} params component setup params + */ + fetchDataWithParams : function(/* params... */) { + var self = this, + waitForPromise, + params = this.processFetchPayload.apply(this, arguments); + + // Enable the spinner before we start fetching data + this.component.toggleSpinnerVisibility(true); + + // @TODO: prevent fetching when no arguments were passed? + // Store the promise locally + this.fetchDataWithParamsPromise = this.fetchDataFromServer.apply(this, params); + + // Append to the end of the + if (this.component.isRootComponent && this.component.setupCompletedPromise) { + waitForPromise = + this.component.setupCompletedPromise + .then(function() { + // Wait until all the components tree is rendered (setup is completed) to load the data into them + return self.fetchDataWithParamsPromise; + }); + } else { + // This component is part of the component tree but is not the root. + // Hence we assume that the component tree is already rendered in the screen + // (setup methods call is completed) + // + // @TODO: Check What happens when a base with proxy is coded inside a base with proxy that already loads it's data + waitForPromise = this.fetchDataWithParamsPromise; + } + + waitForPromise + .then($.proxy(this.processFetchResponse, this)) + // Catch any error in the chain + ['catch']($.proxy(this.errorFetchingData, this)) + // Hide the spinner + .then($.proxy(this.component.toggleSpinnerVisibility, this.component, false)); + }, + /** + * Calls a proxy main fetch for retrieving current component data + * Data should always be retrieved by a proxy (no direct ajax requests) + * + * @param {array} params component setup params + * @return Promise resolves when response data is received + * @abstract + */ + fetchDataFromServer : function(/* params... */) { + var self = this, + params = arguments; + + var dataPromise = new Promise(function(resolve, reject) { + // Children of base component proxy + // By default, it should provide a 'fetch' method + // or this whole method can be overwritten to customize the method + // ju-components/proxy + if (self.opts.proxy != null && self.opts.proxy.fetch) { + self.opts.proxy.fetch(params, resolve, reject); + } else { + Logger.error('FetchHandler: no proxy setup or "fetch" method is not defined'); + } + }); + return dataPromise; + }, + /** + * Process fetch payload data before request + * @param {arguments} payload unprocessed payload data + * component setup arguments + * @return {array|arguments} processed request payload + */ + processFetchPayload : function(/* payload */) { + var payloadHandler = this.payloadHandler; + return payloadHandler.process.apply(payloadHandler, arguments); + }, + /** + * Process fetch response after request + * @param {object} response + * @return {object} processed response + */ + processFetchResponse : function(/* response */) { + var responseHandler = this.responseHandler, + result = responseHandler.process.apply(responseHandler, arguments); + + // Notify data set succesfully from root component + if (result && result.success) { + this.component.fireEventAndNotify(FetchHandler.EV.DATA_READY, result); + } + + return result; + }, + /** + * There is an error trying to fecth data for this component + */ + errorFetchingData : function() { + Logger.error('FetchHandler: error fetching data', arguments); + }, + /** + * Proxy getter + * @return {BaseProxy} + */ + getProxy : function() { + return this.opts.proxy; + }, + /** + * Payload handler getter + * @return {PayloadHandler} payload handler object + */ + getPayloadHandler : function() { + return this.payloadHandler; + }, + /** + * Response handler getter + * @return {ResponseHandler} response handler object + */ + getResponseHandler : function() { + return this.responseHandler; + } + }); + + FetchHandler.classMembers({ + EV : { + DATA_READY : 'dataReady' + } + }); + + // Exporting module + return FetchHandler; + +}); diff --git a/src/flows/fetch/payload.js b/src/flows/fetch/payload.js new file mode 100644 index 0000000..b9d4b71 --- /dev/null +++ b/src/flows/fetch/payload.js @@ -0,0 +1,39 @@ +/** _ + * _ _ _| |_ + * | | | |_ _| + * | |___ _ _ | | |_| + * | '_ \| | | || | | | + * | | | || |_| || | | | + * |_| |_|\___,_||_| |_| + * + * (c) Huli Inc + */ + +/** + * Request handler + * @abstract + */ +define([ + 'ju-components/handler' + ], + function( + BaseHandler + ) { + + 'use strict'; + + var PayloadHandler = BaseHandler.extend({ + /** + * Process payload data for request + * @param {object} payload data + * @return {array|arguments} processed payload data + * @abstract + */ + process : function(/* payload */) { + return arguments; + } + }); + + return PayloadHandler; + +}); diff --git a/src/flows/fetch/response.js b/src/flows/fetch/response.js new file mode 100644 index 0000000..07812ce --- /dev/null +++ b/src/flows/fetch/response.js @@ -0,0 +1,57 @@ +/** _ + * _ _ _| |_ + * | | | |_ _| + * | |___ _ _ | | |_| + * | '_ \| | | || | | | + * | | | || |_| || | | | + * |_| |_|\___,_||_| |_| + * + * (c) Huli Inc + */ + +/** + * Response Handler + * @abstract + */ +define([ + 'ju-components/handler' + ], + function( + BaseHandler + ) { + + 'use strict'; + + var ResponseHandler = BaseHandler.extend({ + /** + * Process the data given by the result of a fetch data method + * @param {object} response server response + * @abstract + */ + process : function(/* response */) { + var component = this.component, + response = arguments.length > 0 ? arguments[0] : null, + args = Array.prototype.slice.call(arguments, 1), + result = null; // setDataResult + + // If the response is explicitly null or undefined + // then we return since there is nothing to process + if (response === null || response === undefined) { + log('ResponseHandler: No data to process...'); + return; + } + // At this point, the server returned data but we need + // to check that has the right format + if (response.data && response.data.response_data) { + result = component.setData.apply(component, $.merge([response.data.response_data], args)); + } else { + Logger.error('Component: response from server does not have the expected format'); + } + + return result; + } + }); + + return ResponseHandler; + +}); diff --git a/src/flows/save/changes-tracker.js b/src/flows/save/changes-tracker.js new file mode 100644 index 0000000..77cd6f4 --- /dev/null +++ b/src/flows/save/changes-tracker.js @@ -0,0 +1,94 @@ +/** _ + * _ _ _| |_ + * | | | |_ _| + * | |___ _ _ | | |_| + * | '_ \| | | || | | | + * | | | || |_| || | | | + * |_| |_|\___,_||_| |_| + * + * (c) Huli Inc + */ + +define([ + 'ju-shared/observable-class' + ], + function( + ObservableClass + ) { + 'use strict'; + + /** + * Tracks changes for on get and save payload construction + * @param {Backbone} backboneInst component backbone instance + */ + var ChangesTracker = ObservableClass.extend({ + init : function(backboneInst) { + this.backbone = backboneInst; + this.isTracking = false; + }, + /** + * Start tracking action + */ + track : function() { + if (this.isTracking) { + Logger.error('ChangesTracker : tracker is already tracking'); + } + this.bindChangeEvents(); + this.isTracking = true; + }, + /** + * Handle event as a modification or change to be track + * @param {string} eventName + * @param {function} handler Optional: custom event callback handler + * Default: modifiedHandler + */ + listenChangeEvent : function(eventName, handler) { + handler = handler || $.proxy(this.modifiedHandler, this); + this.backbone.on(eventName, handler); + }, + /** + * Attach listeners to backbone for on change events + * Components firing those events will be collected as changed for on-demand save + * @uses addModifyEvent, modifiedHandler + * @abstract + */ + bindChangeEvents : function() { + }, + /** + * Handler for when a component report itself or a child as modified + * Fires ChangesTracker.EV.COMPONENT_CHANGED event + * @param {object} changedComponent component to be processed as changed + */ + modifiedHandler : function(/* changedComponent */) { + } + }); + + ChangesTracker.classMembers({ + EV : { + COMPONENT_CHANGED : 'compChanged' + }, + getInst : function(backboneInst) { + // check if the notification center has already an id + if (!backboneInst) { + Logger.error('ChangesTracker: A valid backbone must be provided'); + return; + } + + // ALL changes tracker will be stored as CHANGES_TRACKER at current backbone instance + var sharedInstanceKey = backboneInst._class.SHARED.CHANGES_TRACKER, + changesTracker = backboneInst.getSharedInstance(sharedInstanceKey); + + if (!changesTracker) { + // No instance was defined, create one + changesTracker = new ChangesTracker(backboneInst); + backboneInst.addSharedInstance(sharedInstanceKey, changesTracker); + } + + return changesTracker; + } + }); + + // Exporting module + return ChangesTracker; + +}); diff --git a/src/flows/save/changes-tracker/changed.js b/src/flows/save/changes-tracker/changed.js new file mode 100644 index 0000000..d1b1486 --- /dev/null +++ b/src/flows/save/changes-tracker/changed.js @@ -0,0 +1,66 @@ +/** _ + * _ _ _| |_ + * | | | |_ _| + * | |___ _ _ | | |_| + * | '_ \| | | || | | | + * | | | || |_| || | | | + * |_| |_|\___,_||_| |_| + * + * (c) Huli Inc + */ + +define([ + 'ju-components/flows/save/changes-tracker', + 'ju-components/flows/save/changes/collect-changed-components' + ], + function( + ChangesTracker, + CollectChangedComponentsFn + ) { + 'use strict'; + + /** + * This changes tracker will store changed component in a bag + * so they can later be retrieved for payload construction + */ + var ChangedTracker = ChangesTracker.extend({ + init : function() { + this._super.apply(this, arguments); + + // Changed components collector + CollectChangedComponentsFn.call(this); + }, + modifiedHandler : function(changedComponent) { + var added = this.addChangedComponent(changedComponent); + if (added) { + this.fireEvent(ChangesTracker.EV.COMPONENT_CHANGED, changedComponent); + } + } + }); + + ChangedTracker.classMembers({ + getInst : function(backboneInst) { + // check if the notification center has already an id + if (!backboneInst) { + Logger.error('ChangesTracker: A valid backbone must be provided'); + return; + } + + // ALL changes tracker will be stored as CHANGES_TRACKER at current backbone instance + var sharedInstanceKey = backboneInst._class.SHARED.CHANGES_TRACKER, + changesTracker = backboneInst.getSharedInstance(sharedInstanceKey); + + if (!changesTracker) { + // No instance was defined, create one + changesTracker = new ChangedTracker(backboneInst); + backboneInst.addSharedInstance(sharedInstanceKey, changesTracker); + } + + return changesTracker; + } + }); + + // Exporting module + return ChangedTracker; + +}); diff --git a/src/flows/save/changes-tracker/immediate.js b/src/flows/save/changes-tracker/immediate.js new file mode 100644 index 0000000..254ac84 --- /dev/null +++ b/src/flows/save/changes-tracker/immediate.js @@ -0,0 +1,53 @@ +/** _ + * _ _ _| |_ + * | | | |_ _| + * | |___ _ _ | | |_| + * | '_ \| | | || | | | + * | | | || |_| || | | | + * |_| |_|\___,_||_| |_| + * + * (c) Huli Inc + */ + +define([ + 'ju-components/flows/save/changes-tracker' + ], + function( + ChangesTracker + ) { + 'use strict'; + + /** + * This changes tracker will autosave any change + */ + var ImmediateTracker = ChangesTracker.extend({ + modifiedHandler : function(changedComponent) { + this.fireEvent(ChangesTracker.EV.COMPONENT_CHANGED, changedComponent); + } + }); + + ImmediateTracker.classMembers({ + getInst : function(backboneInst) { + // check if the notification center has already an id + if (!backboneInst) { + Logger.error('ChangesTracker: A valid backbone must be provided'); + return; + } + + // ALL changes tracker will be stored as CHANGES_TRACKER at current backbone instance + var sharedInstanceKey = backboneInst._class.SHARED.CHANGES_TRACKER, + changesTracker = backboneInst.getSharedInstance(sharedInstanceKey); + + if (!changesTracker) { + // No instance was defined, create one + changesTracker = new ImmediateTracker(backboneInst); + backboneInst.addSharedInstance(sharedInstanceKey, changesTracker); + } + + return changesTracker; + } + }); + + // Exporting module + return ImmediateTracker; +}); diff --git a/src/helper/changes-bag.js b/src/flows/save/changes/bag.js similarity index 99% rename from src/helper/changes-bag.js rename to src/flows/save/changes/bag.js index 9a3615e..91e11d9 100644 --- a/src/helper/changes-bag.js +++ b/src/flows/save/changes/bag.js @@ -104,7 +104,6 @@ define([ ChangesBag.classMembers({ createInst : function(changesTracker, rootComponent, changedComponents) { - var changesBag = new ChangesBag(changesTracker); changesBag.buildChangesMap(rootComponent, changedComponents); return changesBag; diff --git a/src/flows/save/changes/collect-changed-components.js b/src/flows/save/changes/collect-changed-components.js new file mode 100644 index 0000000..c06607b --- /dev/null +++ b/src/flows/save/changes/collect-changed-components.js @@ -0,0 +1,78 @@ +/** _ + * _ _ _| |_ + * | | | |_ _| + * | |___ _ _ | | |_| + * | '_ \| | | || | | | + * | | | || |_| || | | | + * |_| |_|\___,_||_| |_| + * + * (c) Huli Inc + */ + +/** + * Adds a changes bag to any changes tracker + */ +define([ + 'ju-components/flows/save/changes/bag' + ], function( + ChangesBag + ) { + 'use strict'; + + /** + * + * @use ObservableClass + * @return {[type]} [description] + */ + var collectChangedComponentsFn = function() { + var self = this; + + // Changed components collector + self.changedComponents = []; + + /** + * Tries adding a changing component to the collector + * @param {object} changedComponent + * @return {Boolean} component was added or not + */ + self.addChangedComponent = function(changedComponent) { + var added = false; + if (self.changedComponents.indexOf(changedComponent) === -1) { + // Only add self event + log('ChangesBagTracker: component change registered'); + self.changedComponents.push(changedComponent); + added = true; + } + return added; + }; + + /** + * Returns an instance of changes bag with the modified components + * that are children of the specified root component + * @todo @klam why is changes bag no define as property of the tracker? + * @param {[type]} rootComponent [description] + * @return ChangesBag [description] + */ + self.getChangesBag = function(rootComponent) { + var changesBag = ChangesBag.createInst(self, rootComponent, self.changedComponents); + return changesBag; + }; + + /** + * Removes the specified components + * @param {[type]} components [description] + * @return {[type]} [description] + */ + self.removeFromChangedComponents = function(components) { + $.each(components, function(index, component) { + var componentIndex = self.changedComponents.indexOf(component); + if (componentIndex > -1) { + self.changedComponents.splice(componentIndex, 1); + } + }); + }; + }; + + // Exporting module + return collectChangedComponentsFn; +}); diff --git a/src/flows/save/handler.js b/src/flows/save/handler.js new file mode 100644 index 0000000..16313f4 --- /dev/null +++ b/src/flows/save/handler.js @@ -0,0 +1,196 @@ +/** _ + * _ _ _| |_ + * | | | |_ _| + * | |___ _ _ | | |_| + * | '_ \| | | || | | | + * | | | || |_| || | | | + * |_| |_|\___,_||_| |_| + * + * (c) Huli Inc + */ + +/** + * Base component with proxy + */ +define([ + 'ju-components/flows/fetch/handler', + 'ju-components/flows/save/strategies/all', + 'ju-components/flows/save/strategies/changed', + 'ju-components/flows/save/strategies/immediate' + ], + function( + FetchHandler, + SaveAllStrategy, + SaveChangedStrategy, + SaveImmediateStrategy + ) { + + 'use strict'; + + /** + * Adds a save handler to a base component lifecycle + * + * @param {object} opts + * { + * ... + * strategy + * } + */ + var SaveHandler = FetchHandler.extend({ + init : function() { + this.setOptions({ + strategy : SaveHandler.STRATEGY.ALL, + tracker : null + }); + + // Initialize proxy + this._super.apply(this, arguments); + + // Define saveHandler in component + this.component.saveHandler = this; + }, + + /** + * Define payload handler base on strategy + * then initialize and run setup + */ + setupPayloadHandler : function() { + this.setupSaveStrategy(); + + // This line is just to avoid confusion and readability + this.payloadHandler = this.saveStrategy; + }, + + setupSaveStrategy : function() { + // Payload handler options + var opts = { + component : this.component + }; + + // Bypass tracker ONLY if a custom one is provided + if (this.opts.tracker) { + opts.tracker = this.opts.tracker; + } + + // Payload handler + switch (this.opts.strategy) { + case SaveHandler.STRATEGY.CHANGED: + this.saveStrategy = new SaveChangedStrategy(opts); + break; + case SaveHandler.STRATEGY.IMMEDIATE: + this.saveStrategy = new SaveImmediateStrategy(opts); + break; + case SaveHandler.STRATEGY.ALL: + case SaveHandler.STRATEGY.NONE: + this.saveStrategy = new SaveAllStrategy(opts); + } + }, + + /** + * Strategy getter + * @return {string} save strategy name + * @match SaveHandler.STRATEGY + */ + getStrategy : function() { + return this.opts.strategy; + }, + + /** + * Save strategy getter + * @return {object} save strategy instance + */ + getSaveStrategy : function() { + return this.saveStrategy; + }, + + /** + * Changes tracker getter + * @return {object} changes tracker instance + */ + getChangesTracker : function() { + return this.getSaveStrategy().getTracker(); + }, + + /** + * Triggers a save action in the current component + * including the children component's data + * + * @return Promise Save promise + */ + saveData : function() { + var errors = this.validateAll(), + savePromise; + + if (!errors || errors.length === 0) { + var saveStrategy = this.getSaveStrategy(); + savePromise = saveStrategy.saveData.apply(saveStrategy, arguments); + } else { + savePromise = this.getInvalidFieldsPromise(errors); + } + + return savePromise; + }, + + /** + * Saves the data to the server using the proxy + * Overwrite this method in the child classes + * @abstract + * @see ju-components/flows/save/strategy + * + * @param object dataToSave data to be saved + * @return Promise A save promise + */ + submitDataToServer : function() {}, + + /** + * Handles the save success event triggered from the payloadHandler by + * bypassing the event to the higher level. + * @abstract + * @see ju-components/flows/save/strategy + */ + onSaveSuccess : function() {}, + + /** + * Returns a rejected promise so it's handled afterwars + * @abstract + */ + getInvalidFieldsPromise : function(errors) { + return Promise.reject(errors); + }, + + /** + * Validates that the current components does not contain any errors. + */ + validateAll : function() { + + var childrenRecursiveOpts = + { + callOnChildren : function(comp) { + // We only call the validate method on the children if the validate method + // does not exists in the current component. + // The validate method is recursive by nature, this is why we prevent + // duplicate recursive calls + return comp.validate === undefined; + }, + flatten : true + }; + + var childrenErrors = this.component.callRecursively(childrenRecursiveOpts, 'validate'), + errors = $.grep(childrenErrors,function(n) { return n; }); //ComponentUtils.flatten(childrenErrors); + + return errors; + } + }); + + SaveHandler.classMembers({ + STRATEGY : { + ALL : 'all', + CHANGED : 'changed', + NONE : 'none', + IMMEDIATE : 'immediate' + } + }); + + return SaveHandler; + +}); diff --git a/src/flows/save/strategies/all.js b/src/flows/save/strategies/all.js new file mode 100644 index 0000000..140b243 --- /dev/null +++ b/src/flows/save/strategies/all.js @@ -0,0 +1,50 @@ +/** _ + * _ _ _| |_ + * | | | |_ _| + * | |___ _ _ | | |_| + * | '_ \| | | || | | | + * | | | || |_| || | | | + * |_| |_|\___,_||_| |_| + * + * (c) Huli Inc + */ + +/** + * Save strategy : All + */ +define([ + 'jquery', + 'ju-components/flows/save/strategy' + ], + function( + $, + SaveStrategy + ) { + 'use strict'; + + /** + * Always save the whole data structure from a root component + * + * ALL strategy uses a base changes tracker + * (no custom modified handler, neither requires a bag, etc) + */ + var SaveAllStrategy = SaveStrategy.extend({ + saveData : function(onSuccess) { + var self = this, + component = this.component, + data = component.getData(), + savePromise = component.saveHandler.submitDataToServer(data, onSuccess); + + savePromise.then(function(response) { + self.onSaveSuccess(response); + component.saveHandler.onSaveSuccess(response); + }); + + return savePromise; + } + }); + + // Exporting module + return SaveAllStrategy; + +}); diff --git a/src/flows/save/strategies/changed.js b/src/flows/save/strategies/changed.js new file mode 100644 index 0000000..453ce2b --- /dev/null +++ b/src/flows/save/strategies/changed.js @@ -0,0 +1,78 @@ +/** _ + * _ _ _| |_ + * | | | |_ _| + * | |___ _ _ | | |_| + * | '_ \| | | || | | | + * | | | || |_| || | | | + * |_| |_|\___,_||_| |_| + * + * (c) Huli Inc + */ + +/** + * Save strategy : Changed + */ +define([ + 'jquery', + 'ju-components/flows/save/strategy', + 'ju-components/flows/save/changes-tracker/changed' + ], + function( + $, + SaveStrategy, + ChangedTracker + ) { + 'use strict'; + + var SaveChangedStrategy = SaveStrategy.extend({ + init : function() { + this.setOptions({ + tracker : ChangedTracker + }); + + this._super.apply(this, arguments); + }, + saveData : function(onSuccess) { + // Select strategy to collect then unsaved data + var self = this, + component = this.component, + changesBag = this.getTracker().getChangesBag(component), + changedChildren = changesBag.getChangedChildrenInstances(), + data = component.getData(changedChildren), + savePromise = component.saveHandler.submitDataToServer(data, onSuccess); + + savePromise.then(function(response) { + // Commits the set of changes + changesBag.commit(); + self.onSaveSuccess(response); + component.saveHandler.onSaveSuccess(response); + }); + + return savePromise; + }, + /** + * Mark components with data as changed in the changes tracker + */ + markComponentsWithData : function(data) { + var component = this.component, + changesTracker = this.getTracker(); + + if (!data || !changesTracker || !component.getComponents()) { + return; + } + + for (var key in data) { + if (data.hasOwnProperty(key)) { + var childComp = component.c(key); + if (childComp) { + changesTracker.modifiedHandler(childComp); + } + } + } + } + }); + + // Exporting module + return SaveChangedStrategy; + +}); diff --git a/src/flows/save/strategies/immediate.js b/src/flows/save/strategies/immediate.js new file mode 100644 index 0000000..4d809c6 --- /dev/null +++ b/src/flows/save/strategies/immediate.js @@ -0,0 +1,62 @@ +/** _ + * _ _ _| |_ + * | | | |_ _| + * | |___ _ _ | | |_| + * | '_ \| | | || | | | + * | | | || |_| || | | | + * |_| |_|\___,_||_| |_| + * + * (c) Huli Inc + */ + +/** + * Save strategy : Immediate + */ +define([ + 'jquery', + 'ju-components/flows/save/strategy', + 'ju-components/flows/save/changes-tracker', + 'ju-components/flows/save/changes-tracker/immediate' + ], + function( + $, + SaveStrategy, + ChangesTracker, + ImmediateTracker + ) { + 'use strict'; + + var SaveImmediateStrategy = SaveStrategy.extend({ + init : function() { + this.setOptions({ + tracker : ImmediateTracker + }); + + this._super.apply(this, arguments); + }, + setup : function() { + this._super.apply(this, arguments); + + // Listening all changed event + // for any event, autosave the changed data + // no need to store data in a collector + this.getTracker().on(ChangesTracker.EV.COMPONENT_CHANGED, $.proxy(this.saveData, this)); + }, + saveData : function(onSuccess) { + var self = this, + component = this.component, + data = component.getData(), + savePromise = component.saveHandler.submitDataToServer(data, onSuccess); + + savePromise.then(function(response) { + self.onSaveSuccess(response); + component.saveHandler.onSaveSuccess(response); + }); + + return savePromise; + } + }); + + // Exporting module + return SaveImmediateStrategy; +}); diff --git a/src/flows/save/strategy.js b/src/flows/save/strategy.js new file mode 100644 index 0000000..f548c81 --- /dev/null +++ b/src/flows/save/strategy.js @@ -0,0 +1,85 @@ +/** _ + * _ _ _| |_ + * | | | |_ _| + * | |___ _ _ | | |_| + * | '_ \| | | || | | | + * | | | || |_| || | | | + * |_| |_|\___,_||_| |_| + * + * (c) Huli Inc + */ + +/** + * Save strategy + * @abstract + */ +define([ + 'jquery', + 'ju-components/flows/fetch/payload', + 'ju-components/flows/save/changes-tracker' + ], + function( + $, + PayloadHandler, + ChangesTracker + ) { + 'use strict'; + + var SaveStrategy = PayloadHandler.extend({ + init : function() { + this.setOptions({ + tracker : ChangesTracker + }); + + this._super.apply(this, arguments); + }, + setup : function() { + this._super.apply(this, arguments); + + // Define changes tracker + this.setupTracker(); + + // Subscribe to events + this.tracker.track(); + }, + setupTracker : function() { + // Backbone must be previously set on component + var backbone = this.component.backbone, + trackerSingletonClass = this.opts.tracker; + + // A tracker class must always be provided by options + this.tracker = trackerSingletonClass.getInst(backbone); + }, + getTracker : function() { + return this.tracker; + }, + /** + * Triggers a save action in the current component + * including the children component's data + * + * @param function onSuccess custom on success callback + * @return Promise Save promise + * @abstract + */ + saveData : function(/* onSuccess */) { + }, + /** + * Handles the save success event triggered from the payloadHandler by + * bypassing the event to the higher level. + * @abstract + */ + onSaveSuccess : function(response) { + this.trigger(SaveStrategy.EV.SAVE_SUCCESS, response); + } + }); + + SaveStrategy.classMembers({ + EV : { + SAVE_ERROR : 'saveError', + SAVE_SUCCESS : 'saveSuccess' + } + }); + + return SaveStrategy; + +}); diff --git a/src/handler.js b/src/handler.js new file mode 100644 index 0000000..4411efd --- /dev/null +++ b/src/handler.js @@ -0,0 +1,76 @@ +/** _ + * _ _ _| |_ + * | | | |_ _| + * | |___ _ _ | | |_| + * | '_ \| | | || | | | + * | | | || |_| || | | | + * |_| |_|\___,_||_| |_| + * + * (c) Huli Inc + */ + +define([ + 'ju-shared/observable-class', + 'ju-components/helper/options' + ], + function( + ObservableClass, + OptionsHelper + ) { + 'use strict'; + + /** + * Base class for handlers definition + */ + var BaseHandler = ObservableClass.extend({ + init : function(opts) { + // Handlers always have a reference to the component related to them + // sort of two ways binding (the component stores the handler instance) + this.setOptions({ + component : null + }); + + // Options manager + this.optsManager.prepareOptions(opts); + this.opts = this.optsManager.getOptions(); + + // Store component + this.component = this.opts.component; + + // Run handler setup + this.setup(); + }, + /** + * Initialize options manager + * @see OptionsHelper + */ + _initOptsManager : function() { + this.optsManager = this.optsManager || new OptionsHelper(); + }, + /** + * Store options in manager + * @decorator + * @see OptionsHelper + */ + setOptions : function() { + this._initOptsManager(); + this.optsManager.setOptions.apply(this.optsManager, arguments); + }, + /** + * Handler setup + * @abstract + */ + setup : function() { + }, + /** + * Run handler main action + * @abstract + */ + process : function() { + } + }); + + // Exporting module + return BaseHandler; + +}); diff --git a/src/helper/changes-tracker.js b/src/helper/changes-tracker.js deleted file mode 100644 index fc154bd..0000000 --- a/src/helper/changes-tracker.js +++ /dev/null @@ -1,140 +0,0 @@ -/** _ - * _ _ _| |_ - * | | | |_ _| - * | |___ _ _ | | |_| - * | '_ \| | | || | | | - * | | | || |_| || | | | - * |_| |_|\___,_||_| |_| - * - * (c) Huli Inc - */ - -define([ - 'require', - 'jquery', - 'ju-shared/observable-class', - 'ju-components/blocks/editable', - 'ju-components/blocks/list', - 'ju-components/blocks/upload', - 'ju-components/helper/changes-bag' - ], - function( - require, - $, - ObservableClass, - EditableComponent, - ListComponent, - UploadAreaComponent, - ChangesBag - ) { - 'use strict'; - - /** - * The ChangesTracker will be used to - */ - var ChangesTracker = ObservableClass.extend({ - init : function(backboneInst) { - this.changedComponents = []; - this.backbone = backboneInst; - this.isTracking = false; - }, - track : function() { - if (this.isTracking) { - return; - } - var modifiedFn = $.proxy(this.modifiedHandler, this); - - // Editable fields - this.backbone.on(EditableComponent.EV.CHANGED, modifiedFn); - this.backbone.on(EditableComponent.EV.EDIT_MODE_ACTIVATED, $.proxy(this.editModeHandler, this)); - - // Lists - this.backbone.on(ListComponent.EV.CHILD_ADDED, modifiedFn); - this.backbone.on(ListComponent.EV.CHILD_REMOVED, modifiedFn); - this.backbone.on(ListComponent.EV.CLEARED, modifiedFn); - - // Upload areas - this.backbone.on(UploadAreaComponent.EV.FILE_ADDED, modifiedFn); - - this.isTracking = true; - }, - - /** - * Called when the user activates the edit mode of a component - */ - editModeHandler : function() { - log('ChangesTracker: component edit mode activated'); - }, - - /** - * Called when a component was modified - * @return {[type]} [description] - */ - modifiedHandler : function(changedComponent) { - if (this.changedComponents.indexOf(changedComponent) === -1) { - // Only add this event - log('ChangesTracker: component change registered'); - this.changedComponents.push(changedComponent); - - this.fireEvent(ChangesTracker.EV.COMPONENT_CHANGED, changedComponent); - } - }, - /** - * Returns a instance of changes bag with the modified components - * that are children of the specified root component - * @param {[type]} rootComponent [description] - * @return ChangesBag [description] - */ - getChangesBag : function(rootComponent) { - var changesBag = ChangesBag.createInst(this, rootComponent, this.changedComponents); - return changesBag; - }, - /** - * Removes the specified components - * @param {[type]} components [description] - * @return {[type]} [description] - */ - removeFromChangedComponents : function(components) { - var self = this; - $.each(components, function(index, component) { - var componentIndex = self.changedComponents.indexOf(component); - if (componentIndex > -1) { - self.changedComponents.splice(componentIndex, 1); - } - }); - } - }); - - ChangesTracker.classMembers({ - EV : { - COMPONENT_CHANGED : 'compChanged', - COMPONENT_ACTIVATED : 'compActivated', - COMPONENT_DEACTIVATED : 'compDeactivated', - SAVE_READY : 'saveReady', - SAVE_NOT_READY : 'saveNotReady' - }, - getInst : function(backboneInst) { - - // check if the notification center has already an id - if (!backboneInst) { - Logger.error('ChangesTracker: A valid notification center must be provided'); - return; - } - - var sharedInstanceKey = backboneInst._class.SHARED.CHANGES_TRACKER, - changesTracker = backboneInst.getSharedInstance(sharedInstanceKey); - - if (!changesTracker) { - // No instance was defined, create one - changesTracker = new ChangesTracker(backboneInst); - backboneInst.addSharedInstance(sharedInstanceKey, changesTracker); - } - - return changesTracker; - } - }); - - // Exporting module - return ChangesTracker; - -}); diff --git a/src/helper/children-loader.js b/src/helper/children-loader.js new file mode 100644 index 0000000..082b516 --- /dev/null +++ b/src/helper/children-loader.js @@ -0,0 +1,112 @@ +/** _ + * _ _ _| |_ + * | | | |_ _| + * | |___ _ _ | | |_| + * | '_ \| | | || | | | + * | | | || |_| || | | | + * |_| |_|\___,_||_| |_| + * + * (c) Huli Inc + */ + +/** + * Configures the children component definition, allows this component to determine + * which components to load in a dynameic way using the parameter passed along + * from the load function of the root component + */ +define([ + 'jquery', + 'ju-shared/class' + ], + function( + $, + Class + ) { + 'use strict'; + + // Enable a base component to load children dynamically (attach methods) based on + // custom logic like permissions or flags. Review following util methods: + + // ---- sample child definition ---- + // component_name : { + // component : Catalog.COMPONENT, + // insertionPoint : '.selector', + // [...flags], + // extendedOpts : {...} + // } + + // @interface: loadChildrenDefinition + + // @warn ONLY initChildrenDef should have access to childrenKey and _addChildrenDef + + var ChildrenLoaderHelper = Class.extend({ + init : function(childrenKeys, childrenDefinitions) { + // Define childrens + this.childrenDef = this.childrenDef || {}; + this.childrenDefinitions = childrenDefinitions || {}; + + // Loaded children keys + // Usually a set of default keys is included at initChildrenDef + this.childrenKeys = childrenKeys || []; + }, + /** + * Defines if a children is accepted from the definition + * Custom implementation must be provided by project + * @abstract + */ + loadChildDefinition : function(/* key, definition */) {}, + // + // Getters + // + getChildrenDefinition : function() { + return this.childrenDef; + }, + getChildrenKeys : function() { + return this.childrenKeys; + }, + getChildrenCount : function() { + return this.getChildrenKeys().length; + }, + getChildKeyByIndex : function(index) { + // Default to null (no key found) + return this.childrenKeys[index] || null; + }, + getChildDefinitionByKey : function(key) { + // Do NOT default to empty object {} + return this.childrenDefinitions[key]; + }, + getChildDefinitionByIndex : function(index) { + return this.getChildDefinitionByKey(this.getChildKeyByIndex(index)); + }, + /** + * Add key for inclusion check + */ + addChildKey : function(key) { + return ($.inArray(key, this.childrenKeys) < 0) ? this.childrenKeys.push(key) : false; + }, + /** + * Checks if a key was loaded + */ + isKeyLoaded : function(key) { + return ($.inArray(key, this.childrenKeys) > -1); + }, + /** + * Check if any key was loaded + */ + isAnyKeyLoaded : function(arrKeys) { + var loaded = false; + for (var i = 0, len = arrKeys.length; i < len; i++) { + var key = arrKeys[i]; + if (this.isKeyLoaded(key)) { + loaded = true; + break; + } + } + return loaded; + } + }); + + // Exporting module + return ChildrenLoaderHelper; + +}); diff --git a/src/helper/options.js b/src/helper/options.js new file mode 100644 index 0000000..f7d2be1 --- /dev/null +++ b/src/helper/options.js @@ -0,0 +1,93 @@ +/** _ + * _ _ _| |_ + * | | | |_ _| + * | |___ _ _ | | |_| + * | '_ \| | | || | | | + * | | | || |_| || | | | + * |_| |_|\___,_||_| |_| + * + * (c) Huli Inc + */ + +/** + * Add options support on any class + * - getOption : fn(key) + * - getOptions : fn() + * - prepareOptions : fn() + * - setOptions : fn(opts) + * + * @warning must be used with decorators on class definition + */ +define([ + 'jquery', + 'ju-shared/class' + ], + function( + $, + Class + ) { + 'use strict'; + + var OptionsHelper = Class.extend({ + init : function() { + // Define options + this._opts = {}; + + // Define options collector + this._initOptsCollector(true); + }, + /** + * Include options in the opts collector + * Set default options and override parents options + * Keeps the flow from child to parent (no untraceable flows with before and after super) + * - Child options always have precedence over parent options + * - Parent options should never overwrite children options + * @warn ALWAYS call setOptions BEFORE _super + * @param {object} class level options to queue + */ + setOptions : function() { + this._optsCollector = this._initOptsCollector(); + // Optimized array prepend + var i, argLen, len = this._optsCollector.length + arguments.length, + optsArr = new Array(len); + + for (i = 0, argLen = arguments.length; i < argLen; i++) { + optsArr[i] = arguments[i]; + } + + for (i = 0; i < len; i++) { + optsArr.push(this._optsCollector[i]); + } + this._optsCollector = optsArr; + }, + /** + * Options getter + */ + getOptions : function() { + return this._opts; + }, + /** + * Single option getter + */ + getOption : function(key) { + return this._opts[key]; + }, + /** + * Transform collected options into this.opts + * @param {object} forcedOpts forced options over all collected options + * normally provided by component definition + */ + prepareOptions : function(forcedOpts) { + // Merge options collector + this._opts = $.extend.apply($, [true, {}].concat(this._optsCollector).concat([forcedOpts])); + this._initOptsCollector(true); + }, + _initOptsCollector : function(clean) { + this._optsCollector = (clean || !this._optsCollector) ? [] : this._optsCollector; + return this._optsCollector; + } + }); + + // Export helper + return OptionsHelper; +}); diff --git a/src/base-with-proxy/proxy.js b/src/proxy.js similarity index 72% rename from src/base-with-proxy/proxy.js rename to src/proxy.js index 6cfda4e..485d2a3 100644 --- a/src/base-with-proxy/proxy.js +++ b/src/proxy.js @@ -32,13 +32,15 @@ define([ }; }, /** - * Query component data - * Only called first time the base-with-proxy is created + * Query initial component data + * + * IMPORTANT: 'fetch' is a naming convention for initial request for component data + * not the same as 'get' prefix which refers to at-any-moment request * * @return Promise * @abstract */ - getPayload : function(data, resolve, reject) {} // jshint ignore:line + fetch : function(data, resolve, reject) {} // jshint ignore:line }); // Exports diff --git a/src/resource-collector.js b/src/resource/collector.js similarity index 99% rename from src/resource-collector.js rename to src/resource/collector.js index 4d41618..817775c 100644 --- a/src/resource-collector.js +++ b/src/resource/collector.js @@ -15,7 +15,7 @@ */ define([ 'ju-shared/class', - 'ju-components/resource/resource-manager' + 'ju-components/resource/manager' ], function( Class, diff --git a/src/resource/lazy-load-helper.js b/src/resource/lazy-load-helper.js index 0631fc3..cb5e7e3 100644 --- a/src/resource/lazy-load-helper.js +++ b/src/resource/lazy-load-helper.js @@ -12,8 +12,8 @@ define([ 'ju-shared/class', 'ju-shared/l10n', 'ju-shared/app-config-manager', - 'ju-components/resource-collector', - 'ju-components/resource/resource-manager', + 'ju-components/resource/collector', + 'ju-components/resource/manager', 'ju-components/resource/storage/template-storage', 'ju-components/resource/storage/options-data-storage', 'ju-components/resource/storage/context-storage', diff --git a/src/resource/resource-manager.js b/src/resource/manager.js similarity index 100% rename from src/resource/resource-manager.js rename to src/resource/manager.js diff --git a/src/resource/resource-proxy.js b/src/resource/proxy.js similarity index 100% rename from src/resource/resource-proxy.js rename to src/resource/proxy.js diff --git a/src/resource/strategy/combined-provider.js b/src/resource/strategy/combined-provider.js index 8e539de..1219f4e 100644 --- a/src/resource/strategy/combined-provider.js +++ b/src/resource/strategy/combined-provider.js @@ -15,7 +15,7 @@ define([ 'ju-shared/dependency-loader', 'ju-shared/l10n', 'ju-components/resource/css-loader', - 'ju-components/resource/resource-proxy', + 'ju-components/resource/proxy', 'ju-components/util' ], function( diff --git a/src/resource/strategy/serverless.js b/src/resource/strategy/serverless.js index 0550216..8bd0164 100644 --- a/src/resource/strategy/serverless.js +++ b/src/resource/strategy/serverless.js @@ -15,7 +15,7 @@ define([ 'ju-shared/dependency-loader', 'ju-shared/l10n', 'ju-components/resource/css-loader', - 'ju-components/resource/resource-proxy', + 'ju-components/resource/proxy', 'ju-components/resource/storage/options-data-storage' ], function( diff --git a/src/resource/strategy/unified-provider.js b/src/resource/strategy/unified-provider.js index 1a724d8..b6081d1 100644 --- a/src/resource/strategy/unified-provider.js +++ b/src/resource/strategy/unified-provider.js @@ -15,7 +15,7 @@ define([ 'ju-shared/dependency-loader', 'ju-shared/l10n', 'ju-components/resource/css-loader', - 'ju-components/resource/resource-proxy' + 'ju-components/resource/proxy' ], function( $,