Add validation and fallback URLs to URL/state sync utility

This commit is contained in:
Andrew Kingston 2022-04-25 15:36:01 +01:00
parent a2bb2aa631
commit 300f1e8ab1
2 changed files with 61 additions and 11 deletions

View File

@ -1,22 +1,45 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import { isChangingPage } from "@roxi/routify"
export const syncURLToState = options => { export const syncURLToState = options => {
const { keys, params, store, goto } = options || {} const { keys, params, store, goto, redirect } = options || {}
if ( if (
!keys?.length || !keys?.length ||
!params?.subscribe || !params?.subscribe ||
!store?.subscribe || !store?.subscribe ||
!goto?.subscribe !goto?.subscribe ||
!redirect?.subscribe
) { ) {
console.warn("syncURLToState invoked with missing parameters")
return return
} }
// We can't dynamically fetch the value of routify stores so we need to // We can't dynamically fetch the value of stateful routify stores so we need
// just subscribe and cache the latest versions. // to just subscribe and cache the latest versions.
// We can grab their initial values as this is during component // We can grab their initial values as this is during component
// initialisation. // initialisation.
let cachedParams = get(params) let cachedParams = get(params)
let cachedGoto = get(goto) let cachedGoto = get(goto)
let cachedRedirect = get(redirect)
let hydrated = false
// Navigate to a certain URL
const gotoUrl = url => {
if (get(isChangingPage) && hydrated) {
return
}
console.log("Navigating to", url)
cachedGoto(url)
}
// Redirect to a certain URL
const redirectUrl = url => {
if (get(isChangingPage) && hydrated) {
return
}
console.log("Redirecting to", url)
cachedRedirect(url)
}
// Updates state with new URL params // Updates state with new URL params
const mapUrlToState = params => { const mapUrlToState = params => {
@ -26,16 +49,27 @@ export const syncURLToState = options => {
for (let key of keys) { for (let key of keys) {
const urlValue = params?.[key.url] const urlValue = params?.[key.url]
const stateValue = state?.[key.state] const stateValue = state?.[key.state]
if (urlValue !== stateValue) { if (urlValue && urlValue !== stateValue) {
console.log( console.log(
`state.${key.state} (${stateValue}) <= url.${key.url} (${urlValue})` `state.${key.state} (${stateValue}) <= url.${key.url} (${urlValue})`
) )
stateUpdates.push(state => { stateUpdates.push(state => {
state[key.state] = urlValue state[key.state] = urlValue
}) })
if (key.validate && key.fallbackUrl) {
if (!key.validate(urlValue)) {
console.log("Invalid URL param!")
redirectUrl(key.fallbackUrl)
hydrated = true
return
}
}
} }
} }
// Mark our initial hydration as completed
hydrated = true
// Avoid updating the store at all if not necessary to prevent a wasted // Avoid updating the store at all if not necessary to prevent a wasted
// store invalidation // store invalidation
if (!stateUpdates.length) { if (!stateUpdates.length) {
@ -62,10 +96,17 @@ export const syncURLToState = options => {
const stateValue = state?.[key.state] const stateValue = state?.[key.state]
url += `/${stateValue}` url += `/${stateValue}`
if (stateValue !== urlValue) { if (stateValue !== urlValue) {
needsUpdate = true
console.log( console.log(
`url.${key.url} (${urlValue}) <= state.${key.state} (${stateValue})` `url.${key.url} (${urlValue}) <= state.${key.state} (${stateValue})`
) )
needsUpdate = true if (key.validate && key.fallbackUrl) {
if (!key.validate(stateValue)) {
console.log("Invalid state param!")
redirectUrl(key.fallbackUrl)
return
}
}
} }
} }
@ -76,8 +117,9 @@ export const syncURLToState = options => {
} }
// Navigate to the new URL // Navigate to the new URL
console.log("Navigating to", url) if (!get(isChangingPage)) {
cachedGoto(url) gotoUrl(url)
}
} }
// Initially hydrate state from URL // Initially hydrate state from URL
@ -89,10 +131,13 @@ export const syncURLToState = options => {
mapUrlToState($urlParams) mapUrlToState($urlParams)
}) })
// Subscribe to goto changes and cache them // Subscribe to routify store changes and cache them
const unsubscribeGoto = goto.subscribe($goto => { const unsubscribeGoto = goto.subscribe($goto => {
cachedGoto = $goto cachedGoto = $goto
}) })
const unsubscribeRedirect = redirect.subscribe($redirect => {
cachedRedirect = $redirect
})
// Subscribe to store changes and keep URL up to date // Subscribe to store changes and keep URL up to date
const unsubscribeStore = store.subscribe(mapStateToUrl) const unsubscribeStore = store.subscribe(mapStateToUrl)
@ -101,6 +146,7 @@ export const syncURLToState = options => {
return () => { return () => {
unsubscribeParams() unsubscribeParams()
unsubscribeGoto() unsubscribeGoto()
unsubscribeRedirect()
unsubscribeStore() unsubscribeStore()
} }
} }

View File

@ -1,20 +1,24 @@
<script> <script>
import { IconSideNav, IconSideNavItem } from "@budibase/bbui" import { IconSideNav, IconSideNavItem } from "@budibase/bbui"
import { params, goto, isActive } from "@roxi/routify" import { params, goto, redirect, isActive } from "@roxi/routify"
import { store } from "builderStore" import { store, allScreens } from "builderStore"
import { syncURLToState } from "helpers/urlStateSync" import { syncURLToState } from "helpers/urlStateSync"
import { onDestroy } from "svelte" import { onDestroy } from "svelte"
// Keep URL and state in sync for selected screen ID
const unsync = syncURLToState({ const unsync = syncURLToState({
keys: [ keys: [
{ {
url: "screenId", url: "screenId",
state: "selectedScreenId", state: "selectedScreenId",
validate: screenId => $allScreens.some(x => x._id === screenId),
fallbackUrl: "../",
}, },
], ],
store, store,
params, params,
goto, goto,
redirect,
}) })
onDestroy(unsync) onDestroy(unsync)