diff --git a/demo/index.js b/demo/index.js index 0466573..ad6894a 100644 --- a/demo/index.js +++ b/demo/index.js @@ -24,24 +24,29 @@ highway.route({ path: '/', before: [ { name: 'test-event', params: [1, 2, 3, 4] }, - (state) => { + async () => { console.log('before middleware') - setTimeout(() => { - console.log('resolve before middleware') - state.resolve() - }, 2000) + + return new Promise(resolve => { + setTimeout(() => { + console.log('resolve before middleware') + resolve() + }, 2000) + }) } ], - action (state) { + async action () { console.log('home controller') - setTimeout(() => { - state.resolve('youhou!') - }, 2000) + return new Promise(resolve => { + setTimeout(() => { + resolve('youhou!') + }, 2000) + }) }, after: [ - () => { - console.log('after middleware') + (state) => { + console.log(`after middleware`, state) } ] }) @@ -55,7 +60,6 @@ highway.route({ action (state) { console.log(state) console.log(`user controller for user #${state.params.id}`) - state.resolve() } }) @@ -77,6 +81,5 @@ highway.route({ path: '/action/query', action (state) { console.log('action query state', state) - state.resolve() } }) diff --git a/src/route.js b/src/route.js index 5fd753a..0b4a053 100644 --- a/src/route.js +++ b/src/route.js @@ -68,43 +68,41 @@ Route.prototype = { const { name, path, action, before, after } = this.definition // Wrap the route action - return function actionWrapper (...args) { + return async function actionWrapper (...args) { // Convert args to object const params = urlComposer.params(path, args) - - // Create promise for async handling of controller execution - return new Promise((resolve, reject) => { - // Trigger `before` events/middlewares - if (before) { - return trigger.exec({ name, events: before, params }) - .then( - // Execute original route action passing route params and promise flow controls - () => Promise.resolve( - action({ resolve, reject, params, query: parseQuery() }) - ), - () => reject( - new Error(`[ backbone-highway ] Route "${name}" was rejected by a "before" middleware`) - ) - ) + const query = parseQuery() + + // Trigger `before` events/middlewares + if (before) { + try { + await trigger.exec({ name, events: before, params, query }) + } catch (err) { + throw new Error(`[backbone-highway] Route "${name}" was rejected by a "before" middleware`) } + } - // Just execute action if no `before` events are declared - return Promise.resolve( - action({ resolve, reject, params, query: parseQuery() }) + // Execute route action and get result + let result + try { + // Wrap action method in a `Promise.resolve()` in case action is not `async` + result = await Promise.resolve( + action({ params, query }) ) - }) - // Wait for promise resolve - .then(result => { - // Trigger `after` events/middlewares - if (after) { - return trigger.exec({ name, events: after, params }) - } - - return true - }).catch(err => { - // TODO What should we do when the action is rejected - console.error('caught action error', err) - }) + } catch (err) { + throw new Error(`[backbone-highway] Route "${name}" was rejected by "action"`) + } + + // Trigger `before` events/middlewares + if (after) { + try { + await trigger.exec({ name, events: after, params, query, result }) + } catch (err) { + throw new Error(`[backbone-higway] Route "${name}" was rejected by an "after" middleware`) + } + } + + return true } }, @@ -113,6 +111,8 @@ Route.prototype = { } } +// Parse query params from `window.location.search` and return an object +// TODO move to `url-composer` or maybe use `query-string` function parseQuery () { const result = {} let query = window.location.search || '' diff --git a/src/trigger.js b/src/trigger.js index 61e31c6..93b4495 100644 --- a/src/trigger.js +++ b/src/trigger.js @@ -2,12 +2,10 @@ import _ from 'underscore' import store from './store' export default { - dispatch (evt, params) { + dispatch ({ evt, params, query, result }) { const { dispatcher } = store.get('options') - if (_.isString(evt)) { - evt = { name: evt } - } + if (_.isString(evt)) evt = { name: evt } if (!dispatcher) { throw new Error(`[ highway ] Event '${evt.name}' could not be triggered, missing dispatcher`) @@ -15,31 +13,33 @@ export default { params = evt.params || params - console.log(`Trigger event ${evt.name}, params:`, params) - - dispatcher.trigger(evt.name, { params }) + dispatcher.trigger(evt.name, { params, query, result }) }, exec (options) { - let { name, events, params } = options + let { name, events, params, query, result } = options if (!_.isEmpty && !_.isArray(events)) { throw new Error(`[ highway ] Route events definition for ${name} needs to be an Array`) } + // Normalize events as an array if (!_.isArray(events)) events = [events] return Promise.all( _.map(events, (evt) => { + // Handle event as a function if (_.isFunction(evt)) { - return new Promise((resolve, reject) => { - evt({ resolve, reject, params }) - return null - }) + // Wrap in a promise in case `evt` is not async + return Promise.resolve( + evt({ params, query, result }) + ) } - this.dispatch(evt, params) - return Promise.resolve() + // Else dispatch event to + this.dispatch({ evt, params, query, result }) + + return true }) ) } diff --git a/test/spec/highway.spec.js b/test/spec/highway.spec.js index 755bd85..1fcbe13 100644 --- a/test/spec/highway.spec.js +++ b/test/spec/highway.spec.js @@ -1,32 +1,32 @@ -const assert = require('assert') -// const defer = require('lodash/defer') -const isFunction = require('lodash/isFunction') -const isObject = require('lodash/isObject') +import assert from 'assert' +import { Events } from 'backbone' +import { isFunction, isObject, isString, extend } from 'lodash' -const highway = require('../../dist/backbone-highway') +import highway from '../../src/index' const location = window.location +const AppEvents = extend({}, Events) const definitions = { home: { name: 'home', path: '/', - action (state) { - return state.resolve() + async action (state) { + return true } }, profile: { name: 'profile', path: '/users/:id', - action (state) { - return state.resolve(state.params.id) + async action (state) { + return state.params.id } }, optional: { name: 'optional', path: '/optional(/path/:param)', - action (state) { - return state.resolve(state.params.param) + async action (state) { + return state.params.param } } } @@ -76,7 +76,9 @@ describe('Backbone.Highway', () => { }) it('should start the router using `start` method', () => { - highway.start() + highway.start({ + dispatcher: AppEvents + }) }) it('should execute routes using the `go` method', () => { @@ -151,11 +153,10 @@ describe('Backbone.Highway', () => { highway.route({ name: 'test-action-query', path: '/test/action/query', - action (state) { + async action (state) { assert.ok(isObject(state.query)) assert.equal(state.query.hello, 'world') - state.resolve() done() } }) @@ -164,4 +165,84 @@ describe('Backbone.Highway', () => { highway.go({ name: 'test-action-query', query: { hello: 'world' } }) ) }) + + it('should handle `before` events', (done) => { + highway.route({ + name: 'before-events', + path: '/before/:data', + before: [ + async ({ params }) => { + assert.equal(params.data, 'events') + } + ], + async action ({ params }) { + assert.equal(params.data, 'events') + done() + } + }) + + assert.ok( + highway.go({ name: 'before-events', params: { data: 'events' } }) + ) + }) + + it('should dispatch named `before` events', (done) => { + AppEvents.on('before-test-event', ({ params }) => { + assert.ok(isObject(params)) + assert.equal(params.what, 'events') + done() + }) + + highway.route({ + name: 'named-before-events', + path: '/named/before/test/:what', + before: [ + 'before-test-event' + ], + action () {} + }) + + assert.ok( + highway.go({ name: 'named-before-events', params: { what: 'events' } }) + ) + + AppEvents.off('before-test-event') + }) + + it('should handle `after` events', (done) => { + highway.route({ + name: 'after-events', + path: '/after/:data', + async action ({ params }) { + assert.equal(params.data, 'events') + + return 'yeah' + }, + after: [ + async ({ params, result }) => { + assert.equal(params.data, 'events') + assert.equal(result, 'yeah') + done() + } + ] + }) + + assert.ok( + highway.go({ name: 'after-events', params: { data: 'events' } }) + ) + }) + + it('should execute 404 controller for missing routes', (done) => { + highway.route({ + name: '404', + action ({ params }) { + assert.ok(isObject(params)) + done() + } + }) + + assert.ok( + !highway.go({ name: 'some-random-inexisting-route' }) + ) + }) })