Skip to content

Instantly share code, notes, and snippets.

@neonstalwart
Created August 9, 2012 05:03
Show Gist options
  • Save neonstalwart/3301151 to your computer and use it in GitHub Desktop.
Save neonstalwart/3301151 to your computer and use it in GitHub Desktop.
separated concerns (presentation model)
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