/**
*
* ## A simple client router
*
* The router allows to navigate between different views in a single-page application.
* It also works with local files paths (file:///)
*
* The router is based on the url hash, for example:
* ```
* /index.html#ui=homepage
* ```
*
* The "ui" parameter is mandatory and represents the main view to display.
* Other parameters can be added to the url hash to manage deeper navigation:
* ```
* /index.html#ui=homepage&applicationId=123&viewId=456
* ```
*
* If you need to display multiple views simultaneously, you can use multiple parameters starting with "ui":
* ```
* /index.html#ui=homepage&ui1=map&ui2=account
* ```
*
* To use the router:
* ```
* kiss.router.navigateTo(newRoute)
* ```
*
* You can pass a single string if you just want to change the main view:
* ```
* kiss.router.navigateTo("homepage")
* ```
*
* This is equivalent to:
* ```
* kiss.router.navigateTo({ui: "homepage"})
* ```
*
* If you need deeper navigation, you can pass an object:
* ```
* kiss.router.navigateTo({ui: "homepage", applicationId: "123", viewId: "456"})
* ```
*
* The router observes url hash changes and automatically triggers new routes accordingly.
*
* When initializing the router, you can optionally define public routes:
*
* ```
* // Init your app router:
* kiss.router.init({
* publicRoutes: ["login", "register"]
* })
*
* // Setting public routes after initialization:
* kiss.router.setPublicRoutes(["login", "register"])
*
* // Adding routing guards, to check if a route is valid before routing:
* kiss.router.addRoutingGuards([
* async function(newRoute) {
* return await app.api.checkViewAuthorization(newRoute)
* }
* ])
*
* // Adding routing actions, to perform some actions after routing:
* kiss.router.addRoutingActions([
* async function() {
* // Do something after routing
* }
* ])
*
* // Navigating to a new route:
* kiss.router.navigateTo({ui: "homepage", applicationId: "123", viewId: "456"})
*
* // Get the current application route by reading the url hash:
* const currentApplicationRoute = kiss.router.getRoute() // {ui: "homepage", applicationId: "123", viewId: "456"}
* ```
*
* @namespace
*
*/
kiss.router = {
/**
* Default list of public routes which doesn't require authentication.
*
* Add custom public routes using addPublicRoutes([...]) method.
*
* By default, the following routes are public:
* - authentication-login
* - authentication-register
* - authentication-reset-password
* - authentication-error
*/
publicRoutes: [
"authentication-login",
"authentication-register",
"authentication-reset-password",
"authentication-error"
],
/**
* Init the router
*
* It will observe any url hash change, which will:
* - perform a custom action before triggering the new route
* - perform a custom action after the routing
*
* @param {object} config - The router config, containing the 2 methods:
* @param {string[]} [config.publicRoutes] - Define public routes (skip login)
*/
init(config = {}) {
// Set public routes
if (config.publicRoutes && Array.isArray(config.publicRoutes)) {
kiss.router.publicRoutes = config.publicRoutes
}
// Observe hash changes
window.onhashchange = async function () {
// Update the application context
const newRoute = kiss.router.getRoute()
// Perform verifications before routing
const doRoute = await kiss.router._beforeRouting(newRoute)
if (!doRoute) return
kiss.context.update(newRoute)
// Execute router actions after routing
await kiss.router._afterRouting()
}
},
/**
* Set the public routes
*
* @param {string[]} publicRoutes
*/
setPublicRoutes(publicRoutes) {
kiss.router.publicRoutes = publicRoutes
},
/**
* Add some public routes
*
* @param {string[]} publicRoutes
*/
addPublicRoutes(publicRoutes) {
kiss.router.publicRoutes = kiss.router.publicRoutes.concat(publicRoutes).unique()
},
/**
* Add routing guards
*
* Guards are used to check if a route is valid **before** routing.
* Must be an array of async functions where each function must return a boolean.
* The route is accepted if all guards return true.
*
* @param {function[]} guards
*/
addRoutingGuards(guards) {
if (guards && Array.isArray(guards)) {
kiss.router.beforeRoutingGuards = kiss.router.beforeRoutingGuards.concat(guards)
}
},
/**
* Add routing actions
*
* Actions are used to perform some actions **after** routing.
* Must be an array of async functions to execute sequentially.
*
* @param {function[]} actions
*/
addRoutingActions(actions) {
if (actions && Array.isArray(actions)) {
kiss.router.afterRoutingActions = kiss.router.afterRoutingActions.concat(actions)
}
},
/**
* Check if the current route (given by the ui parameter) is public
*
* @returns {boolean}
*/
isPublicRoute() {
const currentRoute = kiss.router.getRoute().ui
if (!currentRoute) return false
return kiss.router.publicRoutes.includes(currentRoute)
},
/**
* Navigate to a new hash
* It indirectly triggers the new route by dispatching the window's *hashchange* event.
*
* @param {object|string} newRoute
* @param {boolean} [reset] - Set to true to reset the previous route before routing to a new one
*
* @example
* // Using an object
* const newRoute = {ui: "homepage", applicationId: "123", viewId: "456"}
* kiss.router.navigateTo(newRoute)
*
* // Using a string
* kiss.router.navigateTo("home-start") // Is equivalent to: kiss.router.navigateTo({ui: "home-start"})
*/
async navigateTo(newRoute, reset) {
if (typeof newRoute === "string") newRoute = {
ui: newRoute
}
kiss.router.updateUrlHash(newRoute, reset)
// Perform verifications before routing
// The routing can be interrupted if the method beforeRouting returns false
// const doRoute = await kiss.router._beforeRouting(newRoute)
// if (!doRoute) return
// Propagate the hash change
window.dispatchEvent(new HashChangeEvent("hashchange"))
},
/**
* Get the current application route from the url hash.
*
* For example:
* - if current url is: http://.../...#ui=homepage&applicationId=123&viewId=456
* - the output is: {ui: "homepage", applicationId: "123", viewId: "456"}
*
* @returns {object}
*/
getRoute() {
return kiss.router._toRoute(window.location.hash.slice(1))
},
/**
* Update URL hash according to new route params.
*
* @param {object} newRoute
* @param {boolean} [reset] - True to reset the current hash
*
* @example
* kiss.router.updateUrlHash({chapter: 10, section: 2}, true)
*/
updateUrlHash(newRoute, reset) {
const currentRoute = kiss.router.getRoute()
const toRoute = (reset) ? newRoute : Object.assign(currentRoute, newRoute)
const newHash = "#" + kiss.router._toHash(toRoute)
window.history.pushState(toRoute, toRoute.ui, newHash)
},
/**
* Convert a url hash into an application route object.
*
* For example:
* input: http://.../...#ui=homepage&applicationId=123&viewId=456
* output: {ui: "homepage", applicationId: "123", viewId: "456"}
*
* @private
* @ignore
* @param {string} hash
*
* @returns {object} Object containing the application route
*/
_toRoute(hash) {
const route = {}
hash.split("&").forEach(param => {
const paramName = param.split("=")[0]
if (paramName) route[paramName] = param.split("=")[1]
})
return route
},
/**
* Convert an application route into an url hash
*
* @private
* @ignore
* @param {object} newRoute - The application route
* @returns {string} An url hash
*
* @example
* kiss.router._toHash({ui: "homepage", applicationId: "123", viewId: "456"})
* // URL hash will be: ui=homepage&applicationId=123&viewId=456
*/
_toHash(newRoute) {
const hash = []
Object.keys(newRoute).forEach(key => (newRoute[key]) ? hash.push(key + "=" + newRoute[key]) : "")
return hash.join("&")
},
/**
* Perform some validations before routing.
*
* Validations are defined in kiss.router.validators and can be customized at initialization.
* Standard validation process is:
* - check if the new route is the login page (always accepted)
* - check if the new route is a public route (accepted if true)
* - check if the application is properly loaded (routing accepted if true)
*
* @private
* @ignore
* @param {object} newRoute - Intended application route
* @returns {promise} Resolve to false if the routing must be interrupted for any reason
*/
async _beforeRouting(newRoute) {
// Always accept the login page
if (kiss.router._isLoginPage(newRoute)) return true
// Accept public routes
if (kiss.router._isPublicRoute(newRoute)) return true
// Check if the application is properly loaded
const appLoaded = await kiss.router._isAppLoaded()
if (!appLoaded) return false
// Check if the route is authorized
for (let guardFunction of kiss.router.beforeRoutingGuards) {
const guardResult = await guardFunction(newRoute)
if (!guardResult) return false
}
return true
},
/**
* Perform some actions *after* routing.
*
* Actions are defined in kiss.router.actions and can be customized at initialization.
*
* @private
* @ignore
*/
async _afterRouting() {
for (let actionFunction of kiss.router.afterRoutingActions) {
await actionFunction()
}
},
/**
* Check if the new route is the login page
*
* @private
* @ignore
* @param {object} newRoute
* @returns {boolean} True if the new route is the login page
*
* @example
* kiss.router.validators.isLoginPage({ui: "homepage", applicationId: "123", viewId: "456"}) // false
* kiss.router.validators.isLoginPage({ui: "authentication-login"}) // true
*/
_isLoginPage(newRoute) {
if (newRoute.ui == kiss.session.defaultViews.login) return true
return false
},
/**
* Check if the new route is a public route
*
* @private
* @ignore
* @param {object} newRoute
* @returns {boolean} True if the new route is a public route
*
* @example
* kiss.router.validators.isPublicRoute({ui: "homepage", applicationId: "123", viewId: "456"}) // false
* kiss.router.validators.isPublicRoute({ui: "authentication-login"}) // true
*/
_isPublicRoute(newRoute) {
if (kiss.router.publicRoutes.indexOf(newRoute.ui) != -1) return true
return false
},
/**
* Check if the application is properly loaded.
*
* "Properly" means:
* - required core data is loaded
* - custom application data is loaded (depends on the use case)
*
* Custom data is loaded using a "loader" function defined at initialization with kiss.app.init({...}).
* The loader:
* - is a function that must be fully completed before the routing can occur
* - will be waited for completion if it's async
* - must return a boolean to indicate if the required elements are properly loaded
*
* Once loaded, kiss.app.isLoaded is flagged and the routing can occur.
*
* @private
* @ignore
* @returns {boolean} True if the application is properly loaded
*
* @example
* kiss.router.validators.isAppLoaded() // true
*/
async _isAppLoaded() {
if (!kiss.app.isLoaded) {
// Load core data
let success = await kiss.app.load()
if (!success) return false
// Optionally Load custom data defined when initializing the app, using kiss.app.init({...})
if (kiss.app.loader && typeof kiss.app.loader === "function") {
success = await kiss.app.loader()
if (!success) return false
}
kiss.app.isLoaded = true
}
return true
},
/**
* Optional validations to perform before routing:
* - It's an array of async functions where each function must return a boolean.
* - Must be set up at initialization.
* - The route is accepted if all validators return true.
* - Can be extended at runtime using kiss.router.addRoutingGuards([...])
*
* @private
* @ignore
*/
beforeRoutingGuards: [],
/**
* Default actions to perform after routing.
*
* Can be extended with new actions, using kiss.router.addActions([...])
*
* @private
* @ignore
*/
afterRoutingActions: [
/**
* Default action performed *after* routing.
* By default, it checks the new application route and displays a new view according to the *ui* parameter.
* It can display multiple views simultaneously, using multiple parameters starting with "ui".
*
* @private
* @ignore
*/
async function () {
const newRoute = kiss.router.getRoute()
// Display a new main view if there is a *ui* parameter
// (the main view is "exclusive" to other views in the same container)
if (newRoute.ui) await kiss.views.show(newRoute.ui, null, true)
// Display other views using all parameters starting with "ui" (ui1, ui2, uiMap, uiAccount, etc...)
// This allows, for example, to open secondary windows / popup / information messages...
for (let route of Object.keys(newRoute)) {
if (route.startsWith("ui") && route != "ui") await kiss.views.show(newRoute[route])
}
// Publish the new route
kiss.pubsub.publish("EVT_ROUTE_UPDATED", newRoute)
}
]
}
;
Source