@rowanmanning/renderer v1.0.0-beta.10
@rowanmanning/renderer
Render htm views with Hyperons.
:warning: This is pre-release software, use in production at your own risk
Table of Contents
Requirements
This library requires the following to run:
- Node.js 10+
Usage
Install with npm:
npm install @rowanmanning/renderer
Load the library into your code with a require
call:
const Renderer = require('@rowanmanning/renderer');
Creating a renderer
const renderer = new Renderer(options);
The available options are:
env
: The environment that node is running in. Defaults to theNODE_ENV
environment variablepath
: Where the JavaScript views live. Defaults to<CWD>/view
namespacePaths
: Where the JavaScript views live for different namespaces. See namespaces for more information
Rendering a view
To render the view in <CWD>/view/example
, use:
const output = await renderer.render('example', context);
context
is an object which contains properties that are made available to the view. output
will be the rendered HTML as a string.
If you pass render
an absolute path, it will render the JavaScript file found at that path:
const output = await renderer.render('/path/to/example', context);
Creating a view
View files are JavaScript, and they export functions that do the rendering. This exported function receives one argument which is the render context that was passed into render
.
The function must return a template literal tagged with the html
function provided by this module. This is an htm template tag bound to Hyperons.
const {html} = require('@rowanmanning/renderer');
module.exports = context => {
return html`
<html lang="en">
<head>
<meta charset="utf-8" />
<title>${context.title}</title>
</head>
<body>
<h1>${context.title}</h1>
<p>Content goes here</p>
</body>
</html>
`;
};
The function may be an async
function, if you need to do anything in your view that is asynchronous:
const {html} = require('@rowanmanning/renderer');
module.exports = async context => {
return html`
<html lang="en">
<head>
<meta charset="utf-8" />
<title>${context.title}</title>
</head>
<body>
<h1>${context.title}</h1>
<p>${await doSomethingAsync()}</p>
</body>
</html>
`;
};
See the Hyperons documentation for more information about what's possible.
Namespaces
Namespaces allow you to store view files in multiple places. For example, if you were building a blogging application, you might store views for the administration of blog posts separately from the front-end views. This might also allow for themeable sites which fall back to default views if the chosen theme does not implement them.
You define namespaces when you create a renderer:
const renderer = new Renderer({
// A path is still required for the default view location
path: `${__dirname}/view`,
// The key for each of these is the namespace name, and the
// value is the path where views in this namespace can be found
namespacePaths: {
admin: `${__dirname}/app/admin/view`
}
});
When rendering views, you can use namespaces by prefixing the viewname with them with a colon (:
) as a separator. Based on the config above:
// Render ./view/example.js
const output = await renderer.render('example', context);
// Render ./app/admin/view/example.js
const output = await renderer.render('admin:example', context);
See the namespaces example for a working demo.
Cascade
When rendering, you can specify multiple views in a cascade. When you specify multiple views, the renderer will step through them and return the rendered content for the first view that is found.
In the following example, assume that front-page.js
is the only file that actually exists. It will be rendered as expected:
// Render `./view/front-page.js`
const output = await renderer.render(['home', 'front-page', 'index'], context);
This is particularly useful when combined with namespaces as it allows you to fall back to a default theme, for example:
const renderer = new Renderer({
path: `${__dirname}/my-cool-theme/view`,
namespacePaths: {
default: `${__dirname}/default-theme/view`
}
});
// Render the first view that's found
const output = await renderer.render(['home', 'default:home']);
See the namespaces example for a working demo.
Layouts
Reusing layouts is achievable using standard Node.js require
and composing functions together. It's useful to do this to reduce repetition across your views, keeping boilerplate HTML in one place. There are many ways you could do this, here's an example:
// FILE: view/layout/default.js
const {html} = require('@rowanmanning/renderer');
module.exports = (context, content) => {
return html`
<html lang="en">
<head>
<meta charset="utf-8" />
<title>${context.title}</title>
</head>
<body>
${content}
</body>
</html>
`;
};
// FILE: view/example.js
const {html} = require('@rowanmanning/renderer');
const layout = require('./layout/default');
module.exports = context => {
return layout(context, html`
<h1>${context.title}</h1>
<p>This is an example page.</p>
`);
};
See the layouts example for a working demo.
Partials
Partials are snippets of HTML that can be reused by multiple different views. This is achievable using standard Node.js require
and referencing the shared functions in your views. It's useful to do this to reduce repetition. There are several ways to do this.
See the partials example for working demos of all of these methods.
Using the built-in Partial
class
Extending the built-in Partial
class allows you to keep logic for complex views neatly contained within a single class. You must implement a render
method on any extending class, and the partial is used in the view as a custom element.
// FILE: view/partial/copyright.js
const {html, Partial} = require('@rowanmanning/renderer');
module.exports = class Copyright extends Partial {
render() {
return html`
<small>
Copyright © ${this.currentYear()}, ${this.context.holder}
</small>
`;
}
currentYear() {
return new Date().getFullYear();
}
};
// FILE: view/example.js
const {html} = require('@rowanmanning/renderer');
const Copyright = require('./partial/copyright');
module.exports = context => {
return html`
<html lang="en">
<head>
<meta charset="utf-8" />
<title>${context.title}</title>
</head>
<body>
<h1>${context.title}</h1>
<p>This is an example page.</p>
<p><${Copyright} holder="Rowan Manning"/></p>
</body>
</html>
`;
};
See the htm documentation for more information on using classes as elements.
Using synchronous functions
Synchronous functions can be used as partials for simpler cases, where a full class might be overkill.
// FILE: view/partial/navigation.js
const {html} = require('@rowanmanning/renderer');
module.exports = context => {
return html`
<nav>
<ul>
${context.children.map(child => html`
<li>${child}</li>
`)}
</ul>
</nav>
`;
};
// FILE: view/example.js
const {html} = require('@rowanmanning/renderer');
const navigation = require('./partial/navigation');
module.exports = context => {
return html`
<html lang="en">
<head>
<meta charset="utf-8" />
<title>${context.title}</title>
</head>
<body>
<${navigation}>
<a href="#example1">Example 1</a>
<a href="#example2">Example 2</a>
<//>
<h1>${context.title}</h1>
<p>This is an example page.</p>
</body>
</html>
`;
};
See the htm documentation for more information on using functions as elements.
Using asynchronous functions
Asynchronous functions can be used as partials, but they cannot be used as elements in the same way as a Partial
class or a synchronous function.
// FILE: view/partial/paragraph-from-file.js
const {html} = require('@rowanmanning/renderer');
const {promisify} = require('util');
const readFile = promisify(require('fs').readFile);
module.exports = async filePath => {
const content = await readFile(filePath, 'utf-8');
return html`
<p>${content}</p>
`;
};
// FILE: view/example.js
const {html} = require('@rowanmanning/renderer');
const paragraphFromFile = require('./partial/paragraph-from-file');
module.exports = context => {
return html`
<html lang="en">
<head>
<meta charset="utf-8" />
<title>${context.title}</title>
</head>
<body>
<h1>${context.title}</h1>
${await paragraphFromFile('./content.txt')}
</body>
</html>
`;
};
Use with Express
The renderer.expressViewEngine
method returns an Express view engine that can be used to render views:
const express = require('express');
const Renderer = require('@rowanmanning/renderer');
const app = express();
const renderer = new Renderer();
// The extension must be "js"
app.engine('js', renderer.express());
app.set('view engine', 'js');
See the Express example for a working demo.
Note that there's an important caveat with Express rendering: namespaces and cascades do not work, and the Renderer's path
option is ignored in favour of the Express application's view path.
Use with Koa
The renderer.koa
method returns Koa middleware which can be used to render views:
const Koa = require('koa');
const Renderer = require('@rowanmanning/renderer');
const app = new Koa();
const renderer = new Renderer();
app.use(renderer.koa());
app.use(async context => {
await context.render('example', {
title: 'Example Page'
});
});
The Koa middleware also merges the given render context with context.state
:
app.use(async context => {
context.state.title = 'Example Page';
await context.render('example');
});
See the Koa example for a working demo.
Doctypes
By default an HTML5 doctype is added to the rendered output. You can change the doctype by adding a docytpe
property to the render context
:
const output = await renderer.render('example', {
doctype: '<!DOCTYPE HTML PUBLIC "-//W3C//DTD … ">'
});
To remove the doctype entirely, set to a falsy value:
const output = await renderer.render('example', {
doctype: null
});
Examples
We provide a few example implementations which demonstrate different features:
Basic: the simplest use of the library to render a view. Run
node example/basic
to ouput rendered views to the command line.Koa: render views for a Koa application using middleware. Run
node example/koa
to start the application and visit localhost:8080.Layouts: render views with reusable layout code to reduce repetition. Run
node example/layouts
to ouput rendered views to the command line.Namespaces: using namespaces and cascades to organise views into different folders. Run
node example/namespaces
to ouput rendered views to the command line.Partials: render views using partials to reduce repetition. Run
node example/partials
to ouput rendered views to the command line.
Contributing
To contribute to this library, clone this repo locally and commit your code on a separate branch. Please write unit tests for your code, and run the linter before opening a pull-request:
make test # run all tests
make verify # run all linters
License
Licensed under the MIT license. Copyright © 2019, Rowan Manning
4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago