Skip to content

[feat] beforeNavigate and afterNavigate lifecycle functions #3293

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

Merged
merged 92 commits into from
Jan 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
7496981
feature/add-onBeforeNavigate-listner
zommerberg Dec 4, 2021
3598ff0
minor fixes
zommerberg Dec 4, 2021
d5e665b
naming
zommerberg Dec 5, 2021
83e145b
minor naming changs
zommerberg Dec 5, 2021
667ae97
minor improvements
zommerberg Dec 5, 2021
a6cba14
implement requested changes
zommerberg Dec 8, 2021
e8fff7f
add newline
zommerberg Dec 8, 2021
a521968
newline fix
zommerberg Dec 8, 2021
c19f3cd
docs fixes
zommerberg Dec 8, 2021
73f8d2b
docs fixes #2
zommerberg Dec 8, 2021
32f305e
docs fixes #2
zommerberg Dec 8, 2021
c0a409b
docs spacing
zommerberg Dec 8, 2021
4d54105
minor docs fix
zommerberg Dec 8, 2021
9014c14
speeling fix
zommerberg Dec 8, 2021
a2e46ef
remove if(browser) check
zommerberg Dec 10, 2021
dc3773d
types
zommerberg Dec 10, 2021
7c9def1
snake_case
zommerberg Dec 12, 2021
6055ae7
snake case #2
zommerberg Dec 12, 2021
0c53d10
naming
zommerberg Dec 12, 2021
6c909d4
Update documentation/docs/05-modules.md
zommerberg Dec 13, 2021
3864e40
Update pnpm-lock.yaml
zommerberg Dec 13, 2021
e4c56e1
Merge branch 'sveltejs:master' into master
zommerberg Dec 13, 2021
4d71414
add onNavigate method
zommerberg Dec 13, 2021
714aad1
Merge branch 'sveltejs:master' into master
zommerberg Dec 14, 2021
6ec847a
Merge branch 'sveltejs:master' into master
zommerberg Dec 16, 2021
79e35d6
Merge branch 'sveltejs:master' into master
zommerberg Dec 22, 2021
4d7c34f
Update packages/kit/src/runtime/client/router.js
zommerberg Dec 23, 2021
406d43a
Update packages/kit/test/test.js
zommerberg Dec 23, 2021
a795d78
minor naming and types improvements
zommerberg Dec 23, 2021
0e168e0
add docs
zommerberg Dec 23, 2021
d949133
mroe docs fixes
zommerberg Dec 23, 2021
41c2c56
snake case
zommerberg Dec 23, 2021
42f280d
improve back/forwar browser handling
zommerberg Dec 24, 2021
15f97ba
improve sveltekit:index
zommerberg Dec 24, 2021
a0f3b2f
remove custom event, add callbacks.
zommerberg Dec 24, 2021
9da5934
typo fix
zommerberg Dec 24, 2021
eafddd4
docs
zommerberg Dec 24, 2021
0709fad
remove logs
zommerberg Dec 24, 2021
34cc46e
namig
zommerberg Dec 24, 2021
35af505
Merge branch 'sveltejs:master' into master
zommerberg Dec 26, 2021
da3ad4b
Update packages/kit/src/runtime/client/router.js
zommerberg Dec 28, 2021
7a63543
fixes
zommerberg Dec 28, 2021
9f78e51
improve types
zommerberg Dec 28, 2021
460bb3a
nicer format
zommerberg Dec 28, 2021
90d7689
Added second parameter to `goto` function in tests
PatrickG Dec 28, 2021
0bfa83c
Fixed `history.state` when fixing trailing slash
PatrickG Dec 28, 2021
e5c2056
Added tests for `onBeforeNavigate`
PatrickG Dec 28, 2021
37cf894
Merge pull request #1 from PatrickG/master
zommerberg Dec 28, 2021
efb271f
lints
zommerberg Dec 28, 2021
2ff3e9b
Merge branch 'sveltejs:master' into master
zommerberg Dec 28, 2021
bee8487
add more tests
zommerberg Dec 28, 2021
e679bb4
Merge branch 'sveltejs:master' into master
zommerberg Dec 28, 2021
4a9a355
Merge branch 'sveltejs:master' into master
zommerberg Dec 29, 2021
c86cac6
Merge branch 'sveltejs:master' into master
zommerberg Dec 30, 2021
656e7de
reafactor
zommerberg Jan 2, 2022
a8be2d6
fix if statement
zommerberg Jan 2, 2022
eea4279
fix if statement 2
zommerberg Jan 2, 2022
d482e87
merge master -> zommerberg/master
Rich-Harris Jan 11, 2022
bcfcef3
remove unused files
Rich-Harris Jan 11, 2022
d039816
fix test
Rich-Harris Jan 11, 2022
93e548d
patch changeset
ignatiusmb Jan 12, 2022
a45457d
rename to beforeNavigate/afterNavigate
Rich-Harris Jan 12, 2022
c8909f9
Merge branch 'on-navigate' of github.com:sveltejs/kit into on-navigate
Rich-Harris Jan 12, 2022
f075928
typo
Rich-Harris Jan 12, 2022
ff6c578
make beforeNavigate callbacks synchronous
Rich-Harris Jan 12, 2022
8033edc
merge master -> on-navigate
Rich-Harris Jan 12, 2022
f49d2a4
rename internal functions
Rich-Harris Jan 12, 2022
6eff260
make _navigate take non-position arguments. bit clearer
Rich-Harris Jan 12, 2022
8696c4e
remove sveltekit:index tests - this is non-user-facing implementation…
Rich-Harris Jan 12, 2022
998b5f2
typo
Rich-Harris Jan 12, 2022
d7fbc8a
remove more sveltekit:index stuff
Rich-Harris Jan 12, 2022
000c0d1
separate out beforeNavigate tests
Rich-Harris Jan 12, 2022
7d2ea34
simplify tests
Rich-Harris Jan 12, 2022
4ad899f
rename tests
Rich-Harris Jan 12, 2022
720cc22
handle no-router case
Rich-Harris Jan 12, 2022
8c122ed
simplify some stuff
Rich-Harris Jan 12, 2022
8234ce2
only pass scroll/keepfocus to renderer
Rich-Harris Jan 12, 2022
fd92318
focus beforeNavigate tests
Rich-Harris Jan 12, 2022
d73a87f
manage history index inside _navigate
Rich-Harris Jan 12, 2022
045d12d
neaten up
Rich-Harris Jan 12, 2022
7c67a17
allow _navigate to handle external links; centralise logic
Rich-Harris Jan 12, 2022
069627b
pass { from, to, cancel } to beforeNavigate callbacks
Rich-Harris Jan 12, 2022
07bd649
lint
Rich-Harris Jan 12, 2022
1768573
block unload if appropriate
Rich-Harris Jan 12, 2022
dba49b8
make goto work with external urls
Rich-Harris Jan 12, 2022
c58b06c
unfocus tests
Rich-Harris Jan 12, 2022
1995470
typechecking
Rich-Harris Jan 12, 2022
70810b3
no longer needs to be async
Rich-Harris Jan 12, 2022
23a6e89
update docs
Rich-Harris Jan 12, 2022
652ceea
call afterNavigate callbacks on page load
Rich-Harris Jan 13, 2022
716c95e
typechecking
Rich-Harris Jan 13, 2022
c3948aa
Update documentation/docs/05-modules.md
Rich-Harris Jan 13, 2022
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
5 changes: 5 additions & 0 deletions .changeset/hot-dogs-fry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': patch
---

