1
0
Fork 0
mirror of synced 2024-06-27 02:20:35 +12:00

state management bindings

This commit is contained in:
Martin McKeaveney 2020-02-20 20:19:24 +00:00
parent ab7eec0821
commit b7a5735a05
14 changed files with 306 additions and 511 deletions

View file

@ -5,12 +5,11 @@
"eslint": "^6.8.0", "eslint": "^6.8.0",
"eslint-plugin-prettier": "^3.1.2", "eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-svelte3": "^2.7.3", "eslint-plugin-svelte3": "^2.7.3",
"lerna": "^3.14.1", "lerna": "^3.20.2",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"prettier-plugin-svelte": "^0.7.0", "prettier-plugin-svelte": "^0.7.0",
"svelte": "^3.18.1" "svelte": "^3.18.1"
}, },
"dependencies": {},
"scripts": { "scripts": {
"bootstrap": "lerna bootstrap", "bootstrap": "lerna bootstrap",
"build": "lerna run build", "build": "lerna run build",
@ -20,6 +19,7 @@
"test": "lerna run test", "test": "lerna run test",
"lint": "eslint packages", "lint": "eslint packages",
"lint:fix": "eslint --fix packages", "lint:fix": "eslint --fix packages",
"format": "prettier --write \"{,!(node_modules)/**/}*.{js,jsx,svelte}\"" "format": "prettier --write \"{,!(node_modules)/**/}*.{js,jsx,svelte}\"",
"publish-components": "lerna run publishdev"
} }
} }

File diff suppressed because one or more lines are too long

View file

@ -680,14 +680,14 @@ const _savePage = async s => {
}) })
} }
const saveBackend = async s => { const saveBackend = async state => {
await api.post(`/_builder/api/${appname}/backend`, { await api.post(`/_builder/api/${appname}/backend`, {
appDefinition: { appDefinition: {
hierarchy: s.hierarchy, hierarchy: state.hierarchy,
actions: s.actions, actions: state.actions,
triggers: s.triggers, triggers: state.triggers,
}, },
accessLevels: s.accessLevels, accessLevels: state.accessLevels,
}) })
} }

View file

@ -70,6 +70,7 @@
hierarchy: $store.hierarchy, hierarchy: $store.hierarchy,
} }
$: selectedComponentId = $store.currentComponentInfo._id
</script> </script>
<div class="component-container"> <div class="component-container">
@ -84,6 +85,11 @@
<style> <style>
${styles || ''} ${styles || ''}
.pos-${selectedComponentId} {
border: 2px solid #0055ff;
}
body, html { body, html {
height: 100%!important; height: 100%!important;
} }

View file

