Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

canLeaveState Improvements #160

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ module.exports = function StateProvider(makeRenderer, rootElement, stateRouterOp
return create.length === 0 && destroy.length === 0
}

function allowStateChangeOrRevert(newStateName, newParameters) {
async function allowStateChangeOrRevert(newStateName, newParameters) {
const lastState = lastCompletelyLoadedState.get()
if (lastState.name && statesAreEquivalent(lastState, lastStateStartedActivating.get())) {
const { destroy } = stateChangeLogic(
Expand All @@ -155,17 +155,23 @@ module.exports = function StateProvider(makeRenderer, rootElement, stateRouterOp
}),
)

const canLeaveStates = destroy.every(stateName => {
const canLeaveStates = (await Promise.all(destroy.map(async stateName => {
const state = prototypalStateHolder.get(stateName)
if (state.canLeaveState && typeof state.canLeaveState === 'function') {
const stateChangeAllowed = state.canLeaveState(activeDomApis[stateName])
const stateChangeAllowed = await Promise.resolve(state.canLeaveState(activeDomApis[stateName], {
name: newStateName,
parameters: newParameters,
}))
if (!stateChangeAllowed) {
stateProviderEmitter.emit('stateChangePrevented', stateName)
stateProviderEmitter.emit('stateChangePrevented', stateName, {
name: newStateName,
parameters: newParameters,
})
}
return stateChangeAllowed
}
return true
})
}))).every(Boolean)

if (!canLeaveStates) {
stateProviderEmitter.go(lastState.name, lastState.parameters, { replace: true })
Expand All @@ -175,11 +181,11 @@ module.exports = function StateProvider(makeRenderer, rootElement, stateRouterOp
return true
}

function onRouteChange(state, parameters) {
async function onRouteChange(state, parameters) {
try {
const finalDestinationStateName = prototypalStateHolder.applyDefaultChildStates(state.name)

if (finalDestinationStateName === state.name && allowStateChangeOrRevert(state.name, parameters)) {
if (finalDestinationStateName === state.name && await allowStateChangeOrRevert(state.name, parameters)) {
emitEventAndAttemptStateChange(finalDestinationStateName, parameters)
} else if (finalDestinationStateName !== state.name) {
// There are default child states that need to be applied
Expand All @@ -190,7 +196,7 @@ module.exports = function StateProvider(makeRenderer, rootElement, stateRouterOp
if (theRouteWeNeedToEndUpAt !== currentRoute) {
// change the url to match the full default child state route
stateProviderEmitter.go(finalDestinationStateName, parameters, { replace: true })
} else if (allowStateChangeOrRevert(finalDestinationStateName, parameters)) {
} else if (await allowStateChangeOrRevert(finalDestinationStateName, parameters)) {
// the child state has the same route as the current one, just start navigating there
emitEventAndAttemptStateChange(finalDestinationStateName, parameters)
}
Expand Down
236 changes: 236 additions & 0 deletions test/allow-state-change.js
Original file line number Diff line number Diff line change
Expand Up @@ -474,3 +474,239 @@ test(`canLeaveState will only fire once`, t => {
stateRouter.go(`start.child`)
})
})

test(`canLeaveState will not fire on state load`, t => {
function startTest(t) {
const state = getTestState(t)
const stateRouter = state.stateRouter
t.plan(1)

stateRouter.addState({
name: `start`,
route: `/start`,
querystringParameters: [ `foo` ],
template: {},
resolve() {
return Promise.resolve()
},
})

stateRouter.addState({
name: `end`,
route: `/end`,
template: {},
canLeaveState: () => {
t.fail(`canLeaveState should not be called`)
return false
},
resolve() {
return Promise.resolve()
},
})

return state
}

const stateRouter = startTest(t).stateRouter
let started = false

stateRouter.on(`stateChangeEnd`, state => {
const stateName = state.name
if (stateName === 'start') {
if (!started) {
started = true
stateRouter.go(`end`)
}
} else if (stateName === 'end') {
t.pass('state change was allowed')
t.end()
}
})

stateRouter.on('stateChangePrevented', stateThatPreventedChange => {
t.fail(`state change was prevented by ${ stateThatPreventedChange }`)
})

stateRouter.go(`start`, { foo: `bar` })
})

test(`canLeaveState async`, t => {
let guardCompleted = false

function startTest(t) {
const state = getTestState(t)
const stateRouter = state.stateRouter
t.plan(1)

stateRouter.addState({
name: `start`,
route: `/start`,
querystringParameters: [ `foo` ],
template: {},
canLeaveState: async() => {
const res = await new Promise(resolve => setTimeout(() => resolve(true), 1000))
guardCompleted = true
return res
},
resolve() {
return Promise.resolve()
},
})

stateRouter.addState({
name: `end`,
route: `/end`,
template: {},
resolve() {
t.pass('state loads after async canLeaveState')
return Promise.resolve()
},
})

return state
}

const stateRouter = startTest(t).stateRouter

stateRouter.on('stateChangePrevented', stateThatPreventedChange => {
t.fail(`state change was prevented by ${ stateThatPreventedChange }`)
})

stateRouter.on('stateChangeEnd', state => {
if (state.name === 'start') {
stateRouter.go('end')
} else if (state.name === 'end' && !guardCompleted) {
t.fail('guard did not complete')
} else if (state.name === 'end' && guardCompleted) {
t.end()
}
})

stateRouter.go(`start`, { foo: `bar` })
})

test('canLeaveState passes destination parameters', t => {
function startTest(t) {
const state = getTestState(t)
const stateRouter = state.stateRouter
t.plan(2)

stateRouter.addState({
name: `start`,
route: `/start`,
querystringParameters: [ `foo` ],
template: {},
canLeaveState: (_domApi, destinationState) => {
t.ok(destinationState.parameters.foo === 'bar', 'destination parameters are passed')
t.ok(destinationState.name === 'start', 'destination state is passed')
return true
},
resolve() {
return Promise.resolve()
},
})

return state
}

const stateRouter = startTest(t).stateRouter

stateRouter.on('stateChangePrevented', (stateThatPreventedChange, destinationState) => {
t.fail(`state change was prevented by ${ stateThatPreventedChange }`)
})

stateRouter.on('stateChangeEnd', (state, parameters) => {
if (state.name === 'start' && !parameters.foo) {
stateRouter.go(null, { foo: 'bar' })
}
})

stateRouter.go(`start`)
})

test('stateChangePrevented passes destination parameters', t => {
function startTest(t) {
const state = getTestState(t)
const stateRouter = state.stateRouter
t.plan(2)

stateRouter.addState({
name: `start`,
route: `/start`,
querystringParameters: [ `foo` ],
template: {},
canLeaveState: () => {
return false
},
resolve() {
return Promise.resolve()
},
})

return state
}

const stateRouter = startTest(t).stateRouter

stateRouter.on('stateChangePrevented', (_stateThatPreventedChange, destinationState) => {
t.ok(destinationState.name === 'start', 'destination state is passed')
t.ok(destinationState.parameters.foo === 'bar', 'destination parameters are passed')
t.end()
})

let started = false
stateRouter.on('stateChangeEnd', () => {
if (!started) {
started = true
stateRouter.go(null, { foo: 'bar' })
}
})

stateRouter.go(`start`)
})

test(`canLeaveState async race condition`, t => {
function startTest(t) {
const state = getTestState(t)
const stateRouter = state.stateRouter
t.plan(1)

stateRouter.addState({
name: `start`,
route: `/start`,
querystringParameters: [ `foo` ],
template: {},
canLeaveState: async(_domApi, destinationState) => {
const res = await new Promise(resolve => setTimeout(() => resolve(true), destinationState.parameters.delay))
return res
},
resolve() {
return Promise.resolve()
},
})

stateRouter.addState({
name: `end`,
route: `/end`,
template: {},
resolve() {
return Promise.resolve()
},
})

return state
}

const stateRouter = startTest(t).stateRouter

stateRouter.on('stateChangeEnd', (state, parameters) => {
if (state.name === 'start') {
stateRouter.go('end', { delay: 100 })
setTimeout(() => stateRouter.go('end', { delay: 0 }), 10)
} else if (state.name === 'end') {
t.ok(parameters.delay === '0', 'second state change wins')
}
})

stateRouter.go(`start`, { foo: `bar` })
})