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

Support passing an allowStateChange function that is evaluated befo… #154

Merged
merged 12 commits into from
Jun 16, 2023
63 changes: 54 additions & 9 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ const isFunction = property => obj => typeof obj[property] === `function`
const isThenable = object => object && (typeof object === `object` || typeof object === `function`) && typeof object.then === `function`
const promiseMe = (fn, ...args) => new Promise(resolve => resolve(fn(...args)))

const expectedPropertiesOfAddState = [ `name`, `route`, `defaultChild`, `data`, `template`, `resolve`, `activate`, `querystringParameters`, `defaultQuerystringParameters`, `defaultParameters` ]
const expectedPropertiesOfAddState = [ `name`, `route`, `defaultChild`, `data`, `template`, `resolve`, `activate`, `querystringParameters`, `defaultQuerystringParameters`, `defaultParameters`, `canLeaveState` ]

module.exports = function StateProvider(makeRenderer, rootElement, stateRouterOptions = {}) {
const prototypalStateHolder = StateState()
Expand Down Expand Up @@ -132,24 +132,69 @@ module.exports = function StateProvider(makeRenderer, rootElement, stateRouterOp
return series(stateNames, stateName => renderStateName(parameters, stateName))
}

function statesAreEquivalent(stateA, stateB) {
const { create, destroy } = stateChangeLogic(
compareStartAndEndStates({
original: stateA,
destination: stateB,
}),
)

return create.length === 0 && destroy.length === 0
}

function allowStateChangeOrRevert(newStateName, newParameters) {
const lastState = lastCompletelyLoadedState.get()
if (lastState.name && statesAreEquivalent(lastState, lastStateStartedActivating.get())) {
// Check canLeaveState for all states that will be destroyed or changed
const { create, destroy } = stateChangeLogic(
compareStartAndEndStates({
original: lastState,
destination: {
name: newStateName,
parameters: newParameters,
},
}),
)
const statesNamesToCheck = [ ...create, ...destroy ]
const canLeaveState = statesNamesToCheck.every(stateName => {
const state = prototypalStateHolder.get(stateName)
if (state?.canLeaveState && typeof state.canLeaveState === 'function') {
const stateChangeAllowed = state.canLeaveState(activeDomApis[stateName])
if (!stateChangeAllowed) {
stateProviderEmitter.emit('stateChangePrevented', stateName)
}
return stateChangeAllowed
}
return true
})

if (!canLeaveState) {
stateProviderEmitter.go(lastState.name, lastState.parameters, { replace: true })
}
return canLeaveState
}
return true
}

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

if (finalDestinationStateName === state.name) {
if (finalDestinationStateName === state.name && allowStateChangeOrRevert(state.name, parameters)) {
emitEventAndAttemptStateChange(finalDestinationStateName, parameters)
} else {
} else if (finalDestinationStateName !== state.name) {
Mtn-View marked this conversation as resolved.
Show resolved Hide resolved
// There are default child states that need to be applied

const theRouteWeNeedToEndUpAt = makePath(finalDestinationStateName, parameters)
const currentRoute = router.location.get()

if (theRouteWeNeedToEndUpAt === currentRoute) {
// the child state has the same route as the current one, just start navigating there
emitEventAndAttemptStateChange(finalDestinationStateName, parameters)
} else {
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)) {
// the child state has the same route as the current one, just start navigating there
emitEventAndAttemptStateChange(finalDestinationStateName, parameters)
}
}
} catch (err) {
Expand Down Expand Up @@ -337,12 +382,12 @@ module.exports = function StateProvider(makeRenderer, rootElement, stateRouterOp
stateProviderEmitter.stateIsActive = (stateName = null, parameters = null) => {
const currentState = lastCompletelyLoadedState.get()
const stateNameMatches = currentState.name === stateName
|| currentState.name.indexOf(stateName + `.`) === 0
|| currentState.name.indexOf(`${stateName }.`) === 0
|| stateName === null
const parametersWereNotPassedIn = !parameters

return stateNameMatches
&& (parametersWereNotPassedIn || Object.keys(parameters).every(key => parameters[key] + `` === currentState.parameters[key]))
&& (parametersWereNotPassedIn || Object.keys(parameters).every(key => `${ parameters[key] }` === currentState.parameters[key]))
}

const renderer = makeRenderer(stateProviderEmitter)
Expand Down
14 changes: 8 additions & 6 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,19 @@ Want to use the abstract-state-router without messing with bundlers or package m
# API

- [Instantiate](#instantiate)
- [`options`](#options)
- [`options`](#options)
- [`addState`](#staterouteraddstatename-route-defaultchild-data-template-resolve-activate-querystringparameters-defaultparameters)
- [`resolve`](#resolvedata-parameters-callbackerr-contentredirectstatename-stateparameters)
- [`activate`](#activatecontext)
- [Examples](#addstate-examples)
- [`resolve`](#resolvedata-parameters-callbackerr-contentredirectstatename-stateparameters)
- [`activate`](#activatecontext)
- [Examples](#addstate-examples)
- [`go`](#stateroutergostatename-stateparameters-options)
- [`evaluateCurrentRoute`](#staterouterevaluatecurrentroutefallbackstatename-fallbackstateparameters)
- [`stateIsActive`](#staterouterstateisactivestatename-stateparameters)
- [`makePath`](#stateroutermakepathstatename-stateparameters-options)
- [`getActiveState`](#stateroutergetactivestate)
- [Events](#events)
- [State change](#state-change)
- [DOM API interactions](#dom-api-interactions)
- [State change](#state-change)
- [DOM API interactions](#dom-api-interactions)

## Instantiate

Expand Down Expand Up @@ -115,6 +115,8 @@ If the viewer navigates to a state that has a default child, the router will red

For backwards compatibility reasons, `defaultQuerystringParameters` will work as well (though it does not function any differently).

`canLeaveState` is an optional function with the state's domApi as its sole argument. If it returns `false`, navigation from the state will be prevented. If it is returns `true` or is left `undefined`, state changes will not be prevented.

### resolve(data, parameters, callback(err, content).redirect(stateName, [stateParameters]))

`data` is the data object you passed to the addState call. `parameters` is an object containing the parameters that were parsed out of the route and the query string.
Expand Down
Loading