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
f3e0100fc9
commit
56c25fee90
|
@ -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"
|
||||
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 => {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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: [],
|
||||
})
|
|
@ -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 === "_children" ||
|
||||
propName === "_id" ||
|
||||
propName === "_style"
|
||||
propName === "_style" ||
|
||||
propName === "_code"
|
||||
|
||||
export const setupBinding = (store, rootProps, coreApi, context, rootPath) => {
|
||||
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 { 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
|
||||
}
|
||||
|
|
|
@ -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}` })
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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 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 (
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue