sveltekit-flash-message
Version:
Send temporary data to the next request after redirect. Works with both SSR and client.
195 lines (194 loc) • 7.16 kB
JavaScript
import { get as svelteGet, writable } from 'svelte/store';
import { tick } from 'svelte';
import { BROWSER as browser } from 'esm-env';
import { serialize } from './cookie-es-main/index.js';
import { navigating } from '$app/stores';
import { FlashMessage } from './flashMessage.js';
import { FlashRouter } from './router.js';
const cookieName = 'flash';
const routers = new WeakMap();
function get(page) {
return 'subscribe' in page ? svelteGet(page) : page;
}
function getRouter(page, initialData) {
let router = routers.get(page);
if (!router) {
router = new FlashRouter();
routers.set(page, router);
router.getFlashMessage(get(page).route.id).message.set(initialData);
subscribeToNavigation(page);
}
return router;
}
function subscribeToNavigation(page) {
if (!browser)
return;
navigating.subscribe((nav) => {
//console.log('Navigating:', nav);
if (nav === null) {
const cookieData = parseFlashCookie();
if (cookieData !== undefined) {
//console.log('🚀 ~ afterNavigate:', cookieData, get(page).route.id);
const flash = getRouter(page).getFlashMessage(get(page).route.id);
flash.message.set(cookieData, { concatenateArray: !flash.options.clearArray });
clearFlashCookie(flash.options.flashCookieOptions);
}
}
else {
const navTo = nav?.to?.route.id;
if (navTo) {
const flash = getRouter(page).getFlashMessage(navTo);
if (flash.options.clearOnNavigate && nav.from?.route.id != navTo) {
//console.log('🚀 ~ beforeNavigate ~ clear message on nav to:', navTo);
flash.message.set(undefined);
}
}
}
});
try {
if ('subscribe' in page)
throw '$app/stores used';
// Svelte 5
$effect(() => {
// Track the whole object, like page.subscribe in Svelte 4
page.data;
page.error;
page.form;
page.params;
page.route;
page.state;
page.status;
page.url;
const cookieData = parseFlashCookie();
if (cookieData !== undefined) {
//console.log('🚀 ~ page.subscribe:', cookieData, page.route.id);
const flash = getRouter(page).getFlashMessage(page.route.id);
flash.message.set(cookieData, { concatenateArray: !flash.options.clearArray });
clearFlashCookie(flash.options.flashCookieOptions);
}
});
}
catch (e) {
if (!('subscribe' in page)) {
throw new Error('sveltekit-flash-message cannot use Page from $app/state in Svelte 4. Use $app/stores instead.');
}
// Svelte 4
const p = page;
p.subscribe(($page) => {
const cookieData = parseFlashCookie();
if (cookieData !== undefined) {
//console.log('🚀 ~ page.subscribe:', cookieData, $page.route.id);
const flash = getRouter(page).getFlashMessage($page.route.id);
flash.message.set(cookieData, { concatenateArray: !flash.options.clearArray });
clearFlashCookie(flash.options.flashCookieOptions);
}
});
}
}
export function initFlash(page, options) {
return _initFlash(page, options).message;
}
// @DCI-context
function _initFlash(page, options) {
if (!browser) {
// The SSR version uses a simple store with no options,
// since they are used only on the client.
return new FlashMessage(writable(get(page).data.flash));
}
const _page = get(page);
///// Roles //////////////////////////////////////////////////////////////////
//#region Router /////
// eslint-disable-next-line dci-lint/literal-role-contracts
const Router = getRouter(page, _page.data.flash);
function Router_getFlashMessage() {
const route = Router.routes.get(Page_route());
if (route)
return route;
return options ? Router_createRoute() : Router.getClosestRoute(Page_route());
}
function Router_createRoute() {
return Router.createRoute(Page_route(), Page_initialData(), options);
}
//#endregion
//#region Page
const Page = {
store: page,
route: _page.route.id,
initialdata: _page.data.flash,
navigating
};
function Page_initialData() {
return Page.initialdata;
}
function Page_route() {
return Page.route ?? '';
}
//#endregion
return Router_getFlashMessage();
}
/**
* Retrieves the flash message store for display or modification.
* @param page Page store, imported from `$app/state`.
* @param {FlashOptions} options for the flash message. Can only be set once, usually at the highest level component where getFlash is called for the first time.
* @returns The flash message store.
*/
export function getFlash(page, options) {
return _initFlash(page, options).message;
}
/**
* Update the flash message manually, usually after a fetch request.
* @param page Page store, imported from `$app/state`.
* @param {Promise<void>} update A callback which is executed *before* the message is updated, to delay the message until navigation events are completed, for example when using `goto`.
* @returns {Promise<boolean>} `true` if a flash message existed, `false` if not.
*/
export async function updateFlash(page, update) {
// Update before setting the new message, so navigation events can pass through first.
if (update)
await update();
const cookieData = parseFlashCookie();
if (cookieData !== undefined) {
if (browser)
await tick();
const flash = getRouter(page).getFlashMessage(get(page).route.id);
flash.message.set(cookieData, { concatenateArray: !flash.options.clearArray });
clearFlashCookie(flash.options.flashCookieOptions);
}
return !!cookieData;
}
///////////////////////////////////////////////////////////
function clearFlashCookie(options) {
// Clear parsed cookie
if (browser) {
document.cookie = serialize(cookieName, '', {
...options,
maxAge: 0
});
}
}
function parseFlashCookie() {
const cookieString = document.cookie;
if (!cookieString || !cookieString.includes(cookieName + '='))
return undefined;
function parseCookieString(str) {
const output = {};
if (!str)
return output;
return str
.split(';')
.map((v) => v.split('='))
.reduce((acc, v) => {
acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim());
return acc;
}, output);
}
const cookies = parseCookieString(cookieString);
if (cookies[cookieName]) {
try {
return JSON.parse(cookies[cookieName]);
}
catch (e) {
// Ignore value if parsing failed
}
}
return undefined;
}