Client Api - New state binding method (#105)

* new binding...
- state manager
- one store per screen
- not passing

* client lib binding - tests passing

* binding fully working again post stateManager

* bugfix with button component

* Control flow ("code") now working, tests passing

* Events List now reading from component definition

* fix to button.svelte - missing props._children
This commit is contained in:
Michael Shanks 2020-02-18 12:29:38 +00:00 committed by GitHub
parent d9496fd8fa
commit 4089b52c53
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 743 additions and 155 deletions

View File

@ -0,0 +1,17 @@
export const insertCodeMetadata = props => {
if (props._code && props._code.length > 0) {
props._codeMeta = codeMetaData(props._code)
}
if (!props._children || props._children.length === 0) return
for (let child of props._children) {
insertCodeMetadata(child)
}
}
const codeMetaData = code => {
return {
dependsOnStore: RegExp(/(store.)/g).test(code),
}
}

View File

@ -39,6 +39,7 @@ import {
} from "./loadComponentLibraries"
import { buildCodeForScreens } from "./buildCodeForScreens"
import { generate_screen_css } from "./generate_css"
import { insertCodeMetadata } from "./insertCodeMetadata"
// import { uuid } from "./uuid"
let appname = ""
@ -818,6 +819,8 @@ const setCurrentScreenFunctions = s => {
s.currentPreviewItem === "screen"
? buildCodeForScreens([s.currentPreviewItem])
: "({});"
insertCodeMetadata(s.currentPreviewItem.props)
}
const setScreenType = store => type => {

View File

@ -11,15 +11,17 @@
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers"
export let event
export let eventOptions
export let eventOptions = []
export let open
export let onClose
export let onPropChanged
let eventType = "onClick"
let eventType = ""
let draftEventHandler = { parameters: [] }
$: eventData = event || { handlers: [] }
$: if (!eventOptions.includes(eventType) && eventOptions.length > 0)
eventType = eventOptions[0].name
const closeModal = () => {
onClose()
@ -74,7 +76,7 @@
<h5>Event Type</h5>
{@html getIcon('info', 20)}
</header>
<Select :value={eventType}>
<Select bind:value={eventType}>
{#each eventOptions as option}
<option value={option.name}>{option.name}</option>
{/each}

View File

@ -33,17 +33,11 @@
let events = []
let selectedEvent = null
$: {
events = Object.keys(component)
.filter(key => findType(key) === EVENT_TYPE)
.map(key => ({ name: key, handlers: component[key] }))
}
function findType(propName) {
if (!component._component) return
return components.find(({ name }) => name === component._component)
.props[propName]
const componentDefinition = components.find(c => c.name === component._component)
events = Object.keys(componentDefinition.props)
.filter(propName => componentDefinition.props[propName].type === EVENT_TYPE)
.map(propName => ({ name: propName, handlers: (component[propName] || []) }))
}
const openModal = event => {

View File

@ -36,6 +36,7 @@
"dependencies": {
"@nx-js/compiler-util": "^2.0.0",
"bcryptjs": "^2.4.3",
"deep-equal": "^2.0.1",
"lodash": "^4.17.15",
"lunr": "^2.3.5",
"regexparam": "^1.3.0",

View File

@ -1,74 +1,46 @@
import { writable } from "svelte/store"
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 { attachChildren } from "./render/attachChildren"
import { createTreeNode } from "./render/renderComponent"
import { createTreeNode } from "./render/prepareRenderComponent"
import { screenRouter } from "./render/screenRouter"
import { createStateManager } from "./state/stateManager"
export const createApp = (
document,
componentLibraries,
frontendDefinition,
backendDefinition,
user,
uiFunctions
uiFunctions,
window
) => {
const coreApi = createCoreApi(backendDefinition, user)
backendDefinition.hierarchy = coreApi.templateApi.constructHierarchy(
backendDefinition.hierarchy
)
const pageStore = writable({
_bbuser: user,
})
const relativeUrl = url =>
frontendDefinition.appRootPath
? frontendDefinition.appRootPath + "/" + trimSlash(url)
: url
const apiCall = method => (url, body) =>
fetch(relativeUrl(url), {
method: method,
headers: {
"Content-Type": "application/json",
},
body: body && JSON.stringify(body),
})
const api = {
post: apiCall("POST"),
get: apiCall("GET"),
patch: apiCall("PATCH"),
delete: apiCall("DELETE"),
}
const safeCallEvent = (event, context) => {
const isFunction = obj =>
!!(obj && obj.constructor && obj.call && obj.apply)
if (isFunction(event)) event(context)
}
let routeTo
let currentScreenStore
let currentScreenUbsubscribe
let currentUrl
let screenStateManager
const onScreenSlotRendered = screenSlotNode => {
const onScreenSelected = (screen, store, url) => {
const { getInitialiseParams, unsubscribe } = attachChildrenParams(store)
const stateManager = createStateManager({
store,
coreApi,
frontendDefinition,
componentLibraries,
uiFunctions,
onScreenSlotRendered: () => {},
})
const getAttchChildrenParams = attachChildrenParams(stateManager)
screenSlotNode.props._children = [screen.props]
const initialiseChildParams = getInitialiseParams(screenSlotNode)
const initialiseChildParams = getAttchChildrenParams(screenSlotNode)
attachChildren(initialiseChildParams)(screenSlotNode.rootElement, {
hydrate: true,
force: true,
})
if (currentScreenUbsubscribe) currentScreenUbsubscribe()
currentScreenUbsubscribe = unsubscribe
currentScreenStore = store
if (screenStateManager) screenStateManager.destroy()
screenStateManager = stateManager
currentUrl = url
}
@ -76,46 +48,28 @@ export const createApp = (
routeTo(currentUrl || window.location.pathname)
}
const attachChildrenParams = store => {
let currentState = null
const unsubscribe = store.subscribe(s => {
currentState = s
})
const attachChildrenParams = stateManager => {
const getInitialiseParams = treeNode => ({
bb: getBbClientApi,
coreApi,
store,
document,
componentLibraries,
frontendDefinition,
uiFunctions,
treeNode,
onScreenSlotRendered,
setupState: stateManager.setup,
getCurrentState: stateManager.getCurrentState,
})
const getBbClientApi = (treeNode, componentProps) => {
return {
attachChildren: attachChildren(getInitialiseParams(treeNode)),
context: treeNode.context,
props: componentProps,
call: safeCallEvent,
setStateFromBinding: (binding, value) =>
setStateFromBinding(store, binding, value),
setState: (path, value) => setState(store, path, value),
getStateOrValue: (prop, currentContext) =>
getStateOrValue(currentState, prop, currentContext),
store,
relativeUrl,
api,
isBound,
parent,
}
}
return { getInitialiseParams, unsubscribe }
return getInitialiseParams
}
let rootTreeNode
const pageStateManager = createStateManager({
store: writable({ _bbuser: user }),
coreApi,
frontendDefinition,
componentLibraries,
uiFunctions,
onScreenSlotRendered,
})
const initialisePage = (page, target, urlPath) => {
currentUrl = urlPath
@ -125,7 +79,7 @@ export const createApp = (
_children: [page.props],
}
rootTreeNode.rootElement = target
const { getInitialiseParams } = attachChildrenParams(pageStore)
const getInitialiseParams = attachChildrenParams(pageStateManager)
const initChildParams = getInitialiseParams(rootTreeNode)
attachChildren(initChildParams)(target, {
@ -137,8 +91,8 @@ export const createApp = (
}
return {
initialisePage,
screenStore: () => currentScreenStore,
pageStore: () => pageStore,
screenStore: () => screenStateManager.store,
pageStore: () => pageStateManager.store,
routeTo: () => routeTo,
rootNode: () => rootTreeNode,
}

View File

@ -43,12 +43,12 @@ export const loadBudibase = async (opts) => {
componentLibraries[builtinLibName] = builtins(_window)
const { initialisePage, screenStore, pageStore, routeTo, rootNode } = createApp(
_window.document,
componentLibraries,
frontendDefinition,
backendDefinition,
user,
uiFunctions || {}
uiFunctions || {},
_window
)
const route = _window.location

View File

@ -1,19 +1,17 @@
import { setupBinding } from "../state/stateBinding"
import { split, last } from "lodash/fp"
import { $ } from "../core/common"
import { renderComponent } from "./renderComponent"
import { prepareRenderComponent } from "./prepareRenderComponent"
import { isScreenSlot } from "./builtinComponents"
import deepEqual from "deep-equal"
export const attachChildren = initialiseOpts => (htmlElement, options) => {
const {
uiFunctions,
bb,
coreApi,
store,
componentLibraries,
treeNode,
frontendDefinition,
onScreenSlotRendered,
setupState,
getCurrentState,
} = initialiseOpts
const anchor = options && options.anchor ? options.anchor : null
@ -34,50 +32,46 @@ export const attachChildren = initialiseOpts => (htmlElement, options) => {
htmlElement.classList.add(`lay-${treeNode.props._id}`)
const renderedComponents = []
const childNodes = []
for (let childProps of treeNode.props._children) {
const { componentName, libName } = splitName(childProps._component)
if (!componentName || !libName) return
const { initialProps, bind } = setupBinding(
store,
childProps,
coreApi,
frontendDefinition.appRootPath
)
const componentConstructor = componentLibraries[libName][componentName]
const renderedComponentsThisIteration = renderComponent({
const childNodesThisIteration = prepareRenderComponent({
props: childProps,
parentNode: treeNode,
componentConstructor,
uiFunctions,
htmlElement,
anchor,
initialProps,
bb,
getCurrentState
})
if (
onScreenSlotRendered &&
isScreenSlot(childProps._component) &&
renderedComponentsThisIteration.length > 0
) {
// assuming there is only ever one screen slot
onScreenSlotRendered(renderedComponentsThisIteration[0])
}
for (let comp of renderedComponentsThisIteration) {
comp.unsubscribe = bind(comp.component)
renderedComponents.push(comp)
for (let childNode of childNodesThisIteration) {
childNodes.push(childNode)
}
}
treeNode.children = renderedComponents
if (areTreeNodesEqual(treeNode.children, childNodes)) return treeNode.children
return renderedComponents
for (let node of childNodes) {
const initialProps = setupState(node)
node.render(initialProps)
}
const screenSlot = childNodes.find(n => isScreenSlot(n.props._component))
if (onScreenSlotRendered && screenSlot) {
// assuming there is only ever one screen slot
onScreenSlotRendered(screenSlot)
}
treeNode.children = childNodes
return childNodes
}
const splitName = fullname => {
@ -90,3 +84,19 @@ const splitName = fullname => {
return { libName, componentName }
}
const areTreeNodesEqual = (children1, children2) => {
if (children1.length !== children2.length) return false
if (children1 === children2) return true
let isEqual = false
for (let i = 0; i < children1.length; i++) {
isEqual = deepEqual(children1[i].context, children2[i].context)
if (!isEqual) return false
if (isScreenSlot(children1[i].parentNode.props._component)) {
isEqual = deepEqual(children1[i].props, children2[i].props)
}
if (!isEqual) return false
}
return true
}

View File

@ -1,22 +1,21 @@
export const renderComponent = ({
export const prepareRenderComponent = ({
componentConstructor,
uiFunctions,
htmlElement,
anchor,
props,
initialProps,
bb,
parentNode,
getCurrentState,
}) => {
const func = initialProps._id ? uiFunctions[initialProps._id] : undefined
const func = props._id ? uiFunctions[props._id] : undefined
const parentContext = (parentNode && parentNode.context) || {}
let renderedNodes = []
const render = context => {
let nodesToRender = []
const createNodeAndRender = context => {
let componentContext = parentContext
if (context) {
componentContext = { ...componentContext }
componentContext = { ...context }
componentContext.$parent = parentContext
}
@ -24,33 +23,31 @@ export const renderComponent = ({
thisNode.context = componentContext
thisNode.parentNode = parentNode
thisNode.props = props
nodesToRender.push(thisNode)
parentNode.children.push(thisNode)
renderedNodes.push(thisNode)
thisNode.render = initialProps => {
thisNode.component = new componentConstructor({
target: htmlElement,
props: initialProps,
hydrate: false,
anchor,
})
thisNode.rootElement =
htmlElement.children[htmlElement.children.length - 1]
initialProps._bb = bb(thisNode, props)
thisNode.component = new componentConstructor({
target: htmlElement,
props: initialProps,
hydrate: false,
anchor,
})
thisNode.rootElement = htmlElement.children[htmlElement.children.length - 1]
if (initialProps._id && thisNode.rootElement) {
thisNode.rootElement.classList.add(`pos-${initialProps._id}`)
if (props._id && thisNode.rootElement) {
thisNode.rootElement.classList.add(`pos-${props._id}`)
}
}
}
if (func) {
func(render, parentContext)
func(createNodeAndRender, parentContext, getCurrentState())
} else {
render()
createNodeAndRender()
}
return renderedNodes
return nodesToRender
}
export const createTreeNode = () => ({
@ -59,8 +56,10 @@ export const createTreeNode = () => ({
rootElement: null,
parentNode: null,
children: [],
bindings: [],
component: null,
unsubscribe: () => {},
render: () => {},
get destroy() {
const node = this
return () => {
@ -71,6 +70,10 @@ export const createTreeNode = () => ({
child.destroy()
}
}
for (let onDestroyItem of node.onDestroy) {
onDestroyItem()
}
}
},
onDestroy: [],
})

View File

@ -0,0 +1,70 @@
import { getStateOrValue } from "./getState"
import { setState, setStateFromBinding } from "./setState"
import { trimSlash } from "../common/trimSlash"
import { isBound } from "./isState"
import { attachChildren } from "../render/attachChildren"
export const bbFactory = ({
store,
getCurrentState,
frontendDefinition,
componentLibraries,
uiFunctions,
onScreenSlotRendered,
}) => {
const relativeUrl = url =>
frontendDefinition.appRootPath
? frontendDefinition.appRootPath + "/" + trimSlash(url)
: url
const apiCall = method => (url, body) =>
fetch(relativeUrl(url), {
method: method,
headers: {
"Content-Type": "application/json",
},
body: body && JSON.stringify(body),
})
const api = {
post: apiCall("POST"),
get: apiCall("GET"),
patch: apiCall("PATCH"),
delete: apiCall("DELETE"),
}
const safeCallEvent = (event, context) => {
const isFunction = obj =>
!!(obj && obj.constructor && obj.call && obj.apply)
if (isFunction(event)) event(context)
}
return (treeNode, setupState) => {
const attachParams = {
componentLibraries,
uiFunctions,
treeNode,
onScreenSlotRendered,
setupState,
getCurrentState,
}
return {
attachChildren: attachChildren(attachParams),
context: treeNode.context,
props: treeNode.props,
call: safeCallEvent,
setStateFromBinding: (binding, value) =>
setStateFromBinding(store, binding, value),
setState: (path, value) => setState(store, path, value),
getStateOrValue: (prop, currentContext) =>
getStateOrValue(getCurrentState(), prop, currentContext),
store: store,
relativeUrl,
api,
isBound,
parent,
}
}
}

View File

@ -23,7 +23,8 @@ const isMetaProp = propName =>
propName === "_component" ||
propName === "_children" ||
propName === "_id" ||
propName === "_style"
propName === "_style" ||
propName === "_code"
export const setupBinding = (store, rootProps, coreApi, context, rootPath) => {
const rootInitialProps = { ...rootProps }

View File

@ -0,0 +1,288 @@
import {
isEventType,
eventHandlers,
EVENT_TYPE_MEMBER_NAME,
} from "./eventHandlers"
import { bbFactory } from "./bbComponentApi"
import { getState } from "./getState"
import { attachChildren } from "../render/attachChildren"
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" ||
propName === "_codeMeta"
export const createStateManager = ({
store,
coreApi,
rootPath,
frontendDefinition,
componentLibraries,
uiFunctions,
onScreenSlotRendered,
}) => {
let handlerTypes = eventHandlers(store, coreApi, rootPath)
let currentState
// any nodes that have props that are bound to the store
let nodesBoundByProps = []
// any node whose children depend on code, that uses the store
let nodesWithCodeBoundChildren = []
const getCurrentState = () => currentState
const registerBindings = _registerBindings(
nodesBoundByProps,
nodesWithCodeBoundChildren
)
const bb = bbFactory({
store,
getCurrentState,
frontendDefinition,
componentLibraries,
uiFunctions,
onScreenSlotRendered,
})
const setup = _setup(handlerTypes, getCurrentState, registerBindings, bb)
const unsubscribe = store.subscribe(
onStoreStateUpdated({
setCurrentState: s => (currentState = s),
getCurrentState,
nodesWithCodeBoundChildren,
nodesBoundByProps,
uiFunctions,
componentLibraries,
onScreenSlotRendered,
setupState: setup,
})
)
return {
setup,
destroy: () => unsubscribe(),
getCurrentState,
store,
}
}
const onStoreStateUpdated = ({
setCurrentState,
getCurrentState,
nodesWithCodeBoundChildren,
nodesBoundByProps,
uiFunctions,
componentLibraries,
onScreenSlotRendered,
setupState,
}) => s => {
setCurrentState(s)
// the original array gets changed by components' destroy()
// so we make a clone and check if they are still in the original
const nodesWithBoundChildren_clone = [...nodesWithCodeBoundChildren]
for (let node of nodesWithBoundChildren_clone) {
if (!nodesWithCodeBoundChildren.includes(node)) continue
attachChildren({
uiFunctions,
componentLibraries,
treeNode: node,
onScreenSlotRendered,
setupState,
getCurrentState,
})(node.rootElement, { hydrate: true, force: true })
}
for (let node of nodesBoundByProps) {
setNodeState(s, node)
}
}
const _registerBindings = (nodesBoundByProps, nodesWithCodeBoundChildren) => (
node,
bindings
) => {
if (bindings.length > 0) {
node.bindings = bindings
nodesBoundByProps.push(node)
const onDestroy = () => {
nodesBoundByProps = nodesBoundByProps.filter(n => n === node)
node.onDestroy = node.onDestroy.filter(d => d === onDestroy)
}
node.onDestroy.push(onDestroy)
}
if (
node.props._children &&
node.props._children.filter(c => c._codeMeta && c._codeMeta.dependsOnStore)
.length > 0
) {
nodesWithCodeBoundChildren.push(node)
const onDestroy = () => {
nodesWithCodeBoundChildren = nodesWithCodeBoundChildren.filter(
n => n === node
)
node.onDestroy = node.onDestroy.filter(d => d === onDestroy)
}
node.onDestroy.push(onDestroy)
}
}
const setNodeState = (storeState, node) => {
if (!node.component) return
const newProps = { ...node.bindings.initialProps }
for (let binding of node.bindings) {
const val = getState(storeState, binding.path, binding.fallback)
if (val === undefined && newProps[binding.propName] !== undefined) {
delete newProps[binding.propName]
}
if (val !== undefined) {
newProps[binding.propName] = val
}
}
node.component.$set(newProps)
}
const _setup = (
handlerTypes,
getCurrentState,
registerBindings,
bb
) => node => {
const props = node.props
const context = node.context || {}
const initialProps = { ...props }
const storeBoundProps = []
const currentStoreState = getCurrentState()
for (let propName in props) {
if (isMetaProp(propName)) continue
const val = props[propName]
if (isBound(val) && takeStateFromStore(val)) {
const path = BindingPath(val)
const source = BindingSource(val)
const fallback = BindingFallback(val)
storeBoundProps.push({
path,
fallback,
propName,
source,
})
initialProps[propName] = !currentStoreState
? fallback
: getState(
currentStoreState,
BindingPath(val),
BindingFallback(val),
BindingSource(val)
)
} else if (isBound(val) && takeStateFromContext(val)) {
initialProps[propName] = !context
? val
: getState(
context,
BindingPath(val),
BindingFallback(val),
BindingSource(val)
)
} else if (isEventType(val)) {
const handlersInfos = []
for (let e of val) {
const handlerInfo = {
handlerType: e[EVENT_TYPE_MEMBER_NAME],
parameters: e.parameters,
}
const resolvedParams = {}
for (let paramName in handlerInfo.parameters) {
const paramValue = handlerInfo.parameters[paramName]
if (!isBound(paramValue)) {
resolvedParams[paramName] = () => paramValue
continue
} else if (takeStateFromContext(paramValue)) {
const val = getState(
context,
paramValue[BB_STATE_BINDINGPATH],
paramValue[BB_STATE_FALLBACK]
)
resolvedParams[paramName] = () => val
} else if (takeStateFromStore(paramValue)) {
resolvedParams[paramName] = () =>
getState(
getCurrentState(),
paramValue[BB_STATE_BINDINGPATH],
paramValue[BB_STATE_FALLBACK]
)
continue
} else if (takeStateFromEventParameters(paramValue)) {
resolvedParams[paramName] = eventContext => {
getState(
eventContext,
paramValue[BB_STATE_BINDINGPATH],
paramValue[BB_STATE_FALLBACK]
)
}
}
}
handlerInfo.parameters = resolvedParams
handlersInfos.push(handlerInfo)
}
if (handlersInfos.length === 0) initialProps[propName] = doNothing
else {
initialProps[propName] = async context => {
for (let handlerInfo of handlersInfos) {
const handler = makeHandler(handlerTypes, handlerInfo)
await handler(context)
}
}
}
}
}
registerBindings(node, storeBoundProps)
const setup = _setup(handlerTypes, getCurrentState, registerBindings, bb)
initialProps._bb = bb(node, setup)
return initialProps
}
const makeHandler = (handlerTypes, handlerInfo) => {
const handlerType = handlerTypes[handlerInfo.handlerType]
return context => {
const parameters = {}
for (let p in handlerInfo.parameters) {
parameters[p] = handlerInfo.parameters[p](context)
}
handlerType.execute(parameters)
}
}
const BindingPath = prop => prop[BB_STATE_BINDINGPATH]
const BindingFallback = prop => prop[BB_STATE_FALLBACK]
const BindingSource = prop => prop[BB_STATE_BINDINGSOURCE]

View File

@ -1,4 +1,5 @@
import { load, makePage, makeScreen } from "./testAppDef"
import { EVENT_TYPE_MEMBER_NAME } from "../src/state/eventHandlers"
describe("initialiseApp (binding)", () => {
it("should populate root element prop from store value", async () => {
@ -169,4 +170,177 @@ describe("initialiseApp (binding)", () => {
"header 2 - new val"
)
})
it("should fire events", async () => {
const { dom, app } = await load(
makePage({
_component: "testlib/button",
onClick: [
event("Set State", {
path: "address",
value: "123 Main Street",
}),
],
})
)
const button = dom.window.document.body.children[0]
expect(button.tagName).toBe("BUTTON")
let storeAddress
app.pageStore().subscribe(s => {
storeAddress = s.address
})
button.dispatchEvent(new dom.window.Event("click"))
expect(storeAddress).toBe("123 Main Street")
})
it("should alter event parameters based on store values", async () => {
const { dom, app } = await load(
makePage({
_component: "testlib/button",
onClick: [
event("Set State", {
path: "address",
value: {
"##bbstate": "sourceaddress",
"##bbsource": "store",
"##bbstatefallback": "fallback address",
},
}),
],
})
)
const button = dom.window.document.body.children[0]
expect(button.tagName).toBe("BUTTON")
let storeAddress
app.pageStore().subscribe(s => {
storeAddress = s.address
})
button.dispatchEvent(new dom.window.Event("click"))
expect(storeAddress).toBe("fallback address")
app.pageStore().update(s => {
s.sourceaddress = "new address"
return s
})
button.dispatchEvent(new dom.window.Event("click"))
expect(storeAddress).toBe("new address")
})
it("should take event parameters from context values", async () => {
const { dom, app } = await load(
makePage({
_component: "testlib/button",
_id: "with_context",
onClick: [
event("Set State", {
path: "address",
value: {
"##bbstate": "testKey",
"##bbsource": "context",
"##bbstatefallback": "fallback address",
},
}),
],
})
)
const button = dom.window.document.body.children[0]
expect(button.tagName).toBe("BUTTON")
let storeAddress
app.pageStore().subscribe(s => {
storeAddress = s.address
})
button.dispatchEvent(new dom.window.Event("click"))
expect(storeAddress).toBe("test value")
})
})
it("should rerender components when their code is bound to the store ", async () => {
const { dom, app } = await load(
makePage({
_component: "testlib/div",
_children: [
{
_component: "testlib/div",
_id: "n_clones_based_on_store",
className: "child_div",
},
],
})
)
const rootDiv = dom.window.document.body.children[0]
expect(rootDiv.tagName).toBe("DIV")
expect(rootDiv.children.length).toBe(0)
app.pageStore().update(s => {
s.componentCount = 3
return s
})
expect(rootDiv.children.length).toBe(3)
expect(rootDiv.children[0].className.includes("child_div")).toBe(true)
app.pageStore().update(s => {
s.componentCount = 5
return s
})
expect(rootDiv.children.length).toBe(5)
expect(rootDiv.children[0].className.includes("child_div")).toBe(true)
app.pageStore().update(s => {
s.componentCount = 0
return s
})
expect(rootDiv.children.length).toBe(0)
})
it("should be able to read value from context, passed fromm parent, through code", async () => {
const { dom, app } = await load(
makePage({
_component: "testlib/div",
_children: [
{
_component: "testlib/div",
_id: "n_clones_based_on_store",
className: {
"##bbstate": "index",
"##bbsource": "context",
"##bbstatefallback": "nothing",
},
},
],
})
)
const rootDiv = dom.window.document.body.children[0]
expect(rootDiv.tagName).toBe("DIV")
expect(rootDiv.children.length).toBe(0)
app.pageStore().update(s => {
s.componentCount = 3
return s
})
expect(rootDiv.children.length).toBe(3)
expect(rootDiv.children[0].className.includes("index_0")).toBe(true)
expect(rootDiv.children[1].className.includes("index_1")).toBe(true)
expect(rootDiv.children[2].className.includes("index_2")).toBe(true)
})
const event = (handlerType, parameters) => {
const e = {}
e[EVENT_TYPE_MEMBER_NAME] = handlerType
e.parameters = parameters
return e
}

View File

@ -15,6 +15,7 @@ export const load = async (page, screens = [], url = "/") => {
actions: [],
triggers: [],
})
setComponentCodeMeta(page, screens)
const app = await loadBudibase({
componentLibraries: allLibs(dom.window),
window: dom.window,
@ -47,8 +48,11 @@ export const timeout = ms => new Promise(resolve => setTimeout(resolve, ms))
export const walkComponentTree = (node, action) => {
action(node)
if (node.children) {
for (let child of node.children) {
// works for nodes or props
const children = node.children || node._children
if (children) {
for (let child of children) {
walkComponentTree(child, action)
}
}
@ -68,6 +72,22 @@ const autoAssignIds = (props, count = 0) => {
}
}
// any component with an id that include "based_on_store" is
// assumed to have code that depends on store value
const setComponentCodeMeta = (page, screens) => {
const setComponentCodeMeta_single = props => {
walkComponentTree(props, c => {
if (c._id.indexOf("based_on_store") >= 0) {
c._codeMeta = { dependsOnStore: true }
}
})
}
setComponentCodeMeta_single(page.props)
for (let s of screens || []) {
setComponentCodeMeta_single(s.props)
}
}
const setAppDef = (window, page, screens) => {
window["##BUDIBASE_FRONTEND_DEFINITION##"] = {
componentLibraries: [],
@ -148,6 +168,29 @@ const maketestlib = window => ({
set(opts.props)
opts.target.appendChild(node)
},
button: function(opts) {
const node = window.document.createElement("BUTTON")
let currentProps = { ...opts.props }
const set = props => {
currentProps = Object.assign(currentProps, props)
if (currentProps.onClick) {
node.addEventListener("click", () => {
const testText = currentProps.testText || "hello"
currentProps._bb.call(props.onClick, { testText })
})
}
}
this.$destroy = () => opts.target.removeChild(node)
this.$set = set
this._element = node
set(opts.props)
opts.target.appendChild(node)
},
})
const uiFunctions = {
@ -162,4 +205,15 @@ const uiFunctions = {
render()
}
},
with_context: render => {
render({ testKey: "test value" })
},
n_clones_based_on_store: (render, _, state) => {
const n = state.componentCount || 0
for (let i = 0; i < n; i++) {
render({ index: `index_${i}` })
}
},
}

View File

@ -14,6 +14,7 @@ const { join, resolve, dirname } = require("path")
const sqrl = require("squirrelly")
const { convertCssToFiles } = require("./convertCssToFiles")
const publicPath = require("./publicPath")
const deleteCodeMeta = require("./deleteCodeMeta")
module.exports = async (config, appname, pageName, pkg) => {
const appPath = appPackageFolder(config, appname)
@ -155,6 +156,8 @@ const savePageJson = async (appPath, pageName, pkg) => {
delete pkg.page._screens
}
deleteCodeMeta(pkg.page.props)
await writeJSON(pageFile, pkg.page, {
spaces: 2,
})

View File

@ -0,0 +1,9 @@
module.exports = props => {
if (props._codeMeta) {
delete props._codeMeta
}
for (let child of props._children || []) {
module.exports(child)
}
}

View File

@ -18,6 +18,7 @@ const buildPage = require("./buildPage")
const getPages = require("./getPages")
const listScreens = require("./listScreens")
const saveBackend = require("./saveBackend")
const deleteCodeMeta = require("./deleteCodeMeta")
module.exports.buildPage = buildPage
module.exports.listScreens = listScreens
@ -58,12 +59,15 @@ module.exports.saveScreen = async (config, appname, pagename, screen) => {
if (screen._css) {
delete screen._css
}
deleteCodeMeta(screen.props)
await writeJSON(compPath, screen, {
encoding: "utf8",
flag: "w",
spaces: 2,
})
return screen;
return screen
}
module.exports.renameScreen = async (

View File

@ -34,7 +34,8 @@
return all
}
$: if(_bb.props._children.length > 0) theButton && _bb.attachChildren(theButton)
$: if(_bb.props._children && _bb.props._children.length > 0)
theButton && _bb.attachChildren(theButton)
$: {
cssVariables = {
@ -73,7 +74,7 @@
disabled={disabled || false}
on:click={clickHandler}
style={buttonStyles}>
{#if _bb.props_children.length === 0}{contentText}{/if}
{#if !_bb.props._children || _bb.props._children.length === 0}{contentText}{/if}
</button>
<style>