spineless.js |
|
---|---|
Version 0.3.1 A simple MVC stack without the need of a backbone. https://github.com/heavysixer/spineless The goal of Spineless is to provide “just enough framework” to succeed. If we have done our job you should be able to write your first Spineless app in less than 10 minutes. Spineless is meant to run with virtually no dependencies. In fact in an age of frameworks with massive dependency chains here is a list of things you DO NOT need to run spineless.
Spineless has only two dependencies, JQuery and Mustache.js, both which come bundled with the project inside the /lib directory. Like any good MVC framework Spineless uses the concept of models, controllers and views.
In addition to the normal MVC stack, Spineless also uses the concept of helpers and templates.
Going Spineless in 10 minutes or lessThe entire Spineless application resides inside the “.application” div. An application consists of a collection of controllers which in turn contain a collection of views. Consider the following example:
In this example you’ll see that we have defined an application with a single controller. The name
of the controller is defined by the Routing RequestsRouting requests through Spineless is incredibly painless to make any link a spineless request just add the “route” class. For example:
When the user clicks on this link they will now be routed to the application controller where
the
If you want to manually trigger a route request from within Javascript you can call the
|
|
Helper functions Helpers are developer-created functions that execute during the rendering of specific templates. Just like in Rails, helpers are available globally across all views. To demonstrate, imagine we have two DIV tags with locals supplied as urlencoded JSON object:
As you can see these objects have a property called “name”, each with unique values. These locals are linked to the “hi-my-name-is” template. To create a helper we’ll bind a function to execute whenever the “hi-my-name-is” template is rendered. Doing this will allows us intercept the template instance’s data-locals object and modify it anyway we choose before passing it along to Mustache to render. Here is the full example of the helper function:
|
(function($) {
var Application = function() {
return {
controllers: {},
helpers: {
_default: function(locals) {
return $.extend(true, {},
locals);
}
}
};
};
var Request = function(controller, action, params) {
var p = (typeof(params) === 'undefined') ? {}: params;
return {
controller: $(".controller[data-controller=" + controller + "]"),
view: $(".controller[data-controller=" + controller + "] .view[data-action=" + action + "]"),
params: $.extend(true, p, {
controller: controller,
action: action
})
};
}; |
Router expects a |
var Router = function() {
return {
parseRoute: function(str) {
var hsh = $.extend(true, {},
{
controller: 'application',
action: 'index'
});
while (str.charAt(0) == '/') {
str = str.substr(1);
}
if (str.length > 0) {
$.each(str.split('/'),
function(i, e) {
if (i === 0) {
hsh.controller = e;
}
if (i === 1) {
hsh.action = e;
}
});
}
return hsh;
},
route: function(element) {
var url;
url = element.attr('href') || $(element).attr('data-href');
var route = this.parseRoute(url);
this.get(route.controller, route.action, route.params);
}
};
}; |
PubSub for Spineless eventsSpineless now has a very minimal publisher subscriber (PubSub) events framework. The goal of this is to allow other code executing outside of Spineless to receive updates when internal Spineless events execute, without having to know anything about how Spineless is implemented. Here is a trivial example of creating an observer that is triggered every time a view is done rendering.
When the publisher executes a subscriber’s function it passes a reference to itself and the Spineless app instance as arguments. This allows the receiver to manage it’s subscriptions and gives the function access to the the Spineless current request, params hash among other things. |
var PubSub = function() {
var o = $({});
return {
subscribe: function() {
o.on.apply(o, arguments);
},
unsubscribe: function() {
o.off.apply(o, arguments);
},
publish: function() {
o.trigger.apply(o, arguments);
}
};
};
var Spineless = function(options) {
var root = this;
var templates = function(method, locals) {
return (root.app.helpers.hasOwnProperty(method)) ? root.app.helpers[method].apply(root.app, [locals]) : root.app.helpers._default(locals);
}; |
Passing local variables to templatesWhen rendering templates, Spineless substitutes predefined template variables with those you supply using JSON. The JSON can be provided in at least two ways:
I will explain the helper function method next, but here is a simple example of what the data-locals method looks like: |
var parseLocals = function(view) {
var locals = $(view).attr('data-locals');
if (locals !== undefined) {
locals = $.parseJSON(locals);
} else {
locals = {};
}
return locals;
};
var renderTemplate = function(view) {
var name,
locals,
template;
name = $(view).attr('data-template');
if (name !== undefined) {
locals = parseLocals($(view));
template = $('.templates *[data-template-name=' + name + ']').html();
view.html($.mustache(template, templates(name, locals)));
}
};
var prepareRender = function() {
if (root.request.controller && root.request.view) {
$('.view.active').removeClass('active');
$('.controller.active').removeClass('active');
root.request.view.addClass('active');
root.request.controller.addClass("active");
return root.request.view.find("*[data-template]");
}
return [];
};
var render = function(elements) {
$.each(elements,
function(i, e) {
renderTemplate($(e));
});
}; |
Controller functionsController functions are optional code that developers can write to augment the rendering of the view. Controller functions work much like helper functions do, in that they are executed before the view is returned to the screen. Unlike helper functions which are linked to an arbitrary number of templates; controller functions are scoped to just one controller action. Consider this example which executes when someone visits “/users/update”:
|
var controllerActionAvailable = function() {
return root.app.controllers.hasOwnProperty(root.request.params.controller) &&
root.app.controllers[root.request.params.controller].hasOwnProperty(root.request.params.action);
};
var postRender = function() {
$('body').attr('data-controller', root.request.params.controller);
$('body').attr('data-action', root.request.params.action);
$('body').addClass('rendered');
root.app.publish("afterRender", root.app);
};
var get = function(controller, action, params) {
root.request = new Request(controller, action, params);
root.app.request = root.request;
$('body').removeClass('rendered');
$('html,body').animate({
scrollTop: 0
},
1);
var itemsToRender = prepareRender();
if (controllerActionAvailable()) {
root.app.controllers[root.request.params.controller][root.request.params.action].apply(root.app, [itemsToRender, root.request]);
} else {
render(itemsToRender);
}
postRender();
};
function init(options) {
$(document).on('click', '.route',
function(event) {
event.preventDefault();
root.app.route($(this));
});
$.extend(true, root.app, options);
}
this.app = new Application();
$.extend(true, this.app, new Router());
$.extend(true, this.app, new PubSub());
this.app.get = get;
this.app.render = render;
init(options);
return this.app;
};
$.spineless = function(options) {
return new Spineless(options);
};
})(jQuery); |