Add reusable utility to sync URL params with state
This commit is contained in:
parent
6749dc4f1b
commit
4126e5884d
|
@ -15,7 +15,7 @@ import {
|
|||
tables,
|
||||
} from "stores/backend"
|
||||
import { API } from "api"
|
||||
import { FrontendTypes } from "constants"
|
||||
import { DesignTabs, FrontendTypes } from "constants"
|
||||
import analytics, { Events } from "analytics"
|
||||
import {
|
||||
findComponentType,
|
||||
|
@ -48,6 +48,7 @@ const INITIAL_FRONTEND_STATE = {
|
|||
continueIfAction: false,
|
||||
},
|
||||
currentFrontEndType: "none",
|
||||
selectedDesignTab: DesignTabs.SCREENS,
|
||||
selectedScreenId: "",
|
||||
selectedLayoutId: "",
|
||||
selectedComponentId: "",
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -2,6 +2,30 @@
|
|||
import { IconSideNav, IconSideNavItem } from "@budibase/bbui"
|
||||
import { params, goto } from "@roxi/routify"
|
||||
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>
|
||||
|
||||
<!-- routify:options index=1 -->
|
||||
|
@ -11,32 +35,32 @@
|
|||
<IconSideNavItem
|
||||
icon="WebPage"
|
||||
tooltip="Screens"
|
||||
active={$params.tab === DesignTabs.SCREENS}
|
||||
on:click={() => $goto(`./${DesignTabs.SCREENS}`)}
|
||||
active={$store.selectedDesignTab === DesignTabs.SCREENS}
|
||||
on:click={() => updateTab(DesignTabs.SCREENS)}
|
||||
/>
|
||||
<IconSideNavItem
|
||||
icon="ViewList"
|
||||
tooltip="Components"
|
||||
active={$params.tab === DesignTabs.COMPONENTS}
|
||||
on:click={() => $goto(`./${DesignTabs.COMPONENTS}`)}
|
||||
active={$store.selectedDesignTab === DesignTabs.COMPONENTS}
|
||||
on:click={() => updateTab(DesignTabs.COMPONENTS)}
|
||||
/>
|
||||
<IconSideNavItem
|
||||
icon="Brush"
|
||||
tooltip="Theme"
|
||||
active={$params.tab === DesignTabs.THEME}
|
||||
on:click={() => $goto(`./${DesignTabs.THEME}`)}
|
||||
active={$store.selectedDesignTab === DesignTabs.THEME}
|
||||
on:click={() => updateTab(DesignTabs.THEME)}
|
||||
/>
|
||||
<IconSideNavItem
|
||||
icon="Link"
|
||||
tooltip="Navigation"
|
||||
active={$params.tab === DesignTabs.NAVIGATION}
|
||||
on:click={() => $goto(`./${DesignTabs.NAVIGATION}`)}
|
||||
active={$store.selectedDesignTab === DesignTabs.NAVIGATION}
|
||||
on:click={() => updateTab(DesignTabs.NAVIGATION)}
|
||||
/>
|
||||
<IconSideNavItem
|
||||
icon="Experience"
|
||||
tooltip="Layouts"
|
||||
active={$params.tab === DesignTabs.LAYOUTS}
|
||||
on:click={() => $goto(`./${DesignTabs.LAYOUTS}`)}
|
||||
active={$store.selectedDesignTab === DesignTabs.LAYOUTS}
|
||||
on:click={() => updateTab(DesignTabs.LAYOUTS)}
|
||||
/>
|
||||
</IconSideNav>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue