diff --git a/packages/client/src/createApp.js b/packages/client/src/createApp.js index f279cda5ae..accad9f51e 100644 --- a/packages/client/src/createApp.js +++ b/packages/client/src/createApp.js @@ -1,59 +1,13 @@ -import { - split, - last -} from "lodash/fp"; import {writable} from "svelte/store"; -import { $ } from "./core/common"; -import { - setupBinding -} from "./state/stateBinding"; import { createCoreApi } from "./core"; import { getStateOrValue } from "./state/getState"; import { setState, setStateFromBinding } from "./state/setState"; import { trimSlash } from "./common/trimSlash"; import { isBound } from "./state/isState"; +import { _initialiseChildren } from "./render/initialiseChildren"; -export const createApp = (componentLibraries, appDefinition, user) => { - - const _initialiseChildren = (parentContext, hydrate) => (childrenProps, htmlElement, context, anchor=null) => { - - const childComponents = []; - - if(hydrate) { - while (htmlElement.firstChild) { - htmlElement.removeChild(htmlElement.firstChild); - } - } - - for(let childProps of childrenProps) { - const {componentName, libName} = splitName(childProps._component); - - if(!componentName || !libName) return; - - const {initialProps, bind} = setupBinding( - store, childProps, coreApi, - context || parentContext, appDefinition.appRootPath); - +export const createApp = (componentLibraries, appDefinition, user, uiFunctions) => { - const componentProps = { - ...initialProps, - _bb:bb(context || parentContext, childProps) - }; - - const component = new (componentLibraries[libName][componentName])({ - target: htmlElement, - props: componentProps, - hydrate:false, - anchor - }); - - bind(component); - childComponents.push(component); - } - - return childComponents; - } - const coreApi = createCoreApi(appDefinition, user); appDefinition.hierarchy = coreApi.templateApi.constructHierarchy(appDefinition.hierarchy); const store = writable({ @@ -82,10 +36,10 @@ export const createApp = (componentLibraries, appDefinition, user) => { }); const api = { - post: apiCall("POST"), - get: apiCall("GET"), - patch: apiCall("PATCH"), - delete:apiCall("DELETE") + post: apiCall("POST"), + get: apiCall("GET"), + patch: apiCall("PATCH"), + delete: apiCall("DELETE") }; const safeCallEvent = (event, context) => { @@ -96,11 +50,18 @@ export const createApp = (componentLibraries, appDefinition, user) => { if(isFunction(event)) event(context); } + const initialiseChildrenParams = (parentContext, hydrate) => ({ + bb, coreApi, store, + componentLibraries, appDefinition, + parentContext, hydrate, uiFunctions + }); + const bb = (context, props) => ({ - hydrateChildren: _initialiseChildren(context, true), - appendChildren: _initialiseChildren(context, false), + hydrateChildren: _initialiseChildren(initialiseChildrenParams(context, true)), + appendChildren: _initialiseChildren(initialiseChildrenParams(context, false)), insertChildren: (props, htmlElement, anchor, context) => - _initialiseChildren(context, false)(props, htmlElement, context, anchor), + _initialiseChildren(initialiseChildrenParams(context, false)) + (props, htmlElement, context, anchor), store, relativeUrl, api, @@ -160,17 +121,3 @@ const buildBindings = (boundProps, boundArrays, contextBoundProps) => { return bindings; } - -const splitName = fullname => { - const componentName = $(fullname, [ - split("/"), - last - ]); - - const libName =fullname.substring( - 0, fullname.length - componentName.length - 1); - - return {libName, componentName}; -} - - diff --git a/packages/client/src/index.js b/packages/client/src/index.js index f7003b816b..e75f9a08ab 100644 --- a/packages/client/src/index.js +++ b/packages/client/src/index.js @@ -1,7 +1,9 @@ import { createApp } from "./createApp"; import { trimSlash } from "./common/trimSlash"; -export const loadBudibase = async ({componentLibraries, props, window, localStorage}) => { +export const loadBudibase = async ({ + componentLibraries, props, + window, localStorage, uiFunctions }) => { const appDefinition = window["##BUDIBASE_APPDEFINITION##"]; @@ -33,7 +35,7 @@ export const loadBudibase = async ({componentLibraries, props, window, localStor props = appDefinition.props; } - const _app = createApp(componentLibraries, appDefinition, user); + const _app = createApp(componentLibraries, appDefinition, user, uiFunctions); _app.hydrateChildren( [props], window.document.body); diff --git a/packages/client/src/render/initialiseChildren.js b/packages/client/src/render/initialiseChildren.js new file mode 100644 index 0000000000..50a0052642 --- /dev/null +++ b/packages/client/src/render/initialiseChildren.js @@ -0,0 +1,67 @@ +import { + setupBinding +} from "../state/stateBinding"; +import { + split, + last +} from "lodash/fp"; +import { $ } from "../core/common"; +import { renderComponent } from "./renderComponent"; + +export const _initialiseChildren = (initialiseOpts) => + (childrenProps, htmlElement, context, anchor=null) => { + + const { uiFunctions, bb, coreApi, + store, componentLibraries, + appDefinition, parentContext, hydrate } = initialiseOpts; + + const childComponents = []; + + if(hydrate) { + while (htmlElement.firstChild) { + htmlElement.removeChild(htmlElement.firstChild); + } + } + + for(let childProps of childrenProps) { + + const {componentName, libName} = splitName(childProps._component); + + if(!componentName || !libName) return; + + const {initialProps, bind} = setupBinding( + store, childProps, coreApi, + context || parentContext, appDefinition.appRootPath); + + /// here needs to go inside renderComponent ??? + const componentProps = { + ...initialProps, + _bb:bb(context || parentContext, childProps) + }; + + const componentConstructor = componentLibraries[libName][componentName]; + + const {component} = renderComponent({ + componentConstructor,uiFunctions, + htmlElement, anchor, + parentContext, componentProps}); + + + bind(component); + childComponents.push(component); + } + + return childComponents; +} + +const splitName = fullname => { + const componentName = $(fullname, [ + split("/"), + last + ]); + + const libName =fullname.substring( + 0, fullname.length - componentName.length - 1); + + return {libName, componentName}; +} \ No newline at end of file diff --git a/packages/client/src/render/renderComponent.js b/packages/client/src/render/renderComponent.js new file mode 100644 index 0000000000..890ed4037f --- /dev/null +++ b/packages/client/src/render/renderComponent.js @@ -0,0 +1,38 @@ + +export const renderComponent = ({ + componentConstructor, uiFunctions, + htmlElement, anchor, parentContext, + componentProps}) => { + + const func = uiFunctions[componentProps._id]; + let component; + let componentContext; + const render = (context) => { + + if(context) { + componentContext = {...componentContext}; + componentContext.$parent = parentContext; + } else { + componentContext = parentContext; + } + + component = new componentConstructor({ + target: htmlElement, + props: componentProps, + hydrate:false, + anchor + }); + } + + if(func) { + func(render, parentContext); + } else { + render(); + } + + return ({ + context: componentContext, + component + }); +} + diff --git a/packages/client/src/state/stateBinding.js b/packages/client/src/state/stateBinding.js index 57e9b82c92..86089a413a 100644 --- a/packages/client/src/state/stateBinding.js +++ b/packages/client/src/state/stateBinding.js @@ -15,6 +15,12 @@ import { const doNothing = () => {}; doNothing.isPlaceholder=true; +const isMetaProp = (propName) => + propName === "_component" + || propName === "_children" + || propName === "_id" + || propName === "_style"; + export const setupBinding = (store, rootProps, coreApi, context, rootPath) => { const rootInitialProps = {...rootProps}; @@ -24,13 +30,12 @@ export const setupBinding = (store, rootProps, coreApi, context, rootPath) => { const boundProps = []; const contextBoundProps = []; const componentEventHandlers = []; - const boundArrays = []; for(let propName in props) { - if(propName === "_component") continue; + if(isMetaProp(propName)) continue; - const val = initialProps[propName]; + const val = props[propName]; if(isBound(val) && takeStateFromStore(val)) { @@ -76,21 +81,10 @@ export const setupBinding = (store, rootProps, coreApi, context, rootPath) => { } initialProps[propName] = doNothing; - } else if(Array.isArray(val)) { - const arrayOfBindings = []; - for(let element of val){ - arrayOfBindings.push(getBindings(element, {...element})); - } - - boundArrays.push({ - arrayOfBindings, - propName - }); - } - + } } - return {contextBoundProps, boundProps, componentEventHandlers, boundArrays, initialProps}; + return {contextBoundProps, boundProps, componentEventHandlers, initialProps}; } @@ -98,8 +92,7 @@ export const setupBinding = (store, rootProps, coreApi, context, rootPath) => { const bind = (rootBindings) => (component) => { if(rootBindings.boundProps.length === 0 - && rootBindings.componentEventHandlers.length === 0 - && rootBindings.boundArrays.length === 0) return; + && rootBindings.componentEventHandlers.length === 0) return; const handlerTypes = eventHandlers(store, coreApi, rootPath); @@ -108,7 +101,7 @@ export const setupBinding = (store, rootProps, coreApi, context, rootPath) => { const getPropsFromBindings = (s, bindings) => { - const {boundProps, componentEventHandlers, boundArrays} = bindings; + const {boundProps, componentEventHandlers} = bindings; const newProps = {...bindings.initialProps}; for(let boundProp of boundProps) { @@ -159,18 +152,6 @@ export const setupBinding = (store, rootProps, coreApi, context, rootPath) => { } - for(let boundArray of boundArrays) { - let index = 0; - if(!newProps[boundArray.propName]) - newProps[boundArray.propName] = []; - for(let bindings of boundArray.arrayOfBindings){ - newProps[boundArray.propName][index] = getPropsFromBindings( - s, - bindings); - index++; - } - } - return newProps; } @@ -189,7 +170,6 @@ export const setupBinding = (store, rootProps, coreApi, context, rootPath) => { initialProps:rootInitialProps, bind:bind(bindings), boundProps:bindings.boundProps, - boundArrays: bindings.boundArrays, contextBoundProps: bindings.contextBoundProps }; diff --git a/packages/client/tests/domControlFlow.spec.js b/packages/client/tests/domControlFlow.spec.js new file mode 100644 index 0000000000..7239480ca9 --- /dev/null +++ b/packages/client/tests/domControlFlow.spec.js @@ -0,0 +1,74 @@ +import { load } from "./testAppDef"; + +describe("controlFlow", () => { + + it("should display simple div, with always true render function", async () => { + + const {dom} = await load({ + _component: "testlib/div", + className: "my-test-class", + _id: "always_render" + }); + + expect(dom.window.document.body.children.length).toBe(1); + const child = dom.window.document.body.children[0]; + expect(child.className).toBe("my-test-class"); + + }) + + it("should not display div, with always false render function", async () => { + + const {dom} = await load({ + _component: "testlib/div", + className: "my-test-class", + _id: "never_render" + }); + + expect(dom.window.document.body.children.length).toBe(0); + + }) + + it("should display 3 divs in a looped render function", async () => { + + const {dom} = await load({ + _component: "testlib/div", + className: "my-test-class", + _id: "three_clones" + }); + + expect(dom.window.document.body.children.length).toBe(3); + + const child0 = dom.window.document.body.children[0]; + expect(child0.className).toBe("my-test-class"); + + const child1 = dom.window.document.body.children[1]; + expect(child1.className).toBe("my-test-class"); + + const child2 = dom.window.document.body.children[2]; + expect(child2.className).toBe("my-test-class"); + + }) + + it("should display 3 div, in a looped render, as children", async () => { + const {dom} = await load({ + _component: "testlib/div", + _children: [ + { + _component: "testlib/div", + className: "my-test-class", + _id: "three_clones" + } + ] + }); + + expect(dom.window.document.body.children.length).toBe(1); + + const rootDiv = dom.window.document.body.children[0]; + expect(rootDiv.children.length).toBe(3); + + expect(rootDiv.children[0].className).toBe("my-test-class"); + expect(rootDiv.children[1].className).toBe("my-test-class"); + expect(rootDiv.children[2].className).toBe("my-test-class"); + }) + +}); diff --git a/packages/client/tests/stateBinding.spec.js b/packages/client/tests/stateBinding.spec.js index f34be14419..b5f33a9fd6 100644 --- a/packages/client/tests/stateBinding.spec.js +++ b/packages/client/tests/stateBinding.spec.js @@ -74,59 +74,6 @@ describe("setupBinding", () => { }); - it("should update bound array props when 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"; - s.ArrayVal1 = "item 1 - version 1"; - s.ArrayVal2 = "item 2 - version 1"; - s.ArrayVal3 = "inner array item"; - return s; - }); - - expect(component.props.arrayWithInnerBinding[0].innerBound).toBe("item 1 - version 1"); - expect(component.props.arrayWithInnerBinding[1].innerBound).toBe("item 2 - version 1"); - expect(component.props.arrayWithInnerBinding[0].innerUnbound).toBe("not bound 1"); - expect(component.props.arrayWithInnerBinding[1].innerUnbound).toBe("not bound 2"); - - }); - - it("should update bound nested (2nd level) array props when 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"; - s.ArrayVal1 = "item 1 - version 1"; - s.ArrayVal2 = "item 2 - version 1"; - s.ArrayVal3 = "inner array item"; - return s; - }); - - expect(component.props.arrayWithInnerBinding[2].innerArray[0].innerInnerBound).toBe("inner array item"); - - }); - it("should update event handlers on state change", () => { const {component, store, props} = testSetup(); @@ -235,23 +182,6 @@ const testSetup = () => { path: "Customer.Address", value: binding("addressOverride", "", "event") }) - ], - arrayWithInnerBinding: [ - { - innerBound: binding("ArrayVal1"), - innerUnbound: "not bound 1" - }, - { - innerBound: binding("ArrayVal2"), - innerUnbound: "not bound 2" - }, - { - innerArray: [ - { - innerInnerBound: binding("ArrayVal3") - } - ] - } ] } diff --git a/packages/client/tests/testAppDef.js b/packages/client/tests/testAppDef.js index 0a08978784..e028730890 100644 --- a/packages/client/tests/testAppDef.js +++ b/packages/client/tests/testAppDef.js @@ -3,16 +3,32 @@ import { loadBudibase } from "../src/index"; export const load = async (props) => { const dom = new JSDOM(``); + autoAssignIds(props); setAppDef(dom.window, props); const app = await loadBudibase({ componentLibraries: allLibs(dom.window), window: dom.window, localStorage: createLocalStorage(), - props + props, + uiFunctions }); return {dom, app}; } +// this happens for real by the builder... +// ..this only assigns _ids when missing +const autoAssignIds = (props, count=0) => { + if(!props._id) { + props._id = `auto_id_${count}`; + } + if(props._children) { + for(let child of props._children) { + count += 1; + autoAssignIds(child, count); + } + } +} + const setAppDef = (window, props) => { window["##BUDIBASE_APPDEFINITION##"] = ({ componentLibraries: [], @@ -87,5 +103,18 @@ const maketestlib = (window) => ({ } }); +const uiFunctions = ({ + never_render : (render, parentContext) => {}, + + always_render : (render, parentContext) => { + render(); + }, + + three_clones : (render, parentContext) => { + for(let i = 0; i<3; i++) { + render(); + } + } +});