Skip to content

Instantly share code, notes, and snippets.

@polotek
Created December 6, 2011 20:44
Show Gist options
  • Save polotek/1439922 to your computer and use it in GitHub Desktop.
Save polotek/1439922 to your computer and use it in GitHub Desktop.
/**
* The Article domain object, this can be completely custom or
* part of some ORM. It doesn't really matter. It's an object you
* can call methods on. As long as you return some compatible
* promise-like thing to the data/render cycle
*/
var util = require('util')
, Q = require('q') // https://github.com/kriskowal/q
, db = require('./data-layer')
, Domain = require('./domain');
var Article = function() {
}
util.inherits(Article, Domain.Record);
// Some instance methods
Article.prototype.foo = function(value) {
return this.bar + value;
}
// Static methods
// Some logic here that fleshes out db calls
// into domain objects.
Article.ranked = function(limit) {
var articles;
// Could be your fancy ORM. This returns a promise and
// could be totally lazy
articles = Article.all.sortBy('rank').limit(limit || 100);
// OR
// Could be totally custom using promises
var defer = Q.defer();
articles = defer.promise;
// I'm butchering some couchdb api here, but you get the point
db.view('ranked_articles', function(err, view) {
if(err) { return defer.reject(err); }
var objects = Article.create(view.rows);
defer.resolve(objects);
});
return articles;
}
SomeController.route("GET", "/", [
getUserAndSession,
checkAccess,
function (req, res, params, next) {
// decide which template to show. Let's assume the framework
// has already done the current user lookup
var user = req.currentUser
, template = 'frontindex';
if(user && user.loggedIn) {
template = 'dashboard';
}
// You have a main domain object that you want
// to render. So load it.
Home.get(function(home) {
// Build up your data. This object will be a combination
// of values and promises that represent async calls.
var data = {
someValue: "Some data value here"
, tasks: Task.indexList()
, articles: Article.ranked(10);
}
// The render method does more than just execute
// the template. It also fully resolves the data
// object by composing any nested promises and
// waiting until they're all resolved
home.render(template, data)
.on('error', next)
// So render return a stream. The stream can
// emit data that is ready to be pushed down the
// pipe. We need an event for when the stream is
// ready in case any of the setup fails.
.on('ready', function(templateStream) {
templateStream.on('error', next).pipe(res);
});
});
}]),
<!doctype html>
<html>
<head>
<title>{this.title}</title>
<link rel="stylesheet" href="style.css"/>
</head>
<body>
{this.topbar(data.links)}
<div class="container_16">
<div class="grid_11">
<div class="that_value">Here's your value: {data.someValue}</div>
<div class="section">
{this.summary(data.articles)}</div>
</div>
<div class="grid_5">
<div class="section">
<h3>Sidebar</h3>
{render('task_list', data.tasks)}
{if req.currentUser && req.currentUser.hasRole('admin') }
{ render('admin_links', req.currentUser.adminLinks()) }
{/if}
</div>
</div>
</div>
<ul>
{forEach this.footerLinks() link idx}
<li class="{link.class}">{link.render()}</li>
{/forEach}
</ul>
</body>
</html>
@polotek
Copy link
Author

polotek commented Dec 7, 2011

This is a totally made up, blue sky, vision of what a request cycle might look like. None of the ideas here are fully baked.

There are several interesting things happening here though. Domain objects are just js objects with properties and methods on them. They can be backed by whatever datastore you want and implemented however you prefer. There are just a few important areas where they need to conform to a compatible api. Here the only constraint is that if you're passing data to the render api, it could be a value or a promise-like thing to be resolved asynchronously.

The controller handles a few things.

  • gather the relevant data
  • decide on the appropriate view for the response
  • execute rendering and send it to the response

The controller leverages eventing and pipes where it can. The nice thing is that the template rendering is a stream. It doesn't have to be, but this is the way you would do immediate rendering by flushing to the response as soon as data is available.

The view also has several things happening. This has a lot of stuff even I'm not sure about. But interesting anyway. I actually don't mind a little logic in my templates. It can certainly get out of hand, but it's there responsibility of the dev/designer to manage that. There is the similar idea to my last gist where a template is like a function execution. It's passed some data and it can also execute in some context.

By the time the template logic executes, all promises included in the initial data have been resolved. Doesn't matter if they're lazy or they fired immediately. The resolved value is available here. On line 20 you can see the other side of rendering. The render function is available in templates (the version on an object is the same, it just sets the context). Render doesn't do anything different when called in a template. But we can grab the stream from this sub template and send it out the main templateStream. So even conditional partials are supported with streaming.

The last thing is this.footerLinks() on line 26. Ignore the pseudo-js forEach crap, it's not important. Like the previous data methods, footerLinks is going to return either a value or a promise. Ideally the main template rendering would be paused and some construct would make sure this gets resolved before continuing by looping over the returned value. This could also be in a conditional.

As you can see, this assumes that we're embracing both streams and promises extensively in order to minimize callbacks.

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