forked from tsai/budibase
15 changed files with 131 additions and 457 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