Skip to content

Commit

Permalink
Merge pull request #42 from firstandthird/hapi17_plus
Browse files Browse the repository at this point in the history
Hapi17 plus
  • Loading branch information
orthagonal authored Dec 8, 2017
2 parents 2f735e4 + 5e443b1 commit 654b506
Show file tree
Hide file tree
Showing 27 changed files with 305 additions and 2,190 deletions.
1 change: 0 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,5 @@ language: node_js

node_js:
- 8
- 6

script: npm run test-travis
79 changes: 44 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,49 +3,58 @@
[![Build Status](https://travis-ci.org/firstandthird/hapi-views.svg?branch=master)](https://travis-ci.org/firstandthird/hapi-views)
[![Coverage Status](https://coveralls.io/repos/github/firstandthird/hapi-views/badge.svg?branch=master)](https://coveralls.io/github/firstandthird/hapi-views?branch=master)

`hapi-views` is a hapi plugin that makes it very easy to register routes for views and pull in data. Great for pages that just need either some static data or data from an api.

`hapi-views` is a hapi plugin that lets you quickly and easily create your view routes. You use JSON to specify routes that can pull in dynamic data from any source, then use that data to render an HTML page.
## Features

* define routes from config
* pull in yaml data and pass it to the views
* pull in data from an api endpoint
* pull in data from a server method
* works with any Hapi view engine
* define view routes with JSON instead of programming route handlers
* specify dynamic JSON for your views with [varson](https://github.com/firstandthird/varson)
* pull in data with Hapi server methods

## Usage

```javascript
server.register({
register: require('hapi-views'),
await server.register({
plugin: require('hapi-views'),
options: {
dataPath: __dirname + '/public/pages/',
views: {
'/': {
view: 'landing/view',
yaml: 'landing/data.yaml'
},
'/comments/{id}': {
view: 'landing/view',
api: 'http://jsonplaceholder.typicode.com/comments/{params.id}'
},
'/load': {
view: 'landing/view',
method: 'serverLoad',
routeConfig: { // Any valid hapi route config
plugins: {
'hapi-hello-world': {}
}
}
}
},
// each type defined under 'globals' is loaded for all views
// 'globals' (optional) specifies global data that is common to all views
// data.serverName and data.db will be available to the view engine:
globals: {
yaml: 'global.yaml',
api: 'http://globalBargle.com/icons/{params.id}',
method: 'globalMethod',
inject: '/globals'
},
serverName: 'Bob the Server',
db: 'http://55.55.55.5555:2121/bobDb'
}
// "routes" can contain as many view routes as you like
// the key is the URL and the value is an object that specifies how to process the view for that URL
routes: {
// the URL for this route will be 'http://yourhost.com/homepage/{userId}'
'/homepage/{userId}': {
// 'view' (required) tells the view engine what HTML template to use:
view: 'homepage',
// 'data' (required) tells the view engine what context data to use when rendering the HTML
// this data is itself processed by the template engine in varson:
data: {
// an example of a literal string value:
title: "Your Homepage",
// varson evaluates string values inside the double-bracket '{{' delimiters as Javascript.
// So the view engine will see data.amount as 35:
amount: "{{ 15 + 25 }}",
// the Hapi request object (https://hapijs.com/api#request) can be referenced directly:
userId: "{{request.params.userId}}",
// Hapi server methods (https://hapijs.com/api#server.method()) can be referenced as 'methods'.
// for example, this expression will set data.userInfo to the value returned by calling server.methods.getUserInfo. Works for methods that return a promise as well:
userInfo: "{{methods.getUserInfo(request.params.userId)}}"
},
// 'preProcess' (optional) will be called before the request is processed:
preProcess: (request, options, h) => { }
// 'preResponse (optional) will be called after the request is processed but before the view is returned:
preResponse: (request, options, h) => { processRan = true; }
// 'onError' (optional) will be called if there is an error processing your data or view.
// The value returned by onError will be the result your route returns to the client:
onError: (err, h) => { return boom.badImplementation('this was an error') }
}
}
}
}, function(err) {
});
```

See [test/test.global.js](https://github.com/firstandthird/hapi-views/blob/master/test/test.globals.js) for working examples.
53 changes: 8 additions & 45 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
'use strict';
/* eslint new-cap: 0 */
const async = require('async');
const hoek = require('hoek');
const aug = require('aug');
const renderHandler = require('./lib/handler.js');
const serverMethods = ['api', 'inject', 'yaml', 'method'];
const defaults = {
enableCache: false,
serveStale: false,
Expand All @@ -14,56 +11,22 @@ const defaults = {
views: {}
};

exports.register = function(server, options, next) {
options = hoek.applyToDefaults(defaults, options);
serverMethods.forEach((methodName) => {
// todo: add caching options:
const methodOptions = {};
if (options.enableCache) {
const defaultOptions = {};
if (options.serveStale) {
defaultOptions.dropOnError = false;
}
switch (methodName) {
case 'api':
// cache key will be the url of the api call:
methodOptions.cache = Object.assign({}, defaultOptions, options.cache);
methodOptions.generateKey = function(genRequest, url) {
return typeof url === 'string' ? url : url.url;
};
break;
case 'inject':
// cache key will be the path we're injecting to
methodOptions.cache = Object.assign({}, defaultOptions, options.cache);
methodOptions.generateKey = function(genRequest, url) {
return url;
};
break;
default:
break;
}
}
server.method(`views.${methodName}`, require(`./methods/views/${methodName}.js`), methodOptions);
// also register a cacheless version for routes that need it:
server.method(`views.${methodName}_noCache`, require(`./methods/views/${methodName}.js`));
});
// register the fetch method:
server.method('views.fetch', require('./methods/views/fetch.js'), {});
//routes
async.forEachOfSeries(options.views, (config, path, cb) => {
const register = async (server, options) => {
options = Object.assign({}, defaults, options);
Object.keys(options.routes).forEach((path) => {
const config = options.routes[path];
config.options = options;
server.route({
path,
method: 'get',
handler: renderHandler(config),
config: aug({}, options.routeConfig, config.routeConfig || {})
});

cb();
}, next);
});
};

exports.register.attributes = {
name: 'views',
exports.plugin = {
register,
once: true,
pkg: require('./package.json')
};
86 changes: 23 additions & 63 deletions lib/handler.js
Original file line number Diff line number Diff line change
@@ -1,74 +1,34 @@
'use strict';
const async = require('async');
const str2fn = require('str2fn');
const Boom = require('boom');
const boom = require('boom');
const aug = require('aug');
const pprops = require('p-props');
const varson = require('varson');

module.exports = (viewConfig) => (request, reply) => {
async.autoInject({
preProcess: done => {
if (viewConfig.preProcess) {
return str2fn(request.server.methods, viewConfig.preProcess)(request, viewConfig.options, reply, done);
}
return done();
},
locals: (preProcess, done) => {
if (preProcess) {
return done(null, false);
}
return request.server.methods.views.fetch(request, viewConfig, done);
},
globals: (preProcess, done) => {
if (preProcess) {
return done(null, false);
}
if (!viewConfig.options.globals) {
return done(null, {});
}
return request.server.methods.views.fetch(request, Object.assign({ options: viewConfig.options }, viewConfig.options.globals), done);
module.exports = (viewConfig) => {
return async (request, h) => {
if (viewConfig.preProcess) {
viewConfig.preProcess(request, viewConfig.options, h);
}
}, (err, data) => {
if (err) {
if (typeof viewConfig.options.onError === 'function') {
return viewConfig.options.onError(err, reply);
}
// aug with globals:
const obj = aug({}, viewConfig.data, viewConfig.options.globals);
try {
const data = varson(obj, { request, methods: request.server.methods });
const result = await pprops(data);
if (viewConfig.preResponse) {
viewConfig.preResponse(request, viewConfig.options, result);
}
return h.view(viewConfig.view, result);
} catch (err) {
if (typeof viewConfig.onError === 'function') {
return viewConfig.onError(err, reply);
}
if (typeof viewConfig.options.onError === 'string') {
return str2fn(request.server.methods, viewConfig.options.onError)(err, reply);
return viewConfig.onError(err);
}
if (typeof viewConfig.onError === 'string') {
return str2fn(request.server.methods, viewConfig.onError)(err, reply);
if (typeof viewConfig.options.onError === 'function') {
return viewConfig.options.onError(err);
}
// boom errs can be handed back directly:
if (err.isBoom) {
return reply(err);
throw err;
}
return reply(Boom.boomify(err));
}
// Returned early in preProcess
if (!data.globals && !data.locals) {
return;
}

const combinedData = aug({}, data.globals, data.locals);
if (viewConfig.options.debug) {
request.server.log(['hapi-views', 'debug'], {
data: combinedData,
path: request.url.path
});
}
if (viewConfig.options.allowDebugQuery && (request.query.json === '1' || request.query.debug === '1')) {
return reply(combinedData).type('application/json');
}

if (viewConfig.preResponse) {
return str2fn(request.server.methods, viewConfig.preResponse)(request, viewConfig.options, combinedData, reply, () => {
reply.view(viewConfig.view, combinedData);
});
throw boom.boomify(err);
}
const viewName = typeof viewConfig.view === 'function' ? viewConfig.view(combinedData) : viewConfig.view;
return reply.view(viewName, combinedData);
});
};
};
32 changes: 0 additions & 32 deletions methods/views/api.js

This file was deleted.

22 changes: 0 additions & 22 deletions methods/views/data.js

This file was deleted.

Loading

0 comments on commit 654b506

Please sign in to comment.