-
-
Notifications
You must be signed in to change notification settings - Fork 2k
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
Add onNavigate
lifecycle function, to enable view transitions
#9605
Changes from all commits
a91064e
fb3f63b
a993dcb
46a24bf
e0b6849
79ae0c3
44bd6f8
0b52c89
937675e
70b9825
8a1f528
54b1e46
6b1b4c8
fa16886
02dc70f
f417004
a6aacc0
e96195b
eff8b4a
0e1b593
c68d625
7f868b8
c7c8aa8
5ba3015
4ebf02b
d0e3a74
fa6bc93
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
'@sveltejs/kit': minor | ||
--- | ||
|
||
feat: onNavigate lifecycle function |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -89,6 +89,9 @@ export function create_client(app, target) { | |
/** @type {Array<(navigation: import('@sveltejs/kit').BeforeNavigate) => void>} */ | ||
before_navigate: [], | ||
|
||
/** @type {Array<(navigation: import('@sveltejs/kit').OnNavigate) => import('types').MaybePromise<(() => void) | void>>} */ | ||
on_navigate: [], | ||
|
||
/** @type {Array<(navigation: import('@sveltejs/kit').AfterNavigate) => void>} */ | ||
after_navigate: [] | ||
}; | ||
|
@@ -299,7 +302,8 @@ export function create_client(app, target) { | |
url: new URL(location.href) | ||
}, | ||
willUnload: false, | ||
type: 'enter' | ||
type: 'enter', | ||
complete: Promise.resolve() | ||
}; | ||
callbacks.after_navigate.forEach((fn) => fn(navigation)); | ||
|
||
|
@@ -910,30 +914,17 @@ export function create_client(app, target) { | |
function before_navigate({ url, type, intent, delta }) { | ||
let should_block = false; | ||
|
||
/** @type {import('@sveltejs/kit').Navigation} */ | ||
const navigation = { | ||
from: { | ||
params: current.params, | ||
route: { id: current.route?.id ?? null }, | ||
url: current.url | ||
}, | ||
to: { | ||
params: intent?.params ?? null, | ||
route: { id: intent?.route?.id ?? null }, | ||
url | ||
}, | ||
willUnload: !intent, | ||
type | ||
}; | ||
const nav = create_navigation(current, intent, url, type); | ||
|
||
if (delta !== undefined) { | ||
navigation.delta = delta; | ||
nav.navigation.delta = delta; | ||
} | ||
|
||
const cancellable = { | ||
...navigation, | ||
...nav.navigation, | ||
cancel: () => { | ||
should_block = true; | ||
nav.reject(new Error('navigation was cancelled')); | ||
} | ||
}; | ||
|
||
|
@@ -942,7 +933,7 @@ export function create_client(app, target) { | |
callbacks.before_navigate.forEach((fn) => fn(cancellable)); | ||
} | ||
|
||
return should_block ? null : navigation; | ||
return should_block ? null : nav; | ||
} | ||
|
||
/** | ||
|
@@ -975,9 +966,9 @@ export function create_client(app, target) { | |
blocked | ||
}) { | ||
const intent = get_navigation_intent(url, false); | ||
const navigation = before_navigate({ url, type, delta, intent }); | ||
const nav = before_navigate({ url, type, delta, intent }); | ||
|
||
if (!navigation) { | ||
if (!nav) { | ||
blocked(); | ||
return; | ||
} | ||
|
@@ -990,7 +981,7 @@ export function create_client(app, target) { | |
navigating = true; | ||
|
||
if (started) { | ||
stores.navigating.set(navigation); | ||
stores.navigating.set(nav.navigation); | ||
} | ||
|
||
token = nav_token; | ||
|
@@ -1017,7 +1008,10 @@ export function create_client(app, target) { | |
url = intent?.url || url; | ||
|
||
// abort if user navigated during update | ||
if (token !== nav_token) return false; | ||
if (token !== nav_token) { | ||
nav.reject(new Error('navigation was aborted')); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is getting thrown when updating query params via There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you should open an issue with a reproduction. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be fixed by 1.24.1 (tried to update ?) #10666 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just updated and no more error, thanks |
||
return false; | ||
} | ||
|
||
if (navigation_result.type === 'redirect') { | ||
if (redirect_chain.length > 10 || redirect_chain.includes(url.pathname)) { | ||
|
@@ -1093,6 +1087,28 @@ export function create_client(app, target) { | |
navigation_result.props.page.url = url; | ||
} | ||
|
||
const after_navigate = ( | ||
await Promise.all( | ||
callbacks.on_navigate.map((fn) => | ||
fn(/** @type {import('@sveltejs/kit').OnNavigate} */ (nav.navigation)) | ||
) | ||
) | ||
).filter((value) => typeof value === 'function'); | ||
|
||
if (after_navigate.length > 0) { | ||
function cleanup() { | ||
callbacks.after_navigate = callbacks.after_navigate.filter( | ||
// @ts-ignore | ||
(fn) => !after_navigate.includes(fn) | ||
); | ||
} | ||
|
||
after_navigate.push(cleanup); | ||
|
||
// @ts-ignore | ||
callbacks.after_navigate.push(...after_navigate); | ||
} | ||
|
||
root.$set(navigation_result.props); | ||
} else { | ||
initialize(navigation_result); | ||
|
@@ -1142,8 +1158,10 @@ export function create_client(app, target) { | |
restore_snapshot(current_history_index); | ||
} | ||
|
||
nav.fulfil(undefined); | ||
|
||
callbacks.after_navigate.forEach((fn) => | ||
fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (navigation)) | ||
fn(/** @type {import('@sveltejs/kit').AfterNavigate} */ (nav.navigation)) | ||
); | ||
stores.navigating.set(null); | ||
|
||
|
@@ -1339,6 +1357,17 @@ export function create_client(app, target) { | |
}); | ||
}, | ||
|
||
on_navigate: (fn) => { | ||
onMount(() => { | ||
callbacks.on_navigate.push(fn); | ||
|
||
return () => { | ||
const i = callbacks.on_navigate.indexOf(fn); | ||
callbacks.on_navigate.splice(i, 1); | ||
}; | ||
}); | ||
}, | ||
|
||
disable_scroll_handling: () => { | ||
if (DEV && started && !updating) { | ||
throw new Error('Can only disable scroll handling during navigation'); | ||
|
@@ -1444,19 +1473,17 @@ export function create_client(app, target) { | |
persist_state(); | ||
|
||
if (!navigating) { | ||
const nav = create_navigation(current, undefined, null, 'leave'); | ||
|
||
// If we're navigating, beforeNavigate was already called. If we end up in here during navigation, | ||
// it's due to an external or full-page-reload link, for which we don't want to call the hook again. | ||
/** @type {import('@sveltejs/kit').BeforeNavigate} */ | ||
const navigation = { | ||
from: { | ||
params: current.params, | ||
route: { id: current.route?.id ?? null }, | ||
url: current.url | ||
}, | ||
to: null, | ||
willUnload: true, | ||
type: 'leave', | ||
cancel: () => (should_block = true) | ||
...nav.navigation, | ||
cancel: () => { | ||
should_block = true; | ||
nav.reject(new Error('navigation was cancelled')); | ||
} | ||
}; | ||
|
||
callbacks.before_navigate.forEach((fn) => fn(navigation)); | ||
|
@@ -1990,6 +2017,50 @@ function reset_focus() { | |
} | ||
} | ||
|
||
/** | ||
* @param {import('./types').NavigationState} current | ||
* @param {import('./types').NavigationIntent | undefined} intent | ||
* @param {URL | null} url | ||
* @param {Exclude<import('@sveltejs/kit').NavigationType, 'enter'>} type | ||
*/ | ||
function create_navigation(current, intent, url, type) { | ||
/** @type {(value: any) => void} */ | ||
let fulfil; | ||
|
||
/** @type {(error: any) => void} */ | ||
let reject; | ||
|
||
const complete = new Promise((f, r) => { | ||
fulfil = f; | ||
reject = r; | ||
}); | ||
|
||
/** @type {import('@sveltejs/kit').Navigation} */ | ||
const navigation = { | ||
from: { | ||
params: current.params, | ||
route: { id: current.route?.id ?? null }, | ||
url: current.url | ||
}, | ||
to: url && { | ||
params: intent?.params ?? null, | ||
route: { id: intent?.route?.id ?? null }, | ||
url | ||
}, | ||
willUnload: !intent, | ||
type, | ||
complete | ||
}; | ||
|
||
return { | ||
navigation, | ||
// @ts-expect-error | ||
fulfil, | ||
// @ts-expect-error | ||
reject | ||
}; | ||
} | ||
|
||
if (DEV) { | ||
// Nasty hack to silence harmless warnings the user can do nothing about | ||
const console_warn = console.warn; | ||
|
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feels a bit loose to just return strings here. Should we declare a set of
const
s that are reused? And should it just be "navigation was aborted" or a more techincal/stable-looking thing likeNAV_ABORTED
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you saying the error message part of the public API and the user needs to check which error message is present?