Adds beforeNavigate/afterNavigate lifecycle functions
12 changes: 11 additions & 1 deletion documentation/docs/05-modules.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,19 @@ import { amp, browser, dev, mode, prerendering } from '$app/env';
### $app/navigation

```js
import { disableScrollHandling, goto, invalidate, prefetch, prefetchRoutes } from '$app/navigation';
import {
disableScrollHandling,
goto,
invalidate,
prefetch,
prefetchRoutes,
beforeNavigate,
afterNavigate
} from '$app/navigation';
```

- `afterNavigate(({ from, to }: { from: URL, to: URL }) => void)` - a lifecycle function that runs when the components mounts, and after subsequent navigations while the component remains mounted
- `beforeNavigate(({ from, to, cancel }: { from: URL, to: URL | null, cancel: () => void }) => void)` — a function that runs whenever navigation is triggered whether by clicking a link, calling `goto`, or using the browser back/forward controls. This includes navigation to external sites. `to` will be `null` if the user is closing the page. Calling `cancel` will prevent the navigation from proceeding
- `disableScrollHandling` will, if called when the page is being updated following a navigation (in `onMount` or an action, for example), prevent SvelteKit from applying its normal scroll management. You should generally avoid this, as breaking user expectations of scroll behaviour can be disorienting.
- `goto(href, { replaceState, noscroll, keepfocus, state })` returns a `Promise` that resolves when SvelteKit navigates (or fails to navigate, in which case the promise rejects) to the specified `href`. The second argument is optional:
- `replaceState` (boolean, default `false`) If `true`, will replace the current `history` entry rather than creating a new one with `pushState`
Expand Down
4 changes: 3 additions & 1 deletion packages/kit/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
"goto": true,
"invalidate": true,
"prefetch": true,
"prefetchRoutes": true
"prefetchRoutes": true,
"beforeNavigate": true,
"afterNavigate": true
}
}
16 changes: 16 additions & 0 deletions packages/kit/src/runtime/app/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export const goto = import.meta.env.SSR ? guard('goto') : goto_;
export const invalidate = import.meta.env.SSR ? guard('invalidate') : invalidate_;
export const prefetch = import.meta.env.SSR ? guard('prefetch') : prefetch_;
export const prefetchRoutes = import.meta.env.SSR ? guard('prefetchRoutes') : prefetchRoutes_;
export const beforeNavigate = import.meta.env.SSR ? () => {} : beforeNavigate_;
export const afterNavigate = import.meta.env.SSR ? () => {} : afterNavigate_;

