fetchBindableProperties - complete

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

View File

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

View File

@ -6,9 +6,7 @@ describe("fetch bindable properties", () => {
componentInstanceId: "heading-id", componentInstanceId: "heading-id",
...testData() ...testData()
}) })
const componentBinding = result.find(r => r.instance._id === "search-input-id") const componentBinding = result.find(r => r.instance._id === "search-input-id" && r.type === "instance")
console.debug(componentBinding)
console.debug("result:" + JSON.stringify(result))
expect(componentBinding).toBeDefined() expect(componentBinding).toBeDefined()
expect(componentBinding.type).toBe("instance") expect(componentBinding.type).toBe("instance")
expect(componentBinding.runtimeBinding).toBe("state.search-input-id.value") 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", () => { 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", () => { 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", () => { 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", _component: "@budibase/standard-components/input",
value: "list item" 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"
},
]
},
] ]
}, },
] ]