mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
19 changed files with 194 additions and 468 deletions
@ -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" |
|||
@ -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, |
|||
} |
|||
} |
|||
@ -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] |
|||
@ -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, |
|||
} |
|||
} |
|||
Loading…
Reference in new issue