/**
* @type {import('$app/navigation').goto}
Expand Down Expand Up @@ -61,3 +63,17 @@ async function prefetchRoutes_(pathnames) {

await Promise.all(promises);
}

/**
* @type {import('$app/navigation').beforeNavigate}
*/
function beforeNavigate_(fn) {
if (router) router.before_navigate(fn);
}

/**
* @type {import('$app/navigation').afterNavigate}
*/
function afterNavigate_(fn) {
if (router) router.after_navigate(fn);
}
9 changes: 7 additions & 2 deletions packages/kit/src/runtime/client/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,7 @@ export class Renderer {

// opts must be passed if we're navigating
if (opts) {
const { hash, scroll, keepfocus } = opts;
const { scroll, keepfocus } = opts;

if (!keepfocus) {
getSelection()?.removeAllRanges();
Expand All @@ -302,7 +302,7 @@ export class Renderer {
await tick();

if (this.autoscroll) {
const deep_linked = hash && document.getElementById(hash.slice(1));
const deep_linked = info.url.hash && document.getElementById(info.url.hash.slice(1));
if (scroll) {
scrollTo(scroll.x, scroll.y);
} else if (deep_linked) {
Expand Down Expand Up @@ -378,6 +378,11 @@ export class Renderer {
});

this.started = true;

if (this.router) {
const navigation = { from: null, to: new URL(location.href) };
this.router.callbacks.after_navigate.forEach((fn) => fn(navigation));
}
}

/**
Expand Down
184 changes: 152 additions & 32 deletions packages/kit/src/runtime/client/router.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { onMount } from 'svelte';
import { get_base_uri } from './utils';

function scroll_state() {
Expand Down Expand Up @@ -53,8 +54,21 @@ export class Router {
// make it possible to reset focus
document.body.setAttribute('tabindex', '-1');

// create initial history entry, so we can return here
history.replaceState(history.state || {}, '', location.href);
// keeping track of the history index in order to prevent popstate navigation events if needed
this.current_history_index = history.state?.['sveltekit:index'] ?? 0;

if (this.current_history_index === 0) {
// create initial history entry, so we can return here
history.replaceState({ ...history.state, 'sveltekit:index': 0 }, '', location.href);
}

this.callbacks = {
/** @type {Array<({ from, to, cancel }: { from: URL, to: URL | null, cancel: () => void }) => void>} */
before_navigate: [],

/** @type {Array<({ from, to }: { from: URL | null, to: URL }) => void>} */
after_navigate: []
};
}

init_listeners() {
Expand All @@ -66,8 +80,23 @@ export class Router {
// Reset scrollRestoration to auto when leaving page, allowing page reload
// and back-navigation from other pages to use the browser to restore the
// scrolling position.
addEventListener('beforeunload', () => {
history.scrollRestoration = 'auto';
addEventListener('beforeunload', (e) => {
let should_block = false;

const intent = {
from: this.renderer.current.url,
to: null,
cancel: () => (should_block = true)
};

this.callbacks.before_navigate.forEach((fn) => fn(intent));

if (should_block) {
e.preventDefault();
e.returnValue = '';
} else {
history.scrollRestoration = 'auto';
}
});

// Setting scrollRestoration to manual again when returning to this page.
Expand Down Expand Up @@ -122,7 +151,7 @@ export class Router {
addEventListener('sveltekit:trigger_prefetch', trigger_prefetch);

/** @param {MouseEvent} event */
addEventListener('click', (event) => {
addEventListener('click', async (event) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can remove this async

if (!this.enabled) return;

// Adapted from https://github.com/visionmedia/page.js
Expand Down Expand Up @@ -155,8 +184,6 @@ export class Router {
// Ignore if <a> has a target
if (a instanceof SVGAElement ? a.target.baseVal : a.target) return;

if (!this.owns(url)) return;

// Check if new url only differs by hash
if (url.href.split('#')[0] === location.href.split('#')[0]) {
// Call `pushState` to add url to history so going back works.
Expand All @@ -169,20 +196,48 @@ export class Router {
return;
}

const noscroll = a.hasAttribute('sveltekit:noscroll');
this._navigate(url, noscroll ? scroll_state() : null, false, [], url.hash, {}, 'pushState');
event.preventDefault();
this._navigate({
url,
scroll: a.hasAttribute('sveltekit:noscroll') ? scroll_state() : null,
keepfocus: false,
chain: [],
details: {
state: {},
replaceState: false
},
accepted: () => event.preventDefault(),
blocked: () => event.preventDefault()
});
});

addEventListener('popstate', (event) => {
if (event.state && this.enabled) {
const url = new URL(location.href);
this._navigate(url, event.state['sveltekit:scroll'], false, [], url.hash, null, null);
// if a popstate-driven navigation is cancelled, we need to counteract it
// with history.go, which means we end up back here, hence this check
if (event.state['sveltekit:index'] === this.current_history_index) return;

this._navigate({
url: new URL(location.href),
scroll: event.state['sveltekit:scroll'],
keepfocus: false,
chain: [],
details: null,
accepted: () => {
this.current_history_index = event.state['sveltekit:index'];
},
blocked: () => {
const delta = this.current_history_index - event.state['sveltekit:index'];
history.go(delta);
}
});
}
});
}

/** @param {URL} url */
/**
* Returns true if `url` has the same origin and basepath as the app
* @param {URL} url
*/
owns(url) {
return url.origin === location.origin && url.pathname.startsWith(this.base);
}
Expand Down Expand Up @@ -218,16 +273,19 @@ export class Router {
) {
const url = new URL(href, get_base_uri(document));

if (this.enabled && this.owns(url)) {
return this._navigate(
if (this.enabled) {
return this._navigate({
url,
noscroll ? scroll_state() : null,
scroll: noscroll ? scroll_state() : null,
keepfocus,
chain,
url.hash,
state,
replaceState ? 'replaceState' : 'pushState'
);
details: {
state,
replaceState
},
accepted: () => {},
blocked: () => {}
});
}

location.href = url.href;
Expand Down Expand Up @@ -258,22 +316,73 @@ export class Router {
return this.renderer.load(info);
}

/** @param {({ from, to }: { from: URL | null, to: URL }) => void} fn */
after_navigate(fn) {
onMount(() => {
this.callbacks.after_navigate.push(fn);

return () => {
const i = this.callbacks.after_navigate.indexOf(fn);
this.callbacks.after_navigate.splice(i, 1);
};
});
}

/**
* @param {URL} url
* @param {{ x: number, y: number }?} scroll
* @param {boolean} keepfocus
* @param {string[]} chain
* @param {string} hash
* @param {any} state
* @param {'pushState' | 'replaceState' | null} method
* @param {({ from, to, cancel }: { from: URL, to: URL | null, cancel: () => void }) => void} fn
*/
async _navigate(url, scroll, keepfocus, chain, hash, state, method) {
const info = this.parse(url);
before_navigate(fn) {
onMount(() => {
this.callbacks.before_navigate.push(fn);

return () => {
const i = this.callbacks.before_navigate.indexOf(fn);
this.callbacks.before_navigate.splice(i, 1);
};
});
}

/**
* @param {{
* url: URL;
* scroll: { x: number, y: number } | null;
* keepfocus: boolean;
* chain: string[];
* details: {
* replaceState: boolean;
* state: any;
* } | null;
* accepted: () => void;
* blocked: () => void;
* }} opts
*/
async _navigate({ url, scroll, keepfocus, chain, details, accepted, blocked }) {
const from = this.renderer.current.url;
let should_block = false;

const intent = {
from,
to: url,
cancel: () => (should_block = true)
};

this.callbacks.before_navigate.forEach((fn) => fn(intent));

if (should_block) {
blocked();
return;
}

const info = this.parse(url);
if (!info) {
throw new Error('Attempted to navigate to a URL that does not belong to this app');
location.href = url.href;
return new Promise(() => {
// never resolves
});
}

accepted();

if (!this.navigating) {
dispatchEvent(new CustomEvent('sveltekit:navigation-start'));
}
Expand All @@ -289,13 +398,24 @@ export class Router {
}

info.url = new URL(url.origin + pathname + url.search + url.hash);
if (method) history[method](state, '', info.url);

await this.renderer.handle_navigation(info, chain, false, { hash, scroll, keepfocus });
if (details) {
const change = details.replaceState ? 0 : 1;
details.state['sveltekit:index'] = this.current_history_index += change;
history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', info.url);
}

await this.renderer.handle_navigation(info, chain, false, {
scroll,
keepfocus
});

this.navigating--;
if (!this.navigating) {
dispatchEvent(new CustomEvent('sveltekit:navigation-end'));

const navigation = { from, to: url };
this.callbacks.after_navigate.forEach((fn) => fn(navigation));
}
}
}
2 changes: 2 additions & 0 deletions packages/kit/test/ambient.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ declare global {

const invalidate: (url: string) => Promise<void>;
const prefetch: (url: string) => Promise<void>;
const beforeNavigate: (fn: (url: URL) => void | boolean) => void;
const afterNavigate: (fn: () => void) => void;
const prefetchRoutes: (urls?: string[]) => Promise<void>;
}

Expand Down
Loading