Created
August 9, 2012 05:03
-
-
Save neonstalwart/3301151 to your computer and use it in GitHub Desktop.
separated concerns (presentation model)
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([ | |
'compose/compose', | |
'dojo/Stateful', | |
'dojo/on' | |
], function (compose, Stateful, on) { | |
// TODO: | |
// * need a helper like this.own to help manage handles from eg watch and on | |
// * need a lifecycle - at least destroy/dispose | |
var InputModel = compose(Stateful, { | |
// an input has a value | |
value: null, | |
// for demonstration, presentation-related properties are allowed in this model. it is | |
// meant to be a presentation model. | |
focused: false | |
// any more attributes that define an input? this is mostly just to define an interface | |
}), | |
ValidatedInputModel = InputModel.extend({ | |
// validation error message | |
message: '', | |
// indicates if input is valid | |
valid: undefined, | |
// compose with other validators to override/extend validation logic | |
validate: function () { | |
return true; | |
}, | |
// trigger validation whenever the value is set. this behavior was arbitrarily chosen | |
// to make a simple prototype. in reality, the logic to trigger validation could be | |
// more complex than this. | |
_valueSetter: function (value) { | |
this.set('valid', this.validate(value)); | |
// only update the value if it was valid | |
if (this.valid) { | |
this.value = value; | |
} | |
// re-focus the field if it was not valid | |
else { | |
this.set('focused', true); | |
} | |
} | |
}), | |
// a mixin that validates a value as an SSN based on a simple regular expression | |
SSNValidation = { | |
validate: compose.around(function (validate) { | |
return function (value) { | |
var valid = /^\d{3}\-\d{2}\-\d{4}$/.test(value); | |
if (!valid) { | |
this.set('message', value + ' is not a valid SSN (XXX-XX-XXXX)'); | |
} | |
return valid && validate.apply(this, arguments); | |
}; | |
}) | |
}, | |
// View is a base class for all views. at this point it's only useful for defining an API | |
// but this is where we would add suitable lifecyle methods and helpers (like this.own). | |
// NOTE: using Stateful for the convenience of setters/getters here. in this example, i | |
// don't need watch since nothing observes the view. in practice it could be ok for views | |
// to observe each other but it wouldn't be typical and apart from that, nothing else | |
// observes the views | |
View = compose(Stateful, { | |
model: null, | |
render: compose.required | |
}), | |
InputView = View.extend({ | |
render: function (parent) { | |
if (!this.model) { | |
throw new Error('a model is required to render an Input'); | |
} | |
var el = this.el = document.createElement('input'), | |
model = this.model; | |
if (parent) parent.appendChild(el); | |
this.bind(el, model); | |
}, | |
// bind the element and the model together. this function is convenient for now but it | |
// could turn out not to be a very useful public API. | |
bind: function (el, model) { | |
// updateModel and watchModel are helper functions that use a semaphore to prevent | |
// infinite update loops. this kind of stuff would likely be extracted into a | |
// common place - maybe in the View baseclass. | |
function updateModel(prop, value) { | |
updatingModel[prop] = true; | |
model.set(prop, value); | |
updatingModel[prop] = false; | |
} | |
function watchModel(prop, update) { | |
// set an initial value | |
update(model[prop]); | |
// keep in sync with the model | |
return model.watch(prop, function ($, _, value) { | |
if (!updatingModel[prop]) update(value); | |
}); | |
} | |
// a quick and dirty solution to provide semaphores to break infinite update loops | |
var updatingModel = {}; | |
// update the input when the model changes | |
watchModel('value', function (value) { | |
el.value = value; | |
}); | |
watchModel('focused', function (focused) { | |
if (focused) { | |
el.focus(); | |
} | |
else { | |
el.blur(); | |
} | |
}); | |
// update model when the input changes | |
on(el, 'change', function () { | |
updateModel('value', this.value); | |
}); | |
on(el, 'focus', function () { | |
updateModel('focused', true); | |
}); | |
on(el, 'blur', function () { | |
updateModel('focused', false); | |
}); | |
} | |
}), | |
// this just displays a message based on the state of the valid property of the model | |
MessageView = View.extend({ | |
validMessage: '', | |
invalidMessage: '', | |
_modelSetter: function (model) { | |
var view = this; | |
model.watch('valid', function ($, _, valid) { | |
if (view.el) view.el.innerText = view[valid ? 'validMessage' : 'invalidMessage']; | |
}); | |
this.model = model; | |
}, | |
render: function (parent) { | |
// maybe extract this model checking into the View base class | |
if (!this.model) { | |
throw new Error('a model is required to render an Input'); | |
} | |
var el = this.el = document.createElement('div'); | |
el.innerText = this[this.model.valid ? 'validMessage' : 'invalidMessage']; | |
if (parent) parent.appendChild(el); | |
} | |
}), | |
AlertNotification = { | |
_modelSetter: function (model) { | |
model.watch('message', function ($, _, message) { | |
if (message != null) { | |
alert(message); | |
} | |
}); | |
this.model = model; | |
} | |
}, | |
ValidationTextBox = InputView.extend(AlertNotification), | |
RequiredSSNModel = ValidatedInputModel.extend(SSNValidation), | |
model = new RequiredSSNModel({ | |
// initially set the input to be focused - should be reflected in the view | |
focused: true | |
}), | |
validationTextBox = new ValidationTextBox({ | |
model: model | |
}), | |
// this shares the same model with the validated input | |
messageView = new MessageView({ | |
model: model, | |
invalidMessage: 'invalid input', | |
validMessage: 'looks good!' | |
}); | |
// render the views into the document | |
validationTextBox.render(document.body); | |
messageView.render(document.body); | |
// below are a few tests interacting with the model to see how that is reflected in the view | |
// remove focus from the input by updating the model | |
setTimeout(function() { | |
model.set('focused', false); | |
}, 1000); | |
// simulate setting the input based on data from the server | |
setTimeout(function() { | |
// XXX: there should be a way to set a value without triggering validation since data from | |
// the server should be considered valid | |
model.set('value', 'asdflkj'); | |
}, 2500); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment