budibase/packages/builder/src/helpers/urlStateSync.js

153 lines
4.0 KiB
JavaScript
Raw Normal View History

import { get } from "svelte/store"
import { isChangingPage } from "@roxi/routify"
export const syncURLToState = options => {
const { keys, params, store, goto, redirect } = options || {}
if (
!keys?.length ||
!params?.subscribe ||
!store?.subscribe ||
!goto?.subscribe ||
!redirect?.subscribe
) {
console.warn("syncURLToState invoked with missing parameters")
return
}
// We can't dynamically fetch the value of stateful routify stores so we need
// to just subscribe and cache the latest versions.
// We can grab their initial values as this is during component
// initialisation.
let cachedParams = get(params)
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
const mapUrlToState = params => {
// Determine any required state updates
let stateUpdates = []
const state = get(store)
for (let key of keys) {
const urlValue = params?.[key.url]
const stateValue = state?.[key.state]
if (urlValue && urlValue !== stateValue) {
console.log(
`state.${key.state} (${stateValue}) <= url.${key.url} (${urlValue})`
)
stateUpdates.push(state => {
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
// store invalidation
if (!stateUpdates.length) {
return
}
// Apply the required state updates
console.log("Performing", stateUpdates.length, "state updates")
store.update(state => {
for (let update of stateUpdates) {
update(state)
}
return state
})
}
// Updates the URL with new state values
const mapStateToUrl = state => {
// Determine new URL while checking for changes
let url = ".."
let needsUpdate = false
for (let key of keys) {
const urlValue = cachedParams?.[key.url]
const stateValue = state?.[key.state]
url += `/${stateValue}`
if (stateValue !== urlValue) {
needsUpdate = true
console.log(
`url.${key.url} (${urlValue}) <= state.${key.state} (${stateValue})`
)
if (key.validate && key.fallbackUrl) {
if (!key.validate(stateValue)) {
console.log("Invalid state param!")
redirectUrl(key.fallbackUrl)
return
}
}
}
}
// Avoid updating the URL if not necessary to prevent a wasted render
// cycle
if (!needsUpdate) {
return
}
// Navigate to the new URL
if (!get(isChangingPage)) {
gotoUrl(url)
}
}
// Initially hydrate state from URL
mapUrlToState(cachedParams)
// Subscribe to URL changes and cache them
const unsubscribeParams = params.subscribe($urlParams => {
cachedParams = $urlParams
mapUrlToState($urlParams)
})
// Subscribe to routify store changes and cache them
const unsubscribeGoto = goto.subscribe($goto => {
cachedGoto = $goto
})
const unsubscribeRedirect = redirect.subscribe($redirect => {
cachedRedirect = $redirect
})
// Subscribe to store changes and keep URL up to date
const unsubscribeStore = store.subscribe(mapStateToUrl)
// Return an unsync function to cancel subscriptions
return () => {
unsubscribeParams()
unsubscribeGoto()
unsubscribeRedirect()
unsubscribeStore()
}
}