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

[WIP] Add event beforeRouteChange to confirm whether or not we want to change routes #320

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
27 changes: 27 additions & 0 deletions Router.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ export interface RouteDetailLoaded extends RouteDetail {
name: string
}

/** Detail object for the `beforeRouteChange` event */
export interface BeforeRouteChange {
/** Location we would be leaving */
from: Location
/** Location we would be going to. This is may undefined in some cases, like if `pop` is used programmatically.*/
to?: Location

/** Would this event replace history? If false, it would push a new one, or alternatively go back in the history */
replace: boolean
}

/**
* This is a Svelte component loaded asynchronously.
* It's meant to be used with the `import()` function, such as `() => import('Foo.svelte')}`
Expand Down Expand Up @@ -200,6 +211,22 @@ export default class Router extends SvelteComponent {
* and scroll to top on forward navigation.
*/
restoreScrollState?: boolean
/**
* This function will be called before any attempt to go to another route.
*
* If it returns false, then the route change is fully cancelled, and the current route and location
* stays the same. If it returns true, then the route change proceeds as usual.
*
* By default, it returns a Promise of `true` immediatly, but you can adapt it to return false in certain
* scenarios where you do not want the user to leave from a page or go to a page.
*
* # Note
*
* It will **not** catch any history event manually sent by the browser, such as when the user clicks on
* the "back" button. `window.onpopstate` might be of some use for those kinds of cases, although
* it is limited.
*/
beforeRouteChange?: (routeChange: BeforeRouteChange) => Promise<boolean>,
}

$on(event: 'routeEvent', callback: (event: CustomEvent) => void): () => void
Expand Down
76 changes: 57 additions & 19 deletions Router.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@ import {tick} from 'svelte'
/**
* Returns the current location from the hash.
*
* @param {string} queriedHref - The href we want to location of. By default, the current href of the page.
* @returns {Location} Location object
* @private
*/
function getLocation() {
const hashPosition = window.location.href.indexOf('#/')
let location = (hashPosition > -1) ? window.location.href.substr(hashPosition + 1) : '/'
function getLocation(queriedHref) {
const href = queriedHref != undefined ? queriedHref : window.location.href
const hashPosition = href.indexOf('#/')
let location = (hashPosition > -1) ? href.substr(hashPosition + 1) : '/'

// Check if there's a querystring
const qsPosition = location.indexOf('?')
Expand Down Expand Up @@ -82,12 +84,8 @@ export async function push(location) {
throw Error('Invalid parameter location')
}

// Execute this code when the current call stack is complete
await tick()

// Note: this will include scroll state in history even when restoreScrollState is false
history.replaceState({...history.state, __svelte_spa_router_scrollX: window.scrollX, __svelte_spa_router_scrollY: window.scrollY}, undefined)
window.location.hash = (location.charAt(0) == '#' ? '' : '#') + location
const nextHref = (location.charAt(0) == '#' ? '' : '#') + location
scrollstateHistoryHandler(nextHref)
}

/**
Expand All @@ -96,10 +94,18 @@ export async function push(location) {
* @return {Promise<void>} Promise that resolves after the page navigation has completed
*/
export async function pop() {
// Execute this code when the current call stack is complete
await tick()
const currLocation = getLocation()
const beforeRouteChangePayload = {
from: currLocation,
to: undefined,
replace: false,
}
if (await beforeRouteChange(beforeRouteChangePayload)) {
// Execute this code when the current call stack is complete
await tick()

window.history.back()
window.history.back()
}
}

/**
Expand All @@ -113,10 +119,20 @@ export async function replace(location) {
throw Error('Invalid parameter location')
}

const dest = (location.charAt(0) == '#' ? '' : '#') + location

const beforeRouteChangePayload = {
from: getLocation(),
to: getLocation(dest),
replace: true
}
const canChangeRoute = await beforeRouteChange(beforeRouteChangePayload)
if (!canChangeRoute) {
return
}

// Execute this code when the current call stack is complete
await tick()

const dest = (location.charAt(0) == '#' ? '' : '#') + location
try {
const newState = {
...history.state
Expand Down Expand Up @@ -228,11 +244,23 @@ function linkOpts(val) {
*
* @param {string} href - Destination
*/
function scrollstateHistoryHandler(href) {
// Setting the url (3rd arg) to href will break clicking for reasons, so don't try to do that
history.replaceState({...history.state, __svelte_spa_router_scrollX: window.scrollX, __svelte_spa_router_scrollY: window.scrollY}, undefined)
// This will force an update as desired, but this time our scroll state will be attached
window.location.hash = href
async function scrollstateHistoryHandler(href) {
const currLocation = getLocation()
const nextLocation = getLocation(href)
const beforeRouteChangePayload = {
from: currLocation,
to: nextLocation,
replace: false
}
if (await beforeRouteChange(beforeRouteChangePayload)) {
// Execute this code when the current call stack is complete
await tick()

// Setting the url (3rd arg) to href will break clicking for reasons, so don't try to do that
history.replaceState({...history.state, __svelte_spa_router_scrollX: window.scrollX, __svelte_spa_router_scrollY: window.scrollY}, undefined)
// This will force an update as desired, but this time our scroll state will be attached
window.location.hash = href
}
}
</script>

Expand Down Expand Up @@ -283,6 +311,16 @@ export let prefix = ''
*/
export let restoreScrollState = false

/**
* This function will be called before any attempt to go to another route.
*
* If it returns false, then the route change is fully cancelled, and the current route and location stays the same.
* If it returns true, then the route change proceeds as usual.
*
* By default it returns a Promise of true immediatly, but you can adapt it to return false in certain scenarios.
*/
export let beforeRouteChange = () => Promise.resolve(true)

/**
* Container for a route: path, component
*/
Expand Down
6 changes: 6 additions & 0 deletions test/app/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
on:routeLoaded={routeLoaded}
on:routeLoading={routeLoading}
on:routeEvent={routeEvent}
{beforeChangeRoute}
{restoreScrollState}
/>

Expand Down Expand Up @@ -113,6 +114,11 @@ function conditionsFailed(event) {
replace('/wild/conditions-failed')
}

function beforeChangeRoute(beforeChangeRoutePayload) {
console.log(beforeChangeRoutePayload)
return Promise.resolve(true)
}

// Handles the "routeLoaded" event dispatched by the router after a route has been successfully loaded
function routeLoaded(event) {
// eslint-disable-next-line no-console
Expand Down