windshieldjs
v4.0.1
Published
An Enterprise Rendering Plugin for Hapi.js
Downloads
17
Keywords
Readme
WindshieldJS
An Enterprise Rendering plugin for Hapi.js.
WindshieldJS allows us to separate the data of our pages (and the logic used to obtain that data) from the markup with which we want to render that data. It also lets us separate the components of our pages from one another, so that each component is a reusable module for determining its own data and markup, allowing us to assemble groups of components into different pages.
See documentation site for more details. Please note the docs site is still a work-in-progress.
Table of Contents
- Requirements
- Install
- Usage
- Concepts
Requirements
WindshieldJS 4.x is intended for use with
- Hapi.js 17+
- vision 5+
- Handlebars 4+
- Node 7.6+
Install
npm install --save windshieldjs
Usage
Register
First, you must register the plugin with your Hapi server instance.
Example of registering plugin with options
server.register({
register: require('windshieldjs'),
options: {
rootDir: path.join(__dirname, 'app'),
handlebars: require('handlebars'),
uriContext: '/foo',
routes: require('./app/routes'),
components: require('./app/components'),
path: ['./', '../../node_modules/some-module/src'],
helpersPath: ['helpers', '../../node_modules/some-module/src/helpers']
}
}).catch(function (err) {
console.log(err);
});
See options below for details on each of these options.
Options
components
- A hashmap of all the Windshield component types available on the server. Each key represents the name of the component type, and its value is a component type config.handlebars
- An instance of the Handlebars namespace. Windshield needs to use the same Handlebars instance as is used by your project. To ensure it has access to the same object in memory, you should provide this instance within the config object.helpersPath
- An optional array that specifies directories holding handlebars helpers. The default value is['helpers']
, relative torootDir
, meaning handlebars will look for helpers inrootDir/helpers
by default. You can also add absolute paths.path
- An optional array that specifies parent directories where vision will look for the layouts sub-directory. The default value is['./']
, relative torootDir
, meaning vision will look for templates in[rootDir]/layouts
by default.rootDir
- A string representing the base path used as prefix forpath
andhelpersPath
. Windshield will assume all page-level templates are kept in alayouts
directory under this path. This should be the absolute path to the root directory of your project. (See note below about project structure requirements).routes
- An array of route definitions.uriContext
- This is the base URI under which windshield will register all of your routes. For example, if you seturiContext
to "/foo", and you have a route defined as "/bar", that route will be accessible at "/foo/bar".
Project Structure
By default, WindshieldJS expects your project to have the following structure:
- rootDir (as specified by the
rootDir
option)- helpers (location of all helpers to be used by Handlebars, can be changed with
helpersPath
option) - layouts (location of all page-level templates)
- helpers (location of all helpers to be used by Handlebars, can be changed with
The path
option would allow you to change which directories in rootDir
where the
layouts directory is expected, but Windshield still expects the directory to be called
"layouts".
The location of all other files (routes, components, etc) do not matter so long as the code that registers Windshield can access them.
Request lifecycle
When Hapi.js serves a request to a Windshield route, it performs the following steps:
Get the route context from the route settings.
Create a page context object based on the following:
- A copy of the route context.
- The results of running each of the route prerequisites.
- The results of running each of the page adapters
Render each of the component definitions in the page context
Assemble all the rendered component objects into a rendered page object
Use the rendered page object to determine the page layout template to use.
Execute the page layout template with data from the rendered page object to produce HTML.
Set the response with the rendered HTML.
Rendering components
The page context may contain an associations
property that defines
groups of child component definitions. Each child may contain its own
associations
property defining its own children.
To render components from these definitions, Windshield processes them recursively, depth-first. A component will not be rendered itself until all of its descendants have been rendered.
Each component definition has a reference to a component type that should be used to render it. The component type provides a set of templates and a component adapter that is used to process data. The component definition can be configured to determine which template it used, and to shape the data passed into the adapter.
The rendering process goes like this:
- Render all descendants.
- Get the child's component type .
- Create a component context object based on the following:
- The data defined in the child
- The defaults provided by the component type
- The rendered descendant components.
- The result of running component type's adapter with page and component data
- Choose one of the templates defined in the component type, based on the child's settings
- Compile the template with the component context to produce a rendered component object.
The rendered component object will contain properties for markup
(the HTML produced from compiling the template) and exported
(data that
is deliberately exposed for parent and ancestor components to access)
Concepts
Route Definition
Windshield processes route definitions into Hapi route configuration objects.
Each route definition is an object with the following members:
method
- The route's HTTP method (default is "GET")path
- A string which acts as a path expression. It's handed off directly to Hapi's router when Windshield sets up your route.context
- A context objectadapters
- An array of page adapter functions and route prerequisite objects. When Windshield is initialized it immediately separates these out into two arrays. It would be far less confusing to define these in two separate route config properties, which is planned for a future release.pageFilter
- (optional) A Promise-returning function which will receive the final composed page object immediately before it is applied to the page layout template and any component templates. It provides one last chance for the developer to modify the page object. This can be useful for cases where the data contained in one component affects another component on the page.
Context Object
Each Windshield route defines an object called the route context.
If this object is not configured for the route, an empty object is used by
default. The route context is accessible during the request lifecycle
through the Hapi request object, via request.route.settings.app.context
.
The route context is intended to remain static during the life of the server. During a request, a copy of it is processed and modified by the route's prerequisites and page adapters to produce a page context.
A page context is expected to have the following properties:
layout
- A string referring to the file name for the Handlebars template which will be used to produce the final HTML response.attributes
- A hashmap of page-level attributes. These can be interpreted By the page adapters and component adapters, and also passed into Handlebars expressions in the layout template.associations
- A hashmap where each key is an "association name" that represents a "zone" on the page, and its value is an array of component definitions which will be used to render the content of those zones.
Technically, all of the above properties are optional: You may define a page context with no layout, no attributes, and no associations, and Windshield will simply look for a default.html template and try to render it without empty data.
Once the page context has been fully assembled by the route prerequisites and
the page adapters, its associations
object is used to render all of the
page's child components. Each child will have a component adapter, and
the page context is passed into this adapter so that it can produce a
rendered component based on information about its parent page.
After all the child components have been rendered, the rendered components and page context are used to assemble a rendered page object.
Component Type Config
A component type config is used to render component instances for that type. It determines what templates are available to use for producing the instance's HTML markup, and the logic used to determine the instance's data. Each instance will have its own markup and data, but the means of producing that markup and data are defined by its type.
Each component type config is an object containing the following members:
adapter
- A component adapter function.defaults
- Set of properties to include in every instance rendered from this component type.templates
- hashmap of Promise-returning functions used to produce all the Handlebars templates available for this component type.partials
- hashmap of Promise-returning functions used to register the Handlebars partials that are used by this component's templates.
Component Definition
A page context object defines its child components using component definitions. These objects describe the component type config that should be used to render it, and additional settings for the rendering process.
Component definitions are kept in the page context's associations
property.
A component definition may also have its own associations
property
containing child component definitions. As a result, a page context object
can be a very complex, nested structure.
A component definition is an object that can contain the following members:
component
- The name of the component type config that should be used to render this component.data
- (optional) data to pass into the component adapter during rendering.layout
- (optional) The name of a template defined in the component. If this is not defined, the parent association name is used.associations
- A hashmap where each key is an "association name" that represents a "zone" on the component, and its value is an array of child component definitions that will be used to render the content of those zones.
Route Prerequisite
A Windshield route prerequisite is a customized version of Hapi's route prerequisite.
In Windshield, a route prerequisite must be an object with the following members:
method
- A function that accepts three arguments: the Windshield route context, the Hapi request object, and the Hapireply
interface.assign
- (optional) key name used to assign the response of the method on the request object inrequest.pre
Windshield configures these objects so that Hapi will run them like normal prerequisites in the request lifecycle.
If the assign
property is, for example, "foo", the response produced by
method
will be stored at request.pre.foo
, and merged into the page
context immediately before the page adapters are executed.
If the assign
property is not defined, the method
is still executed,
but its response is lost, unless it uses the reply().takeover()
method
to take over the reply interface.
Example
Consider the following route definition:
{
context: {
layout: "example"
}
path: '/listings',
adapters: [
doSomething
]
}
Assuming the prerequisite has been defined like this:
const doSomething = {
method: function (context, request, reply) {
reply({
associations: {
body: [
{ component: "something" }
]
}
});
},
assign: 'foo'
}
The page context will be
{
layout: "example",
associations: {
body: [
{ component: "something" }
]
}
}
Page Adapter
A page adapter is a Promise-returning function. During the request lifecycle, the route's handler executes all of its page adapters, after all of the route rerequisites have completed. The values resolved from each page adapter are merged into a copy of the route context, producing a page context that is used to render all child components and the page itself.
Example
Consider the following route definition:
{
context: {
layout: "example"
}
path: '/listings',
adapters: [
headerAdapter,
searchAdapter,
footerAdapter,
]
}
Assuming the following page adapters have been defined:
function headerAdapter(context, request) {
return Promise.resolve({
associations: {
header: [
{ component: "globalNav" }
]
}
});
}
function searchAdapter(context, request) {
return Promise.resolve({
attributes: {
title: "Cars.com"
},
associations: {
main: [
{ component: "searchWidget" },
{ component: "carListings" }
]
}
});
}
function footerAdapter(context, request) {
return Promise.resolve({
associations: {
footer: [
{ component: "footerNav" }
]
}
});
}
The resulting page context, after all adapters have resolve, would be merged together by Windshield and look like this:
{
layout: "example",
attributes: {
title: "Cars.com"
},
associations: {
header: [
{ component: "globalNav" }
],
main: [
{ component: "searchWidget" },
{ component: "carListings" }
],
footer: [
{ component: “footerNav" }
],
}
}
Component Adapter
A component adapter is a Promise-returning function defined by a component type config, which is used to process data for a component definition. The result resolved from the component adapter is then used to produce a rendered component object that contains the component markup.
A component adapter accepts three parameters: The component's context, the page context, and the Hapi request object. The component context is assembled from the component definition and the defaults provided by the component type config.
The value resolved from the component adapter is expected to be an object with the following methods:
data
- An arbitrary object which contains data to use when executing The component template.export
- (optional) An object containing data which we want to expose for parent components and the page object.exportAs
- (optional) String representing the key name that should be used when storing theexport
data in the rendered component object.
If the adapter's result does not follow the above format, the value is
wrapped in the above format, where data
is the result and export
and
exportAs
are undefined. This pattern is deprecated, however, and will
produce a warning if used.
License
Apache-2.0