Skip to content

Instantly share code, notes, and snippets.

@addyosmani
Last active February 7, 2019 15:02
Show Gist options
  • Star 38 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save addyosmani/3769967 to your computer and use it in GitHub Desktop.
Save addyosmani/3769967 to your computer and use it in GitHub Desktop.
Cranium

Cranium is a minimalist MVC implementation I wrote to demonstrate how a developer might write their own MVC library within 20 minutes. The purpose of this exercise is to better appreciate what solutions like Backbone.js provide you out of the box.

You may use jQuery and Underscore (or lo-dash) for your implementation.

Note: This code is released strictly for educational purposes and should not be used in production. If you are searching for an MV* framework, TodoMVC.com may help with your selection process.

/* Cranium MVC
* A minimalist MVC implementation written for
* demonstration purposes at my workshops
* http://addyosmani.com
* Copyright (c) 2012 Addy Osmani; Licensed MIT */
var Cranium = Cranium || {};
// Set DOM selection utility
var $ = document.querySelectorAll.bind(document) || this.jQuery || this.Zepto;
// Mix in to any object in order to provide it with custom events.
var Events = Cranium.Events = {
channels: {},
eventNumber: 0,
trigger: function (events, data) {
for (var topic in Cranium.Events.channels){
if (Cranium.Events.channels.hasOwnProperty(topic)) {
if (topic.split("-")[0] == events){
Cranium.Events.channels[topic](data) !== false || delete Cranium.Events.channels[topic];
}
}
}
},
on: function (events, callback) {
Cranium.Events.channels[events + --Cranium.Events.eventNumber] = callback;
},
off: function(topic) {
delete Cranium.Events.channels[topic];
}
};
// Domain-related data model
var Model = Cranium.Model = function (attributes) {
this.id = _.uniqueId('model');
this.attributes = attributes || {};
};
Cranium.Model.prototype.get = function(attr) {
return this.attributes[attr];
};
Cranium.Model.prototype.set = function(attrs){
if (_.isObject(attrs)) {
_.extend(this.attributes, attrs);
this.change(attrs);
}
return this;
};
Cranium.Model.prototype.toJSON = function(options) {
return _.clone(this.attributes);
};
Cranium.Model.prototype.change = function(attrs){
this.trigger(this.id + 'update', attrs);
};
_.extend(Cranium.Model.prototype, Cranium.Events);
// DOM View
var View = Cranium.View = function (options) {
_.extend(this, options);
this.id = _.uniqueId('view');
};
_.extend(Cranium.View.prototype, Cranium.Events);
// Controller tying together a model and view
var Controller = Cranium.Controller = function(options){
_.extend(this, options);
this.id = _.uniqueId('controller');
var parts, selector, eventType;
if(this.events){
_.each(this.events, function(method, eventName){
parts = eventName.split('.');
selector = parts[0];
eventType = parts[1];
$(selector)['on' + eventType] = this[method];
}.bind(this));
}
};
<div class="container">Foo</div>
<button id="inc">Increment</button>
<button id="alerter">Alert</button>
<script type="text/template" class="counter-template">
<h1><%- counter %></h1>
</script>
<script src="underscore-min.js"></script>
<script src="cranium.js"></script>
<script src="example.js"></script>
// Let's create a basic application
var myModel = new Cranium.Model({
counter: 0,
incr: function () {
myModel.set({ counter: ++this.counter });
}
});
var myView = new Cranium.View({
el: '.container',
template: _.template($('.counter-template').innerHTML),
observe: function (model) {
this.on(model.id + 'update', function (data) {
$(this.el).innerHTML = this.template( model.toJSON() );
}.bind(this));
}
});
var myController = new Cranium.Controller({
// Specify the model to update
model: myModel,
// and the view to observe this model
view: myView,
events: {
"#inc.click" : "increment",
"#alerter.click" : "alerter"
},
// Initialize everything
initialize: function () {
this.view.observe(this.model);
return this;
},
increment: function () {
myController.model.attributes.incr();
return this;
},
alerter: function(){
alert("Yo!");
}
});
// Let's kick start things off
myController.initialize(myModel, myView).increment().increment();
// Some further experiments with Underscore utils
var myModel2 = new Cranium.Model({
caption: 'hello!'
});
console.log(_.any([myModel, myModel2, null]));
console.log(_.pluck([myModel, myModel2], 'id'));
console.log(_.shuffle([myModel, myModel2]));
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title></title>
<meta name="description" content="">
</head>
<body>
<div id="todo">
</div>
<script type="text/template" class="todo-template">
<div>
<input id="todo_complete" type="checkbox" <%= completed %>>
<%= title %>
</div>
</script>
<script src="underscore-min.js"></script>
<script src="cranium.js"></script>
<script src="todo_example.js"></script>
</body>
</html>
// And todo instance
var todo1 = new Cranium.Model({
title: "",
completed: ""
});
console.log("First todo title - nothing set: " + todo1.get('title'));
todo1.set({title: "Do something"});
console.log("Its changed now: " + todo1.get('title'));
''
// View instance
var todoView = new Cranium.View({
// DOM element selector
el: '#todo',
// Todo template; Underscore temlating used
template: _.template($('.todo-template').innerHTML),
init: function (model) {
this.render( model.toJSON() );
this.on(model.id + 'update', this.render.bind(this));
},
render: function (data) {
console.log("View about to render.");
$(this.el).innerHTML = this.template( data );
}
});
var todoController = new Cranium.Controller({
// Specify the model to update
model: todo1,
// and the view to observe this model
view: todoView,
events: {
"#todo.click" : "toggleComplete"
},
// Initialize everything
initialize: function () {
this.view.init(this.model);
return this;
},
// Toggles the value of the todo in the Model
toggleComplete: function () {
var completed = todoController.model.get('completed');
console.log("Todo old 'completed' value?", completed);
todoController.model.set({ completed: (!completed) ? 'checked': '' });
console.log("Todo new 'completed' value?", todoController.model.get('completed'));
return this;
}
});
// Let's kick start things off
todoController.initialize();
todo1.set({ title: "Due to this change Model will notify View and it will re-render"});
@dotnetCarpenter
Copy link

Strictly speaking, at line 11:

var $ = document.querySelector.bind(document) || this.jQuery || this.Zepto;

should be

var $ = document.querySelectorAll.bind(document) || this.jQuery || this.Zepto;

as document.querySelector only select's the first element whereas querySelectorAll, jQuery and Zepto returns all found elements.

I know I'm being pedantic...

@hmsimha
Copy link

hmsimha commented Dec 4, 2014

In Cranium.js, line 11:

var $ = document.querySelectorAll.bind(document) || this.jQuery || this.Zepto;

won't this always evaluate to document.querySelectorAll.bind(document) (meaning it won't use jQuery or Zepto even if they are defined). I'm submitting a pull request to the Appendix for "Developing Backbone.js Applications" to correct this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment