bindings can now be just a string (store.someValue)

This commit is contained in:
Michael Shanks 2020-02-21 14:44:48 +00:00
parent 098a851cdb
commit ab0db65048
15 changed files with 131 additions and 457 deletions

View File

@ -4,13 +4,11 @@ import {
BB_STATE_BINDINGPATH,
BB_STATE_FALLBACK,
BB_STATE_BINDINGSOURCE,
} from "@budibase/client/src/state/isState"
isBound,
parseBinding,
} from "@budibase/client/src/state/parseBinding"
export const isBinding = value =>
!isString(value) &&
value &&
isString(value[BB_STATE_BINDINGPATH]) &&
value[BB_STATE_BINDINGPATH].length > 0
export const isBinding = isBound
export const setBinding = ({ path, fallback, source }, binding = {}) => {
if (isNonEmptyString(path)) binding[BB_STATE_BINDINGPATH] = path
@ -19,10 +17,6 @@ export const setBinding = ({ path, fallback, source }, binding = {}) => {
return binding
}
export const getBinding = binding => ({
path: binding[BB_STATE_BINDINGPATH] || "",
fallback: binding[BB_STATE_FALLBACK] || "",
source: binding[BB_STATE_BINDINGSOURCE] || "store",
})
export const getBinding = parseBinding
const isNonEmptyString = s => isString(s) && s.length > 0

View File

@ -14,7 +14,7 @@ import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers"
import {
isBound,
BB_STATE_BINDINGPATH,
} from "@budibase/client/src/state/isState"
} from "@budibase/client/src/state/parseBinding"
const defaultDef = typeName => () => ({
type: typeName,

View File

@ -1,6 +1,6 @@
import { createProps } from "../src/userInterface/pagesParsing/createProps"
import { keys, some } from "lodash/fp"
import { BB_STATE_BINDINGPATH } from "@budibase/client/src/state/isState"
import { BB_STATE_BINDINGPATH } from "@budibase/client/src/state/parseBinding"
import { stripStandardProps } from "./testData"
describe("createDefaultProps", () => {

View File

@ -31,6 +31,7 @@ export const createApp = (
componentLibraries,
uiFunctions,
onScreenSlotRendered: () => {},
routeTo,
})
const getAttchChildrenParams = attachChildrenParams(stateManager)
screenSlotNode.props._children = [screen.props]
@ -69,6 +70,8 @@ export const createApp = (
componentLibraries,
uiFunctions,
onScreenSlotRendered,
// seems weird, but the routeTo variable may not be available at this point
routeTo: url => routeTo(url),
})
const initialisePage = (page, target, urlPath) => {

View File

@ -1,7 +1,7 @@
import { getStateOrValue } from "./getState"
import { setState, setStateFromBinding } from "./setState"
import { trimSlash } from "../common/trimSlash"
import { isBound } from "./isState"
import { isBound } from "./parseBinding"
import { attachChildren } from "../render/attachChildren"
export const bbFactory = ({

View File

@ -7,7 +7,7 @@ import { getNewChildRecordToState, getNewRecordToState } from "./coreHandlers"
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
export const eventHandlers = (store, coreApi, rootPath) => {
export const eventHandlers = (store, coreApi, rootPath, routeTo) => {
const handler = (parameters, execute) => ({
execute,
parameters,
@ -44,6 +44,8 @@ export const eventHandlers = (store, coreApi, rootPath) => {
getNewRecordToState(coreApi, setStateWithStore)
),
"Navigate To": handler(["url"], param => routeTo(param && param.url)),
Authenticate: handler(["username", "password"], api.authenticate),
}
}

View File

@ -1,10 +1,5 @@
import { isUndefined, isObject } from "lodash/fp"
import {
isBound,
BB_STATE_BINDINGPATH,
BB_STATE_FALLBACK,
takeStateFromStore,
} from "./isState"
import { parseBinding, isStoreBinding } from "./parseBinding"
export const getState = (s, path, fallback) => {
if (!s) return fallback
@ -39,20 +34,12 @@ export const getState = (s, path, fallback) => {
export const getStateOrValue = (globalState, prop, currentContext) => {
if (!prop) return prop
if (isBound(prop)) {
const stateToUse = takeStateFromStore(prop) ? globalState : currentContext
const binding = parseBinding(prop)
return getState(
stateToUse,
prop[BB_STATE_BINDINGPATH],
prop[BB_STATE_FALLBACK]
)
}
if (binding) {
const stateToUse = isStoreBinding(binding) ? globalState : currentContext
if (prop.path && prop.source) {
const stateToUse = prop.source === "store" ? globalState : currentContext
return getState(stateToUse, prop.path, prop.fallback)
return getState(stateToUse, binding.path, binding.fallback)
}
return prop

View File

@ -1,16 +0,0 @@
export const BB_STATE_BINDINGPATH = "##bbstate"
export const BB_STATE_BINDINGSOURCE = "##bbsource"
export const BB_STATE_FALLBACK = "##bbstatefallback"
export const isBound = prop =>
prop !== undefined && prop[BB_STATE_BINDINGPATH] !== undefined
export const takeStateFromStore = prop =>
prop[BB_STATE_BINDINGSOURCE] === undefined ||
prop[BB_STATE_BINDINGSOURCE] === "store"
export const takeStateFromContext = prop =>
prop[BB_STATE_BINDINGSOURCE] === "context"
export const takeStateFromEventParameters = prop =>
prop[BB_STATE_BINDINGSOURCE] === "event"

View File

@ -0,0 +1,59 @@
export const BB_STATE_BINDINGPATH = "##bbstate"
export const BB_STATE_BINDINGSOURCE = "##bbsource"
export const BB_STATE_FALLBACK = "##bbstatefallback"
export const isBound = prop => !!parseBinding(prop)
export const parseBinding = prop => {
if (!prop) return false
if (isBindingExpression(prop)) {
return parseBindingExpression(prop)
}
if (isAlreadyBinding(prop)) {
return {
path: prop.path,
source: prop.source || "store",
fallback: prop.fallback,
}
}
if (hasBindingObject(prop)) {
return {
path: prop[BB_STATE_BINDINGPATH],
fallback: prop[BB_STATE_FALLBACK] || "",
source: prop[BB_STATE_BINDINGSOURCE] || "store",
}
}
}
export const isStoreBinding = binding => binding && binding.source === "store"
export const isContextBinding = binding => binding && binding.source === "context"
export const isEventBinding = binding => binding && binding.source === "event"
const hasBindingObject = prop =>
typeof prop === "object" && prop[BB_STATE_BINDINGPATH] !== undefined
const isAlreadyBinding = prop => typeof prop === "object" && prop.path
const isBindingExpression = prop =>
typeof prop === "string" &&
(prop.startsWith("store.") ||
prop.startsWith("context.") ||
prop.startsWith("event.") ||
prop.startsWith("route."))
const parseBindingExpression = prop => {
let source = prop.split(".")[0]
let path = prop.replace(`${source}.`, "")
if (source === "route") {
source = "store"
path = `##routeParams.${path}`
}
const fallback = ""
return {
fallback,
source,
path,
}
}

View File

@ -1,5 +1,5 @@
import { isObject } from "lodash/fp"
import { BB_STATE_BINDINGPATH } from "./isState"
import { parseBinding } from "./parseBinding"
export const setState = (store, path, value) => {
if (!path || path.length === 0) return
@ -30,5 +30,8 @@ export const setState = (store, path, value) => {
})
}
export const setStateFromBinding = (store, binding, value) =>
setState(store, binding[BB_STATE_BINDINGPATH], value)
export const setStateFromBinding = (store, binding, value) => {
const parsedBinding = parseBinding(binding)
if (!parsedBinding) return
return setState(store, parsedBinding.path, value)
}

View File

@ -1,179 +0,0 @@
import {
isEventType,
eventHandlers,
EVENT_TYPE_MEMBER_NAME,
} from "./eventHandlers"
import { getState } from "./getState"
import {
isBound,
takeStateFromStore,
takeStateFromContext,
takeStateFromEventParameters,
BB_STATE_FALLBACK,
BB_STATE_BINDINGPATH,
BB_STATE_BINDINGSOURCE,
} from "./isState"
const doNothing = () => {}
doNothing.isPlaceholder = true
const isMetaProp = propName =>
propName === "_component" ||
propName === "_children" ||
propName === "_id" ||
propName === "_style" ||
propName === "_code"
export const setupBinding = (store, rootProps, coreApi, context, rootPath) => {
const rootInitialProps = { ...rootProps }
const getBindings = (props, initialProps) => {
const boundProps = []
const contextBoundProps = []
const componentEventHandlers = []
for (let propName in props) {
if (isMetaProp(propName)) continue
const val = props[propName]
if (isBound(val) && takeStateFromStore(val)) {
const binding = BindingPath(val)
const source = BindingSource(val)
const fallback = BindingFallback(val)
boundProps.push({
path: binding,
fallback,
propName,
source,
})
initialProps[propName] = fallback
} else if (isBound(val) && takeStateFromContext(val)) {
const binding = BindingPath(val)
const fallback = BindingFallback(val)
const source = BindingSource(val)
contextBoundProps.push({
path: binding,
fallback,
propName,
source,
})
initialProps[propName] = !context
? val
: getState(context, binding, fallback, source)
} else if (isEventType(val)) {
const handlers = { propName, handlers: [] }
componentEventHandlers.push(handlers)
for (let e of val) {
handlers.handlers.push({
handlerType: e[EVENT_TYPE_MEMBER_NAME],
parameters: e.parameters,
})
}
initialProps[propName] = doNothing
}
}
return {
contextBoundProps,
boundProps,
componentEventHandlers,
initialProps,
}
}
const bind = rootBindings => component => {
if (
rootBindings.boundProps.length === 0 &&
rootBindings.componentEventHandlers.length === 0
)
return
const handlerTypes = eventHandlers(store, coreApi, rootPath)
const unsubscribe = store.subscribe(rootState => {
const getPropsFromBindings = (s, bindings) => {
const { boundProps, componentEventHandlers } = bindings
const newProps = { ...bindings.initialProps }
for (let boundProp of boundProps) {
const val = getState(s, boundProp.path, boundProp.fallback)
if (val === undefined && newProps[boundProp.propName] !== undefined) {
delete newProps[boundProp.propName]
}
if (val !== undefined) {
newProps[boundProp.propName] = val
}
}
for (let boundHandler of componentEventHandlers) {
const closuredHandlers = []
for (let h of boundHandler.handlers) {
const handlerType = handlerTypes[h.handlerType]
closuredHandlers.push(eventContext => {
const parameters = {}
for (let pname in h.parameters) {
const p = h.parameters[pname]
parameters[pname] = !isBound(p)
? p
: takeStateFromStore(p)
? getState(s, p[BB_STATE_BINDINGPATH], p[BB_STATE_FALLBACK])
: takeStateFromEventParameters(p)
? getState(
eventContext,
p[BB_STATE_BINDINGPATH],
p[BB_STATE_FALLBACK]
)
: takeStateFromContext(p)
? getState(
context,
p[BB_STATE_BINDINGPATH],
p[BB_STATE_FALLBACK]
)
: p[BB_STATE_FALLBACK]
}
handlerType.execute(parameters)
})
}
newProps[boundHandler.propName] = async context => {
for (let runHandler of closuredHandlers) {
await runHandler(context)
}
}
}
return newProps
}
const rootNewProps = getPropsFromBindings(rootState, rootBindings)
component.$set(rootNewProps)
})
return unsubscribe
}
const bindings = getBindings(rootProps, rootInitialProps)
return {
initialProps: rootInitialProps,
bind: bind(bindings),
boundProps: bindings.boundProps,
contextBoundProps: bindings.contextBoundProps,
}
}
const BindingPath = prop => prop[BB_STATE_BINDINGPATH]
const BindingFallback = prop => prop[BB_STATE_FALLBACK]
const BindingSource = prop => prop[BB_STATE_BINDINGSOURCE]

View File

@ -7,15 +7,7 @@ import { bbFactory } from "./bbComponentApi"
import { getState } from "./getState"
import { attachChildren } from "../render/attachChildren"
import {
isBound,
takeStateFromStore,
takeStateFromContext,
takeStateFromEventParameters,
BB_STATE_FALLBACK,
BB_STATE_BINDINGPATH,
BB_STATE_BINDINGSOURCE,
} from "./isState"
import { parseBinding } from "./parseBinding"
const doNothing = () => {}
doNothing.isPlaceholder = true
@ -36,8 +28,9 @@ export const createStateManager = ({
componentLibraries,
uiFunctions,
onScreenSlotRendered,
routeTo,
}) => {
let handlerTypes = eventHandlers(store, coreApi, rootPath)
let handlerTypes = eventHandlers(store, coreApi, rootPath, routeTo)
let currentState
// any nodes that have props that are bound to the store
@ -180,35 +173,25 @@ const _setup = (
const val = props[propName]
if (isBound(val) && takeStateFromStore(val)) {
const path = BindingPath(val)
const source = BindingSource(val)
const fallback = BindingFallback(val)
const binding = parseBinding(val)
const isBound = !!binding
if (isBound) binding.propName = propName
storeBoundProps.push({
path,
fallback,
propName,
source,
})
if (isBound && binding.source === "store") {
storeBoundProps.push(binding)
initialProps[propName] = !currentStoreState
? fallback
? binding.fallback
: getState(
currentStoreState,
BindingPath(val),
BindingFallback(val),
BindingSource(val)
binding.path,
binding.fallback,
binding.source
)
} else if (isBound(val) && takeStateFromContext(val)) {
} else if (isBound && binding.source === "context") {
initialProps[propName] = !context
? val
: getState(
context,
BindingPath(val),
BindingFallback(val),
BindingSource(val)
)
: getState(context, binding.path, binding.fallback, binding.source)
} else if (isEventType(val)) {
const handlersInfos = []
for (let e of val) {
@ -219,31 +202,28 @@ const _setup = (
const resolvedParams = {}
for (let paramName in handlerInfo.parameters) {
const paramValue = handlerInfo.parameters[paramName]
if (!isBound(paramValue)) {
const paramBinding = parseBinding(paramValue)
if (!paramBinding) {
resolvedParams[paramName] = () => paramValue
continue
} else if (takeStateFromContext(paramValue)) {
} else if (paramBinding.source === "context") {
const val = getState(
context,
paramValue[BB_STATE_BINDINGPATH],
paramValue[BB_STATE_FALLBACK]
paramBinding.path,
paramBinding.fallback
)
resolvedParams[paramName] = () => val
} else if (takeStateFromStore(paramValue)) {
} else if (paramBinding.source === "store") {
resolvedParams[paramName] = () =>
getState(
getCurrentState(),
paramValue[BB_STATE_BINDINGPATH],
paramValue[BB_STATE_FALLBACK]
paramBinding.path,
paramBinding.fallback
)
continue
} else if (takeStateFromEventParameters(paramValue)) {
} else if (paramBinding.source === "event") {
resolvedParams[paramName] = eventContext => {
getState(
eventContext,
paramValue[BB_STATE_BINDINGPATH],
paramValue[BB_STATE_FALLBACK]
)
getState(eventContext, paramBinding.path, paramBinding.fallback)
}
}
}
@ -282,7 +262,3 @@ const makeHandler = (handlerTypes, handlerInfo) => {
handlerType.execute(parameters)
}
}
const BindingPath = prop => prop[BB_STATE_BINDINGPATH]
const BindingFallback = prop => prop[BB_STATE_FALLBACK]
const BindingSource = prop => prop[BB_STATE_BINDINGSOURCE]

View File

@ -39,6 +39,23 @@ describe("initialiseApp (binding)", () => {
expect(rootDiv.className.includes("newvalue")).toBe(true)
})
it("should update root element from store, using binding expression", async () => {
const { dom, app } = await load(
makePage({
_component: "testlib/div",
className: "store.divClassName",
})
)
app.pageStore().update(s => {
s.divClassName = "newvalue"
return s
})
const rootDiv = dom.window.document.body.children[0]
expect(rootDiv.className.includes("newvalue")).toBe(true)
})
it("should populate child component with store value", async () => {
const { dom } = await load(
makePage({

View File

@ -1,177 +0,0 @@
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) => {
const b = {}
b[BB_STATE_BINDINGPATH] = path
b[BB_STATE_FALLBACK] = fallback
b[BB_STATE_BINDINGSOURCE] = source || "store"
return b
}
const event = (handlerType, parameters) => {
const e = {}
e[EVENT_TYPE_MEMBER_NAME] = handlerType
e.parameters = parameters
return e
}
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

@ -1,9 +1,14 @@
export default ({ indexes, helpers }) =>
indexes.map(i => ({
name: `Table based on index: ${i.name} `,
props: tableProps(i, helpers.indexSchema(i)),
props: tableProps(
i,
helpers.indexSchema(i).filter(c => !excludedColumns.includes(c.name))
),
}))
const excludedColumns = ["id", "key", "sortKey", "type", "isNew"]
const tableProps = (index, indexSchema) => ({
_component: "@budibase/materialdesign-components/Datatable",
_children: [