From e62fbf8ef72b769fada83725ca3e9fed8922b6dd Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Tue, 24 Nov 2020 11:02:10 +0000 Subject: [PATCH] Add component data binding and simplify context sharing --- .../client/src/components/ClientApp.svelte | 4 +- .../client/src/components/Component.svelte | 38 +++++++++++++++--- .../client/src/components/DataProvider.svelte | 12 +++--- packages/client/src/components/Router.svelte | 4 +- packages/client/src/sdk.js | 3 +- packages/client/src/store/auth.js | 7 ---- packages/client/src/store/binding.js | 21 ++++++++++ packages/client/src/store/data.js | 27 +++++++++++++ packages/client/src/store/dataContext.js | 39 ------------------- packages/client/src/store/index.js | 5 ++- .../standard-components/src/Button.svelte | 7 +++- packages/standard-components/src/Card.svelte | 7 +++- .../src/CardHorizontal.svelte | 7 +++- .../standard-components/src/Container.svelte | 28 ++++++------- .../standard-components/src/DatePicker.svelte | 4 +- packages/standard-components/src/Embed.svelte | 4 +- packages/standard-components/src/Form.svelte | 4 +- .../standard-components/src/Heading.svelte | 14 +++---- packages/standard-components/src/Icon.svelte | 4 +- packages/standard-components/src/Image.svelte | 4 +- packages/standard-components/src/Input.svelte | 21 +++------- packages/standard-components/src/Link.svelte | 4 +- packages/standard-components/src/List.svelte | 4 +- packages/standard-components/src/Login.svelte | 4 +- .../standard-components/src/Navigation.svelte | 4 +- .../standard-components/src/RichText.svelte | 2 +- .../src/ScreenSlotPlaceholder.svelte | 4 +- .../src/StackedList.svelte | 4 +- packages/standard-components/src/Text.svelte | 26 ++++++------- .../src/charts/ApexChart.svelte | 6 +-- .../src/grid/Component.svelte | 6 +-- 31 files changed, 180 insertions(+), 148 deletions(-) create mode 100644 packages/client/src/store/binding.js create mode 100644 packages/client/src/store/data.js delete mode 100644 packages/client/src/store/dataContext.js diff --git a/packages/client/src/components/ClientApp.svelte b/packages/client/src/components/ClientApp.svelte index a0be2120b5..f4af80ab83 100644 --- a/packages/client/src/components/ClientApp.svelte +++ b/packages/client/src/components/ClientApp.svelte @@ -2,11 +2,11 @@ import { setContext, onMount } from "svelte" import Component from "./Component.svelte" import SDK from "../sdk" - import { routeStore, screenStore, createDataContextStore } from "../store" + import { routeStore, screenStore, createDataStore } from "../store" // Provide contexts setContext("sdk", SDK) - setContext("data", createDataContextStore()) + setContext("data", createDataStore()) let loaded = false diff --git a/packages/client/src/components/Component.svelte b/packages/client/src/components/Component.svelte index b17dd0d7d2..f003033f08 100644 --- a/packages/client/src/components/Component.svelte +++ b/packages/client/src/components/Component.svelte @@ -3,6 +3,8 @@ import { writable } from "svelte/store" import * as ComponentLibrary from "@budibase/standard-components" import Router from "./Router.svelte" + import { enrichDataBinding } from "../utils" + import { bindingStore } from "../store" export let definition = {} @@ -23,6 +25,19 @@ return props } + // Enriches data bindings to real values based on data context + const enrichDataBindings = (data, bindings, props) => { + const state = { + ...data, + ...bindings, + } + let enrichedProps = {} + Object.entries(props).forEach(([key, value]) => { + enrichedProps[key] = enrichDataBinding(value, state) + }) + return enrichedProps + } + // Gets the component constructor for the specified component const getComponentConstructor = name => { return name === "screenslot" ? Router : ComponentLibrary[componentName] @@ -32,14 +47,25 @@ $: componentName = extractComponentName(definition._component) $: constructor = getComponentConstructor(componentName) $: componentProps = extractValidProps(definition) - $: dataContext = getContext("data") - $: enrichedProps = dataContext.actions.enrichDataBindings(componentProps) $: children = definition._children + $: id = definition._id + $: dataStore = getContext("data") + $: enrichedProps = enrichDataBindings( + $dataStore, + $bindingStore, + componentProps + ) - // Set observable style context - const styleStore = writable({}) - setContext("style", styleStore) - $: styleStore.set({ ...definition._styles, id: definition._id }) + // Update component context + // ID is duplicated inside style so that the "styleable" helper can set + // an ID data tag for unique reference to components + const componentStore = writable({}) + setContext("component", componentStore) + $: componentStore.set({ + id, + styles: { ...definition._styles, id }, + dataContext: $dataStore.data, + }) {#if constructor} diff --git a/packages/client/src/components/DataProvider.svelte b/packages/client/src/components/DataProvider.svelte index 08defe93b6..4ee04d722c 100644 --- a/packages/client/src/components/DataProvider.svelte +++ b/packages/client/src/components/DataProvider.svelte @@ -1,19 +1,19 @@ diff --git a/packages/client/src/components/Router.svelte b/packages/client/src/components/Router.svelte index 4921e65c87..0f89924b66 100644 --- a/packages/client/src/components/Router.svelte +++ b/packages/client/src/components/Router.svelte @@ -5,7 +5,7 @@ import Screen from "./Screen.svelte" const { styleable } = getContext("sdk") - const styles = getContext("style") + const component = getContext("component") $: routerConfig = getRouterConfig($routeStore.routes) @@ -26,7 +26,7 @@ {#if routerConfig} -
+
{/if} diff --git a/packages/client/src/sdk.js b/packages/client/src/sdk.js index cad3e20bd5..3a5a3f80ac 100644 --- a/packages/client/src/sdk.js +++ b/packages/client/src/sdk.js @@ -1,5 +1,5 @@ import * as API from "./api" -import { authStore, routeStore, screenStore } from "./store" +import { authStore, routeStore, screenStore, bindingStore } from "./store" import { styleable, getAppId } from "./utils" import { link as linkable } from "svelte-spa-router" import DataProvider from "./components/DataProvider.svelte" @@ -13,4 +13,5 @@ export default { linkable, getAppId, DataProvider, + setBindableValue: bindingStore.actions.setBindableValue, } diff --git a/packages/client/src/store/auth.js b/packages/client/src/store/auth.js index 54ee35e8da..ac616e9240 100644 --- a/packages/client/src/store/auth.js +++ b/packages/client/src/store/auth.js @@ -5,9 +5,6 @@ import { writable } from "svelte/store" const createAuthStore = () => { const store = writable("") - /** - * Logs a user in. - */ const logIn = async ({ username, password }) => { const user = await API.logIn({ username, password }) if (!user.error) { @@ -15,10 +12,6 @@ const createAuthStore = () => { location.reload() } } - - /** - * Logs a user out. - */ const logOut = () => { store.set("") const appId = getAppId() diff --git a/packages/client/src/store/binding.js b/packages/client/src/store/binding.js new file mode 100644 index 0000000000..b279142c32 --- /dev/null +++ b/packages/client/src/store/binding.js @@ -0,0 +1,21 @@ +import { writable } from "svelte/store" + +const createBindingStore = () => { + const store = writable({}) + + const setBindableValue = (value, componentId) => { + store.update(state => { + if (componentId) { + state[componentId] = value + } + return state + }) + } + + return { + subscribe: store.subscribe, + actions: { setBindableValue }, + } +} + +export const bindingStore = createBindingStore() diff --git a/packages/client/src/store/data.js b/packages/client/src/store/data.js new file mode 100644 index 0000000000..9cd44eebb4 --- /dev/null +++ b/packages/client/src/store/data.js @@ -0,0 +1,27 @@ +import { writable } from "svelte/store" +import { cloneDeep } from "lodash/fp" + +const initialValue = { + data: null, +} + +export const createDataStore = existingContext => { + const initial = existingContext ? cloneDeep(existingContext) : initialValue + const store = writable(initial) + + // Adds a context layer to the data context tree + const addContext = (row, componentId) => { + store.update(state => { + if (componentId) { + state[componentId] = row + state.data = row + } + return state + }) + } + + return { + subscribe: store.subscribe, + actions: { addContext }, + } +} diff --git a/packages/client/src/store/dataContext.js b/packages/client/src/store/dataContext.js deleted file mode 100644 index 90acc42664..0000000000 --- a/packages/client/src/store/dataContext.js +++ /dev/null @@ -1,39 +0,0 @@ -import { writable, get } from "svelte/store" -import { enrichDataBinding } from "../utils" -import { cloneDeep } from "lodash/fp" - -const initialValue = { - data: null, -} - -export const createDataContextStore = existingContext => { - const initial = existingContext ? cloneDeep(existingContext) : initialValue - const store = writable(initial) - - // Adds a context layer to the data context tree - const addContext = (row, componentId) => { - store.update(state => { - if (row && componentId) { - state[componentId] = row - state.data = row - } - return state - }) - } - - // Enriches props by running mustache and filling in any data bindings present - // in the prop values - const enrichDataBindings = props => { - const state = get(store) - let enrichedProps = {} - Object.entries(props).forEach(([key, value]) => { - enrichedProps[key] = enrichDataBinding(value, state) - }) - return enrichedProps - } - - return { - subscribe: store.subscribe, - actions: { addContext, enrichDataBindings }, - } -} diff --git a/packages/client/src/store/index.js b/packages/client/src/store/index.js index 4416ca809a..3730f39ee0 100644 --- a/packages/client/src/store/index.js +++ b/packages/client/src/store/index.js @@ -1,5 +1,8 @@ export { authStore } from "./auth" export { routeStore } from "./routes" export { screenStore } from "./screens" -export { createDataContextStore } from "./dataContext" export { builderStore } from "./builder" +export { bindingStore } from "./binding" + +// Data stores are layered and duplicated, so it is not a singleton +export { createDataStore } from "./data" diff --git a/packages/standard-components/src/Button.svelte b/packages/standard-components/src/Button.svelte index 6dbbcfc4cf..1d64c5e8bd 100644 --- a/packages/standard-components/src/Button.svelte +++ b/packages/standard-components/src/Button.svelte @@ -2,14 +2,17 @@ import { getContext } from "svelte" const { styleable } = getContext("sdk") - const styles = getContext("style") + const component = getContext("component") export let className = "default" export let disabled = false export let text - diff --git a/packages/standard-components/src/Card.svelte b/packages/standard-components/src/Card.svelte index e4fcd4a7d6..148c6f0a89 100644 --- a/packages/standard-components/src/Card.svelte +++ b/packages/standard-components/src/Card.svelte @@ -3,7 +3,7 @@ import { cssVars } from "./helpers" const { styleable } = getContext("sdk") - const styles = getContext("style") + const component = getContext("component") export const className = "" export let imageUrl = "" @@ -26,7 +26,10 @@ $: showImage = !!imageUrl -
+
{#if showImage}{/if}

{heading}

diff --git a/packages/standard-components/src/CardHorizontal.svelte b/packages/standard-components/src/CardHorizontal.svelte index ea4fb3734e..df47097180 100644 --- a/packages/standard-components/src/CardHorizontal.svelte +++ b/packages/standard-components/src/CardHorizontal.svelte @@ -3,7 +3,7 @@ import { cssVars } from "./helpers" const { styleable } = getContext("sdk") - const styles = getContext("style") + const component = getContext("component") export const className = "" export let imageUrl = "" @@ -29,7 +29,10 @@ $: showImage = !!imageUrl -
+
{#if showImage}{/if}
diff --git a/packages/standard-components/src/Container.svelte b/packages/standard-components/src/Container.svelte index 546787dc5d..4de4aaae84 100644 --- a/packages/standard-components/src/Container.svelte +++ b/packages/standard-components/src/Container.svelte @@ -2,62 +2,62 @@ import { getContext } from "svelte" const { styleable } = getContext("sdk") - const styles = getContext("style") + const component = getContext("component") export let className = "" export let type = "div" {#if type === 'div'} -
+
{:else if type === 'header'} -
+
{:else if type === 'main'} -
+
{:else if type === 'footer'} -