/*
* MVC object model
*
* Requires:
* common_js.js
* tajax.js
*/
/**
* mvc namespace
*/
var mvc = {};
/**
* mvc.model namespace
*
* The model class can be any Object, really. The following methods construct
* extra fields and methods on the model to tie changes in the model to the UI.
*
* Naming conventions:
* - items in the instance that start with an underscore ('_') and
* dollar-sign ('$') are ignored.
* - the setter/getter methods are added on each of the remaining items,
* and are named with the property prefixed by a dollar-sign ('$'). So,
* the field "user" would have a getter/setter method named "$user".
* - private data used by the constructed properties are stored with a prefix
* of '$_'.
*/
mvc.model = {};
/**
* public static void mvc.model.setup(Object model)
*
* Adds setter/getter methods to the model, with the proper interceptor
* chaining. After a model has been setup, it should not be further
* initialized. If a model contains objects that should also be considered
* models, they should be separately setup (i.e. this method does not handle
* recursive setups).
*
* interceptors take the form:
* = function(model, oldVal, newVal, getsetArgs) { ... }
* where:
* model: the model whose value is being altered
* oldVal: the value held by the model before the request to change
* newVal: the value requested to change the model's parameter to
* getsetArgs: the actual arguments which invoked the setter (useful for
* array index setters, but otherwise can be ignored).
* A non-null return code will register an error and prevent further interceptors
* from running. In the case of pre-sets, this will trigger an invocation to the
* error handler interceptors.
*
* @param model the model object to initialize.
*/
mvc.model.setup = function(model, __silent) {
mvc.model.__init_model(model, __silent);
for (var p in model) {
var startC = p.charAt(0);
if (startC != '$' && startC != '_') {
mvc.model.setupParameter(model, p, false);
}
// else ignore the property for our binding purposes.
}
}
/**
* public static void mvc.model.setupParameter(Object model, String parameterName,
* boolean readonly = false)
*
* @param model the model object to initialize
* @param parameterName the name of the parameter to initialize on the model
* @param readonly true if the model does not allow setting this value,
* otherwise false.
*/
mvc.model.setupParameter = function(model, parameterName, readonly) {
mvc.model.__init_model(model, true);
if (! readonly) {
readonly = false;
} else {
readonly = true;
}
model.$_writeable[parameterName] = ! readonly;
model.$_inget[parameterName] = false;
model['$' + parameterName] = function() {
return mvc.model.__getset(this, parameterName, arguments);
};
if (js.isArray(model[parameterName])) {
model.$_type[parameterName] = 1;
} else {
model.$_type[parameterName] = 0;
}
}
/**
* public static void mvc.model.setReadOnly(Object model, String paramName1, ...)
*
* @param model the model object to initialize
* @param paramNameX the property on the model object to set to read-only
*/
mvc.model.setReadOnly = function() {
var i = 0;
var model = arguments[i++];
if (model) {
if (! model.$_setup) {
js.failure('model ' + model + ' must be initialized.');
}
for (; i < arguments.length; i++) {
model.$_writeable[arguments[i]] = false;
}
}
}
/**
* private static void mvc.model.__init_model(Object model, boolean silent)
*/
mvc.model.__init_model = function(model, silent) {
if (model.$_setup) {
if (! silent) {
js.failure('model ' + model + ' was already initialized.');
}
return;
}
model.$_pre = {};
model.$_post = {};
model.$_handleErr = {};
model.$_type = {};
model.$_writeable = {};
model.$_inget = {};
model.$_setup = true;
model.$_errors = null;
model.$error = mvc.model.__error;
}
/**
* protected static void mvc.model._tieView(Object model, mvc.view view)
*
* Ties the updateX() methods in view to the model's setter, and possibly
* adds the necessary setters to the model. View updates are always called
* after the model's data was updated.
*/
mvc.model._tieView = function(model, view) {
for (p in view) {
// update
mvc.model.__tie(model, view, p, 'update_', model.$_post);
mvc.model.__tie(model, view, p, 'update', model.$_post);
// error
mvc.model.__tie(model, view, p, 'error', model.$_handleErr);
}
};
/**
* protected static void mvc.model._tieController(Object model, mvc.controller controller)
*/
mvc.model._tieController = function(model, controller) {
for (p in controller) {
// validate
mvc.model.__tie(model, controller, p, 'validate', model.$_pre);
}
}
/**
* private void mvc.model.__tie(Object model, Object obj, String objProp,
* String prefix, Array<[Function, Object]> interceptors)
*
* TODO: to conserve a bit of memory, change from a list of a list, to
* in-lining the inner list, since it's always 2 elements.
*/
mvc.model.__tie = function(model, obj, objProp, prefix, interceptors) {
if (! model.$_setup) {
js.failure('model ' + model + ' must be initialized.');
}
if (objProp.substring(0, prefix.length) == prefix) {
var x = objProp.substring(prefix.length);
var xi = x.charAt(0);
if (xi != '' && xi != '_' && xi != '$') {
if (model['$' + x]) {
if (! interceptors[x]) {
interceptors[x] = [ [ obj[objProp], obj ] ]
} else {
interceptors[x].push([ obj[objProp], obj ]);
}
}
}
}
}
/**
* public String[] mvc.model.$error()
*
* @return the list of errors associated with this model, or null if there
* are no errors.
*/
mvc.model.__error = function() {
if (this.$_errors) {
return this.$_errors;
}
return null;
};
/**
* private static void mvc.model.__getset(Object that, String field, Object[] args)
*
* @param that the model instance
* @param field the name of the field to get/set
* @param args the arguments passed to the get/set method
*/
mvc.model.__getset = function(that, field, args) {
var i = that.$_type[field];
// eventually, i may be allowed to be > 1, but for now, it's only used
// for array indicies.
var oldVal = null;
if (i > 0) {
var j = 0; // eventually, something like: for (var j = 0; j < i; j++)
oldVal = that[field][args[j]];
} else {
oldVal = that[field];
}
js.debug("getset: field = " + field + ", arglen = " + args.length + ", i = " + i);
if (args.length > i) {
var newVal = args[i];
var error = false;
if (! that.$_writeable[field]) {
error = true;
var m = 'field ' + field + ' is read-only';
mvc.model.__addError(that, m);
} else if (that.$_inget[field]) {
error = true;
var m = 'field ' + field + ' already being set';
mvc.model.__addError(that, m);
} else if (mvc.model.__$interceptor(that.$_pre[field], that, field, oldVal, newVal, args)) {
// set was okay
that.$_inget[field] = true;
if (i > 0) {
var j = 0; // eventually, something like: for (var j = 0; j < i; j++)
that[field][args[j]] = newVal;
} else {
that[field] = newVal;
}
js.debug("wrote '"+newVal+"' to "+field);
mvc.model.__$interceptor(that.$_post[field], that, field, oldVal, newVal, args);
// new value now set
oldVal = newVal;
that.$_inget[field] = false;
} else {
error = true;
}
if (error) {
// error in the set, possibly from a read-only flag
mvc.model.__$interceptor(that.$_handleErr[field], that, field, oldVal, newVal, args);
}
}
return oldVal;
};
mvc.model.__$interceptor = function(interceptors, that, field, oldVal, newVal, args) {
var ret = true;
if (interceptors) {
for (var i = 0; i < interceptors.length; i++) {
var err = false;
var func = interceptors[i][0];
var obj = interceptors[i][1];
js.debug("calling "+that+"."+func+" for field "+field);
if (obj) {
err = func.call(obj, that, oldVal, newVal, args);
} else {
err = func(that, val, args);
}
if (err) {
mvc.model.__addError(that, err);
ret = false;
}
}
}
return ret;
};
mvc.model.__addError = function(model, err) {
js.error(err);
if (model.$_errors) {
model.$_errors.push(err);
} else {
model.$_errors = [ err ];
}
}
// ========================================================================
/**
* The basic "view" class. There should be one instance of these per UI
* component. Note that multiple view instances can be tied to a single
* model instance.
*/
mvc.view = js.Class();
mvc.view.prototype.setModel = function(model) {
if (this.model) {
js.failure('Already set model for view ' + this);
return;
}
mvc.model._tieView(model, this);
this.model = model;
}
mvc.view.prototype.update = function() {
for (p in this) {
if (p.substring(0,6) == 'update' && p.length > 6) {
this[p]();
}
}
}
mvc.view.prototype.showTemplate = function(templateId) {
js.debug("parsing template id " + templateId);
var d = document.getElementById(templateId);
if (! d) {
this._noTemplate(templateId);
return;
}
var newText = '';
try {
newText = js.parseMsg(d.innerHTML, this);
} catch (e) {
js.except("Error parsing template", e);
}
js.debug("parsed template as: " + newText);
document.writeln(newText);
}
mvc.view.prototype._noTemplate = function(templateId) {
js.failure('Could not find template element ' + templateId);
}
// =========================================================================
/*
* mvc messages
*/
mvc.messages = {};
js.messages.language_code = "en";
js.messages.language = "US English";