@ -1,28 +1,28 @@
<script> <script>
import IconButton from "../../common/IconButton.svelte"; import IconButton from "../../common/IconButton.svelte"
import PlusButton from "../../common/PlusButton.svelte"; import PlusButton from "../../common/PlusButton.svelte"
import Select from "../../common/Select.svelte"; import Select from "../../common/Select.svelte"
import Input from "../../common/Input.svelte"; import StateBindingCascader from "./StateBindingCascader.svelte"
import StateBindingControl from "../StateBindingControl.svelte"; import StateBindingControl from "../StateBindingControl.svelte"
import { find, map, keys, reduce, keyBy } from "lodash/fp"; import { find, map, keys, reduce, keyBy } from "lodash/fp"
import { pipe, userWithFullAccess } from "../../common/core"; import { pipe, userWithFullAccess } from "../../common/core"
import { import {
EVENT_TYPE_MEMBER_NAME, EVENT_TYPE_MEMBER_NAME,
allHandlers, allHandlers,
} from "../../common/eventHandlers"; } from "../../common/eventHandlers"
import { store } from "../../builderStore"; import { store } from "../../builderStore"
export let handler; export let handler
export let onCreate; export let onCreate
export let onChanged; export let onChanged
export let onRemoved; export let onRemoved
export let index; export let index
export let newHandler; export let newHandler
let eventOptions; let eventOptions
let handlerType; let handlerType
let parameters = []; let parameters = []
$: eventOptions = allHandlers( $: eventOptions = allHandlers(
{ hierarchy: $store.hierarchy }, { hierarchy: $store.hierarchy },
@ -30,53 +30,77 @@
hierarchy: $store.hierarchy, hierarchy: $store.hierarchy,
actions: keyBy("name")($store.actions), actions: keyBy("name")($store.actions),
}) })
); )
$: { $: {
if (handler) { if (handler) {
handlerType = handler[EVENT_TYPE_MEMBER_NAME]; handlerType = handler[EVENT_TYPE_MEMBER_NAME]
parameters = Object.entries(handler.parameters).map(([name, value]) => ({ parameters = Object.entries(handler.parameters).map(([name, value]) => ({
name, name,
value, value,
})); }))
} else { } else {
// Empty Handler // Empty Handler
handlerType = ""; handlerType = ""
parameters = []; parameters = []
} }
} }
const handlerChanged = (type, params) => { const handlerChanged = (type, params) => {
const handlerParams = {}; const handlerParams = {}
for (let param of params) { for (let param of params) {
handlerParams[param.name] = param.value; handlerParams[param.name] = param.value
if (param.value.startsWith("context")) {
const [_, contextKey] = param.value.split(".");
handlerParams[param.name] = {
"##bbstate": contextKey,
"##bbsource": "context",
"##bbstatefallback": "balls to that",
}
console.log("Param starts with context", param.value);
};
if (param.value.startsWith("event")) {
const [_, eventKey] = param.value.split(".");
handlerParams[param.name] = {
"##bbstate": eventKey,
"##bbsource": "event",
"##bbstatefallback": "balls to that",
}
};
} }
console.log(type, params, handlerParams);
const updatedHandler = { const updatedHandler = {
[EVENT_TYPE_MEMBER_NAME]: type, [EVENT_TYPE_MEMBER_NAME]: type,
parameters: handlerParams, parameters: handlerParams,
}; }
onChanged(updatedHandler, index); onChanged(updatedHandler, index)
}; }
const handlerTypeChanged = e => { const handlerTypeChanged = e => {
const handlerType = eventOptions.find( const handlerType = eventOptions.find(
handler => handler.name === e.target.value handler => handler.name === e.target.value
); )
const defaultParams = handlerType.parameters.map(param => ({ const defaultParams = handlerType.parameters.map(param => ({
name: param, name: param,
value: "", value: "",
})); }))
handlerChanged(handlerType.name, defaultParams); handlerChanged(handlerType.name, defaultParams)
}; }
const onParameterChanged = index => e => { const onParameterChanged = index => e => {
const newParams = [...parameters]; const value = e.target.value
newParams[index].value = e.target.value; const newParams = [...parameters]
handlerChanged(handlerType, newParams); newParams[index].value = e.target.value
}; handlerChanged(handlerType, newParams)
}
</script> </script>
<div class="type-selector-container {newHandler && 'new-handler'}"> <div class="type-selector-container {newHandler && 'new-handler'}">
@ -91,11 +115,8 @@
</Select> </Select>
</div> </div>
{#if parameters} {#if parameters}
{#each parameters as param, idx} {#each parameters as parameter, idx}
<div class="handler-option"> <StateBindingCascader on:change={onParameterChanged(idx)} {parameter} />
<span>{param.name}</span>
<Input on:change={onParameterChanged(idx)} value={param.value} />
</div>
{/each} {/each}
{/if} {/if}
</div> </div>

View file

@ -0,0 +1,33 @@
<script>
import IconButton from "../../common/IconButton.svelte"
import PlusButton from "../../common/PlusButton.svelte"
import Select from "../../common/Select.svelte"
import Input from "../../common/Input.svelte"
import StateBindingControl from "../StateBindingControl.svelte"
import { find, map, keys, reduce, keyBy } from "lodash/fp"
import { pipe, userWithFullAccess } from "../../common/core"
import {
EVENT_TYPE_MEMBER_NAME,
allHandlers,
} from "../../common/eventHandlers"
import { store } from "../../builderStore"
export let parameter
</script>
<div class="handler-option">
<span>{parameter.name}</span>
<Input on:change value={parameter.value} />
</div>
<style>
.handler-option {
display: flex;
flex-direction: column;
}
span {
font-size: 12px;
margin-bottom: 5px;
}
</style>

View file

@ -15,7 +15,7 @@
let bindingSource = "store" let bindingSource = "store"
let bindingValue = "" let bindingValue = ""
const bind = (path, fallback, source) => { const bindValueToSource = (path, fallback, source) => {
if (!path) { if (!path) {
onChanged(fallback) onChanged(fallback)
return return
@ -25,12 +25,12 @@
} }
const setBindingPath = value => const setBindingPath = value =>
bind(value, bindingFallbackValue, bindingSource) bindValueToSource(value, bindingFallbackValue, bindingSource)
const setBindingFallback = value => bind(bindingPath, value, bindingSource) const setBindingFallback = value => bindValueToSource(bindingPath, value, bindingSource)
const setBindingSource = value => const setBindingSource = source =>
bind(bindingPath, bindingFallbackValue, value) bindValueToSource(bindingPath, bindingFallbackValue, source)
$: { $: {
const binding = getBinding(value) const binding = getBinding(value)

View file

@ -4,7 +4,7 @@ import { listRecords } from "./listRecords"
import { authenticate } from "./authenticate" import { authenticate } from "./authenticate"
import { saveRecord } from "./saveRecord" import { saveRecord } from "./saveRecord"
export const createApi = ({ rootPath, setState, getState }) => { export const createApi = ({ rootPath = "", setState, getState }) => {
const apiCall = method => ({ const apiCall = method => ({
url, url,
body, body,

View file

@ -2,6 +2,10 @@ import { createApp } from "./createApp"
import { trimSlash } from "./common/trimSlash" import { trimSlash } from "./common/trimSlash"
import { builtins, builtinLibName } from "./render/builtinComponents" import { builtins, builtinLibName } from "./render/builtinComponents"
/**
* create a web application from static budibase definition files.
* @param {object} opts - configuration options for budibase client libary
*/
export const loadBudibase = async (opts) => { export const loadBudibase = async (opts) => {
let componentLibraries = opts && opts.componentLibraries let componentLibraries = opts && opts.componentLibraries

View file

@ -21,7 +21,7 @@ export const eventHandlers = (store, coreApi, rootPath, routeTo) => {
}) })
const api = createApi({ const api = createApi({
rootPath: rootPath, rootPath,
setState: setStateWithStore, setState: setStateWithStore,
getState: (path, fallback) => getState(currentState, path, fallback), getState: (path, fallback) => getState(currentState, path, fallback),
}) })

View file

@ -5,28 +5,29 @@ export const setState = (store, path, value) => {
if (!path || path.length === 0) return if (!path || path.length === 0) return
const pathParts = path.split(".") const pathParts = path.split(".")
const safeSetPath = (obj, currentPartIndex = 0) => {
const safeSetPath = (state, currentPartIndex = 0) => {
const currentKey = pathParts[currentPartIndex] const currentKey = pathParts[currentPartIndex]
if (pathParts.length - 1 == currentPartIndex) { if (pathParts.length - 1 == currentPartIndex) {
obj[currentKey] = value state[currentKey] = value
return return
} }
if ( if (
obj[currentKey] === null || state[currentKey] === null ||
obj[currentKey] === undefined || state[currentKey] === undefined ||
!isObject(obj[currentKey]) !isObject(obj[currentKey])
) { ) {
obj[currentKey] = {} state[currentKey] = {}
} }
safeSetPath(obj[currentKey], currentPartIndex + 1) safeSetPath(state[currentKey], currentPartIndex + 1)
} }
store.update(s => { store.update(state => {
safeSetPath(s) safeSetPath(state)
return s return state
}) })
} }

View file

@ -0,0 +1,174 @@
import { setupBinding } from "../src/state/stateBinding"
import {
BB_STATE_BINDINGPATH,
BB_STATE_FALLBACK,
BB_STATE_BINDINGSOURCE,
} from "../src/state/isState"
import { EVENT_TYPE_MEMBER_NAME } from "../src/state/eventHandlers"
import { writable } from "svelte/store"
import { isFunction } from "lodash/fp"
describe("setupBinding", () => {
it("should correctly create initials props, including fallback values", () => {
const { store, props, component } = testSetup()
const { initialProps } = testSetupBinding(store, props, component)
expect(initialProps.boundWithFallback).toBe("Bob")
expect(initialProps.boundNoFallback).toBeUndefined()
expect(initialProps.unbound).toBe("hello")
expect(isFunction(initialProps.eventBound)).toBeTruthy()
initialProps.eventBound()
})
it("should update component bound props when store is updated", () => {
const { component, store, props } = testSetup()
const { bind } = testSetupBinding(store, props, component)
bind(component)
store.update(s => {
s.FirstName = "Bobby"
s.LastName = "Thedog"
s.Customer = {
Name: "ACME inc",
Address: "",
}
s.addressToSet = "123 Main Street"
return s
})
expect(component.props.boundWithFallback).toBe("Bobby")
expect(component.props.boundNoFallback).toBe("Thedog")
expect(component.props.multiPartBound).toBe("ACME inc")
})
it("should not update unbound props when store is updated", () => {
const { component, store, props } = testSetup()
const { bind } = testSetupBinding(store, props, component)
bind(component)
store.update(s => {
s.FirstName = "Bobby"
s.LastName = "Thedog"
s.Customer = {
Name: "ACME inc",
Address: "",
}
s.addressToSet = "123 Main Street"
return s
})
expect(component.props.unbound).toBe("hello")
})
it("should update event handlers on state change", () => {
const { component, store, props } = testSetup()
const { bind } = testSetupBinding(store, props, component)
bind(component)
expect(component.props.boundToEventOutput).toBe("initial address")
component.props.eventBound()
expect(component.props.boundToEventOutput).toBe("event fallback address")
store.update(s => {
s.addressToSet = "123 Main Street"
return s
})
component.props.eventBound()
expect(component.props.boundToEventOutput).toBe("123 Main Street")
})
it("event handlers should recognise event parameter", () => {
const { component, store, props } = testSetup()
const { bind } = testSetupBinding(store, props, component)
bind(component)
expect(component.props.boundToEventOutput).toBe("initial address")
component.props.eventBoundUsingEventParam({
addressOverride: "Overridden Address",
})
expect(component.props.boundToEventOutput).toBe("Overridden Address")
store.update(s => {
s.addressToSet = "123 Main Street"
return s
})
component.props.eventBound()
expect(component.props.boundToEventOutput).toBe("123 Main Street")
component.props.eventBoundUsingEventParam({
addressOverride: "Overridden Address",
})
expect(component.props.boundToEventOutput).toBe("Overridden Address")
})
it("should bind initial props to supplied context", () => {
const { component, store, props } = testSetup()
const { bind } = testSetupBinding(store, props, component, {
ContextValue: "Real Context Value",
})
bind(component)
expect(component.props.boundToContext).toBe("Real Context Value")
})
});
const testSetupBinding = (store, props, component, context) => {
const setup = setupBinding(store, props, undefined, context)
component.props = setup.initialProps // svelte does this for us in real life
return setup
}
const testSetup = () => {
const c = {}
c.props = {}
c.$set = propsToSet => {
for (let pname in propsToSet) c.props[pname] = propsToSet[pname]
}
const binding = (path, fallback, source) => ({
[BB_STATE_BINDINGPATH]: path,
[BB_STATE_FALLBACK]: fallback,
[BB_STATE_BINDINGSOURCE]: source || "store"
});
const event = (handlerType, parameters) => ({
[EVENT_TYPE_MEMBER_NAME]: handlerType,
parameters
});
const props = {
boundWithFallback: binding("FirstName", "Bob"),
boundNoFallback: binding("LastName"),
unbound: "hello",
multiPartBound: binding("Customer.Name", "ACME"),
boundToEventOutput: binding("Customer.Address", "initial address"),
boundToContext: binding("ContextValue", "context fallback", "context"),
eventBound: [
event("Set State", {
path: "Customer.Address",
value: binding("addressToSet", "event fallback address"),
}),
],
eventBoundUsingEventParam: [
event("Set State", {
path: "Customer.Address",
value: binding("addressOverride", "", "event"),
}),
],
}
return {
component: c,
store: writable({}),
props,
}
}

View file

@ -74,6 +74,7 @@
"tel", "time", "week"], "tel", "time", "week"],
"default":"text" "default":"text"
}, },
"onChange": "event",
"className": "string" "className": "string"
}, },
"tags": ["form"] "tags": ["form"]

View file

@ -3080,7 +3080,7 @@ kind-of@^6.0.0, kind-of@^6.0.2:
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd"
integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
lerna@^3.14.1: lerna@^3.20.2:
version "3.20.2" version "3.20.2"
resolved "https://registry.yarnpkg.com/lerna/-/lerna-3.20.2.tgz#abf84e73055fe84ee21b46e64baf37b496c24864" resolved "https://registry.yarnpkg.com/lerna/-/lerna-3.20.2.tgz#abf84e73055fe84ee21b46e64baf37b496c24864"
integrity sha512-bjdL7hPLpU3Y8CBnw/1ys3ynQMUjiK6l9iDWnEGwFtDy48Xh5JboR9ZJwmKGCz9A/sarVVIGwf1tlRNKUG9etA== integrity sha512-bjdL7hPLpU3Y8CBnw/1ys3ynQMUjiK6l9iDWnEGwFtDy48Xh5JboR9ZJwmKGCz9A/sarVVIGwf1tlRNKUG9etA==