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:
parent
d9496fd8fa
commit
4089b52c53
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,6 +39,7 @@ import {
|
||||||
} from "./loadComponentLibraries"
|
} from "./loadComponentLibraries"
|
||||||
import { buildCodeForScreens } from "./buildCodeForScreens"
|
import { buildCodeForScreens } from "./buildCodeForScreens"
|
||||||
import { generate_screen_css } from "./generate_css"
|
import { generate_screen_css } from "./generate_css"
|
||||||
|
import { insertCodeMetadata } from "./insertCodeMetadata"
|
||||||
// import { uuid } from "./uuid"
|
// import { uuid } from "./uuid"
|
||||||
|
|
||||||
let appname = ""
|
let appname = ""
|
||||||
|
@ -818,6 +819,8 @@ const setCurrentScreenFunctions = s => {
|
||||||
s.currentPreviewItem === "screen"
|
s.currentPreviewItem === "screen"
|
||||||
? buildCodeForScreens([s.currentPreviewItem])
|
? buildCodeForScreens([s.currentPreviewItem])
|
||||||
: "({});"
|
: "({});"
|
||||||
|
|
||||||
|
insertCodeMetadata(s.currentPreviewItem.props)
|
||||||
}
|
}
|
||||||
|
|
||||||
const setScreenType = store => type => {
|
const setScreenType = store => type => {
|
||||||
|
|
|
@ -11,15 +11,17 @@
|
||||||
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers"
|
import { EVENT_TYPE_MEMBER_NAME } from "../../common/eventHandlers"
|
||||||
|
|
||||||
export let event
|
export let event
|
||||||
export let eventOptions
|
export let eventOptions = []
|
||||||
export let open
|
export let open
|
||||||
export let onClose
|
export let onClose
|
||||||
export let onPropChanged
|
export let onPropChanged
|
||||||
|
|
||||||
let eventType = "onClick"
|
let eventType = ""
|
||||||
let draftEventHandler = { parameters: [] }
|
let draftEventHandler = { parameters: [] }
|
||||||
|
|
||||||
$: eventData = event || { handlers: [] }
|
$: eventData = event || { handlers: [] }
|
||||||
|
$: if (!eventOptions.includes(eventType) && eventOptions.length > 0)
|
||||||
|
eventType = eventOptions[0].name
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
onClose()
|
onClose()
|
||||||
|
@ -74,7 +76,7 @@
|
||||||
<h5>Event Type</h5>
|
<h5>Event Type</h5>
|
||||||
{@html getIcon('info', 20)}
|
{@html getIcon('info', 20)}
|
||||||
</header>
|
</header>
|
||||||
<Select :value={eventType}>
|
<Select bind:value={eventType}>
|
||||||
{#each eventOptions as option}
|
{#each eventOptions as option}
|
||||||
<option value={option.name}>{option.name}</option>
|
<option value={option.name}>{option.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -33,17 +33,11 @@
|
||||||
let events = []
|
let events = []
|
||||||
let selectedEvent = null
|
let selectedEvent = null
|
||||||
|
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
events = Object.keys(component)
|
const componentDefinition = components.find(c => c.name === component._component)
|
||||||
.filter(key => findType(key) === EVENT_TYPE)
|
events = Object.keys(componentDefinition.props)
|
||||||
.map(key => ({ name: key, handlers: component[key] }))
|
.filter(propName => componentDefinition.props[propName].type === EVENT_TYPE)
|
||||||
}
|
.map(propName => ({ name: propName, handlers: (component[propName] || []) }))
|
||||||
|
|
||||||
function findType(propName) {
|
|
||||||
if (!component._component) return
|
|
||||||
return components.find(({ name }) => name === component._component)
|
|
||||||
.props[propName]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const openModal = event => {
|
const openModal = event => {
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nx-js/compiler-util": "^2.0.0",
|
"@nx-js/compiler-util": "^2.0.0",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
"deep-equal": "^2.0.1",
|
||||||
"lodash": "^4.17.15",
|
"lodash": "^4.17.15",
|
||||||
"lunr": "^2.3.5",
|
"lunr": "^2.3.5",
|
||||||
"regexparam": "^1.3.0",
|
"regexparam": "^1.3.0",
|
||||||
|
|
|
@ -1,74 +1,46 @@
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
import { createCoreApi } from "./core"
|
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 { attachChildren } from "./render/attachChildren"
|
||||||
import { createTreeNode } from "./render/renderComponent"
|
import { createTreeNode } from "./render/prepareRenderComponent"
|
||||||
import { screenRouter } from "./render/screenRouter"
|
import { screenRouter } from "./render/screenRouter"
|
||||||
|
import { createStateManager } from "./state/stateManager"
|
||||||
|
|
||||||
export const createApp = (
|
export const createApp = (
|
||||||
document,
|
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
frontendDefinition,
|
frontendDefinition,
|
||||||
backendDefinition,
|
backendDefinition,
|
||||||
user,
|
user,
|
||||||
uiFunctions
|
uiFunctions,
|
||||||
|
window
|
||||||
) => {
|
) => {
|
||||||
const coreApi = createCoreApi(backendDefinition, user)
|
const coreApi = createCoreApi(backendDefinition, user)
|
||||||
backendDefinition.hierarchy = coreApi.templateApi.constructHierarchy(
|
backendDefinition.hierarchy = coreApi.templateApi.constructHierarchy(
|
||||||
backendDefinition.hierarchy
|
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 routeTo
|
||||||
let currentScreenStore
|
|
||||||
let currentScreenUbsubscribe
|
|
||||||
let currentUrl
|
let currentUrl
|
||||||
|
let screenStateManager
|
||||||
|
|
||||||
const onScreenSlotRendered = screenSlotNode => {
|
const onScreenSlotRendered = screenSlotNode => {
|
||||||
const onScreenSelected = (screen, store, url) => {
|
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]
|
screenSlotNode.props._children = [screen.props]
|
||||||
const initialiseChildParams = getInitialiseParams(screenSlotNode)
|
const initialiseChildParams = getAttchChildrenParams(screenSlotNode)
|
||||||
attachChildren(initialiseChildParams)(screenSlotNode.rootElement, {
|
attachChildren(initialiseChildParams)(screenSlotNode.rootElement, {
|
||||||
hydrate: true,
|
hydrate: true,
|
||||||
force: true,
|
force: true,
|
||||||
})
|
})
|
||||||
if (currentScreenUbsubscribe) currentScreenUbsubscribe()
|
if (screenStateManager) screenStateManager.destroy()
|
||||||
currentScreenUbsubscribe = unsubscribe
|
screenStateManager = stateManager
|
||||||
currentScreenStore = store
|
|
||||||
currentUrl = url
|
currentUrl = url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,46 +48,28 @@ export const createApp = (
|
||||||
routeTo(currentUrl || window.location.pathname)
|
routeTo(currentUrl || window.location.pathname)
|
||||||
}
|
}
|
||||||
|
|
||||||
const attachChildrenParams = store => {
|
const attachChildrenParams = stateManager => {
|
||||||
let currentState = null
|
|
||||||
const unsubscribe = store.subscribe(s => {
|
|
||||||
currentState = s
|
|
||||||
})
|
|
||||||
|
|
||||||
const getInitialiseParams = treeNode => ({
|
const getInitialiseParams = treeNode => ({
|
||||||
bb: getBbClientApi,
|
|
||||||
coreApi,
|
|
||||||
store,
|
|
||||||
document,
|
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
frontendDefinition,
|
|
||||||
uiFunctions,
|
uiFunctions,
|
||||||
treeNode,
|
treeNode,
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
|
setupState: stateManager.setup,
|
||||||
|
getCurrentState: stateManager.getCurrentState,
|
||||||
})
|
})
|
||||||
|
|
||||||
const getBbClientApi = (treeNode, componentProps) => {
|
return getInitialiseParams
|
||||||
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 }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let rootTreeNode
|
let rootTreeNode
|
||||||
|
const pageStateManager = createStateManager({
|
||||||
|
store: writable({ _bbuser: user }),
|
||||||
|
coreApi,
|
||||||
|
frontendDefinition,
|
||||||
|
componentLibraries,
|
||||||
|
uiFunctions,
|
||||||
|
onScreenSlotRendered,
|
||||||
|
})
|
||||||
|
|
||||||
const initialisePage = (page, target, urlPath) => {
|
const initialisePage = (page, target, urlPath) => {
|
||||||
currentUrl = urlPath
|
currentUrl = urlPath
|
||||||
|
@ -125,7 +79,7 @@ export const createApp = (
|
||||||
_children: [page.props],
|
_children: [page.props],
|
||||||
}
|
}
|
||||||
rootTreeNode.rootElement = target
|
rootTreeNode.rootElement = target
|
||||||
const { getInitialiseParams } = attachChildrenParams(pageStore)
|
const getInitialiseParams = attachChildrenParams(pageStateManager)
|
||||||
const initChildParams = getInitialiseParams(rootTreeNode)
|
const initChildParams = getInitialiseParams(rootTreeNode)
|
||||||
|
|
||||||
attachChildren(initChildParams)(target, {
|
attachChildren(initChildParams)(target, {
|
||||||
|
@ -137,8 +91,8 @@ export const createApp = (
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
initialisePage,
|
initialisePage,
|
||||||
screenStore: () => currentScreenStore,
|
screenStore: () => screenStateManager.store,
|
||||||
pageStore: () => pageStore,
|
pageStore: () => pageStateManager.store,
|
||||||
routeTo: () => routeTo,
|
routeTo: () => routeTo,
|
||||||
rootNode: () => rootTreeNode,
|
rootNode: () => rootTreeNode,
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,12 +43,12 @@ export const loadBudibase = async (opts) => {
|
||||||
componentLibraries[builtinLibName] = builtins(_window)
|
componentLibraries[builtinLibName] = builtins(_window)
|
||||||
|
|
||||||
const { initialisePage, screenStore, pageStore, routeTo, rootNode } = createApp(
|
const { initialisePage, screenStore, pageStore, routeTo, rootNode } = createApp(
|
||||||
_window.document,
|
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
frontendDefinition,
|
frontendDefinition,
|
||||||
backendDefinition,
|
backendDefinition,
|
||||||
user,
|
user,
|
||||||
uiFunctions || {}
|
uiFunctions || {},
|
||||||
|
_window
|
||||||
)
|
)
|
||||||
|
|
||||||
const route = _window.location
|
const route = _window.location
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
import { setupBinding } from "../state/stateBinding"
|
|
||||||
import { split, last } from "lodash/fp"
|
import { split, last } from "lodash/fp"
|
||||||
import { $ } from "../core/common"
|
import { $ } from "../core/common"
|
||||||
import { renderComponent } from "./renderComponent"
|
import { prepareRenderComponent } from "./prepareRenderComponent"
|
||||||
import { isScreenSlot } from "./builtinComponents"
|
import { isScreenSlot } from "./builtinComponents"
|
||||||
|
import deepEqual from "deep-equal"
|
||||||
|
|
||||||
export const attachChildren = initialiseOpts => (htmlElement, options) => {
|
export const attachChildren = initialiseOpts => (htmlElement, options) => {
|
||||||
const {
|
const {
|
||||||
uiFunctions,
|
uiFunctions,
|
||||||
bb,
|
|
||||||
coreApi,
|
|
||||||
store,
|
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
treeNode,
|
treeNode,
|
||||||
frontendDefinition,
|
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
|
setupState,
|
||||||
|
getCurrentState,
|
||||||
} = initialiseOpts
|
} = initialiseOpts
|
||||||
|
|
||||||
const anchor = options && options.anchor ? options.anchor : null
|
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}`)
|
htmlElement.classList.add(`lay-${treeNode.props._id}`)
|
||||||
|
|
||||||
const renderedComponents = []
|
const childNodes = []
|
||||||
for (let childProps of treeNode.props._children) {
|
for (let childProps of treeNode.props._children) {
|
||||||
const { componentName, libName } = splitName(childProps._component)
|
const { componentName, libName } = splitName(childProps._component)
|
||||||
|
|
||||||
if (!componentName || !libName) return
|
if (!componentName || !libName) return
|
||||||
|
|
||||||
const { initialProps, bind } = setupBinding(
|
|
||||||
store,
|
|
||||||
childProps,
|
|
||||||
coreApi,
|
|
||||||
frontendDefinition.appRootPath
|
|
||||||
)
|
|
||||||
|
|
||||||
const componentConstructor = componentLibraries[libName][componentName]
|
const componentConstructor = componentLibraries[libName][componentName]
|
||||||
|
|
||||||
const renderedComponentsThisIteration = renderComponent({
|
const childNodesThisIteration = prepareRenderComponent({
|
||||||
props: childProps,
|
props: childProps,
|
||||||
parentNode: treeNode,
|
parentNode: treeNode,
|
||||||
componentConstructor,
|
componentConstructor,
|
||||||
uiFunctions,
|
uiFunctions,
|
||||||
htmlElement,
|
htmlElement,
|
||||||
anchor,
|
anchor,
|
||||||
initialProps,
|
getCurrentState
|
||||||
bb,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (
|
for (let childNode of childNodesThisIteration) {
|
||||||
onScreenSlotRendered &&
|
childNodes.push(childNode)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 => {
|
const splitName = fullname => {
|
||||||
|
@ -90,3 +84,19 @@ const splitName = fullname => {
|
||||||
|
|
||||||
return { libName, componentName }
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -1,22 +1,21 @@
|
||||||
export const renderComponent = ({
|
export const prepareRenderComponent = ({
|
||||||
componentConstructor,
|
componentConstructor,
|
||||||
uiFunctions,
|
uiFunctions,
|
||||||
htmlElement,
|
htmlElement,
|
||||||
anchor,
|
anchor,
|
||||||
props,
|
props,
|
||||||
initialProps,
|
|
||||||
bb,
|
|
||||||
parentNode,
|
parentNode,
|
||||||
|
getCurrentState,
|
||||||
}) => {
|
}) => {
|
||||||
const func = initialProps._id ? uiFunctions[initialProps._id] : undefined
|
const func = props._id ? uiFunctions[props._id] : undefined
|
||||||
|
|
||||||
const parentContext = (parentNode && parentNode.context) || {}
|
const parentContext = (parentNode && parentNode.context) || {}
|
||||||
|
|
||||||
let renderedNodes = []
|
let nodesToRender = []
|
||||||
const render = context => {
|
const createNodeAndRender = context => {
|
||||||
let componentContext = parentContext
|
let componentContext = parentContext
|
||||||
if (context) {
|
if (context) {
|
||||||
componentContext = { ...componentContext }
|
componentContext = { ...context }
|
||||||
componentContext.$parent = parentContext
|
componentContext.$parent = parentContext
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,33 +23,31 @@ export const renderComponent = ({
|
||||||
thisNode.context = componentContext
|
thisNode.context = componentContext
|
||||||
thisNode.parentNode = parentNode
|
thisNode.parentNode = parentNode
|
||||||
thisNode.props = props
|
thisNode.props = props
|
||||||
|
nodesToRender.push(thisNode)
|
||||||
|
|
||||||
parentNode.children.push(thisNode)
|
thisNode.render = initialProps => {
|
||||||
renderedNodes.push(thisNode)
|
thisNode.component = new componentConstructor({
|
||||||
|
target: htmlElement,
|
||||||
|
props: initialProps,
|
||||||
|
hydrate: false,
|
||||||
|
anchor,
|
||||||
|
})
|
||||||
|
thisNode.rootElement =
|
||||||
|
htmlElement.children[htmlElement.children.length - 1]
|
||||||
|
|
||||||
initialProps._bb = bb(thisNode, props)
|
if (props._id && thisNode.rootElement) {
|
||||||
|
thisNode.rootElement.classList.add(`pos-${props._id}`)
|
||||||
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 (func) {
|
if (func) {
|
||||||
func(render, parentContext)
|
func(createNodeAndRender, parentContext, getCurrentState())
|
||||||
} else {
|
} else {
|
||||||
render()
|
createNodeAndRender()
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderedNodes
|
return nodesToRender
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createTreeNode = () => ({
|
export const createTreeNode = () => ({
|
||||||
|
@ -59,8 +56,10 @@ export const createTreeNode = () => ({
|
||||||
rootElement: null,
|
rootElement: null,
|
||||||
parentNode: null,
|
parentNode: null,
|
||||||
children: [],
|
children: [],
|
||||||
|
bindings: [],
|
||||||
component: null,
|
component: null,
|
||||||
unsubscribe: () => {},
|
unsubscribe: () => {},
|
||||||
|
render: () => {},
|
||||||
get destroy() {
|
get destroy() {
|
||||||
const node = this
|
const node = this
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -71,6 +70,10 @@ export const createTreeNode = () => ({
|
||||||
child.destroy()
|
child.destroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (let onDestroyItem of node.onDestroy) {
|
||||||
|
onDestroyItem()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onDestroy: [],
|
||||||
})
|
})
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,7 +23,8 @@ const isMetaProp = propName =>
|
||||||
propName === "_component" ||
|
propName === "_component" ||
|
||||||
propName === "_children" ||
|
propName === "_children" ||
|
||||||
propName === "_id" ||
|
propName === "_id" ||
|
||||||
propName === "_style"
|
propName === "_style" ||
|
||||||
|
propName === "_code"
|
||||||
|
|
||||||
export const setupBinding = (store, rootProps, coreApi, context, rootPath) => {
|
export const setupBinding = (store, rootProps, coreApi, context, rootPath) => {
|
||||||
const rootInitialProps = { ...rootProps }
|
const rootInitialProps = { ...rootProps }
|
||||||
|
|
|
@ -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]
|
|
@ -1,4 +1,5 @@
|
||||||
import { load, makePage, makeScreen } from "./testAppDef"
|
import { load, makePage, makeScreen } from "./testAppDef"
|
||||||
|
import { EVENT_TYPE_MEMBER_NAME } from "../src/state/eventHandlers"
|
||||||
|
|
||||||
describe("initialiseApp (binding)", () => {
|
describe("initialiseApp (binding)", () => {
|
||||||
it("should populate root element prop from store value", async () => {
|
it("should populate root element prop from store value", async () => {
|
||||||
|
@ -169,4 +170,177 @@ describe("initialiseApp (binding)", () => {
|
||||||
"header 2 - new val"
|
"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
|
||||||
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ export const load = async (page, screens = [], url = "/") => {
|
||||||
actions: [],
|
actions: [],
|
||||||
triggers: [],
|
triggers: [],
|
||||||
})
|
})
|
||||||
|
setComponentCodeMeta(page, screens)
|
||||||
const app = await loadBudibase({
|
const app = await loadBudibase({
|
||||||
componentLibraries: allLibs(dom.window),
|
componentLibraries: allLibs(dom.window),
|
||||||
window: dom.window,
|
window: dom.window,
|
||||||
|
@ -47,8 +48,11 @@ export const timeout = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||||
export const walkComponentTree = (node, action) => {
|
export const walkComponentTree = (node, action) => {
|
||||||
action(node)
|
action(node)
|
||||||
|
|
||||||
if (node.children) {
|
// works for nodes or props
|
||||||
for (let child of node.children) {
|
const children = node.children || node._children
|
||||||
|
|
||||||
|
if (children) {
|
||||||
|
for (let child of children) {
|
||||||
walkComponentTree(child, action)
|
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) => {
|
const setAppDef = (window, page, screens) => {
|
||||||
window["##BUDIBASE_FRONTEND_DEFINITION##"] = {
|
window["##BUDIBASE_FRONTEND_DEFINITION##"] = {
|
||||||
componentLibraries: [],
|
componentLibraries: [],
|
||||||
|
@ -148,6 +168,29 @@ const maketestlib = window => ({
|
||||||
set(opts.props)
|
set(opts.props)
|
||||||
opts.target.appendChild(node)
|
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 = {
|
const uiFunctions = {
|
||||||
|
@ -162,4 +205,15 @@ const uiFunctions = {
|
||||||
render()
|
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}` })
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ const { join, resolve, dirname } = require("path")
|
||||||
const sqrl = require("squirrelly")
|
const sqrl = require("squirrelly")
|
||||||
const { convertCssToFiles } = require("./convertCssToFiles")
|
const { convertCssToFiles } = require("./convertCssToFiles")
|
||||||
const publicPath = require("./publicPath")
|
const publicPath = require("./publicPath")
|
||||||
|
const deleteCodeMeta = require("./deleteCodeMeta")
|
||||||
|
|
||||||
module.exports = async (config, appname, pageName, pkg) => {
|
module.exports = async (config, appname, pageName, pkg) => {
|
||||||
const appPath = appPackageFolder(config, appname)
|
const appPath = appPackageFolder(config, appname)
|
||||||
|
@ -155,6 +156,8 @@ const savePageJson = async (appPath, pageName, pkg) => {
|
||||||
delete pkg.page._screens
|
delete pkg.page._screens
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteCodeMeta(pkg.page.props)
|
||||||
|
|
||||||
await writeJSON(pageFile, pkg.page, {
|
await writeJSON(pageFile, pkg.page, {
|
||||||
spaces: 2,
|
spaces: 2,
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
module.exports = props => {
|
||||||
|
if (props._codeMeta) {
|
||||||
|
delete props._codeMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let child of props._children || []) {
|
||||||
|
module.exports(child)
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ const buildPage = require("./buildPage")
|
||||||
const getPages = require("./getPages")
|
const getPages = require("./getPages")
|
||||||
const listScreens = require("./listScreens")
|
const listScreens = require("./listScreens")
|
||||||
const saveBackend = require("./saveBackend")
|
const saveBackend = require("./saveBackend")
|
||||||
|
const deleteCodeMeta = require("./deleteCodeMeta")
|
||||||
|
|
||||||
module.exports.buildPage = buildPage
|
module.exports.buildPage = buildPage
|
||||||
module.exports.listScreens = listScreens
|
module.exports.listScreens = listScreens
|
||||||
|
@ -58,12 +59,15 @@ module.exports.saveScreen = async (config, appname, pagename, screen) => {
|
||||||
if (screen._css) {
|
if (screen._css) {
|
||||||
delete screen._css
|
delete screen._css
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteCodeMeta(screen.props)
|
||||||
|
|
||||||
await writeJSON(compPath, screen, {
|
await writeJSON(compPath, screen, {
|
||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
flag: "w",
|
flag: "w",
|
||||||
spaces: 2,
|
spaces: 2,
|
||||||
})
|
})
|
||||||
return screen;
|
return screen
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.renameScreen = async (
|
module.exports.renameScreen = async (
|
||||||
|
|
|
@ -34,7 +34,8 @@
|
||||||
return all
|
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 = {
|
cssVariables = {
|
||||||
|
@ -73,7 +74,7 @@
|
||||||
disabled={disabled || false}
|
disabled={disabled || false}
|
||||||
on:click={clickHandler}
|
on:click={clickHandler}
|
||||||
style={buttonStyles}>
|
style={buttonStyles}>
|
||||||
{#if _bb.props_children.length === 0}{contentText}{/if}
|
{#if !_bb.props._children || _bb.props._children.length === 0}{contentText}{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
Loading…
Reference in New Issue