Skip to content

Instantly share code, notes, and snippets.

@donabrams
Created October 21, 2011 15:31
Show Gist options
  • Select an option

  • Save donabrams/1304126 to your computer and use it in GitHub Desktop.

Select an option

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.
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