|
/* |
|
Before we can even talk about the model and the view, we |
|
have to talk about how they communicate with each other. |
|
Emulating the built in DOM element event emmitter interface |
|
allows the model and the view to communicate without having |
|
to know how the other operates. This principle of decoupling |
|
can make things frustrating sometimes, but there is a reason |
|
why the browser uses this interface, and we're using it for |
|
that same reason of insulating one piece of functionality |
|
from another to reduce the cost of changes. |
|
*/ |
|
|
|
function EventEmitter() { |
|
// I'm using a convention here of naming properties with |
|
// an underscore to indicate that this is not part of the |
|
// public API of the class. In other words we're telling |
|
// people not to use this property, and that it's used |
|
// internally. |
|
this._events = {}; |
|
} |
|
|
|
EventEmitter.prototype.addEventListener = function(event, listener) { |
|
// This says set listeners equal to this._events[event] if it |
|
// exists, otherwise a new array. At the same time it checks |
|
// whether or not this._events[event] exists, and if not sets |
|
// it equal to the same array. |
|
var listeners = this._events[event] = this._events[event] || []; |
|
listeners.push(listener); |
|
}; |
|
|
|
EventEmitter.prototype.removeEventListener = function(event, listener) { |
|
var listeners = this._events[event]; |
|
if (listeners) { |
|
listeners.splice(listeners.indexOf(listener), 1); |
|
} |
|
}; |
|
|
|
EventEmitter.prototype.triggerEvent = function(event) { |
|
var listeners = this._events[event]; |
|
// This grabs the arguments passed to the function |
|
// after the event argument. Since the arguments |
|
// object isn't really an array, we have to use |
|
// the Array prototype's slice method in this |
|
// manner to achieve that. |
|
var args = Array.prototype.slice.call(arguments, 1); |
|
var listeners = this._events[event]; |
|
|
|
if (listeners) { |
|
for (var i = 0; i < listeners.length; i++) { |
|
listeners[i].apply(this, args); |
|
} |
|
} |
|
}; |
|
|
|
function Model(data) { |
|
// Applying the EventEmitter constructor function |
|
// to our model object allows it to be configured |
|
// as an event emitter as well as a model. This is |
|
// part of how it "inherits" that objects functionality. |
|
// The other part being the call to Object.create to |
|
// set the Model function's prototype. |
|
|
|
EventEmitter.apply(this, arguments); |
|
|
|
// We'll initialize `this.data` to an empty object, |
|
// which we'll use to store the data in our model. |
|
this.data = {}; |
|
|
|
for (var key in data) { |
|
if (data.hasOwnProperty(key)) { |
|
// This set method is defined below, but it |
|
// stores values in our model. |
|
this.set(key, data[key]); |
|
} |
|
} |
|
} |
|
|
|
Model.prototype = Object.create(EventEmitter.prototype); |
|
|
|
/* |
|
OK, so that seems like the silliest constructor |
|
function ever. Why would I want an object that |
|
simply transfers data from one object to another |
|
object? It turns out that primitive values like |
|
numbers and strings are already wrapped in objects |
|
for the same reason: to expose a set of methods |
|
to interact with the values in that object. So |
|
here we go: |
|
*/ |
|
|
|
Model.prototype.get = function(key) { |
|
return this.data[key]; |
|
}; |
|
|
|
Model.prototype.set = function(key, value) { |
|
this.data[key] = value; |
|
this.triggerEvent('change', key, value); |
|
}; |
|
|
|
/* |
|
So these functions might seem a little superfluous at |
|
first glance. What if your API returned an object with |
|
a key called "toString" though, and you still wanted |
|
your data object to have a toString method? This solves |
|
that issue by using one object to store the data, and |
|
wrapping it in an object to expose an API. |
|
|
|
So this class becomes a wrapper for our data, and we can |
|
extend it if need be to add methods specific to the type |
|
of data we're modeling. We could also add methods that do |
|
our ajax requests if our data is coming or going to a server, |
|
which is extremely convenient. |
|
|
|
Also, because our model is an event emitter and a "change" |
|
event is triggered when we set properties, we can "watch" |
|
and react to changes in model values. This turns out to |
|
be extremely useful. |
|
|
|
Here's another type of model object, a collection. This |
|
is basically an object that wraps an array of models. it |
|
is also an event emitter and triggers 'add' and 'remove' |
|
events when a model is added or removed. |
|
*/ |
|
|
|
function Collection(models) { |
|
// We also want our collection to behave like an event |
|
// emitter, so we apply the EventEmitter function to |
|
// `this` (the collection object) here as well. |
|
EventEmitter.apply(this, arguments); |
|
// We initialize `this.models` to an empty array which |
|
// we'll use to store our models. |
|
this.models = []; |
|
if (models) { |
|
// So what this says is "For each model, call my |
|
// add function, and make it so that `this` in the |
|
// add function is the same as `this` here in the |
|
// constructor function. |
|
models.forEach(this.add, this); |
|
} |
|
} |
|
|
|
Collection.prototype = Object.create(EventEmitter.prototype); |
|
|
|
Collection.prototype.add = function(model) { |
|
// This line says "If the `model` variable is a plain |
|
// object and not a `Model`, create a `Model` object |
|
// out of it. This ensures that our collection is only |
|
// storing models, and makes it more convenient to add |
|
// data to our collection. |
|
if (!(model instanceof Model)) model = new Model(model); |
|
this.models.push(model); |
|
this.triggerEvent('add', model); |
|
}; |
|
|
|
Collection.prototype.remove = function(model) { |
|
var modelIndex = this.models.indexOf(model); |
|
|
|
if (modelIndex !== -1) { |
|
this.models.splice(this.models.indexOf(model), 1); |
|
this.triggerEvent('remove', model); |
|
} |
|
}; |
|
|
|
Collection.prototype.forEach = function(cb, context) { |
|
// Here we are giving our collection it's own `forEach` |
|
// method that acts just like the `Array` object's built |
|
// in `forEach` method. |
|
this.models.forEach(cb, context); |
|
}; |
|
|
|
Object.defineProperty(Collection.prototype, 'length', { |
|
// This is the ES5 syntax for creating a read only |
|
// property called `length`. We want people to be |
|
// able to access the number of models in our |
|
// collection using `collection.length` just like |
|
// a regular property, but we don't want them to |
|
// be able to modify it. |
|
get : function() { |
|
return this.models.length; |
|
} |
|
}); |
|
|
|
/* |
|
So this is our basic view. It takes either a collection or |
|
a model It defines a single root dom element associated with |
|
the view. If you pass one it wraps it in a jQuery object, and |
|
if you don't it creates a new div or whatever tagname you |
|
specify. |
|
*/ |
|
|
|
function View(options) { |
|
EventEmitter.apply(this, arguments); |
|
|
|
if (options.model) this.model = options.model; |
|
if (options.collection) this.collection = options.collection; |
|
|
|
if (options.el) { |
|
this.$el = $(options.el); |
|
} else { |
|
this.$el = $(document.createElement(this.tagName)); |
|
} |
|
if (this.className || options.className) { |
|
this.$el.attr('class', this.className || options.className); |
|
} |
|
} |
|
|
|
View.prototype = Object.create(EventEmitter.prototype); |
|
|
|
View.prototype.tagName = 'div'; |
|
|
|
View.prototype.render = function() { |
|
// We haven't defined a tempalate function on the prototype, |
|
// this is something that individual views can choose to |
|
// implement based on thier needs. |
|
if (this.template) { |
|
this.$el.html(this.template()); |
|
} |
|
}; |
|
|
|
/* |
|
So now that we have a basic framework, we can get started |
|
writing our practical application. Basically what this app |
|
does is take the value from an input and put it into a list. |
|
You could imagine though that the data from the input is |
|
sent to the server instead, or the data in the list is |
|
generated by an api call. |
|
|
|
So this is our first view. It mediates click events on a button, |
|
grabs some data from an input, puts the data into a model, and |
|
adds it to a collection. |
|
*/ |
|
|
|
function FormView() { |
|
View.apply(this, arguments); |
|
} |
|
|
|
FormView.prototype = Object.create(View.prototype); |
|
|
|
FormView.prototype.render = function() { |
|
// Since this view doesn't have a template, this call to |
|
// the View's render function does nothing, but it's a |
|
// good practice to do it just the same in case the |
|
// internal workings of the view change in the future. |
|
View.prototype.render.apply(this, arguments); |
|
|
|
this.$el.find('button').on('click', this.getInput.bind(this)); |
|
}; |
|
|
|
FormView.prototype.getInput = function() { |
|
var value = this.$el.find('input').val(); |
|
|
|
var model = new Model({ |
|
value : value |
|
}); |
|
|
|
this.collection.add(model); |
|
}; |
|
|
|
/* |
|
This view takes a collection and creates a bunch of |
|
list item views using the models in the collection. |
|
it also listens for changes in the collection and |
|
adds ListItemViews for the new models. |
|
*/ |
|
|
|
function ListItemView() { |
|
View.apply(this, arguments); |
|
} |
|
|
|
ListItemView.prototype = Object.create(View.prototype); |
|
|
|
ListItemView.prototype.tagName = 'li'; |
|
ListItemView.prototype.className = 'list-group-item'; |
|
|
|
ListItemView.prototype.template = function() { |
|
// I've trivially wrapped the model value in a span to illustrate |
|
// how you can use this function to generate HTML |
|
return '<span>' + this.model.get('value') + '</span>'; |
|
}; |
|
|
|
function ListView() { |
|
View.apply(this, arguments); |
|
this.children = []; |
|
this.collection.addEventListener('add', this.addListItem.bind(this)); |
|
} |
|
|
|
ListView.prototype = Object.create(View.prototype); |
|
|
|
ListView.prototype.render = function() { |
|
View.prototype.render.apply(this, arguments); |
|
this.collection.forEach(this.addListItem, this); |
|
}; |
|
|
|
ListView.prototype.addListItem = function(model) { |
|
this.addChild(new ListItemView({ |
|
model: model |
|
})); |
|
}; |
|
|
|
ListView.prototype.addChild = function(view) { |
|
this.$el.append(view.$el); |
|
this.children.push(view); |
|
|
|
view.render(); |
|
}; |
|
|
|
ListView.prototype.removeChild = function(view) { |
|
var viewIndex = this.children.indexOf(view); |
|
if (viewIndex !== -1) { |
|
view.$el.remove(); |
|
this.children.splice(viewIndex, 1); |
|
} |
|
}; |
|
|
|
/* |
|
Here is how it all comes together: |
|
*/ |
|
$(function() { |
|
var valueCollection = new Collection([{ |
|
value: 'Try adding more items to the list...' |
|
}]); |
|
|
|
var formView = new FormView({ |
|
el: '#value-form', |
|
collection: valueCollection |
|
}); |
|
|
|
var listView = new ListView({ |
|
el: '#value-list', |
|
collection: valueCollection |
|
}); |
|
|
|
/* |
|
That's the whole setup. Now we render our views: |
|
*/ |
|
|
|
formView.render(); |
|
listView.render(); |
|
|
|
/* |
|
And that's it! Now everything works, and because |
|
we've divided the concerns into separate objects |
|
we can easily add new functionality. If you want |
|
to play around with it, try adding a button to the |
|
list items and write some code that removes them |
|
from the list. |
|
*/ |
|
}); |