Skip to content

Commit

Permalink
Merge pull request #1 from jwilm/layouts
Browse files Browse the repository at this point in the history
Add layout support
  • Loading branch information
jwilm committed Dec 27, 2013
2 parents 5547bd8 + 7855a39 commit 4795a43
Show file tree
Hide file tree
Showing 11 changed files with 331 additions and 50 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ test:
--harmony-generators \
-R spec \
test/render \
--bail
test/unit

.PHONY: test
120 changes: 102 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,41 @@
koa-hbs
=======

Handlebars Templates via Generators for [Koa](https://github.com/koajs/koa/)
[Handlebars](http://handlebarsjs.com) Templates via Generators for
[Koa](https://github.com/koajs/koa/)

## Foreward
This is package offers minimum viability. Registering partials and synchronous
helpers is supported, but asynchronous helpers and layouts are not. Layouts are
next on the list.
[![Build Status][travis-badge]][repo-url]

## Forward
Things that are supported:
- Registering helpers
- Registering partials
- Specify a directory or multiple directories of partials to register
- Single default layout
- Alternative layouts
- Template caching (actually, this cannot currently be disabled)

Things that will be, but are **not** yet supported:
- Asynchronous helpers
- Content blocks

## Usage
koa-hbs is middleware. Configure the default instance by passing an options hash to #middleware, or create an independent instance using #create().
koa-hbs is middleware. Configure the default instance by passing an options hash
to #middleware or on an independant from #create().

```javascript
var koa = require('koa');
var hbs = require('koa-hbs');

var app = koa();

// koa-hbs is middleware. Use it before you want to render a view
// koa-hbs is middleware. `use` it before you want to render a view
app.use(hbs.middleware({
viewPath: __dirname + '/views'
}));

// Render is attached to the koa context. Call this.render in your middleware
// to attach your rendered html to the response body.
// Render is attached to the koa context. Call `this.render` in your middleware
// to attach rendered html to the koa response body.
app.use(function *() {
yield this.render('main', {title: 'koa-hbs'});
})
Expand All @@ -32,22 +44,94 @@ app.listen(3000);

```

After a template has been rendered, the template function is cached. `#render#
accepts two arguements - the template to render, and an object containing local
variables to be inserted into the template. The result is assigned to Koa's
`this.response.body`.

### Registering Helpers
Helpers are registered using the #registerHelper method. Here is an example
using the default instance (helper stolen from official Handlebars
[docs](http://handlebarsjs.com):

```javascript
hbs = require('koa-hbs');

hbs.registerHelper('link', function(text, url) {
text = hbs.Utils.escapeExpression(text);
url = hbs.Utils.escapeExpression(url);

var result = '<a href="' + url + '">' + text + '</a>';

return new hbs.SafeString(result);
});
```

registerHelper, Utils, and SafeString all proxy to an internal Handlebars
instance. If passing an alternative instance of Handlebars to the middleware
configurator, make sure to do so before registering your helpers via the koa-hbs
proxies, or just register your helpers directly via your Handlebars instance.

### Registering Partials
The simple way to register partials is to stick them all in a directory, and
pass the `partialsPath` option when generating the middleware. Say your views
are in `./views`, and your partials are in `./views/partials`. Configuring the
middleware as

### Layouts
Passing `defaultLayout` with the a layout name will cause all templates to be
inserted into the `{{{body}}}` expression of the layout. This might look like
the following.

```html
<!DOCTYPE html>
<html>
<head>
<title>{{title}}</title>
</head>
<body>
{{{body}}}
</body>
</html>
```

In addition to, or alternatively, you may specify a layout to render a template
into. Simply specify `{{!< layoutName }}` somewhere in your template. koa-hbs
will load your layout from `layoutsPath` if defined, or from `viewPath`
otherwise.

At this time, only a single content block (`{{{body}}}`) is supported. Block and
contentFor helpers are on the list of features to implement.

### Options
The plan for koa-hbs is to offer identical functionality as express-hbs (eventaully). These options are supported _now_.
The plan for koa-hbs is to offer identical functionality as express-hbs
(eventaully). These options are supported _now_.

- `viewPath`: [_required_] Where to load templates from
- `viewPath`: [_required_] Full path from which to load templates
(`Array|String`)
- `handlebars`: Pass your own instance of handlebars
- `templateOptions`: Options to pass to `template()`
- `extname`: Alter the default template extension (default: `.hbs`)
- `partialsPath`: Use this directory for partials
- `templateOptions`: Hash of
[options](http://handlebarsjs.com/execution.html#Options) to pass to
`template()`
- `extname`: Alter the default template extension (default: `'.hbs'`)
- `partialsPath`: Full path to partials directory (`Array|String`)
- `defaultLayout`: Name of the default layout
- `layoutsPath`: Full path to layouts directory (`String`)

These options are **NOT** supported (because we don't support layouts ... yet).
These options are **NOT** supported yet.

- `contentHelperName`: Alter `contentFor` helper name
- `blockHelperName`: Alter `block` helper name
- `defaultLayout`: Name of the default layout
- `layoutsDir`: Load layouts from here



## Example
You can run the included example via `npm install koa` and `node --harmony app.js` from the example folder.
You can run the included example via `npm install koa` and
`node --harmony app.js` from the example folder.

## Credits
Functionality and code were inspired/taken from
[express-hbs](https://github.com/barc/express-hbs/).

[travis-badge]: https://travis-ci.org/jwilm/koa-hbs.png?branch=master
[repo-url]: https://travis-ci.org/jwilm/koa-hbs
141 changes: 120 additions & 21 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ var path = require('path');
var co = require('co');
var readdirp = require('readdirp');

/* Capture the layout name; thanks express-hbs */
var rLayoutPattern = /{{!<\s+([A-Za-z0-9\._\-\/]+)\s*}}/;

/**
* file reader returning a thunk
* @param filename {String} Name of file to read
Expand Down Expand Up @@ -37,6 +40,11 @@ exports.create = function() {

function Hbs() {
if(!(this instanceof Hbs)) return new Hbs();

this.handlebars = require('handlebars').create();

this.Utils = this.handlebars.Utils;
this.SafeString = this.handlebars.SafeString;
}

/**
Expand All @@ -52,19 +60,19 @@ Hbs.prototype.configure = function (options) {
// Attach options
var options = options || {};
this.viewPath = options.viewPath;
this.handlebars = options.handlebars || require('handlebars').create();
this.handlebars = options.handlebars || this.handlebars;
this.templateOptions = options.templateOptions || {};
this.extname = options.extname || '.hbs';
this.partialsPath = options.partialsPath || '';

// Support for these options is planned, but not yet supported:
this.contentHelperName = options.contentHelperName || 'contentFor';
this.blockHelperName = options.blockHelperName || 'block';
this.defaultLayout = options.defaultLayout || '';
this.layoutsDir = options.layoutsDir || '';
this.layoutsPath = options.layoutsPath || '';

// Register partials in options partialsPath(s)
this.registerPartials();
this.partialsRegistered = false;

// Cache templates and layouts
this.cache = {};

return this;
};
Expand All @@ -74,25 +82,113 @@ Hbs.prototype.configure = function (options) {
*
* @api public
*/

Hbs.prototype.middleware = function(options) {
var hbs = this;
this.configure(options);

var render = function *(templateName, args) {
var templatePath = path.join(hbs.viewPath, templateName + hbs.extname);
// No caching yet
var tplFile = yield read(templatePath);
var template = hbs.handlebars.compile(tplFile.toString());

this.body = template(args, hbs.templateOptions);
};
var render = this.createRenderer();

return function *(next) {
this.render = render;
yield next;
};
}

/**
* Create a render generator to be attached to koa context
*/

Hbs.prototype.createRenderer = function() {
var hbs = this;

return function *(tpl, locals) {
var tplPath = path.join(hbs.viewPath, tpl + hbs.extname),
template, rawTemplate, layoutTemplate;

locals = locals || {};

// Initialization... move these actions into another function to remove
// unnecessary checks
if(!hbs.partialsRegistered)
yield hbs.registerPartials();

if(!hbs.layoutTemplate)
hbs.layoutTemplate = yield hbs.cacheLayout();

// Load the template
if(!hbs.cache[tpl]) {
rawTemplate = yield read(tplPath);
hbs.cache[tpl] = {
template: hbs.handlebars.compile(rawTemplate)
}

// Load layout if specified
if(rLayoutPattern.test(rawTemplate)) {
var layout = rLayoutPattern.exec(rawTemplate)[1];
console.log(layout);
var rawLayout = yield hbs.loadLayoutFile(layout);
hbs.cache[tpl].layoutTemplate = hbs.handlebars.compile(rawLayout);
}
}

template = hbs.cache[tpl].template;
layoutTemplate = hbs.cache[tpl].layoutTemplate || hbs.layoutTemplate;

// Run the compiled templates
locals.body = template(locals, hbs.templateOptions);
this.body = layoutTemplate(locals, hbs.templateOptions);
};
}

/**
* Get layout path
*/

Hbs.prototype.getLayoutPath = function(layout) {
if(this.layoutsPath)
return path.join(this.layoutsPath, layout + this.extname);

return path.join(this.viewPath, layout + this.extname);
}

/**
* Get a default layout. If none is provided, make a noop
*/

Hbs.prototype.cacheLayout = function(layout) {
var hbs = this;
return co(function* () {
// Create a default layout to always use
if(!layout && !hbs.defaultLayout)
return hbs.handlebars.compile("{{{body}}}");

// Compile the default layout if one not passed
if(!layout) layout = hbs.defaultLayout;

var layoutTemplate;
try {
var rawLayout = yield hbs.loadLayoutFile(layout);
layoutTemplate = hbs.handlebars.compile(rawLayout);
} catch (err) {
console.error(err.stack);
}

return layoutTemplate;
});
}

/**
* Load a layout file
*/

Hbs.prototype.loadLayoutFile = function(layout) {
var hbs = this;
return function(done) {
var file = hbs.getLayoutPath(layout);
read(file)(done);
};
}

/**
* Register helper to internal handlebars instance
*/
Expand All @@ -105,15 +201,15 @@ Hbs.prototype.registerHelper = function() {
* Register partial with internal handlebars instance
*/

Hbs.prototype.registerPartial = function() {
Hbs.prototype.registerPartial = function() {
this.handlebars.registerPartial.apply(this.handlebars, arguments);
}
}

/**
* Register directory of partials
*/

Hbs.prototype.registerPartials = function(cb) {
Hbs.prototype.registerPartials = function (cb) {
var self = this, partials, dirpArray, files = [], names = [], partials,
rname = /^[a-zA-Z_-]+/, readdir;

Expand All @@ -131,7 +227,7 @@ Hbs.prototype.registerPartials = function(cb) {
};

/* Read in partials and register them */
co(function *() {
return co(function *() {
try {
readdirpResults = yield self.partialsPath.map(readdir);

Expand All @@ -149,9 +245,12 @@ Hbs.prototype.registerPartials = function(cb) {
self.registerPartial(names[i], partials[i]);
}

self.partialsRegistered = true;
} catch(e) {
console.error('Error caught while registering partials');
console.error(e);
}
})();
};

return cb && cb(null, self);
});
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "koa-hbs",
"version": "0.2.0",
"version": "0.3.0",
"description": "Handlebars Templates via Generators for Koa",
"main": "index.js",
"repository": {
Expand Down
12 changes: 12 additions & 0 deletions test/app/assets/layouts/alternative.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>Alternative {{title}}</title>
</head>
<body>
<h1>ALTERNATIVE LAYOUT</h1>
<main>
{{{body}}}
</main>
</body>
</html>
Loading

0 comments on commit 4795a43

Please sign in to comment.