Getting started

Installation

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')
    }
}, function (err) {
    if (err) console.log(err);
});

See options below for details on each of these options.

Options

rootDir

This should be the absolute path to the root directory of your project. Page layouts and Handlebars helpers will be looked for at this location. (See note below about project structure requirements).

handlebars

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.

uriContext

This is the base URI under which windshield will register all of your routes. For example, if you set uriContext to “/foo”, and you have a route defined as “/bar”, that route will be accessible at “/foo/bar”.

routes

The routes property on the Windshield config object is an array of route definitions.

Each route definition within the routes array option is a configuration object with the following properties:

  • path - This is a string which acts as a path expression. It’s handed off directly to Hapi’s router when Windshield sets up your route.
  • context - This is an object which may contain route specific context to be referenced by the adapters.
  • adapters - This is an array of adapter implementations. An adapter is Promise-returning function which resolves with a page object. (More on page objects below.)
  • pageFilter - This is an optional property which can be set as 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.

components

The components property on the Windshield config object is an object which serves as a map of component implementations. The property names on this object are component names and the value of each property is a component’s implementation object.

Each component implementation is an object with the following properties: template, Model and adapter. The template property is the only thing that is required, the Model and adapter are optional. A model should be a constructor function which recieves and returns an object. A Model is used for simple translation and transformation of component data. An adapter is a function which returns the Promise—this promise should resolve with an object. Adapters are used for pulling in external data for the component to use from client libraries, etc.

The template property of the component implementation should be a function which returns a Promise—this Promise should resolve with a Handlebars template string. To make this as easy as possible, a helper method called readTemplate is available on the windshield object which takes the absolute path to a template file and generated the proper function for export.

Page Adapters & Objects

A page object is an object with the following properties: layout, attributes and associations. All page object properties are optional.

  • layout - This is a string the value of which is the name of the Handlebars layout to use for the overall page.
  • attributes - This is an object each property of which is a string. These serve as page-level attributes.
  • associations - This is an object which serves as a map of “named associations”. You can think of an association as a zone within the page layout. Each property name is the assocation name, and the value of each property is a collection of component objects to be contained within that association.

Adapters are Promise-returning functions which resolve with page objects. Each route definition has an array of adapters, each of which will be called and resolve with a page object. All the resultant page objects are then merged back together to create one single page object which can be applied to the page layout.

For example, take the following route definition:

{
    path: '/listings',
    adapters: [
        headerAdapter,
        searchAdapter,
        footerAdapter,
    ]
}

The headerAdapter in the example above, might return a partial page object which looks like this:

{
    associations: {
        header: [
            { component: "globalNav" }
        ]
    }
}

The searchAdapter in the example above, might return a partial page object which looks like this:

{
    attributes: {
        title: "Cars.com"
    },
    associations: {
        main: [
            { component: "searchWidget" },
            { component: "carListings" }
        ]
    }
}

The footerAdapter in the example above, might return a partial page object which looks like this:

{
    associations: {
        footer: [
            { component: "footerNav" }
        ]
    }
}

The resulting page object, after all adapters have resolve, would be composed together by Windshield and would look like this:

{
    layout: "default",
    attributes: {
        title: "Cars.com"
    },
    associations: {
        header: [
            { component: "globalNav" }
        ],
        main: [
            { component: "searchWidget" },
            { component: "carListings" }
        ],
        footer: [
            { component: “footerNav" }
        ],
    }
}

Component Adapters & Objects

Components can also have their own adapters. The data which resolves from component adapter will be added back onto the component object within the data property. Component objects should return an object with their data contained inside a data property. This specification did not used to exists and currently a can component can still resolve with its data at the object root instead of inside a data property but this usage pattern is now deprecated and will not be supported in coming releases. After the component adapter has been processed, a component object within the page object might then look like this:

{
    component: "globalNav",
    data: {
        items: [
            {
                displayName: "Buy",
                href: "/for-sale/"
            },
            {
                displayName: "Sell & Trade",
                href: "/sell/"
            },
            {
                displayName: "Service & Repair",
                href: "/auto-repair/"
            },
            {
                displayName: "News",
                href: "/news/"
            }
        ]
    }
}

In this way, each component referenced in the original page object, as generated by the page adapters, is hydrated until all of the data needed to render the final HTML of the page is contained in one composite page object.

Optionally, a component can also resolve with an export and an exportAs property. Data “exported” in this manner will then be usable within any template (whether it be a layout template or a component template) by using the {exported} handlerbars helper. This is a new feature. More details on this coming soon.


Scaffolding

windshieldjs comes with a binary CLI tool to generate new components, adapters and layouts. Run ./node_modules/.bin/windshield from the project root of any project that has windshieldjs installed and you’ll be lead through a series of prompts.

./node_modules/.bin/windshield

Project Structure

WindshieldJS is mostly driven by configuration, but due to the way Hapi’s “vision” plugin works, your main page “layouts” directory must be located directly within the rootDir of your project. If you are using Handlebars helpers, you must add a helpers directory inside rootDir as well.

  • rootDir
    • helpers
    • layouts