Bifocals

The view component of the roads framework

This project is maintained by Dashron

Bifocals

A node.js View library with support for asynchronous sub-views (aka partials) and interchangeable rendering systems.

Table of Contents

Installation

npm install bifocals
var bifocals_module = require('bifocals');
var Bifocals = bifocals_module.Bifocals;
Note: As of 1.2.0 Bifocals requires node 0.10.0 or higher for streams2

Basic Use

templates/index.html

<html>
    <head></head>
    <body>
        {{{header}}}
    </body>
</html>

templates/header.html

<header>
    {{title}}
</header>

server.js

// Each content type needs to be registered to a Renderer.
// This tells bifocals to render all text/html views via handlebars.
bifocals_module.addRenderer('text/html', require('./renderers/handlebars_renderer'));

http_module.createServer(function (request, response) {
    // Create the parent:
    var view = new Bifocals(response);

    // Assign a content type.
    view.content_type = 'text/html';

    // Create the child:
    var child = view.child("header");
    child.set('title, "Hello World");
    child.render("templates/header.html");

    // Write the view to the response
    view.render("templates/index.html");
}

That's it! When the client requests this page, the following html will be displayed:

<html>
    <head></head>
    <body>
        <header>
            Hello World
        </header>
    </body>
</html>

Why do I need this library?

The previous example does not show the true benefits of the library. These benefits are apparent when performing asynchronous actions. Consider this more complex example:

http_module.createServer(function (request, response) {
    // Create the parent:
    var view = new Bifocals(response);

    // Assign a content type.
    view.content_type = 'text/html';

    // Create multiple children:
    var users_child = view.child("content");
    var blog_child = view.child("blog");

    // Retrieve some rows from the database, and call back to me later
    // I will explain in more detail with the next database call
    database.query('select * from posts', function (err, rows) {
        if (err) {
            view.statusError();
        } else {
            if (rows.length) {
                blog_child.set('posts', rows);
                blog_child.render('templates/blog/many.html');
            } else {
                blog_child.render('templates/blog/empty.html');
            }
        });
    });

    // Retrieve some more rows from the database, and call back to me later.
    // Because both of the child views are handled in the callback, we have no idea which view will be rendered first. The view attached to the fastest query will be the first view to render.
    // The library handles this all for you, the order does not matter.
    database.query('select * from user', function (err, rows) {
        if (err) {
            // If an error occurred, notify the user and end the rendering immediately. This will not wait for the other database query to finish, so you can shave some time off of notifying the user.
            view.statusError();
        } else {
            if (rows.length) {
                user_child.set('users', rows);
                user_child.render('templates/user/many.html');
            } else {
                user_child.render('templates/user/empty.html');
            }
        });
    });


    // Write the view to the response
    // At this point, neither of the children will have rendered.
    // The view is flagged as "ready", and whenever the final child has rendered this view will render.
    view.render("templates/index.html");
});

Interactive Example

Each box below represents one bifocals object.
Lines connect parent objects to one or more children.
To call the function render(), click the box.
Try clicking them in every order you can think of, they all should still work.

READY
 
 
READY
READY
 
 
READY
READY
 
READY

More Examples

When you install bifocals, you also install an example.
It is located in the "example" directory and requires handlebars to run. (npm install handlebars)

Once run (node server.js), the example starts a web server.
If you access localhost:8125 you will see the output of one view with two children, all of which were rendered via handlebars.

If you access localhost:8125/static you will see the output of one view which was rendered using the static file renderer.

If you access localhost:8125/unauth you will receive a 401 Unauthorized status code, and the output of a template.

If you access any other path, such as localhost:8125/fakefakefake, it should send a proper 404 error code.

Express Middleware

Included in the examples directory is a technique for using Bifocals as your method for rendering templates in express.

First you need to configure your app with a views directory.

app.set('views', __dirname + '/templates');
Second you need to configure bifocals with a text/html renderer. You can use the same rendering function you would for express. The following example uses the consolidate library, which was built so express could use any template system.

bifocals_module.addRenderer('text/html', require('consolidate').handlebars);
Third, you need to tell your app to use the middleware.

app.use(bifocals_module.__express({
        app : app
    }));
Once configured, a Bifocals object is created for each request. The response object provided to each route is modified in two ways to accomidate this.

First, render is rewritten to use that core bifocals object. The parameters of this function have not changed. Second, a child method has been added to the response object. It is identical to Bifocals' Child function.

With these two functions, you should be able to use all of the features of bifocals from within express. If you need more, email me at aaron@dashron.com and we can discuss how to create a better integration.

For an example, check out express-server.js.

Important notes

Writing your own Renderer

You must extend the Renderer class:

var FileRenderer = module.exports = function FileRenderer() {
    Renderer.call(this);
}

util_module.inherits(FileRenderer, Renderer);

You must define a render function. It does not need to take any parameters, but you will be provided the proper template if possible. This template will be a string, defining the full path to the target template. If you wish to cache this file it should be handled via your renderer.

Each renderer will be assigned a "response" object. It may or may not be a ServerResponse object, so you can only expect expect two functions, write(mixed) and end().

write(mixed) should be called any time data is complete and ready to be sent to the user, while end() should be called to signify the end of data.

Additionally each renderer will have an "_error" function. This function will be provided with the view's error handler, it should be called any time an error occurs.

FileRenderer.prototype.render = function (template) {
    var _self = this;
    // Find the template and load it from disk
    var stream = fs_module.createReadStream(template);

    stream.on('data', function (data) {
        // Write data to the response
        _self.response.write(data);
    });

    stream.on('error', function (err) {
        // Call the renderer's error function
        _self._error(err);
    });

    stream.on('end', function () {
        // Signal the end of rendering
        _self.response.end();
    });
}

