fetchBindableProperties - complete

This commit is contained in:
Michael Shanks 2020-08-04 16:11:46 +01:00
parent 549327c885
commit e4bcd4020b
2 changed files with 156 additions and 55 deletions

View File

@ -1,3 +1,5 @@
import { cloneDeep, difference, fill } from "lodash"
const stubBindings = [
{
// type: instance represents a bindable property of a component
@ -10,7 +12,9 @@ const stubBindings = [
},
{
type: "context",
instance: { /** a component instance **/},
instance: {
/** a component instance **/
},
// how the binding expression persists, and is used in the app at runtime
runtimeBinding: "context._parent.<key of model/record>",
// how the binding exressions looks to the user of the builder
@ -19,74 +23,97 @@ const stubBindings = [
]
export default function({ componentInstanceId, screen, components, models }) {
const { target, targetAncestors, bindableInstances, bindableContexts } = walk(
{
instance: screen.props,
targetId: componentInstanceId,
components,
models,
}
)
const walkResult = walk({
// cloning so we are free to mutate props (e.g. by adding _contexts)
instance: cloneDeep(screen.props),
targetId: componentInstanceId,
components,
models,
})
return [
...bindableInstances
.filter(isComponentInstanceAvailable)
.map(componentInstanceToBindable),
...bindableContexts.map(contextToBindables),
...walkResult.bindableInstances
.filter(isInstanceInSharedContext(walkResult))
.map(componentInstanceToBindable(walkResult)),
...walkResult.target._contexts.map(contextToBindables(walkResult)).flat(),
]
}
const isComponentInstanceAvailable = i => true
const isInstanceInSharedContext = walkResult => i =>
// should cover
// - neither are in any context
// - both in same context
// - instance is in ancestor context of target
i.instance._contexts.length <= walkResult.target._contexts.length &&
difference(i.instance._contexts, walkResult.target._contexts).length === 0
// turns a component instance prop into binding expressions
// used by the UI
const componentInstanceToBindable = i => ({
type: "instance",
instance: i.instance,
// how the binding expression persists, and is used in the app at runtime
runtimeBinding: `state.${i.instance._id}.${i.prop}`,
// how the binding exressions looks to the user of the builder
readableBinding: `${i.instance._instanceName}`,
})
const componentInstanceToBindable = walkResult => i => {
const lastContext =
i.instance._contexts.length &&
i.instance._contexts[i.instance._contexts.length - 1]
const contextParentPath = lastContext
? getParentPath(walkResult, lastContext)
: ""
const contextToBindables = c => {
const contextParentNumber = 0
const contextParentPath = Array[contextParentNumber]
.map(() => "_parent")
.join(".")
return Object.keys(c.schema).map(k => ({
// if component is inside context, then the component lives
// in context at runtime (otherwise, in state)
const stateOrContext = lastContext ? "context" : "state"
return {
type: "instance",
instance: i.instance,
// how the binding expression persists, and is used in the app at runtime
runtimeBinding: `${stateOrContext}.${contextParentPath}${i.instance._id}.${i.prop}`,
// how the binding exressions looks to the user of the builder
readableBinding: `${i.instance._instanceName}`,
}
}
const contextToBindables = walkResult => c => {
const contextParentPath = getParentPath(walkResult, c)
return Object.keys(c.model.schema).map(k => ({
type: "context",
instance: c.instance,
// how the binding expression persists, and is used in the app at runtime
runtimeBinding: `context.${contextParentPath}.${k}`,
runtimeBinding: `context.${contextParentPath}data.${k}`,
// how the binding exressions looks to the user of the builder
readableBinding: `${c.instance._instanceName}.${c.schema.name}.${k}`,
readableBinding: `${c.instance._instanceName}.${c.model.name}.${k}`,
}))
}
const getParentPath = (walkResult, context) => {
// describes the number of "_parent" in the path
// clone array first so original array is not mtated
const contextParentNumber = [...walkResult.target._contexts]
.reverse()
.indexOf(context)
return (
new Array(contextParentNumber).fill("_parent").join(".") +
// trailing . if has parents
(contextParentNumber ? "." : "")
)
}
const walk = ({ instance, targetId, components, models, result }) => {
if (!result) {
result = {
currentAncestors: [],
currentContexts: [],
target: null,
targetAncestors: [],
bindableInstances: [],
bindableContexts: [],
parentMap: {},
allContexts: [],
currentContexts: [],
}
}
if (!instance._contexts) instance._contexts = []
// "component" is the component definition (object in component.json)
const component = components[instance._component]
const parentInstance =
result.currentAncestors.length > 0 &&
result.currentAncestors[result.currentAncestors.length - 1]
if (instance._id === targetId) {
// set currentParents to be result parents
result.targetAncestors = result.currentAncestors
result.bindableContexts = result.currentContexts
// found it
result.target = instance
} else {
@ -102,22 +129,24 @@ const walk = ({ instance, targetId, components, models, result }) => {
})
}
}
// a component that provides context to it's children
const contextualInstance = component.context && instance[component.context]
if (contextualInstance) {
// add to currentContexts (ancestory of context)
// before walking children
const schema = models.find(m => m._id === instance[component.context])
.schema
result.currentContexts.push({ instance, schema })
const model = models.find(m => m._id === instance[component.context])
result.currentContexts.push({ instance, model })
}
const currentContexts = [...result.currentContexts]
for (let child of instance._children || []) {
result.parentMap[child._id] = parentInstance._id
result.currentAncestors.push(instance)
// attaching _contexts of components, for eas comparison later
// these have been deep cloned above, so shouln't modify the
// original component instances
child._contexts = currentContexts
walk({ instance: child, targetId, components, models, result })
result.currentAncestors.pop()
}
if (contextualInstance) {

View File

@ -6,9 +6,7 @@ describe("fetch bindable properties", () => {
componentInstanceId: "heading-id",
...testData()
})
const componentBinding = result.find(r => r.instance._id === "search-input-id")
console.debug(componentBinding)
console.debug("result:" + JSON.stringify(result))
const componentBinding = result.find(r => r.instance._id === "search-input-id" && r.type === "instance")
expect(componentBinding).toBeDefined()
expect(componentBinding.type).toBe("instance")
expect(componentBinding.runtimeBinding).toBe("state.search-input-id.value")
@ -24,19 +22,73 @@ describe("fetch bindable properties", () => {
})
it("should return model schema, when inside a context", () => {
const result = fetchBindableProperties({
componentInstanceId: "list-item-input-id",
...testData()
})
const contextBindings = result.filter(r => r.instance._id === "list-id" && r.type==="context")
expect(contextBindings.length).toBe(2)
const namebinding = contextBindings.find(b => b.runtimeBinding === "context.data.name")
expect(namebinding).toBeDefined()
expect(namebinding.readableBinding).toBe("list-name.Test Model.name")
const descriptionbinding = contextBindings.find(b => b.runtimeBinding === "context.data.description")
expect(descriptionbinding).toBeDefined()
expect(descriptionbinding.readableBinding).toBe("list-name.Test Model.description")
})
it("should return model schema, for grantparent context", () => {
const result = fetchBindableProperties({
componentInstanceId: "child-list-item-input-id",
...testData()
})
const contextBindings = result.filter(r => r.type==="context")
expect(contextBindings.length).toBe(4)
const namebinding_parent = contextBindings.find(b => b.runtimeBinding === "context._parent.data.name")
expect(namebinding_parent).toBeDefined()
expect(namebinding_parent.readableBinding).toBe("list-name.Test Model.name")
const descriptionbinding_parent = contextBindings.find(b => b.runtimeBinding === "context._parent.data.description")
expect(descriptionbinding_parent).toBeDefined()
expect(descriptionbinding_parent.readableBinding).toBe("list-name.Test Model.description")
const namebinding_own = contextBindings.find(b => b.runtimeBinding === "context.data.name")
expect(namebinding_own).toBeDefined()
expect(namebinding_own.readableBinding).toBe("child-list-name.Test Model.name")
const descriptionbinding_own = contextBindings.find(b => b.runtimeBinding === "context.data.description")
expect(descriptionbinding_own).toBeDefined()
expect(descriptionbinding_own.readableBinding).toBe("child-list-name.Test Model.description")
})
it("should return bindable component props, from components in same context", () => {
const result = fetchBindableProperties({
componentInstanceId: "list-item-heading-id",
...testData()
})
const componentBinding = result.find(r => r.instance._id === "list-item-input-id" && r.type === "instance")
expect(componentBinding).toBeDefined()
expect(componentBinding.runtimeBinding).toBe("context.list-item-input-id.value")
})
it("should not return model props from child context", () => {
it("should not return components from child context", () => {
const result = fetchBindableProperties({
componentInstanceId: "list-item-heading-id",
...testData()
})
const componentBinding = result.find(r => r.instance._id === "child-list-item-input-id" && r.type === "instance")
expect(componentBinding).not.toBeDefined()
})
it("should return bindable component props, from components in same context (when nested context)", () => {
const result = fetchBindableProperties({
componentInstanceId: "child-list-item-heading-id",
...testData()
})
const componentBinding = result.find(r => r.instance._id === "child-list-item-input-id" && r.type === "instance")
expect(componentBinding).toBeDefined()
})
@ -83,6 +135,26 @@ const testData = () => {
_component: "@budibase/standard-components/input",
value: "list item"
},
{
_id: "child-list-id",
_component: "@budibase/standard-components/list",
_instanceName: "child-list-name",
model: "test-model-id",
_children: [
{
_id: "child-list-item-heading-id",
_instanceName: "child list item heading",
_component: "@budibase/standard-components/heading",
text: "hello"
},
{
_id: "child-list-item-input-id",
_instanceName: "Child List Item Input",
_component: "@budibase/standard-components/input",
value: "child list item"
},
]
},
]
},
]