Add reusable utility to sync URL params with state

This commit is contained in:
Andrew Kingston 2022-04-21 17:06:11 +01:00
parent 50b818fec8
commit 4f94430c96
3 changed files with 142 additions and 11 deletions

View File

@ -15,7 +15,7 @@ import {
tables, tables,
} from "stores/backend" } from "stores/backend"
import { API } from "api" import { API } from "api"
import { FrontendTypes } from "constants" import { DesignTabs, FrontendTypes } from "constants"
import analytics, { Events } from "analytics" import analytics, { Events } from "analytics"
import { import {
findComponentType, findComponentType,
@ -48,6 +48,7 @@ const INITIAL_FRONTEND_STATE = {
continueIfAction: false, continueIfAction: false,
}, },
currentFrontEndType: "none", currentFrontEndType: "none",
selectedDesignTab: DesignTabs.SCREENS,
selectedScreenId: "", selectedScreenId: "",
selectedLayoutId: "", selectedLayoutId: "",
selectedComponentId: "", selectedComponentId: "",

View File

@ -0,0 +1,106 @@
import { get } from "svelte/store"
export const syncURLToState = options => {
const { keys, params, store, goto } = options || {}
if (
!keys?.length ||
!params?.subscribe ||
!store?.subscribe ||
!goto?.subscribe
) {
return
}
// We can't dynamically fetch the value of 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)
// 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 !== stateValue) {
console.log(
`state.${key.state} (${stateValue}) <= url.${key.url} (${urlValue})`
)
stateUpdates.push(state => {
state[key.state] = urlValue
})
}
}
// 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) {
console.log(
`url.${key.url} (${urlValue}) <= state.${key.state} (${stateValue})`
)
needsUpdate = true
}
}
// Avoid updating the URL if not necessary to prevent a wasted render
// cycle
if (!needsUpdate) {
return
}
// Navigate to the new URL
console.log("Navigating to", url)
cachedGoto(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 goto changes and cache them
const unsubscribeGoto = goto.subscribe($goto => {
cachedGoto = $goto
})
// 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()
unsubscribeStore()
}
}

View File

@ -2,6 +2,30 @@
import { IconSideNav, IconSideNavItem } from "@budibase/bbui" import { IconSideNav, IconSideNavItem } from "@budibase/bbui"
import { params, goto } from "@roxi/routify" import { params, goto } from "@roxi/routify"
import { DesignTabs } from "constants" import { DesignTabs } from "constants"
import { store } from "builderStore"
import { syncURLToState } from "helpers/urlStateSync"
import { onDestroy } from "svelte"
const updateTab = tab => {
store.update(state => {
state.selectedDesignTab = tab
return state
})
}
const unsync = syncURLToState({
keys: [
{
url: "tab",
state: "selectedDesignTab",
},
],
store,
params,
goto,
})
onDestroy(unsync)
</script> </script>
<!-- routify:options index=1 --> <!-- routify:options index=1 -->
@ -11,32 +35,32 @@
<IconSideNavItem <IconSideNavItem
icon="WebPage" icon="WebPage"
tooltip="Screens" tooltip="Screens"
active={$params.tab === DesignTabs.SCREENS} active={$store.selectedDesignTab === DesignTabs.SCREENS}
on:click={() => $goto(`./${DesignTabs.SCREENS}`)} on:click={() => updateTab(DesignTabs.SCREENS)}
/> />
<IconSideNavItem <IconSideNavItem
icon="ViewList" icon="ViewList"
tooltip="Components" tooltip="Components"
active={$params.tab === DesignTabs.COMPONENTS} active={$store.selectedDesignTab === DesignTabs.COMPONENTS}
on:click={() => $goto(`./${DesignTabs.COMPONENTS}`)} on:click={() => updateTab(DesignTabs.COMPONENTS)}
/> />
<IconSideNavItem <IconSideNavItem
icon="Brush" icon="Brush"
tooltip="Theme" tooltip="Theme"
active={$params.tab === DesignTabs.THEME} active={$store.selectedDesignTab === DesignTabs.THEME}
on:click={() => $goto(`./${DesignTabs.THEME}`)} on:click={() => updateTab(DesignTabs.THEME)}
/> />
<IconSideNavItem <IconSideNavItem
icon="Link" icon="Link"
tooltip="Navigation" tooltip="Navigation"
active={$params.tab === DesignTabs.NAVIGATION} active={$store.selectedDesignTab === DesignTabs.NAVIGATION}
on:click={() => $goto(`./${DesignTabs.NAVIGATION}`)} on:click={() => updateTab(DesignTabs.NAVIGATION)}
/> />
<IconSideNavItem <IconSideNavItem
icon="Experience" icon="Experience"
tooltip="Layouts" tooltip="Layouts"
active={$params.tab === DesignTabs.LAYOUTS} active={$store.selectedDesignTab === DesignTabs.LAYOUTS}
on:click={() => $goto(`./${DesignTabs.LAYOUTS}`)} on:click={() => updateTab(DesignTabs.LAYOUTS)}
/> />
</IconSideNav> </IconSideNav>
</div> </div>