From c7b8220a62b3991a255e9ae9ad97fd1d3dcdb4fa Mon Sep 17 00:00:00 2001 From: Martin McKeaveney Date: Sat, 30 May 2020 00:14:41 +0100 Subject: [PATCH] significant client lib refactor --- .../EventsEditor/StateBindingCascader.svelte | 3 +- .../WorkflowPanel/blockDefinitions.js | 1 + .../client/src/api/workflow/orchestrator.js | 6 +- packages/client/src/common/trimSlash.js | 1 - packages/client/src/createApp.js | 38 +++--- .../src/render/prepareRenderComponent.js | 20 +++- packages/client/src/render/screenRouter.js | 34 ++---- packages/client/src/state/bbComponentApi.js | 8 +- packages/client/src/state/eventHandlers.js | 9 +- packages/client/src/state/getState.js | 49 +------- packages/client/src/state/parseBinding.js | 110 +++++++++--------- packages/client/src/state/setState.js | 39 ++----- packages/client/src/state/stateManager.js | 86 ++++++++------ packages/client/src/state/store.js | 16 +++ 14 files changed, 198 insertions(+), 222 deletions(-) delete mode 100644 packages/client/src/common/trimSlash.js create mode 100644 packages/client/src/state/store.js diff --git a/packages/builder/src/components/userInterface/EventsEditor/StateBindingCascader.svelte b/packages/builder/src/components/userInterface/EventsEditor/StateBindingCascader.svelte index 09aa7ae9ad..79cd776c61 100644 --- a/packages/builder/src/components/userInterface/EventsEditor/StateBindingCascader.svelte +++ b/packages/builder/src/components/userInterface/EventsEditor/StateBindingCascader.svelte @@ -17,13 +17,14 @@ export let onChange let isOpen = false +
{parameter.name}
{#if parameter.name === 'workflow'} - {#each $workflowStore.workflows as workflow} {/each} diff --git a/packages/builder/src/pages/[application]/workflow/WorkflowPanel/blockDefinitions.js b/packages/builder/src/pages/[application]/workflow/WorkflowPanel/blockDefinitions.js index 813099389d..c96a657bac 100644 --- a/packages/builder/src/pages/[application]/workflow/WorkflowPanel/blockDefinitions.js +++ b/packages/builder/src/pages/[application]/workflow/WorkflowPanel/blockDefinitions.js @@ -12,6 +12,7 @@ const ACTION = { }, NAVIGATE: { name: "Navigate", + tagline: "Navigate to {{url}}", icon: "ri-navigation-line", description: "Navigate to another page.", environment: "CLIENT", diff --git a/packages/client/src/api/workflow/orchestrator.js b/packages/client/src/api/workflow/orchestrator.js index 7d28915d92..bd82daa9a9 100644 --- a/packages/client/src/api/workflow/orchestrator.js +++ b/packages/client/src/api/workflow/orchestrator.js @@ -44,7 +44,8 @@ export const clientStrategy = { // Means that it's bound to state or workflow context mappedArgs[arg] = mustache.render(argValue, { context: this.context, - state: api.getState() + // TODO: map to the real state + state: {} }); } // if (argValue.startsWith("$")) { @@ -88,6 +89,9 @@ export const clientStrategy = { } } + if (block.actionId === "NAVIGATE") { + } + if (block.actionId === "DELAY") { await this.delay(block.args.time) } diff --git a/packages/client/src/common/trimSlash.js b/packages/client/src/common/trimSlash.js deleted file mode 100644 index 5f403ee092..0000000000 --- a/packages/client/src/common/trimSlash.js +++ /dev/null @@ -1 +0,0 @@ -export const trimSlash = str => str.replace(/^\/+|\/+$/g, "") diff --git a/packages/client/src/createApp.js b/packages/client/src/createApp.js index 37e560bb87..9da7c80905 100644 --- a/packages/client/src/createApp.js +++ b/packages/client/src/createApp.js @@ -15,17 +15,17 @@ export const createApp = ({ let screenStateManager const onScreenSlotRendered = screenSlotNode => { - const onScreenSelected = (screen, store, url) => { + const onScreenSelected = (screen, url) => { const stateManager = createStateManager({ - store, frontendDefinition, componentLibraries, onScreenSlotRendered: () => {}, routeTo, appRootPath: frontendDefinition.appRootPath, }) + const getAttachChildrenParams = attachChildrenParams(stateManager) screenSlotNode.props._children = [screen.props] - const initialiseChildParams = attachChildrenParams(stateManager, screenSlotNode) + const initialiseChildParams = getAttachChildrenParams(screenSlotNode) attachChildren(initialiseChildParams)(screenSlotNode.rootElement, { hydrate: true, force: true, @@ -35,11 +35,11 @@ export const createApp = ({ currentUrl = url } - routeTo = screenRouter( - frontendDefinition.screens, + routeTo = screenRouter({ + screens: frontendDefinition.screens, onScreenSelected, - frontendDefinition.appRootPath - ) + appRootPath: frontendDefinition.appRootPath + }) const fallbackPath = window.location.pathname.replace( frontendDefinition.appRootPath, "" @@ -47,17 +47,21 @@ export const createApp = ({ routeTo(currentUrl || fallbackPath) } - const attachChildrenParams = (stateManager, treeNode) => ({ - componentLibraries, - treeNode, - onScreenSlotRendered, - setupState: stateManager.setup, - getCurrentState: stateManager.getCurrentState, - }); + const attachChildrenParams = stateManager => { + const getInitialiseParams = treeNode => ({ + componentLibraries, + treeNode, + onScreenSlotRendered, + setupState: stateManager.setup, + getCurrentState: stateManager.getCurrentState, + }) + + return getInitialiseParams + } let rootTreeNode const pageStateManager = createStateManager({ - store: writable({ _bbuser: user }), + // store: writable({ _bbuser: user }), frontendDefinition, componentLibraries, onScreenSlotRendered, @@ -73,8 +77,8 @@ export const createApp = ({ rootTreeNode.props = { _children: [page.props], } - rootTreeNode.rootElement = target - const initChildParams = attachChildrenParams(pageStateManager, rootTreeNode) + const getInitialiseParams = attachChildrenParams(pageStateManager) + const initChildParams = getInitialiseParams(rootTreeNode) attachChildren(initChildParams)(target, { hydrate: true, diff --git a/packages/client/src/render/prepareRenderComponent.js b/packages/client/src/render/prepareRenderComponent.js index 8efb577ed3..b92f946f90 100644 --- a/packages/client/src/render/prepareRenderComponent.js +++ b/packages/client/src/render/prepareRenderComponent.js @@ -1,10 +1,12 @@ +import { appStore } from "../state/store" +import mustache from "mustache"; + export const prepareRenderComponent = ({ ComponentConstructor, htmlElement, anchor, props, - parentNode, - getCurrentState, + parentNode }) => { const parentContext = (parentNode && parentNode.context) || {} @@ -36,6 +38,20 @@ export const prepareRenderComponent = ({ if (props._id && thisNode.rootElement) { thisNode.rootElement.classList.add(`${componentName}-${props._id}`) } + + // make this node listen to the store + if (thisNode.stateBound) { + const unsubscribe = appStore.subscribe(state => { + const storeBoundProps = { ...initialProps._bb.props }; + for (let prop in storeBoundProps) { + if (typeof storeBoundProps[prop] === "string") { + storeBoundProps[prop] = mustache.render(storeBoundProps[prop], { state }); + } + } + thisNode.component.$set(storeBoundProps); + }); + thisNode.unsubscribe = unsubscribe + } } } diff --git a/packages/client/src/render/screenRouter.js b/packages/client/src/render/screenRouter.js index f04df723a6..58a2277843 100644 --- a/packages/client/src/render/screenRouter.js +++ b/packages/client/src/render/screenRouter.js @@ -1,8 +1,8 @@ import regexparam from "regexparam" -import { writable } from "svelte/store" +import { routerStore } from "../state/store"; // TODO: refactor -export const screenRouter = (screens, onScreenSelected, appRootPath) => { +export const screenRouter = ({ screens, onScreenSelected, appRootPath }) => { const makeRootedPath = url => { if (appRootPath) { if (url) return `${appRootPath}${url.startsWith("/") ? "" : "/"}${url}` @@ -41,13 +41,14 @@ export const screenRouter = (screens, onScreenSelected, appRootPath) => { }) } - const storeInitial = {} - storeInitial["##routeParams"] = params - const store = writable(storeInitial) + routerStore.update(state => { + state["##routeParams"] = params; + return state; + }) const screenIndex = current !== -1 ? current : fallback - onScreenSelected(screens[screenIndex], store, _url) + onScreenSelected(screens[screenIndex], _url) try { !url.state && history.pushState(_url, null, _url) @@ -56,29 +57,8 @@ export const screenRouter = (screens, onScreenSelected, appRootPath) => { } } - function click(e) { - const x = e.target.closest("a") - const y = x && x.getAttribute("href") - - if ( - e.ctrlKey || - e.metaKey || - e.altKey || - e.shiftKey || - e.button || - e.defaultPrevented - ) - return - - if (!y || x.target || x.host !== location.host) return - - e.preventDefault() - route(y) - } - addEventListener("popstate", route) addEventListener("pushstate", route) - addEventListener("click", click) return route } diff --git a/packages/client/src/state/bbComponentApi.js b/packages/client/src/state/bbComponentApi.js index 9b904741ba..30925f14df 100644 --- a/packages/client/src/state/bbComponentApi.js +++ b/packages/client/src/state/bbComponentApi.js @@ -5,6 +5,8 @@ import { isBound } from "./parseBinding" import { attachChildren } from "../render/attachChildren" import { getContext, setContext } from "./getSetContext" +export const trimSlash = str => str.replace(/^\/+|\/+$/g, "") + export const bbFactory = ({ store, getCurrentState, @@ -61,11 +63,7 @@ export const bbFactory = ({ 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), + setState, getContext: getContext(treeNode), setContext: setContext(treeNode), store: store, diff --git a/packages/client/src/state/eventHandlers.js b/packages/client/src/state/eventHandlers.js index c4471bafbf..926eff89ca 100644 --- a/packages/client/src/state/eventHandlers.js +++ b/packages/client/src/state/eventHandlers.js @@ -1,6 +1,7 @@ import { setState } from "./setState" import { getState } from "./getState" import { isArray, isUndefined } from "lodash/fp" +import { appStore } from "./store"; import { createApi } from "../api" @@ -12,8 +13,6 @@ export const eventHandlers = (store, rootPath, routeTo) => { parameters, }) - const setStateWithStore = (path, value) => setState(store, path, value) - let currentState store.subscribe(state => { currentState = state @@ -21,11 +20,11 @@ export const eventHandlers = (store, rootPath, routeTo) => { const api = createApi({ rootPath, - setState: setStateWithStore, - getState: (path, fallback) => getState(currentState, path, fallback) + setState, + getState: (path, fallback) => getState(path, fallback) }) - const setStateHandler = ({ path, value }) => setState(store, path, value) + const setStateHandler = ({ path, value }) => setState(path, value) return { "Set State": handler(["path", "value"], setStateHandler), diff --git a/packages/client/src/state/getState.js b/packages/client/src/state/getState.js index 49e3453e52..980bb82b6e 100644 --- a/packages/client/src/state/getState.js +++ b/packages/client/src/state/getState.js @@ -1,49 +1,10 @@ // import { isUndefined, isObject } from "lodash/fp" +import { get } from "svelte/store"; import getOr from "lodash/fp/getOr"; -// import { parseBinding, isStoreBinding } from "./parseBinding" +import { appStore } from "./store"; -export const getState = (state, path, fallback) => { - if (!state) return fallback +export const getState = (path, fallback) => { if (!path || path.length === 0) return fallback - return getOr(fallback, path, state); - - // if (path === "$") return state - - // const pathParts = path.split(".") - // const safeGetPath = (obj, currentPartIndex = 0) => { - // const currentKey = pathParts[currentPartIndex] - - // if (pathParts.length - 1 == currentPartIndex) { - // const value = obj[currentKey] - // if (isUndefined(value)) return fallback - // else return value - // } - - // if ( - // obj[currentKey] === null || - // obj[currentKey] === undefined || - // !isObject(obj[currentKey]) - // ) { - // return fallback - // } - - // return safeGetPath(obj[currentKey], currentPartIndex + 1) - // } - - // return safeGetPath(state) -} - -// export const getStateOrValue = (globalState, prop, currentContext) => { -// if (!prop) return prop - -// const binding = parseBinding(prop) - -// if (binding) { -// const stateToUse = isStoreBinding(binding) ? globalState : currentContext - -// return getState(stateToUse, binding.path, binding.fallback) -// } - -// return prop -// } + return getOr(fallback, path, get(appStore)); +} \ No newline at end of file diff --git a/packages/client/src/state/parseBinding.js b/packages/client/src/state/parseBinding.js index 1cbda7c244..4ecf9887ef 100644 --- a/packages/client/src/state/parseBinding.js +++ b/packages/client/src/state/parseBinding.js @@ -1,67 +1,67 @@ -export const BB_STATE_BINDINGPATH = "##bbstate" -export const BB_STATE_BINDINGSOURCE = "##bbsource" -export const BB_STATE_FALLBACK = "##bbstatefallback" +// export const BB_STATE_BINDINGPATH = "##bbstate" +// export const BB_STATE_BINDINGSOURCE = "##bbsource" +// export const BB_STATE_FALLBACK = "##bbstatefallback" -export const isBound = prop => !!parseBinding(prop) +// export const isBound = prop => !!parseBinding(prop) -/** - * - * @param {object|string|number} prop - component property to parse for a dynamic state binding - * @returns {object|boolean} - */ -export const parseBinding = prop => { - if (!prop) return false +// /** +// * +// * @param {object|string|number} prop - component property to parse for a dynamic state binding +// * @returns {object|boolean} +// */ +// export const parseBinding = prop => { +// if (!prop) return false - if (isBindingExpression(prop)) { - return parseBindingExpression(prop) - } +// if (isBindingExpression(prop)) { +// return parseBindingExpression(prop) +// } - if (isAlreadyBinding(prop)) { - return { - path: prop.path, - source: prop.source || "store", - fallback: prop.fallback, - } - } +// if (isAlreadyBinding(prop)) { +// return { +// path: prop.path, +// source: prop.source || "store", +// fallback: prop.fallback, +// } +// } - if (hasBindingObject(prop)) { - return { - path: prop[BB_STATE_BINDINGPATH], - fallback: prop[BB_STATE_FALLBACK] || "", - source: prop[BB_STATE_BINDINGSOURCE] || "store", - } - } -} +// if (hasBindingObject(prop)) { +// return { +// path: prop[BB_STATE_BINDINGPATH], +// fallback: prop[BB_STATE_FALLBACK] || "", +// source: prop[BB_STATE_BINDINGSOURCE] || "store", +// } +// } +// } -export const isStoreBinding = binding => binding && binding.source === "store" -export const isContextBinding = binding => - binding && binding.source === "context" -// export const isEventBinding = binding => binding && binding.source === "event" +// export const isStoreBinding = binding => binding && binding.source === "store" +// export const isContextBinding = binding => +// binding && binding.source === "context" +// // export const isEventBinding = binding => binding && binding.source === "event" -const hasBindingObject = prop => - typeof prop === "object" && prop[BB_STATE_BINDINGPATH] !== undefined +// const hasBindingObject = prop => +// typeof prop === "object" && prop[BB_STATE_BINDINGPATH] !== undefined -const isAlreadyBinding = prop => typeof prop === "object" && prop.path +// const isAlreadyBinding = prop => typeof prop === "object" && prop.path -const isBindingExpression = prop => - typeof prop === "string" && - (prop.startsWith("state.") || - prop.startsWith("context.") || - prop.startsWith("event.") || - prop.startsWith("route.")) +// const isBindingExpression = prop => +// typeof prop === "string" && +// (prop.startsWith("state.") || +// prop.startsWith("context.") || +// prop.startsWith("event.") || +// prop.startsWith("route.")) -const parseBindingExpression = prop => { - let [source, ...rest] = prop.split(".") - let path = rest.join(".") +// const parseBindingExpression = prop => { +// let [source, ...rest] = prop.split(".") +// let path = rest.join(".") - if (source === "route") { - source = "state" - path = `##routeParams.${path}` - } +// if (source === "route") { +// source = "state" +// path = `##routeParams.${path}` +// } - return { - fallback: "", // TODO: provide fallback support - source, - path, - } -} +// return { +// fallback: "", // TODO: provide fallback support +// source, +// path, +// } +// } diff --git a/packages/client/src/state/setState.js b/packages/client/src/state/setState.js index 8554a35028..d50e6e6e62 100644 --- a/packages/client/src/state/setState.js +++ b/packages/client/src/state/setState.js @@ -1,40 +1,17 @@ -// import isObject from "lodash/fp/isObject" import set from "lodash/fp/set"; -import { parseBinding } from "./parseBinding" +import { appStore } from "./store"; -export const setState = (store, path, value) => { +export const setState = (path, value) => { if (!path || path.length === 0) return - // const pathParts = path.split(".") - - // const safeSetPath = (state, currentPartIndex = 0) => { - // const currentKey = pathParts[currentPartIndex] - - // if (pathParts.length - 1 == currentPartIndex) { - // state[currentKey] = value - // return - // } - - // if ( - // state[currentKey] === null || - // state[currentKey] === undefined || - // !isObject(state[currentKey]) - // ) { - // state[currentKey] = {} - // } - - // safeSetPath(state[currentKey], currentPartIndex + 1) - // } - - store.update(state => { - // safeSetPath(state) + appStore.update(state => { state = set(path, value, state); return state }) } -export const setStateFromBinding = (store, binding, value) => { - const parsedBinding = parseBinding(binding) - if (!parsedBinding) return - return setState(store, parsedBinding.path, value) -} +// export const setStateFromBinding = (store, binding, value) => { +// const parsedBinding = parseBinding(binding) +// if (!parsedBinding) return +// return setState(store, parsedBinding.path, value) +// } diff --git a/packages/client/src/state/stateManager.js b/packages/client/src/state/stateManager.js index 208b8f35c8..2ecf882962 100644 --- a/packages/client/src/state/stateManager.js +++ b/packages/client/src/state/stateManager.js @@ -8,6 +8,7 @@ import { createTreeNode } from "../render/prepareRenderComponent" import { getState } from "./getState" import { attachChildren } from "../render/attachChildren" import mustache from "mustache" +import { appStore } from "./store"; import { parseBinding } from "./parseBinding" @@ -24,14 +25,14 @@ const isMetaProp = propName => propName === "_styles" export const createStateManager = ({ - store, + // store, appRootPath, frontendDefinition, componentLibraries, onScreenSlotRendered, routeTo, }) => { - let handlerTypes = eventHandlers(store, appRootPath, routeTo) + let handlerTypes = eventHandlers(appStore, appRootPath, routeTo) let currentState // any nodes that have props that are bound to the store @@ -45,33 +46,40 @@ export const createStateManager = ({ // nodesBoundByProps, // nodesWithCodeBoundChildren // ) + const bb = bbFactory({ - store, + store: appStore, getCurrentState, frontendDefinition, componentLibraries, onScreenSlotRendered, }) - const setup = _setup(handlerTypes, getCurrentState, bb) + const setup = _setup({ handlerTypes, getCurrentState, bb, store: appStore }) - const unsubscribe = store.subscribe( - onStoreStateUpdated({ - setCurrentState: state => (currentState = state), - getCurrentState, - // nodesWithCodeBoundChildren, - // nodesBoundByProps, - componentLibraries, - onScreenSlotRendered, - setupState: setup, - }) - ) + // TODO: remove + const unsubscribe = appStore.subscribe(state => { + console.log("store updated", state); + return state; + }); + + // const unsubscribe = store.subscribe( + // onStoreStateUpdated({ + // setCurrentState: state => (currentState = state), + // getCurrentState, + // // nodesWithCodeBoundChildren, + // // nodesBoundByProps, + // componentLibraries, + // onScreenSlotRendered, + // setupState: setup, + // }) + // ) return { setup, destroy: () => unsubscribe(), getCurrentState, - store, + store: appStore, } } @@ -80,16 +88,19 @@ const onStoreStateUpdated = ({ getCurrentState, componentLibraries, onScreenSlotRendered, - setupState, + setupState }) => state => { - setCurrentState(state) - attachChildren({ - componentLibraries, - treeNode: createTreeNode(), - onScreenSlotRendered, - setupState, - getCurrentState, - })(document.querySelector("#app"), { hydrate: true, force: true }) + // fire the state update event to re-render anything bound to this + // setCurrentState(state) + + // setCurrentState(state) + // attachChildren({ + // componentLibraries, + // treeNode: createTreeNode(), + // onScreenSlotRendered, + // setupState, + // getCurrentState, + // })(document.querySelector("#app"), { hydrate: true, force: true }) // // the original array gets changed by components' destroy() // // so we make a clone and check if they are still in the original @@ -154,32 +165,41 @@ const onStoreStateUpdated = ({ // node.component.$set(newProps) // } -const _setup = ( +const _setup = ({ handlerTypes, getCurrentState, - bb -) => node => { - - console.log(node); + bb, + store +}) => node => { const props = node.props const context = node.context || {} const initialProps = { ...props } // const storeBoundProps = [] const currentStoreState = getCurrentState() + console.log("node", node); + + // console.log("node", node); + // console.log("nodeComponent", node.component); + for (let propName in props) { if (isMetaProp(propName)) continue const propValue = props[propName] // const binding = parseBinding(propValue) - // const isBound = !!binding + // TODO: better binding stuff + const isBound = typeof propValue === "string" && propValue.startsWith("{{"); - if (typeof propValue === "string") { + if (isBound) { initialProps[propName] = mustache.render(propValue, { state: currentStoreState, context }) + + if (!node.stateBound) { + node.stateBound = true + } } // if (isBound) binding.propName = propName @@ -254,7 +274,7 @@ const _setup = ( // registerBindings(node, storeBoundProps) - const setup = _setup(handlerTypes, getCurrentState, bb) + const setup = _setup({ handlerTypes, getCurrentState, bb, store }) initialProps._bb = bb(node, setup) return initialProps diff --git a/packages/client/src/state/store.js b/packages/client/src/state/store.js new file mode 100644 index 0000000000..e7e6351184 --- /dev/null +++ b/packages/client/src/state/store.js @@ -0,0 +1,16 @@ +import { writable } from "svelte/store"; + +const appStore = writable({}); +appStore.actions = { + +}; + +const routerStore = writable({}); +routerStore.actions = { + +} + +export { + appStore, + routerStore +}