2022-04-21 18:06:11 +02:00
|
|
|
import { get } from "svelte/store"
|
2022-04-25 16:36:01 +02:00
|
|
|
import { isChangingPage } from "@roxi/routify"
|
2022-04-21 18:06:11 +02:00
|
|
|
|
|
|
|
export const syncURLToState = options => {
|
2022-04-25 20:33:43 +02:00
|
|
|
const { keys, params, store, goto, redirect, baseUrl = "." } = options || {}
|
2022-04-21 18:06:11 +02:00
|
|
|
if (
|
|
|
|
!keys?.length ||
|
|
|
|
!params?.subscribe ||
|
|
|
|
!store?.subscribe ||
|
2022-04-25 16:36:01 +02:00
|
|
|
!goto?.subscribe ||
|
|
|
|
!redirect?.subscribe
|
2022-04-21 18:06:11 +02:00
|
|
|
) {
|
2022-04-25 16:36:01 +02:00
|
|
|
console.warn("syncURLToState invoked with missing parameters")
|
2022-04-21 18:06:11 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-04-25 16:36:01 +02:00
|
|
|
// We can't dynamically fetch the value of stateful routify stores so we need
|
|
|
|
// to just subscribe and cache the latest versions.
|
2022-04-21 18:06:11 +02:00
|
|
|
// We can grab their initial values as this is during component
|
|
|
|
// initialisation.
|
|
|
|
let cachedParams = get(params)
|
|
|
|
let cachedGoto = get(goto)
|
2022-04-25 16:36:01 +02:00
|
|
|
let cachedRedirect = get(redirect)
|
|
|
|
let hydrated = false
|
2022-04-25 20:33:43 +02:00
|
|
|
let debug = false
|
|
|
|
const log = (...params) => debug && console.log(...params)
|
2022-04-25 16:36:01 +02:00
|
|
|
|
|
|
|
// Navigate to a certain URL
|
|
|
|
const gotoUrl = url => {
|
|
|
|
if (get(isChangingPage) && hydrated) {
|
|
|
|
return
|
|
|
|
}
|
2022-04-25 20:33:43 +02:00
|
|
|
log("Navigating to", url)
|
2022-04-25 16:36:01 +02:00
|
|
|
cachedGoto(url)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Redirect to a certain URL
|
|
|
|
const redirectUrl = url => {
|
|
|
|
if (get(isChangingPage) && hydrated) {
|
|
|
|
return
|
|
|
|
}
|
2022-04-25 20:33:43 +02:00
|
|
|
log("Redirecting to", url)
|
2022-04-25 16:36:01 +02:00
|
|
|
cachedRedirect(url)
|
|
|
|
}
|
2022-04-21 18:06:11 +02:00
|
|
|
|
|
|
|
// 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]
|
2022-04-25 16:36:01 +02:00
|
|
|
if (urlValue && urlValue !== stateValue) {
|
2022-04-25 20:33:43 +02:00
|
|
|
log(
|
2022-04-21 18:06:11 +02:00
|
|
|
`state.${key.state} (${stateValue}) <= url.${key.url} (${urlValue})`
|
|
|
|
)
|
|
|
|
stateUpdates.push(state => {
|
|
|
|
state[key.state] = urlValue
|
|
|
|
})
|
2022-04-25 16:36:01 +02:00
|
|
|
if (key.validate && key.fallbackUrl) {
|
|
|
|
if (!key.validate(urlValue)) {
|
2022-04-25 20:33:43 +02:00
|
|
|
log("Invalid URL param!")
|
2022-04-25 16:36:01 +02:00
|
|
|
redirectUrl(key.fallbackUrl)
|
|
|
|
hydrated = true
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2022-04-21 18:06:11 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-25 16:36:01 +02:00
|
|
|
// Mark our initial hydration as completed
|
|
|
|
hydrated = true
|
|
|
|
|
2022-04-21 18:06:11 +02:00
|
|
|
// Avoid updating the store at all if not necessary to prevent a wasted
|
|
|
|
// store invalidation
|
|
|
|
if (!stateUpdates.length) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Apply the required state updates
|
2022-04-25 20:33:43 +02:00
|
|
|
log("Performing", stateUpdates.length, "state updates")
|
2022-04-21 18:06:11 +02:00
|
|
|
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
|
2022-04-25 20:33:43 +02:00
|
|
|
let url = baseUrl
|
2022-04-21 18:06:11 +02:00
|
|
|
let needsUpdate = false
|
|
|
|
for (let key of keys) {
|
|
|
|
const urlValue = cachedParams?.[key.url]
|
|
|
|
const stateValue = state?.[key.state]
|
|
|
|
url += `/${stateValue}`
|
|
|
|
if (stateValue !== urlValue) {
|
2022-04-25 16:36:01 +02:00
|
|
|
needsUpdate = true
|
2022-04-25 20:33:43 +02:00
|
|
|
log(
|
2022-04-21 18:06:11 +02:00
|
|
|
`url.${key.url} (${urlValue}) <= state.${key.state} (${stateValue})`
|
|
|
|
)
|
2022-04-25 16:36:01 +02:00
|
|
|
if (key.validate && key.fallbackUrl) {
|
|
|
|
if (!key.validate(stateValue)) {
|
2022-04-25 20:33:43 +02:00
|
|
|
log("Invalid state param!")
|
2022-04-25 16:36:01 +02:00
|
|
|
redirectUrl(key.fallbackUrl)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2022-04-21 18:06:11 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Avoid updating the URL if not necessary to prevent a wasted render
|
|
|
|
// cycle
|
|
|
|
if (!needsUpdate) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Navigate to the new URL
|
2022-04-25 16:36:01 +02:00
|
|
|
if (!get(isChangingPage)) {
|
|
|
|
gotoUrl(url)
|
|
|
|
}
|
2022-04-21 18:06:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Initially hydrate state from URL
|
|
|
|
mapUrlToState(cachedParams)
|
|
|
|
|
|
|
|
// Subscribe to URL changes and cache them
|
|
|
|
const unsubscribeParams = params.subscribe($urlParams => {
|
|
|
|
cachedParams = $urlParams
|
|
|
|
mapUrlToState($urlParams)
|
|
|
|
})
|
|
|
|
|
2022-04-25 16:36:01 +02:00
|
|
|
// Subscribe to routify store changes and cache them
|
2022-04-21 18:06:11 +02:00
|
|
|
const unsubscribeGoto = goto.subscribe($goto => {
|
|
|
|
cachedGoto = $goto
|
|
|
|
})
|
2022-04-25 16:36:01 +02:00
|
|
|
const unsubscribeRedirect = redirect.subscribe($redirect => {
|
|
|
|
cachedRedirect = $redirect
|
|
|
|
})
|
2022-04-21 18:06:11 +02:00
|
|
|
|
|
|
|
// 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()
|
2022-04-25 16:36:01 +02:00
|
|
|
unsubscribeRedirect()
|
2022-04-21 18:06:11 +02:00
|
|
|
unsubscribeStore()
|
|
|
|
}
|
|
|
|
}
|