Function Reference

Module Reference


addRenderer(content_type, renderer)

content_type : string

renderer : Renderer

Registers a renderer object to a content type.

Every content type used must have an associated renderer. Included in the "renderers" folder are two examples, one for serving files directly, and one for rendering templates through Handlebars.

getRenderer(content_type)

content_type : string

Returns the appropriate renderer for the provided content_type parameter.

If no renderer has been registered to the content type, an error is thrown.

RENDER_STATES

An object containing all of the states a bifocal object can be in.

// todo : provide explicit definitions of each state

Bifocals(response)

response : http response object

Constructor for the bifocal object. This should only be called for a root view, any child views should be spawned off the root via child.

When the render method is called, it will write out to the response object provided to this consstructor.

Your first task should be to assign an error handler. Be careful with the error handler. If statusError handles all failed templates and then fails, you will become locked in an infinite loop.

var view = new Bifocals(response);
view.error(function (error) {
    console.log(error);
    view.statusError(error);
});

Bifocals Reference


content_type

The value to put in the responses content-type header. Also used to determine which renderer should be used.

template

A string that is provided along with all view data to the final render step. If this string is set, it will override the template provided to the render function.

When provided to the renderer, it is prefixed with the dir property.

dir

A string which is prefixed onto the "template" before it is send to a renderer.

This allows you to set a standard template directory early on, and keep it out of your render calls.

parent

The bifocal object that created this object.

Changing this to a different object will likely break the rendering hierarchy entirely. // todo what does the parent element have here.

root

The original bifocal object, which started the whole series of child elements.

Changing this to a different object will likely break the rendering flow completely.

isRendered()

Returns true or false, depending on whether the view has completed its rendering process.

set(key, value)

key : string

value : mixed

Stores a key value pair to be provided to the renderer.

This is useful for template engines.

get(key)

key : string

Retrieves a value that was assigned via set, based on the values key.

canRender()

Returns true or false, depending on whether the view is able to be rendered immediately.

render(template, force)

template : string (varies, depending on the renderer)

force : optional boolean default false

Combines the data and template into a string, and writes it all out to the response.

The first parameter should be a path to your template. If you have set the dir property, it will be prepended to the template parameter. For the Handlebars renderer, the final combined template needs to result in an absolute file path.

If you have already assigned a template to the view (via the template property), the template parameter will be ignored.

The second parameter is a boolean. If true, all child views will be canceled and the render will happen immediately. This is generally useful for handling errors.

This should be the last function you manually call on a view. If you want to use child views, they must be created before this render call, but you can keep using them after this render call.

cancelRender()

Stops all child views from rendering, and ensures that this view will not be rendered.

buildRenderer()

Builds a renderer object from various data located within this view.

error(fn)

fn : function(error)

Assigns an error handler to this view. Child elements will inherit this error handler. The callback will be provided one parameter: the error.

child(key, template)

key : string

template : optional string (varies, depending on the renderer)

Creates a child view, which this view will depend on.

The first parameter is a key. The final rendered output of the child will be set to the parent view using this key. For example:

var view = new Bifocals();
var child = view.child('a');
child.set('content', 'hello');

// template.html is simply {{{content}}}
child.render('template.html');

view.get('a') === 'hello';

The second parameter is a template. If you provide a template here, it will be considered the same as child.template = template. This means it will override the template provided to the render(template,force) function call.

setStatusCode(code)

code : number

This should only be called on the parent. This sets the status code in the response headers.

setHeaders(headers)

headers : object

Sets a collection of headers, where each key:value pair is a Header:value pair.

statusNotFound(template)

template : optional string (varies, depending on the renderer)

End rendering immediately with a 404: Not found status code, and renders the template if provided.

statusUnauthorized(template)

template : optional string (varies, depending on the renderer)

End rendering immediately with a 401: Unauthorized status code, and renders the template if provided.

statusError(error, template)

error : optional error

template : optional template (varies, depending on the renderer)

End rendering immediately with a 500: Error code, and renders the template if provided Additionally the error is assigned to the view with the key "error".

statusCreated(redirect_url)

redirect_url : string (url)

End rendering immediately with a 201: Created code. redirect_url is sent via the Location header, and should point to the newly created resource.

statusRedirect(redirect_url)

redirect_url : string (url)

End rendering immediately with a 302: Found code. redirect_url is sent via the Location header, which the client should automatically follow. todo: support other 300's

statusNotModified()

End rendering immediately with a 304: Not Modified code. This should be in response to a request with cache headers, and the client should respond by displaying the previously cached response. //todo : as a parameter take some headers to control this? date, etag, expires, cache control

statusUnsupportedMethod(supported_methods)

supported_methods : array

Ends rendering immediately with a 405: Unsupported Method code. This should be in response to a failed request, if the request would be successful when made with an alternative http method.

Renderer Reference


exports.Renderer = function()

Constructor for the base Renderer

response

Automatically set when Bifocals builds a renderer. This will be an object with write(data) and end() functions.

data

Automatically set when Bifocals builds a renderer. This will be an object of key value pairs.

error(fn)

fn : function(error)

returns : this

Assigns a function that will be called any time an error occurs in the renderer. The assigned function should be called via the renderer's _error property.

end(fn)

fn : function()

returns : this

Assigns a function that will be called when the renderer is complete. The assigned function should be called via the _end property.

Coding Standards

If an object property is prefixed with an underscore (_), ignore it. Changing these properties is not supported and has not been considered in the development of the library.

If an object property is not prefixed with an underscore (_), and is not a function, you may change it however you see fit. Just read the documentation and understand the side effects of changing the variable.

Future Features:

"Fill"