Created
October 21, 2011 15:31
-
-
Save donabrams/1304126 to your computer and use it in GitHub Desktop.
Deferred Widget solution with templates, states, and stores. Not really complete at this time.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| define(["jquery/1.5.1/jquery", "Class", "jquery.store", "jquery.executeDeferredStack", "jquery.templater"], | |
| function($, Class, Store, executeDeferredStack, Templater) { | |
| //TODO: somehow let widget state be exposed without depending on widget internals | |
| // | |
| // This is a widget state implementation. | |
| // | |
| // args: | |
| // Recommended: | |
| // templateName/String - should map to a widget.templates[templateName] | |
| // actions/{} - map of action String to either a String or function()->String. | |
| // This value should resolve to a widget action name | |
| // functionStack/[String or function()->$.Deferred] - list of functions to execute in order. | |
| // * - any functions to mixin for functionStack | |
| // keepPreviousTemplate/Boolean | |
| // | |
| var WidgetState = Class.extend({ | |
| init: function(args) { | |
| $.extend(true, this, args); | |
| }, | |
| functionStack: ['loadTemplate', | |
| 'decorateActionElements'], | |
| keepPreviousTemplate: false, | |
| // | |
| // Loads the next state by executing the functionStack | |
| // Returns a promise | |
| // | |
| loadState: function(widget, data) { | |
| return executeDeferredStack(this.functionStack, [widget, data], this); | |
| }, | |
| // | |
| // Gets the next state if the action given is taken | |
| // for the given widget | |
| // | |
| getNextState: function(widget, action) { | |
| var dfd = $.Deferred(); | |
| var resolveState = function(stateName) { | |
| dfd.resolve(stateName === null ? null : widget.states[stateName]); | |
| } | |
| var nextState = this.actions[action]; | |
| if (nextState && $.isFunction(nextState)) { | |
| nextState(widget, this).done(resolveState); | |
| } | |
| else { | |
| resolveState(nextState); | |
| } | |
| return dfd.promise(); | |
| }, | |
| // | |
| // Get the template determined by this.templateName | |
| // stored on the widget. Then, apply the template | |
| // to the widgetElement with the widget data. | |
| // | |
| loadTemplate: function(widget, data, templateName) { | |
| var template = widget.templates[templateName ? templateName : this.templateName]; | |
| return template.applyTemplate( | |
| $.extend(true, {messages:widget.getMessages(),errors:widget.getErrors()}, widget.getData()), | |
| //TODO: this is the only tie to the DOM/jQuery standard widgets in this Class | |
| widget.element, | |
| this.keepPreviousTemplate); | |
| }, | |
| // | |
| // Decorate any node with the class .widgetAction to call doAction( data-action ) | |
| // when an event occurs. The default event is 'click' (override with data-event). | |
| // | |
| decorateActionElements: function(widget) { | |
| var dfd = $.Deferred(); | |
| $(".widgetAction",widget.element).each(function(i, o) { | |
| var l = $(o); | |
| var action = l.data("action"); | |
| var eventTrigger = l.data("event") ? l.data("event") : 'click'; | |
| l.bind(eventTrigger, function(event) { | |
| widget.doAction(action); | |
| event.stopProgagation(); | |
| }); | |
| }); | |
| dfd.resolve(); | |
| return dfd.promise(); | |
| } | |
| }); | |
| // | |
| // This is a statefulWidget: a widget that exists by transitioning between WidgetState. | |
| // | |
| // It also supports Deferred data get/saving via stores. | |
| // | |
| var StatefulWidget = Class.extend({ | |
| init: function(args) { | |
| if (args.data) { | |
| this.data = args.data; | |
| } | |
| //convention for if appUrl is defined, default the storeBaseUrl and the templateBaseUrl | |
| if (args.appUrl) { | |
| args.storeBaseUrl = args.storeBaseUrl ? args.storeBaseUrl : (args.appUrl + "/"); | |
| args.templateBaseUrl = args.templateBaseUrl ? args.templateBaseUrl : (args.appUrl + "/templates/"); | |
| } | |
| if (args.states) { | |
| this.states = args.states; | |
| for (var i = 0; i < this.states.length;i++) { | |
| var state = this.states[i]; | |
| if (!(state instanceof WidgetState)) { | |
| //convention for if a string is provided only, assume its a templateName | |
| if (typeof state === "string") { | |
| state = { | |
| templateName: state | |
| }; | |
| } | |
| this.states[i] = new WidgetState(state); | |
| } | |
| //explicit definition of initial state. | |
| if (state.initialState) { | |
| if (this.initialState) { | |
| throw "two initial states defined"; | |
| } | |
| this.initialState = this.states[i]; | |
| } | |
| } | |
| } | |
| //convention to use first state defined for initial state is not defined explicitly | |
| if (!this.initialState && this.states && this.states.length) { | |
| this.initialState = this.states[0]; | |
| } | |
| if (args.stores) { | |
| this.stores = args.stores; | |
| for (var i = 0; i < this.stores.length;i++) { | |
| var store = this.stores[i]; | |
| if (!(store instanceof Store)) { | |
| //convention for if a string is provided only, assume its a url | |
| if (typeof store === "string") { | |
| store = { | |
| url: store | |
| }; | |
| } | |
| //convention for using storeBaseUrl shortcut | |
| if (args.storeBaseUrl && store.url && (store.url.substring(0, 4) !== "http")) { | |
| store.url = args.storeBaseUrl + store.url; | |
| } | |
| this.stores[i] = new Store(store); | |
| } | |
| } | |
| } | |
| if (args.templates) { | |
| this.templates = args.templates; | |
| for (var i = 0; i < this.templates.length;i++) { | |
| var template = this.templates[i]; | |
| if (!(template instanceof Templater)) { | |
| //convention for if a string is provided only, assume its a url | |
| if (typeof template === "string") { | |
| template = { | |
| url: template | |
| }; | |
| } | |
| //convention for using templateBaseUrl shortcut | |
| if (args.templateBaseUrl && template.url && (template.url.substring(0, 4) !== "http")) { | |
| template.url = args.templateBaseUrl + template.url; | |
| } | |
| this.templates[i] = new Templater(template); | |
| } | |
| } | |
| } | |
| }, | |
| // | |
| // This function saves the state of the widget to | |
| // a non-local store. When implemented, this widget | |
| // should be able to restore to this state. | |
| // | |
| // Keep this synchronous (you can use synchronous ajax requests) | |
| // | |
| saveState: function() {}, | |
| // You don't have to pass in data, but if you do it overrides and mixes | |
| // in with the current data. | |
| // Returns a copy and does not modify existing data. | |
| getData: function(dataIn) { | |
| var data = {}; | |
| $.extend(true, data, this.data, dataIn); | |
| return data; | |
| }, | |
| saveData: function(data) { | |
| this.data = data; | |
| }, | |
| getErrors: function() { | |
| return this.errors; | |
| }, | |
| getMessages: function() { | |
| return this.messages; | |
| }, | |
| addError: function(error) { | |
| this.errors = this.errors || []; | |
| if ($.isArray(error)) { | |
| for (var err in error) { | |
| this.errors.push(err); | |
| } | |
| } else { | |
| this.errors.push(error); | |
| } | |
| }, | |
| addMessage: function(message) { | |
| this.messages = this.messages || []; | |
| if ($.isArray(message)) { | |
| for (var msg in message) { | |
| data.messsages.push(msg); | |
| } | |
| } else { | |
| this.messages.push(message); | |
| } | |
| }, | |
| clearErrorsAndMessages: function() { | |
| this.errors = null; | |
| this.messages = null; | |
| this.saveData(data); | |
| }, | |
| callStore: function(data, storeName) { | |
| return this.stores[storeName].fetch(data); | |
| }, | |
| //TODO: refactor to use deferred pipeline | |
| doAction: function(action, dataIn, queueIfChangingState) { | |
| // If currently changing state, ignore other requests to change | |
| // state unless queueIfChangingState is true | |
| var toPromise = null; | |
| if (!this.changingState) { | |
| toPromise = this.changingState = $.Deferred(); | |
| var that = this; | |
| var resolveStateChange = function() { | |
| var state = that.changingState; | |
| that.changingState = null; | |
| state.resolve(); | |
| }; | |
| var rejectStateChange = function() { | |
| var state = that.changingState; | |
| that.changingState = null; | |
| state.reject(); | |
| }; | |
| var changeState = function(newState) { | |
| //TODO: call onUnload on old state | |
| that.currentState = newState; | |
| newState.loadState(that, dataIn) | |
| .done(resolveStateChange) | |
| .reject(rejectStateChange); | |
| }; | |
| if (this.currentState) { | |
| this.currentState.getNextState(this, action) | |
| .done(changeState) | |
| .fail(rejectStateChange); | |
| } else if (this.initialState) { | |
| changeState(this.initialState); | |
| } else { //no initial state specified, doAction ignored | |
| return; | |
| } | |
| } else if (queueIfChangingState) { | |
| var that = this; | |
| toPromise = $.Deferred(); | |
| this.changingState.done(function() { | |
| that.doAction(action, dataIn, queueIfChangingState) | |
| .then(function() {toRet.resolve(arguments);}, function() {toRet.reject(arguments);}); | |
| }); | |
| } | |
| else { | |
| return; | |
| } | |
| return toPromise.promise(); | |
| } | |
| // | |
| // This is a utility function to execute an action after a delay. | |
| // | |
| // Returns a promise that if resolved returns the result of doAction | |
| // | |
| delayedActions: [], | |
| delayAction: function(delay, widget, action, data) { | |
| var dfd = $.Deferred(); | |
| var startDelay = function() { | |
| setTimeout( | |
| function() { | |
| if (!dfd.isRejected() && !dfd.isResolved()) { | |
| dfd.resolve(widget.doAction(action, data, true)); | |
| } | |
| }, | |
| delay); | |
| }; | |
| this.delayedActions.push(dfd); | |
| if (this.delayedActions.length == 1) { | |
| startDelay(); | |
| } else { | |
| this.delayedActions[this.delayedActions.length-2].done(startDelay); | |
| } | |
| return dfd; | |
| }, | |
| }); | |
| $.widgetState = WidgetState; | |
| $.statefulWidget = StatefulWidget; | |
| return StatefulWidget; | |
| }); | |
| //TODO: implement below: (used to be in StatefulWidget init) | |
| /* | |
| var unloadFunc = function() { | |
| if (!that.stateSaved) { | |
| that.saveState(); | |
| that.stateSaved = true; | |
| } | |
| }; | |
| $("form").sureSubmit(unloadFunc); | |
| //if submitting via javascript, the submit event is not triggered. This makes sure it works! jquery function FTW | |
| $.fn.sureSubmit = function(func) { | |
| this.bind("submit", func).each(function(i, d) { | |
| if (!d._origSubmit) { | |
| d._origSubmit = d.submit; | |
| d.submit = function() { | |
| func(); | |
| d._origSubmit(); | |
| }; | |
| } else { | |
| var f2 = d.submit; | |
| d.submit = function() { | |
| func(); | |
| f2(); | |
| }; | |
| } | |
| }); | |
| };*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment