From 4126e5884d10d5a0611b3851d551a13b2f0b08a2 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Thu, 21 Apr 2022 17:06:11 +0100 Subject: [PATCH] Add reusable utility to sync URL params with state --- .../src/builderStore/store/frontend.js | 3 +- packages/builder/src/helpers/urlStateSync.js | 106 ++++++++++++++++++ .../app/[application]/design/_layout.svelte | 44 ++++++-- 3 files changed, 142 insertions(+), 11 deletions(-) create mode 100644 packages/builder/src/helpers/urlStateSync.js diff --git a/packages/builder/src/builderStore/store/frontend.js b/packages/builder/src/builderStore/store/frontend.js index 3ffc890c7d..dd844ef5ab 100644 --- a/packages/builder/src/builderStore/store/frontend.js +++ b/packages/builder/src/builderStore/store/frontend.js @@ -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: "", diff --git a/packages/builder/src/helpers/urlStateSync.js b/packages/builder/src/helpers/urlStateSync.js new file mode 100644 index 0000000000..22ef98ded7 --- /dev/null +++ b/packages/builder/src/helpers/urlStateSync.js @@ -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() + } +} diff --git a/packages/builder/src/pages/builder/app/[application]/design/_layout.svelte b/packages/builder/src/pages/builder/app/[application]/design/_layout.svelte index b7a20f24c5..0ea4e8456d 100644 --- a/packages/builder/src/pages/builder/app/[application]/design/_layout.svelte +++ b/packages/builder/src/pages/builder/app/[application]/design/_layout.svelte @@ -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) @@ -11,32 +35,32 @@ $goto(`./${DesignTabs.SCREENS}`)} + active={$store.selectedDesignTab === DesignTabs.SCREENS} + on:click={() => updateTab(DesignTabs.SCREENS)} /> $goto(`./${DesignTabs.COMPONENTS}`)} + active={$store.selectedDesignTab === DesignTabs.COMPONENTS} + on:click={() => updateTab(DesignTabs.COMPONENTS)} /> $goto(`./${DesignTabs.THEME}`)} + active={$store.selectedDesignTab === DesignTabs.THEME} + on:click={() => updateTab(DesignTabs.THEME)} /> $goto(`./${DesignTabs.NAVIGATION}`)} + active={$store.selectedDesignTab === DesignTabs.NAVIGATION} + on:click={() => updateTab(DesignTabs.NAVIGATION)} /> $goto(`./${DesignTabs.LAYOUTS}`)} + active={$store.selectedDesignTab === DesignTabs.LAYOUTS} + on:click={() => updateTab(DesignTabs.LAYOUTS)} />