Skip to content

Commit

Permalink
feat: support asynchronous results for canActivate, canDeactivate and…
Browse files Browse the repository at this point in the history
… node listeners
  • Loading branch information
troch committed Jul 21, 2015
1 parent 3799587 commit 445892a
Show file tree
Hide file tree
Showing 7 changed files with 331 additions and 233 deletions.
2 changes: 2 additions & 0 deletions karma.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ module.exports = function(config) {
files: [
'node_modules/route-node/node_modules/path-parser/dist/umd/path-parser.js',
'node_modules/route-node/dist/umd/route-node.js',
'dist/test/constants.js',
'dist/test/async.js',
'dist/test/transition.js',
'dist/test/router5.js',
'tests/*.js'
Expand Down
121 changes: 53 additions & 68 deletions modules/Router5.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import RouteNode from 'route-node/modules/RouteNode'
import Transition from './Transition'

let nameToIDs = name => {
return name.split('.').reduce((ids, name) => {
ids.push(ids.length ? ids[ids.length - 1] + '.' + name : name)
return ids
}, [])
}
import RouteNode from 'route-node/modules/RouteNode'
import transition from './transition'
import constants from './constants'

let makeState = (name, params, path) => ({name, params, path})

Expand Down Expand Up @@ -67,10 +61,8 @@ export default class Router5 {
* @return {Router5} The Router5 instance
*/
addNode(name, path, canActivate) {
try {
this.rootNode.addNode(name, path)
if (canActivate) this._canAct[name] = canActivate
} catch (e) {}
this.rootNode.addNode(name, path)
if (canActivate) this._canAct[name] = canActivate
return this
}

Expand All @@ -83,18 +75,20 @@ export default class Router5 {
if (!state) return
if (this.lastKnownState && this.areStatesEqual(state, this.lastKnownState)) return

let canTransition = this._transition(state, this.lastKnownState)
if (!canTransition) {
let url = this.buildUrl(this.lastKnownState.name, this.lastKnownState.params)
window.history.pushState(this.lastKnownState, '', url)
}
this._transition(state, this.lastKnownState, (err) => {
if (!err) {
let url = this.buildUrl(this.lastKnownState.name, this.lastKnownState.params)
window.history.pushState(this.lastKnownState, '', url)
}
})
}

/**
* Start the router
* @return {Router5} The router instance
* @param {Function} [done] A callback which will be called when starting is done
* @return {Router5} The router instance
*/
start() {
start(done) {
if (this.started) return this
this.started = true

Expand All @@ -105,8 +99,9 @@ export default class Router5 {
if (startState) {
this.lastKnownState = startState
window.history.replaceState(this.lastKnownState, '', this.buildUrl(startState.name, startState.params))
if (done) done()
} else if (this.options.defaultRoute) {
this.navigate(this.options.defaultRoute, this.options.defaultParams, {replace: true})
this.navigate(this.options.defaultRoute, this.options.defaultParams, {replace: true}, done)
}
// Listen to popstate
window.addEventListener('popstate', this.onPopState.bind(this))
Expand Down Expand Up @@ -245,7 +240,8 @@ export default class Router5 {
* @return {Router5} The router instance
*/
removeNodeListener(name, cb) {
return this._removeListener('^' + name, cb);
this._cbs['^' + name] = [];
return this
}

/**
Expand Down Expand Up @@ -296,7 +292,6 @@ export default class Router5 {
* @return {Router5} The router instance
*/
canActivate(name, canActivate) {
if (this._canAct[name]) console.warn(`A canActivate was alread registered for route node ${name}.`)
this._canAct[name] = canActivate
return this
}
Expand Down Expand Up @@ -347,57 +342,42 @@ export default class Router5 {
/**
* @private
*/
_transition(toState, fromState) {
if (!fromState) {
this.lastKnownState = toState
this._invokeListeners('*', toState, fromState)
return true
}
_transition(toState, fromState, done) {
// Cancel current transition
// if (this._tr) this._tr()

let i
let cannotDeactivate = false
let cannotActivate = false
let fromStateIds = nameToIDs(fromState.name)
let toStateIds = nameToIDs(toState.name)
let maxI = Math.min(fromStateIds.length, toStateIds.length)

for (i = 0; i < maxI; i += 1) {
if (fromStateIds[i] !== toStateIds[i]) break
}
this._tr = transition(router, toState, fromState, (err) => {
this._tr = null

cannotDeactivate =
fromStateIds.slice(i).reverse()
.map(id => this._cmps[id])
.filter(comp => comp && comp.canDeactivate)
.some(comp => !comp.canDeactivate(toState, fromState))
if (err) {
if (done) done(err)
return
}


if (!cannotDeactivate) {
cannotActivate = toStateIds.slice(i)
.map(id => this._canAct[id])
.filter(canAct => canAct)
.some(canAct => !canAct(toState, fromState))
}

if (!cannotDeactivate && !cannotActivate) {
this.lastKnownState = toState
this._invokeListeners('^' + (i > 0 ? fromStateIds[i - 1] : ''), toState, fromState)
this._invokeListeners('=' + toState.name, toState, fromState)
this._invokeListeners('*', toState, fromState)
}

return !cannotDeactivate && !cannotActivate
if (done) done(null, true)
})

return () => { if (this._tr) this._tr() }
}

/**
* Navigate to a specific route
* @param {String} name The route name
* @param {Object} [params={}] The route params
* @param {Object} [opts={}] The route options (replace, reload)
* @return {Boolean} Whether or not transition was allowed
* @param {String} name The route name
* @param {Object} [params={}] The route params
* @param {Object} [opts={}] The route options (replace, reload)
* @param {Function} [done] A callback (err, res) to call when transition has been performed
* either successfully or unsuccessfully.
* @return {Function} A cancellation function
*/
navigate(name, params = {}, opts = {}) {
if (!this.started) return
navigate(name, params = {}, opts = {}, done) {
if (!this.started) {
done(constants.ROUTER_NOT_STARTED)
return
}

let path = this.buildPath(name, params)
let url = this.buildUrl(name, params)
Expand All @@ -409,15 +389,20 @@ export default class Router5 {

// Do not proceed further if states are the same and no reload
// (no desactivation and no callbacks)
if (sameStates && !opts.reload) return
if (sameStates && !opts.reload) {
done(constants.SAME_STATES)
return
}

// Transition and amend history
let canTransition = this._transition(this.lastStateAttempt, this.lastKnownState)
return this._transition(this.lastStateAttempt, this.lastKnownState, (err) => {
if (err) {
if (done) done(err)
return
}

if (canTransition && !sameStates) {
window.history[opts.replace ? 'replaceState' : 'pushState'](this.lastStateAttempt, '', url)
}

return canTransition
if (done) done(null, true)
})
}
}
101 changes: 54 additions & 47 deletions modules/Transition.js
Original file line number Diff line number Diff line change
@@ -1,66 +1,73 @@
import asyncProcess from './async'
import constants from './constants'

let nameToIDs = name => {
return name.split('.').reduce((ids, name) => {
ids.push(ids.length ? ids[ids.length - 1] + '.' + name : name)
return ids
return ids.concat(ids.length ? ids[ids.length - 1] + '.' + name : name)
}, [])
}

let boolToPromise = res => {
if (res.then) return res
return new Promise((resolve, reject) => res || res === undefined ? resolve() : reject())
}
export default function transition(router, toState, fromState, callback) {
let cancelled = false
let cancel = () => cancelled = true
let done = (err, res) => callback(cancelled ? constants.TRANSITION_CANCELLED : err, res)

let processFn = (fn, toState, fromState) => {
return new Promise((resolve, reject) => {
return boolToPromise(fn(toState, fromState))
})
}
let i
let fromStateIds = fromState ? nameToIDs(fromState.name) : []
let toStateIds = nameToIDs(toState.name)
let maxI = Math.min(fromStateIds.length, toStateIds.length)

let process = (functions, toState, fromState) => {
if (functions.length) {
return processFn(functions[0], toState, fromState)
.then(() => process(functions.slice(1), toState, fromState))
for (i = 0; i < maxI; i += 1) {
if (fromStateIds[i] !== toStateIds[i]) break
}
return boolToPromise(true)
}

export default class Transition {
constructor(router, toState, fromState) {
let i
let fromStateIds = fromState ? nameToIDs(fromState.name) : []
let toStateIds = nameToIDs(toState.name)
let maxI = Math.min(fromStateIds.length, toStateIds.length)

for (i = 0; i < maxI; i += 1) {
if (fromStateIds[i] !== toStateIds[i]) break
}
let toDeactivate = fromStateIds.slice(i).reverse()
let toActivate = toStateIds.slice(i)
let intersection = i > 0 ? fromStateIds[i - 1] : ''

return new Promise((resolveTransition, rejectTransition) => {
this.cancel = rejectTransition
let toDeactivate = fromStateIds.slice(i).reverse()
let toActivate = toStateIds.slice(i)
let intersection = fromState ? (i > 0 ? fromStateIds[i - 1] : '') : null

// Deactivate
let canDeactivate = (toState, fromState, cb) => {
if (cancelled) done()
else {
let canDeactivateFunctions = toDeactivate
.map(name => router._cmps(name))
.map(name => router._cmps[name])
.filter(comp => comp && comp.canDeactivate)
.map(comp => comp.canDeactivate)

let deactivation = process(canDeactivateFunctions, toState, fromState)
asyncProcess(
canDeactivateFunctions, toState, fromState,
(err, res) => cb(err ? constants.CANNOT_DEACTIVATE : null, res)
)
}
}

// Activate
let activation = deactivation.then(() => {
let canActivateFunctions = toActivate
.map(name => router._canAct[name])
.filter(_ => _)
return process(canActivateFunctions, toState, fromState)
})
let canActivate = (toState, fromState, cb) => {
if (cancelled) done()
else {
let canActivateFunctions = toActivate
.map(name => router._canAct[name])
.filter(_ => _)

// Node listener
let nodeListener = activation.then(() => router._invokeListeners('^' + intersection, toState, fromState))
asyncProcess(
canActivateFunctions, toState, fromState,
(err, res) => cb(err ? constants.CANNOT_ACTIVATE : null, res)
)
}
}

nodeListener.then(resolveTransition, rejectTransition)
});
let nodeListener = (toState, fromState, cb) => {
if (cancelled) done()
else {
let listeners = router._cbs['^' + intersection] || []
asyncProcess(
listeners, toState, fromState,
(err, res) => cb(err ? constants.NODE_LISTENER_ERR : null, res),
true
)
}
}

let pipeline = fromState ? [canDeactivate, canActivate, nodeListener] : [canActivate]
asyncProcess(pipeline, toState, fromState, done)

return cancel
}
35 changes: 35 additions & 0 deletions modules/async.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
export default function asyncProcess(functions, toState, fromState, callback, allowNoResult = false) {
let remainingSteps = functions || []

let processFn = (done) => {
if (!remainingSteps.length) return true

let len = remainingSteps[0].length
let res = remainingSteps[0](toState, fromState, done)

if (typeof res === 'boolean') done(!res, res);

else if (res && typeof res.then === 'function') {
res.then(() => done(null, true), () => done(true, null))
}

else if (len < 3 && allowNoResult) done(null, true)

return false
}

let iterate = (err, res) => {
if (err) callback(err)
else {
remainingSteps = remainingSteps.slice(1)
next()
}
}

let next = () => {
let finished = processFn(iterate)
if (finished) callback(null, true)
}

next()
}
10 changes: 10 additions & 0 deletions modules/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const constants = {
ROUTER_NOT_STARTED : 0,
SAME_STATES : 1,
CANNOT_DEACTIVATE : 2,
CANNOT_ACTIVATE : 3,
NODE_LISTENER_ERR : 4,
TRANSITION_CANCELLED : 5,
}

export default constants

0 comments on commit 445892a

Please sign in to comment.