define([
"lodash",
"thorax",
"larynx",
"bauplan.error.view"
], function (_, Thorax, Larynx, ErrorView) {
/**
* @module bauplan%control%view
* @description ## Control component view
*
* var ControlView = require("bauplan.control.view");
*
* Provides default control views and methods to register additional control types, formats and primitives
*
* Any control for an attribute that is registered as having any of these kinds will have that kind’s phase methods executed automatically when the corresponding phase runs
*
* #### Kinds
*
* Primitives represent native JSON schema types that can be assigned to a model’s attribute, eg. string, boolean, integer
*
* Formats represent format types that can be assigned to a model’s attribute
*
* Control types represent a spcecialised control that that can be assigned to a model’s attribute
*
* #### Phase methods
*
* All kinds can provide any of the following phase methods as needed
* - prepare -
* does any extra work required to get the value during the control’s initialisation
* - format -
* formats the value to be displayed
* - normalize -
* ensures that value is in the right format
*
* eg. collates data if in several input fields
* or transforms a date string in to an actual date object
* - validate -
* validates the normalized value, adding any errors encountered to an error stack
*
* NB. It is preferable to use the {@link template:control%view} template rather than make a new ControlView directly
*
* @extends Thorax.View
* @return {constructor} ControlView
*/
/**
* @method extend
* @return {constructor} ControlView
* @static
* @param {string} [valueAttribute=value] Control property to use to obtain controls’s value
*/
var ControlView = Thorax.View.extend({
/**
* @member {string} name=control View name
*/
name: "control",
/**
* @member {string} template=control.view Template key
*/
template: "control.view",
/**
* @member {array} kinds List of control kinds
*/
kinds: ["primitive", "format", "controltype"],
/**
* @member {object} primitives Primitive kind register
*/
primitives: {},
/**
* @member {object} formats Format kind register
*/
formats: {},
/**
* @member {object} controltypes Control type kind register
*/
controltypes: {},
/**
* @member {string} [valueAttribute=value] Control property to use to obtain controls’s value
*/
valueAttribute: "value",
/**
* @method initialize
* @inner
* @param {object} options Options
*
* @param {string} options.control-name The name to use for the control inoput’s name (or equivalent)
*
* @param {string} [options.edit=true] Edit mode - whether to render an editable or display version of the control view
*
* @param {string} [options.display=false] Inverse alias for edit. NB. has no effect if edit is defined
*
* @param {object} [options.model] Model to bind to the control (controlmodel). Can also be passed as control-model. If not passed, a new empty Thorax Model is used.
*
* @param {boolean} [options.instant-validate] Whether to perform instant validation on the control
*
* @param {boolean} [options.required=false] Whether the control is required to have a value
*
* @param {boolean} [options.multiline=false] Whether a text input is allowed to have multiple lines
*
* @param {string} [options.value] The initial value for the control. If not provided the control will retrieve the value from the control model using the control's name (control-name) as the property
*
* @param {string} [options.control-id] Value to use for control’s id. If not provided the control will use control-{{controlname}}
*
* @param {string} [options.phrasekey] Phrasekey to use for control.
*
* If not provided the control will attempt to use
* - {{controlmodel._phrasekey}}
* - model.{{controlmodel._model}}.{{controlname}}
*
* @param {string} [options.label] Text for control’s label.
*
* If not provided the control will attempt to use {{phrasekey}}.label as a lookup key, falling back to using the controlname if none is found
*
* @param {string} [options.placeholder] Text to use for placeholder
*
* If not provided the control will attempt to use {{phrasekey}}.placeholder
*
* @param {string} [options.note] Text for control’s note.
*
* If not provided the control will attempt to use {{phrasekey}}.note
*
* @param {boolean} [options.control-keyup=false] Whether to validate on keyup
*
* @param {string} [options.control-type] Overrides any controltype that may have been specified by the controlmodel’s schema
*
* @param {string} [options.control-primitive] Overrides any primitive that may have been specified by the controlmodel’s schema
*
* @param {string} [options.control-format] Overrides any format that may have been specified by the controlmodel’s schema
*
* @param {viewmodel} [options.parent] The parent view this control should belong to
*
* @description
* - Determines whether mode is edit or display
* - if schema property has fixed, then the control is uneditable and mode will always be display
* - Sets empty controlmodel if none passed
* - Sets dirty model
* - Removes any property prefixed with control-
* - Adds relevant initialization methods
* - Applies relevant prepare methods
* - Attaches schema
* - Creates clean model for control
* - Create messages model for control
* - Update model
*/
initialize: function(options) {
var newoptions = _.extend({}, options);
options = newoptions;
//console.log("control.view.init da noo", options, "this", this);
// set this.controlparent to default parent in context since this.parent doesn't exist yet
this.controlparent = options.parent;
this.edit = options.edit;
if (this.edit === undefined) {
this.edit = !options.display;
}
// but is it fixed?
this.controlname = options["control-name"];
var controlname = this.controlname;
this.controlmodel = options.model || options["control-model"] || new Thorax.Model();
var controlmodel = this.controlmodel;
// or indeed, get it from this.controlparent.model!!!!!
this.dirtymodel = new Thorax.Model(controlmodel ? _.extend({}, controlmodel.attributes) : {});
//var schemaProperties;
var schema = controlmodel ? controlmodel.schema : undefined;
//var schemaProperties = schema.properties;
var schemaControl;
var fixedControl;
if (schema && schema.properties && schema.properties[controlname]) {
schemaControl = schema.properties[controlname];
}
this.controlschema = schemaControl;
this.primitive = "string";
this.controloptions = {};
if (this.controlschema) {
if (this.controlschema.type) {
this.primitive = this.controlschema.type;
}
if (this.controlschema.format) {
this.format = this.controlschema.format;
}
if (this.controlschema.controltype) {
this.controltype = this.controlschema.controltype;
}
if (this.controlschema.controloptions) {
this.controloptions = this.controlschema.controloptions;
}
fixedControl = this.controlschema.fixed;
}
if (this.fixed !== undefined) {
fixedControl = this.fixed;
}
if (fixedControl && controlmodel && controlmodel.get(controlname)) {
this.edit = false;
}
//console.log("schemaControl", schemaControl);
function getControlParam(key, bool, attributeKeyMap) {
var param;
if (options[key] !== undefined) {
param = options[key];
delete options[key];
} else {
if (schemaControl) {
var lookupKey = attributeKeyMap||key;
param = schemaControl[lookupKey];
}
}
if (bool) {
param = !!param;
}
return param;
}
this.keyup = getControlParam("control-keyup", true, "keyup");
this.instantValidation = getControlParam("instant-validate", true, "instant-validate");
this.instantValidation = true; // total override for now
this.required = getControlParam("required", true);
this.multiline = getControlParam("multiline", true);
var controlvalue = options.value !== undefined ? options.value : this.controlmodel.get(this.controlname);
var phrasekey = options.phrasekey;
if (!phrasekey && controlmodel) {
phrasekey = controlmodel._phrasekey || "model." + controlmodel._model + "." + controlname;
}
this.phrasekey = phrasekey;
var label = options.label;
if (label === undefined) {
label = Larynx.Phrase.get(phrasekey, {_append: "label"}) || controlname;
}
this.label = label;
var id = options["control-id"] || "control-"+this.controlname;
var placeholder = options.placeholder;
if (placeholder === undefined) {
placeholder = Larynx.Phrase.get(phrasekey, {_append: "placeholder"});
}
this.placeholder = placeholder; // uh, why?
var note = options.note;
if (note === undefined) {
note = Larynx.Phrase.get(phrasekey, {_append: "note"});
}
this.controltype = options["control-type"] || this.controltype || (this.multiline ? "textarea" : "text");
this.primitive = options["control-primitive"] || this.primitive;
this.format = options["control-format"] || this.format;
// this should probably come before this.keyup, instantValidation etc.
// or rather they should come after this
// but first, a design decision on whether real-time checking is the default or not
for (var k in this.kinds) {
var kind = this.kinds[k];
var kinds = kind + "s";
if (this[kinds].initialize) {
var extras = this[kinds].initialize[this[kind]] || {};
for (var addProp in extras) {
this[addProp] = extras[addProp];
}
}
}
var control = {
name: this.controlname,
controlid: id,
value: controlvalue,
label: label
};
if (placeholder) {
control.placeholder = placeholder;
}
if (note) {
control.note = note;
}
control = this.preparePrimitive(control, phrasekey, options);
control = this.prepareFormat(control, phrasekey, options);
control = this.prepareControl(control, phrasekey, options);
if (this.controlschema) {
if (this.controlschema.exactLength) {
//this.controlschema.maxLength = this.controlschema.exactLength;
//this.controlschema.minLength = this.controlschema.exactLength;
}
if (this.controlschema.maxLength) {
control.maxlength = this.controlschema.maxLength;
}
}
delete options["control-name"];
delete options.phrasekey;
delete options.label;
delete options["control-type"];
delete options["control-primitive"];
delete options["control-format"];
delete options.edit;
delete options.display;
delete options.className;
delete options["control-model"];
// delete anything else beginning with control-
// anything prefixed input- goes for input
for (var op in options) {
var pop = op.replace(/^control-/, "");
if (pop !== op) {
this.controloptions[pop] = options[op];
delete options[op];
}
}
this.controlschema = this.controlschema || {};
// hmmmmmm
control = _.extend({}, options, control);
var model = new Thorax.Model(control);
this.setModel(model); //, {render:false});
this.messages = new Thorax.Model();
// allow errors, info to be passed explicitly
this.updateControl(model.get(this.valueAttribute));
},
/**
* @method context
* @override
* @description Deletes attributes meant for label and not for control input
*
* Assigns template to text.{{mode}} if controltype does not have a template
*
* Overrides Thorax.View.prototype.context
* @return {object} View attributes
*/
context: function() {
this.controlparent = this.controlparent || this.parent;
this.controldirtymodel = this.controlparent.dirtymodel;
var attrs = _.extend({}, this.model.attributes);
attrs.attributes = _.extend({}, this.model.attributes);
attrs.attributes.id = attrs.attributes.controlid;
delete attrs.attributes.controlid;
delete attrs.attributes.label;
delete attrs.placeholder;
attrs.controltype = this.controltype;
var tmpltype = "." + (this.edit ? "edit" : "display");
var tmpl = this.controltype + tmpltype;
if (!Thorax.templates[tmpl]) {
// could check for multiline and set to textarea?
tmpl = "text" + tmpltype;
}
attrs.controltemplate = tmpl;
return attrs;
},
/**
* @member {boolean} controlError Whether the control has an error
* @private
* @inner
*/
controlError: false,
/**
* @member {boolean} controlErrorFlag Whether the control should display an error
* @private
* @inner
*/
controlErrorFlag: false,
/**
* @member {array} errorDisplayStack Array of errors for control
* @private
* @inner
*/
errorDisplayStack: undefined,
/**
* @method startErrorReport
* @private
* @inner
* @description Notes the existence of any previous errors and resets this.controlError, this.controlErrorFlag and this.errorDisplayStack
*/
startErrorReport: function() {
this.hadError = this.getError();
this.controlError = false;
this.controlErrorFlag = false;
this.errorDisplayStack = undefined;
},
/**
* @method addError
* @param {string} err Error name (can be a phrasekey)
* @param {object} options Following options
* @param {boolean} [options.display=false] Whether to display error
* @param {boolean} [options.forceDisplay=false] Whether to force display
* @param {string} [options.method=push] Method to use to add error to stack
* @description Sets this.controlError to true
* If the error is to be displayed also sets this.controlErrorFlag and adds the error to this.errorDisplayStack using the method specified (push by default)
*/
addError: function (err, options) {
//console.log("addError - this", this, "this.controlError", this.controlError);
options = options || {};
this.controlError = true;
if (options.forceDisplay) {
options.display = true;
}
if (options.flag) {
this.controlErrorFlag = true;
} else if (options.display !== false) {
this.controlErrorFlag = true;
this.errorDisplayStack = this.errorDisplayStack || [];
this.errorDisplayStack[options.method || "push"](err);
}
},
/**
* @method getError
* @return {boolean} Whether control has an error
*/
getError: function() {
return this.controlError;
},
/**
* @method getDisplayErrors
* @private
* @inner
* @return {array} List of errors to be displayed for the control
*/
getDisplayErrors: function() {
return this.errorDisplayStack;
},
/**
* @method prepareMethod
* @private
* @inner
* @param {string} kind Name of kind
* @param {control} control Control view object
* @param {string} [phrasekey] Optional phrasekey for lookups
* @param {object} options Additional options
* @description Generic handler for applying kinds prepare methods to control
* @return {control} Updated control
*/
prepareMethod: function (kind, control, phrasekey, options) {
var actualMethod = "prepare";
//if ()
var kinds = kind + "s";
if (this[kinds].prepare && this[kinds].prepare[this[kind]]) {
control = this[kinds].prepare[this[kind]].apply(this, [control, phrasekey, options]);
}
return control;
},
/**
* @method preparePrimitive
* @private
* @inner
* @param {control} control Control view object
* @param {string} [phrasekey] Optional phrasekey for lookups
* @param {object} options Additional options
* @description Passes params to {@link module:bauplan%control%view~prepareMethod}
* @return {control} Updated control
*/
preparePrimitive: function (control, phrasekey, options) {
return this.prepareMethod("primitive", control, phrasekey, options);
},
/**
* @method prepareFormat
* @private
* @inner
* @param {control} control Control view object
* @param {string} [phrasekey] Optional phrasekey for lookups
* @param {object} options Additional options
* @description Passes params to {@link module:bauplan%control%view~prepareMethod}
* @return {control} Updated control
*/
prepareFormat: function (control, phrasekey, options) {
return this.prepareMethod("format", control, phrasekey, options);
},
/**
* @method prepareControl
* @private
* @inner
* @param {control} control Control view object
* @param {string} [phrasekey] Optional phrasekey for lookups
* @param {object} options Additional options
* @description Passes params to {@link module:bauplan%control%view~prepareMethod}
* @return {control} Updated control
*/
prepareControl: function (control, phrasekey, options) {
return this.prepareMethod("controltype", control, phrasekey, options);
},
/**
* @method registeredMethod
* @private
* @inner
* @param {*} value Control value
* @param {string} kind Name of kind
* @param {control} control Control view object
* @param {object} options Additional options
* @description Generic handler for applying kinds methods to control
* @return {control} Updated control
*/
registeredMethod: function (value, kind, method, options) {
var kinds = kind + "s";
if (this[kinds][method] && this[kinds][method][this[kind]]) {
value = this[kinds][method][this[kind]].apply(this, [value, options]);
}
return value;
},
/**
* @method normalizeMethod
* @private
* @inner
* @param {*} value Control value
* @param {string} kind Name of kind
* @description Generic normalize method handler for kinds.
* Passes params to {@link module:bauplan%control%view~registeredMethod}
* @return {*} Updated value
*/
normalizeMethod: function (value, kind) {
return this.registeredMethod(value, kind, "normalize");
},
/**
* @method normalizePrimitive
* @private
* @inner
* @param {*} value Control value
* @description Passes params to {@link module:bauplan%control%view~normalizeMethod}
* @return {*} Updated value
*/
normalizePrimitive: function(value) {
return this.normalizeMethod(value, "primitive");
},
/**
* @method normalizeFormat
* @private
* @inner
* @param {*} value Control value
* @description Passes params to {@link module:bauplan%control%view~normalizeMethod}
* @return {*} Updated value
*/
normalizeFormat: function (value) {
//console.log("normalizeFormat", this.normalizeMethod(value, "format"));
return this.normalizeMethod(value, "format");
},
/**
* @method normalizeControlType
* @private
* @inner
* @param {*} value Control value
* @description Passes params to {@link module:bauplan%control%view~normalizeMethod}
* @return {*} Updated value
*/
normalizeControlType: function (value) {
return this.normalizeMethod(value, "controltype");
},
/**
* @method normalizeControl
* @private
* @inner
* @param {*} value Control value
* @description Calls {@link module:bauplan%control%view~normalizePrimitive}, {@link module:bauplan%control%view~normalizeFormat}, {@link module:bauplan%control%view~normalizeControlType}
* @return {*} Updated value
*/
normalizeControl: function (value) {
value = this.normalizePrimitive(value);
value = this.normalizeFormat(value);
value = this.normalizeControlType(value);
return value;
},
/**
* @method validateRequired
* @param {*} value Control value
* @param {object} options Additonal validation options
* @description Returns whether or not a control has a value
*
* If the field is required the following values are treated as absent
*
* - undefined
* - empty string
* - false if primitive is boolean
* - null if primitive is not null
*
* If the value is absent and the control is required, a required error is added to the control unless
*
* - the control model either had no previous value for the property
* - options.forecDisplay is true
*
* Id the field is not required, absent is calculated based on the truthiness of the value
*
* @return {boolean} absent Whether control has no value
*/
validateRequired: function (value, options) {
var absent;
if (this.required) {
if (value === undefined) {
absent = true;
} else if (value === "") {
absent = true;
} else if (value === false && this.primitive === "boolean") {
absent = true;
} else if (value === null && this.primitive !== "null") {
absent = true;
}
}
if (absent) {
// if it previously had a value or we really want to know
var newoptions = {};
var display = !!(this.controlmodel.get(this.controlname) || options.forceDisplay);
if (options.forceDisplay) {
display = true;
}
//newoptions.display
//console.log("display", display, this.controlname, this.controltype);
this.addError("required", {display: display});
}
// cheeky! if it's not required and has no value, don't do any of the other validation
// NB. normalisation should make sure that value isn't a different kind of falsy
if (!value && !this.required) {
absent = true;
}
return absent;
},
/**
* @method validateMethod
* @private
* @inner
* @param {*} value Control value
* @param {string} kind Name of kind
* @param {object} options Additional validation options
* @description Generic validation method for kinds.
* Passes params to {@link module:bauplan%control%view~registeredMethod}
* @return {errorModel} messages
*/
validateMethod: function (value, kind, options) {
this.registeredMethod(value, kind, "validate", options);
return this.messages;
},
/**
* @method validatePrimitive
* @private
* @inner
* @param {*} value Control value
* @param {object} options Additional validation options
* @description Generic validation method for kinds.
* Passes params to {@link module:bauplan%control%view~validateMethod}
* @return {errorModel} messages
*/
validatePrimitive: function (value, options) {
return this.validateMethod(value, "primitive", options);
},
/**
* @method validateFormat
* @private
* @inner
* @param {*} value Control value
* @param {object} options Additional validation options
* @description Generic validation method for kinds.
* Passes params to {@link module:bauplan%control%view~validateMethod}
* @return {errorModel} messages
*/
validateFormat: function (value, options) {
return this.validateMethod(value, "format", options);
},
/**
* @method validateControlType
* @private
* @inner
* @param {*} value Control value
* @param {object} options Additional validation options
* @description Generic validation method for kinds.
* Passes params to {@link module:bauplan%control%view~validateMethod}
* @return {errorModel} messages
*/
validateControlType: function (value, options) {
return this.validateMethod(value, "controltype", options);
},
/**
* @method validateControl
* @private
* @inner
* @param {*} value Control value
* @param {object} options Additional validation options
* @param {string} [options.error] Manually passed error name/phrasekey
* @param {boolean} [options.rendered=true] Whether to flag up any error
* @param {boolean} [options.forceDisplay=true] Whether to force the display of any error
* @param {boolean} [options.norevalidation=false] Whether to trigger revalidation of all the parent view's controls
* @description Does nothing if in display mode
*
* - Clears previous errors
* - Trims string values
* - Normalizes value
* - Adds error explicitly passed in options
* - Calls
* - this.validateRequired
* - this.validatePrimitive
* - this.validateFormat
* - this.validateControlType
* - call flagError if appropriate
* - silently sets dirtymodel
* - silently sets controldirtymodel (if there is one)
* - if value has changed, sets controlparent's initialValues to false
* - updates parent view if appropriate
* @return {*} Updated value
*/
validateControl: function (value, options) {
if (!this.edit) {
return value;
}
this.startErrorReport();
// TODO: stick T12 in here and add to String.prototype
if (typeof value === "string") {
value = value.replace(/^\s+/,"").replace(/\s+$/,"");
}
value = this.normalizeControl(value);
if (options.error) {
this.addError(options.error, options);
} else {
var absentRequired = this.validateRequired(value, options);
if (!absentRequired) {
this.validatePrimitive(value, options);
//if (this.co) Do generics stuff here
this.validateFormat(value, options);
this.validateControlType(value, options);
}
}
if (!options.rendered) {
var flagit;
if (options.forceDisplay) {
flagit = this.getError();
}
this.flagError(flagit);
}
// in case we need to know the value that failed to get validated,
// ie. what's the current state of the control
this.dirtymodel.set(this.valueAttribute, value, {silent:true});
if (this.controldirtymodel) {
this.controldirtymodel.set(this.controlname, value, {silent:true});
}
/*var hasError = this.getError();
if (!hasError && !options.norevalidation) {
console.log("updating", this.controlname, options);
// why set this if it's the first time through and nothing's changed?
this.model.set(this.valueAttribute, value, {silent:true});
// why did this need force?
// if so, an invalid state can be set
// MO currently is, all good, update the model and then just save it
// the model will always be in a good state since we don't allow trash to be saved
// therefore must check that none of the contained control views of the page has an error
// - but if a control is not dirty but invalid we must perform a validation of all controls first
//if (options.force) {
// a) options.silent
// b) move to validateControl along with the dirty model
// need to be able to reinsert the cursor at the right place if we do the move
// otherwise we go in to loop of if silent, won't get notifications
// if not silent, everything gets triggered for re-rendering
// good job, I already wrote some code to do that
// c) likewise, why isn't updateParent in validateControl?
// then this becomes just a display thang / forceValidate / showValidate
var silence = true;
if (value !== this.controlmodel.get(this.controlname)) {
this.controlparent.initialValues = false;
this.controlmodel.set(this.controlname, value, {silent:silence});
}
//}
}
this.updateParent(options);*/
//!this.getError() &&
//console.log(this.controlname, "error", this.getError(), value, this.controlmodel.get(this.controlname));
if (this.controlparent && value !== this.controlmodel.get(this.controlname)) {
this.controlparent.initialValues = false;
}
if (!options.norevalidation) {
this.updateParent(options);
}
return value;
},
/**
* @method flagError
* @private
* @inner
* @param {boolean} [hasError=false] Whether to show an error.
* Forced to true if this.controlErrorFlag
* @description Toggles control-error class on control view element
*
*/
flagError: function (hasError) {
if (hasError === undefined) {
hasError = false;
}
if (this.controlErrorFlag) {
hasError = true;
}
// Do not toggle if this all resolves to true
if (!hasError && this.getError() && this.hadError) {
if (this.getDisplayErrors()) {
return;
}
}
this.$el.toggleClass("control-error", hasError);
},
/**
* @method displayErrors
* @param {string} value Control's value
* @description Looks up and computes error templates
*
* Checks in turn for the a i18n phrase for the keys and uses the first non-empty value
*
* 1. {{phrasekey}}.error.{{error}} (as key for phrase lookup)
* 2. control.error.{{error}} (as key for phrase lookup)
* 3. {{error}} (as is)
*
* eg. if there was an oops error on the bar property of the foo model
*
* 1. model.foo.bar.error.oops
* 2. control.error.oops
* 3. "oops"
*
* The control's name, label and value are passed to Larynx Phrase where they can be used for substitution purposes
*/
displayErrors: function (value) {
var displayErrors = this.getDisplayErrors();
if (displayErrors) {
for (var err = 0; err < displayErrors.length; err++) {
var errorBundle = {
_append: "error",
_appendix: displayErrors[err],
name: this.controlname,
label: this.label,
value: value
};
displayErrors[err] = Larynx.Phrase.get(this.phrasekey, errorBundle) || Larynx.Phrase.get("control", errorBundle) || displayErrors[err];
}
this.messages.set("error", displayErrors);
} else {
this.messages.unset("error");
}
},
/**
* @method updateControl
* @param {*} value Control value.
* If passed as an object, will get the value using this.getValue() and set options to value’s value
* @param {object} options Update options
* @description Validate the control
* Display errors (if any)
* If no error
* - update model silently
* - update control model if changed (silently if the option is set)
*/
updateControl: function (value, options) {
/*if (!this.controlparent) {
return;
}*/
if (typeof value === "object") {
options = value;
value = this.getValue();
}
options = options || {};
// TODO: Here goes proper validation.
// Either use JSV or have a bunch of validation functions that can return an array of error codes
// Apart from not supporting the uri-ification of included schemas like JSV does, I like the second way more
// Also, have a pre-filter here. After all we set the value on the actual object
// This also allows composite controls to do their stuff
value = this.validateControl(value, options);
this.displayErrors(value);
var hasError = this.getError();
if (!hasError) {
// why set this if it's the first time through and nothing's changed?
this.model.set(this.valueAttribute, value, {silent:true});
// why did this need force?
// if so, an invalid state can be set
// MO currently is, all good, update the model and then just save it
// the model will always be in a good state since we don't allow trash to be saved
// therefore must check that none of the contained control views of the page has an error
// - but if a control is not dirty but invalid we must perform a validation of all controls first
//if (options.force) {
// a) options.silent
// b) move to validateControl along with the dirty model
// need to be able to reinsert the cursor at the right place if we do the move
// otherwise we go in to loop of if silent, won't get notifications
// if not silent, everything gets triggered for re-rendering
// good job, I already wrote some code to do that
// c) likewise, why isn't updateParent in validateControl?
// then this becomes just a display thang / forceValidate / showValidate
if (value !== this.controlmodel.get(this.controlname)) {
//this.controlparent.initialValues = false;
var silence = this.controloptions.silent || false;
this.controlmodel.set(this.controlname, value, {silent:silence});
}
//}
}
// TODO: actualErrors vs displayErrors
// should be in validateControl
//this.updateParent(options);
},
/**
* @method instantValidateControl
* @private
* @inner
* @param {*} value Control's value
* @param {object} [options] Validation options
* @description Calls validation on a control immediately and updates parent view
*/
instantValidateControl: function (value, options) {
options = _.extend({instantValidate:true}, options);
this.messages.unset("error");
this.validateControl(value, options);
if (!this.getError() && this.hadError) {
this.messages.unset("error");
}
// should be in validateControl - but check on them options
this.updateParent();
},
/**
* @method updateParent
* @private
* @inner
* @param {object} [options] Options
* @param {boolean} [options.norevalidation] Whether to prevent revalidation
* @description If the control has a parent view
* - disable parent view form if it has an error
* - revalidate all parent view controls if the control previously had an error
*/
updateParent: function (options) {
// move the parent model (and dirtymodel) sets here?
if (this.controlparent) {
if (this.getError()) {
this.controlparent.disableForm();
} else if (true || this.hadError) {
if (!options || !options.norevalidation) {
this.controlparent.validateAllControls();
}
}
}
},
/**
* @method getValue
* @description Returns control value irrespective of control type
* @return {*} The control's current value
*/
getValue: function () {
var value;
var $el = this.$el.find("input, textarea, select");
if ($el.length) {
value = $el.val();
if (this.valueAttribute === "checked") {
value = $el.is(":checked");
}
} else {
// is this really any good?
value = this.model.get(this.valueAttribute);
}
return value;
},
/**
* @todo EVENTS
*/
events: {
//"focus input": function (e) {
// console.log(e, this.$el.find("input").get(0));
//},
"change input, textarea, select": function(e) {
var value = this.getValue();
// hang on - this gets called when the input value is updated
// can we prevent a double dip here?
this.updateControl(value);
},
"keyup input, textarea": function(e) {
var value = this.getValue();
if (this.keyup) {
this.updateControl(value);
} else if (this.instantValidation) {
this.instantValidateControl(value);
}
},
"keydown input": function(e) {
var restrictinput = this.restrictinput || this.primitive;
if (restrictinput === "integer" || restrictinput === "number" || restrictinput === "payment-card") {
if (e.altKey) {
e.preventDefault();
e.stopPropagation();
return false;
}
}
},
"keypress input[type=text]": function(e) {
var abort;
var restrictinput = this.restrictinput || this.primitive;
// gah, this.keypress
if (this.keypress) {
} else if (restrictinput === "integer") {
abort = ((e.charCode < 48 || e.charCode > 57) && e.charCode !== 13);
} else if (restrictinput === "number") {
abort = ((e.charCode < 48 || e.charCode > 57) && e.charCode !== 13 && e.charCode !== 46);
if (!abort) {
var val = this.getValue();
abort = (e.charCode === 46 && val.indexOf(".") !== -1);
}
} else if (restrictinput === "payment-card") {
abort = ((e.charCode < 48 || e.charCode > 57) && e.charCode !== 13 && e.charCode !== 32 && e.charCode !== 45);
}
if (abort) {
e.preventDefault();
e.stopPropagation();
return false;
}
},
"keypress input": function(e) {
var cp = this.controlparent;
if (e.charCode === 13 && cp) {
cp.validateAllControls({update:true});
if (cp.hasNoErrors && !cp.disabled) {
cp.saveForm(cp.saveOptions, e);
}
}
}
}
});
/**
* @method addMethod
* @private
* @inner
* @param {object} attrs
* @description Attaches method of right kind to control
*/
function addMethod (attrs) {
attrs.kind = attrs.kind || "controltype";
var kindplural = attrs.kind+"s";
if (!ControlView.prototype[kindplural]) {
ControlView.prototype[kindplural] = {};
}
var kinds = ControlView.prototype[kindplural];
var method = attrs.method;
if (!kinds[method]) {
kinds[method] = {};
}
kinds[method][attrs.name] = attrs.fn;
}
/**
* @method addTypeMethods
* @private
* @inner
* @param {object} attrs
* @description Generic handler to allow additonal controls to add kinds methods
*
* Checks the existence of the following methods and, if found, adds it to the control
* - initialize
* - prepare
* - normalize
* - validate
*/
function addTypeMethods (attrs) {
var methods = ["prepare", "normalize", "validate", "initialize"];
for (var i = 0; i < methods.length; i++) {
var method = methods[i];
if (attrs[method]) {
attrs.method = methods[i];
attrs.fn = attrs[method];
addMethod(attrs);
}
}
}
/**
* @method addControlType
* @param {object} attrs
* @static
* @description Allows additonal controls to add controltypes
*/
ControlView.addControlType = function (attrs) {
attrs.kind = "controltype";
addTypeMethods(attrs);
};
/**
* @method addPrimitive
* @param {object} attrs
* @static
* @description Allows additonal controls to add primitives
*/
ControlView.addPrimitive = function(attrs) {
attrs.kind = "primitive";
addTypeMethods(attrs);
};
/**
* @method addFormat
* @param {object} attrs
* @static
* @description Allows additonal controls to add formats
*/
ControlView.addFormat = function (attrs) {
attrs.kind = "format";
addTypeMethods(attrs);
};
/**
* @method addNormalization
* @param {object} attrs
* @static
* @description Allows additonal controls to add normalization methods
*/
ControlView.addNormalization = function (attrs) {
attrs.method = "normalize";
addMethod(attrs);
};
/**
* @method addPreparation
* @param {object} attrs
* @static
* @description Allows additonal controls to add preparation methods
*/
ControlView.addPreparation = function (attrs) {
attrs.method = "prepare";
addMethod(attrs);
};
/**
* @method addValidation
* @param {object} attrs
* @static
* @description Allows additonal controls to add validation methods
*/
ControlView.addValidation = function (attrs) {
attrs.method = "validate";
addMethod(attrs);
};
return ControlView;
});