Client Lib > Ability to inject code around initialise children (#68)

* removed binding references to array type

* refactored initialiseChildren into seperate file

* render function, with code blocks - tested simple cases

* few mores tests for control flow

* md components - getting TestApp to work

* new render wrapper - bug fix
This commit is contained in:
Michael Shanks 2020-01-28 14:14:53 +00:00 committed by GitHub
parent 449b173605
commit 6cb523478d
8 changed files with 248 additions and 174 deletions

View File

@ -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};
}

View File

@ -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,11 @@ 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);

View File

@ -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};
}

View File

@ -0,0 +1,41 @@
export const renderComponent = ({
componentConstructor, uiFunctions,
htmlElement, anchor, parentContext,
componentProps}) => {
const func = componentProps._id
? uiFunctions[componentProps._id]
: undefined;
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
});
}

View File

@ -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
};

View File

@ -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");
})
});

View File

@ -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")
}
]
}
]
}

View File

@ -3,16 +3,32 @@ import { loadBudibase } from "../src/index";
export const load = async (props) => {
const dom = new JSDOM(`<!DOCTYPE html><html><body></body><html>`);
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();
}
}
});