significant client lib refactor

This commit is contained in:
Martin McKeaveney 2020-05-30 00:14:41 +01:00
parent b4b90fcaeb
commit c7b8220a62
14 changed files with 198 additions and 222 deletions

View File

@ -17,13 +17,14 @@
export let onChange export let onChange
let isOpen = false let isOpen = false
</script> </script>
<div class="handler-option"> <div class="handler-option">
<span>{parameter.name}</span> <span>{parameter.name}</span>
<div class="handler-input"> <div class="handler-input">
{#if parameter.name === 'workflow'} {#if parameter.name === 'workflow'}
<select class="budibase__input" {onChange} value={parameter.value}> <select class="budibase__input" on:change={onChange} bind:value={parameter.value}>
{#each $workflowStore.workflows as workflow} {#each $workflowStore.workflows as workflow}
<option value={workflow._id}>{workflow.name}</option> <option value={workflow._id}>{workflow.name}</option>
{/each} {/each}

View File

@ -12,6 +12,7 @@ const ACTION = {
}, },
NAVIGATE: { NAVIGATE: {
name: "Navigate", name: "Navigate",
tagline: "Navigate to <b>{{url}}</b>",
icon: "ri-navigation-line", icon: "ri-navigation-line",
description: "Navigate to another page.", description: "Navigate to another page.",
environment: "CLIENT", environment: "CLIENT",

View File

@ -44,7 +44,8 @@ export const clientStrategy = {
// Means that it's bound to state or workflow context // Means that it's bound to state or workflow context
mappedArgs[arg] = mustache.render(argValue, { mappedArgs[arg] = mustache.render(argValue, {
context: this.context, context: this.context,
state: api.getState() // TODO: map to the real state
state: {}
}); });
} }
// if (argValue.startsWith("$")) { // if (argValue.startsWith("$")) {
@ -88,6 +89,9 @@ export const clientStrategy = {
} }
} }
if (block.actionId === "NAVIGATE") {
}
if (block.actionId === "DELAY") { if (block.actionId === "DELAY") {
await this.delay(block.args.time) await this.delay(block.args.time)
} }

View File

@ -1 +0,0 @@
export const trimSlash = str => str.replace(/^\/+|\/+$/g, "")

View File

@ -15,17 +15,17 @@ export const createApp = ({
let screenStateManager let screenStateManager
const onScreenSlotRendered = screenSlotNode => { const onScreenSlotRendered = screenSlotNode => {
const onScreenSelected = (screen, store, url) => { const onScreenSelected = (screen, url) => {
const stateManager = createStateManager({ const stateManager = createStateManager({
store,
frontendDefinition, frontendDefinition,
componentLibraries, componentLibraries,
onScreenSlotRendered: () => {}, onScreenSlotRendered: () => {},
routeTo, routeTo,
appRootPath: frontendDefinition.appRootPath, appRootPath: frontendDefinition.appRootPath,
}) })
const getAttachChildrenParams = attachChildrenParams(stateManager)
screenSlotNode.props._children = [screen.props] screenSlotNode.props._children = [screen.props]
const initialiseChildParams = attachChildrenParams(stateManager, screenSlotNode) const initialiseChildParams = getAttachChildrenParams(screenSlotNode)
attachChildren(initialiseChildParams)(screenSlotNode.rootElement, { attachChildren(initialiseChildParams)(screenSlotNode.rootElement, {
hydrate: true, hydrate: true,
force: true, force: true,
@ -35,11 +35,11 @@ export const createApp = ({
currentUrl = url currentUrl = url
} }
routeTo = screenRouter( routeTo = screenRouter({
frontendDefinition.screens, screens: frontendDefinition.screens,
onScreenSelected, onScreenSelected,
frontendDefinition.appRootPath appRootPath: frontendDefinition.appRootPath
) })
const fallbackPath = window.location.pathname.replace( const fallbackPath = window.location.pathname.replace(
frontendDefinition.appRootPath, frontendDefinition.appRootPath,
"" ""
@ -47,17 +47,21 @@ export const createApp = ({
routeTo(currentUrl || fallbackPath) routeTo(currentUrl || fallbackPath)
} }
const attachChildrenParams = (stateManager, treeNode) => ({ const attachChildrenParams = stateManager => {
componentLibraries, const getInitialiseParams = treeNode => ({
treeNode, componentLibraries,
onScreenSlotRendered, treeNode,
setupState: stateManager.setup, onScreenSlotRendered,
getCurrentState: stateManager.getCurrentState, setupState: stateManager.setup,
}); getCurrentState: stateManager.getCurrentState,
})
return getInitialiseParams
}
let rootTreeNode let rootTreeNode
const pageStateManager = createStateManager({ const pageStateManager = createStateManager({
store: writable({ _bbuser: user }), // store: writable({ _bbuser: user }),
frontendDefinition, frontendDefinition,
componentLibraries, componentLibraries,
onScreenSlotRendered, onScreenSlotRendered,
@ -73,8 +77,8 @@ export const createApp = ({
rootTreeNode.props = { rootTreeNode.props = {
_children: [page.props], _children: [page.props],
} }
rootTreeNode.rootElement = target const getInitialiseParams = attachChildrenParams(pageStateManager)
const initChildParams = attachChildrenParams(pageStateManager, rootTreeNode) const initChildParams = getInitialiseParams(rootTreeNode)
attachChildren(initChildParams)(target, { attachChildren(initChildParams)(target, {
hydrate: true, hydrate: true,

View File

@ -1,10 +1,12 @@
import { appStore } from "../state/store"
import mustache from "mustache";
export const prepareRenderComponent = ({ export const prepareRenderComponent = ({
ComponentConstructor, ComponentConstructor,
htmlElement, htmlElement,
anchor, anchor,
props, props,
parentNode, parentNode
getCurrentState,
}) => { }) => {
const parentContext = (parentNode && parentNode.context) || {} const parentContext = (parentNode && parentNode.context) || {}
@ -36,6 +38,20 @@ export const prepareRenderComponent = ({
if (props._id && thisNode.rootElement) { if (props._id && thisNode.rootElement) {
thisNode.rootElement.classList.add(`${componentName}-${props._id}`) 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
}
} }
} }

View File

@ -1,8 +1,8 @@
import regexparam from "regexparam" import regexparam from "regexparam"
import { writable } from "svelte/store" import { routerStore } from "../state/store";
// TODO: refactor // TODO: refactor
export const screenRouter = (screens, onScreenSelected, appRootPath) => { export const screenRouter = ({ screens, onScreenSelected, appRootPath }) => {
const makeRootedPath = url => { const makeRootedPath = url => {
if (appRootPath) { if (appRootPath) {
if (url) return `${appRootPath}${url.startsWith("/") ? "" : "/"}${url}` if (url) return `${appRootPath}${url.startsWith("/") ? "" : "/"}${url}`
@ -41,13 +41,14 @@ export const screenRouter = (screens, onScreenSelected, appRootPath) => {
}) })
} }
const storeInitial = {} routerStore.update(state => {
storeInitial["##routeParams"] = params state["##routeParams"] = params;
const store = writable(storeInitial) return state;
})
const screenIndex = current !== -1 ? current : fallback const screenIndex = current !== -1 ? current : fallback
onScreenSelected(screens[screenIndex], store, _url) onScreenSelected(screens[screenIndex], _url)
try { try {
!url.state && history.pushState(_url, null, _url) !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("popstate", route)
addEventListener("pushstate", route) addEventListener("pushstate", route)
addEventListener("click", click)
return route return route
} }

View File

@ -5,6 +5,8 @@ import { isBound } from "./parseBinding"
import { attachChildren } from "../render/attachChildren" import { attachChildren } from "../render/attachChildren"
import { getContext, setContext } from "./getSetContext" import { getContext, setContext } from "./getSetContext"
export const trimSlash = str => str.replace(/^\/+|\/+$/g, "")
export const bbFactory = ({ export const bbFactory = ({
store, store,
getCurrentState, getCurrentState,
@ -61,11 +63,7 @@ export const bbFactory = ({
context: treeNode.context, context: treeNode.context,
props: treeNode.props, props: treeNode.props,
call: safeCallEvent, call: safeCallEvent,
setStateFromBinding: (binding, value) => setState,
setStateFromBinding(store, binding, value),
setState: (path, value) => setState(store, path, value),
// getStateOrValue: (prop, currentContext) =>
// getStateOrValue(getCurrentState(), prop, currentContext),
getContext: getContext(treeNode), getContext: getContext(treeNode),
setContext: setContext(treeNode), setContext: setContext(treeNode),
store: store, store: store,

View File

@ -1,6 +1,7 @@
import { setState } from "./setState" import { setState } from "./setState"
import { getState } from "./getState" import { getState } from "./getState"
import { isArray, isUndefined } from "lodash/fp" import { isArray, isUndefined } from "lodash/fp"
import { appStore } from "./store";
import { createApi } from "../api" import { createApi } from "../api"
@ -12,8 +13,6 @@ export const eventHandlers = (store, rootPath, routeTo) => {
parameters, parameters,
}) })
const setStateWithStore = (path, value) => setState(store, path, value)
let currentState let currentState
store.subscribe(state => { store.subscribe(state => {
currentState = state currentState = state
@ -21,11 +20,11 @@ export const eventHandlers = (store, rootPath, routeTo) => {
const api = createApi({ const api = createApi({
rootPath, rootPath,
setState: setStateWithStore, setState,
getState: (path, fallback) => getState(currentState, path, fallback) getState: (path, fallback) => getState(path, fallback)
}) })
const setStateHandler = ({ path, value }) => setState(store, path, value) const setStateHandler = ({ path, value }) => setState(path, value)
return { return {
"Set State": handler(["path", "value"], setStateHandler), "Set State": handler(["path", "value"], setStateHandler),

View File

@ -1,49 +1,10 @@
// import { isUndefined, isObject } from "lodash/fp" // import { isUndefined, isObject } from "lodash/fp"
import { get } from "svelte/store";
import getOr from "lodash/fp/getOr"; import getOr from "lodash/fp/getOr";
// import { parseBinding, isStoreBinding } from "./parseBinding" import { appStore } from "./store";
export const getState = (state, path, fallback) => { export const getState = (path, fallback) => {
if (!state) return fallback
if (!path || path.length === 0) return fallback if (!path || path.length === 0) return fallback
return getOr(fallback, path, state); return getOr(fallback, path, get(appStore));
}
// 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
// }

View File

@ -1,67 +1,67 @@
export const BB_STATE_BINDINGPATH = "##bbstate" // export const BB_STATE_BINDINGPATH = "##bbstate"
export const BB_STATE_BINDINGSOURCE = "##bbsource" // export const BB_STATE_BINDINGSOURCE = "##bbsource"
export const BB_STATE_FALLBACK = "##bbstatefallback" // 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 // * @param {object|string|number} prop - component property to parse for a dynamic state binding
* @returns {object|boolean} // * @returns {object|boolean}
*/ // */
export const parseBinding = prop => { // export const parseBinding = prop => {
if (!prop) return false // if (!prop) return false
if (isBindingExpression(prop)) { // if (isBindingExpression(prop)) {
return parseBindingExpression(prop) // return parseBindingExpression(prop)
} // }
if (isAlreadyBinding(prop)) { // if (isAlreadyBinding(prop)) {
return { // return {
path: prop.path, // path: prop.path,
source: prop.source || "store", // source: prop.source || "store",
fallback: prop.fallback, // fallback: prop.fallback,
} // }
} // }
if (hasBindingObject(prop)) { // if (hasBindingObject(prop)) {
return { // return {
path: prop[BB_STATE_BINDINGPATH], // path: prop[BB_STATE_BINDINGPATH],
fallback: prop[BB_STATE_FALLBACK] || "", // fallback: prop[BB_STATE_FALLBACK] || "",
source: prop[BB_STATE_BINDINGSOURCE] || "store", // source: prop[BB_STATE_BINDINGSOURCE] || "store",
} // }
} // }
} // }
export const isStoreBinding = binding => binding && binding.source === "store" // export const isStoreBinding = binding => binding && binding.source === "store"
export const isContextBinding = binding => // export const isContextBinding = binding =>
binding && binding.source === "context" // binding && binding.source === "context"
// export const isEventBinding = binding => binding && binding.source === "event" // // export const isEventBinding = binding => binding && binding.source === "event"
const hasBindingObject = prop => // const hasBindingObject = prop =>
typeof prop === "object" && prop[BB_STATE_BINDINGPATH] !== undefined // 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 => // const isBindingExpression = prop =>
typeof prop === "string" && // typeof prop === "string" &&
(prop.startsWith("state.") || // (prop.startsWith("state.") ||
prop.startsWith("context.") || // prop.startsWith("context.") ||
prop.startsWith("event.") || // prop.startsWith("event.") ||
prop.startsWith("route.")) // prop.startsWith("route."))
const parseBindingExpression = prop => { // const parseBindingExpression = prop => {
let [source, ...rest] = prop.split(".") // let [source, ...rest] = prop.split(".")
let path = rest.join(".") // let path = rest.join(".")
if (source === "route") { // if (source === "route") {
source = "state" // source = "state"
path = `##routeParams.${path}` // path = `##routeParams.${path}`
} // }
return { // return {
fallback: "", // TODO: provide fallback support // fallback: "", // TODO: provide fallback support
source, // source,
path, // path,
} // }
} // }

View File

@ -1,40 +1,17 @@
// import isObject from "lodash/fp/isObject"
import set from "lodash/fp/set"; 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 if (!path || path.length === 0) return
// const pathParts = path.split(".") appStore.update(state => {
// 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)
state = set(path, value, state); state = set(path, value, state);
return state return state
}) })
} }
export const setStateFromBinding = (store, binding, value) => { // export const setStateFromBinding = (store, binding, value) => {
const parsedBinding = parseBinding(binding) // const parsedBinding = parseBinding(binding)
if (!parsedBinding) return // if (!parsedBinding) return
return setState(store, parsedBinding.path, value) // return setState(store, parsedBinding.path, value)
} // }

View File

@ -8,6 +8,7 @@ import { createTreeNode } from "../render/prepareRenderComponent"
import { getState } from "./getState" import { getState } from "./getState"
import { attachChildren } from "../render/attachChildren" import { attachChildren } from "../render/attachChildren"
import mustache from "mustache" import mustache from "mustache"
import { appStore } from "./store";
import { parseBinding } from "./parseBinding" import { parseBinding } from "./parseBinding"
@ -24,14 +25,14 @@ const isMetaProp = propName =>
propName === "_styles" propName === "_styles"
export const createStateManager = ({ export const createStateManager = ({
store, // store,
appRootPath, appRootPath,
frontendDefinition, frontendDefinition,
componentLibraries, componentLibraries,
onScreenSlotRendered, onScreenSlotRendered,
routeTo, routeTo,
}) => { }) => {
let handlerTypes = eventHandlers(store, appRootPath, routeTo) let handlerTypes = eventHandlers(appStore, appRootPath, routeTo)
let currentState let currentState
// any nodes that have props that are bound to the store // any nodes that have props that are bound to the store
@ -45,33 +46,40 @@ export const createStateManager = ({
// nodesBoundByProps, // nodesBoundByProps,
// nodesWithCodeBoundChildren // nodesWithCodeBoundChildren
// ) // )
const bb = bbFactory({ const bb = bbFactory({
store, store: appStore,
getCurrentState, getCurrentState,
frontendDefinition, frontendDefinition,
componentLibraries, componentLibraries,
onScreenSlotRendered, onScreenSlotRendered,
}) })
const setup = _setup(handlerTypes, getCurrentState, bb) const setup = _setup({ handlerTypes, getCurrentState, bb, store: appStore })
const unsubscribe = store.subscribe( // TODO: remove
onStoreStateUpdated({ const unsubscribe = appStore.subscribe(state => {
setCurrentState: state => (currentState = state), console.log("store updated", state);
getCurrentState, return state;
// nodesWithCodeBoundChildren, });
// nodesBoundByProps,
componentLibraries, // const unsubscribe = store.subscribe(
onScreenSlotRendered, // onStoreStateUpdated({
setupState: setup, // setCurrentState: state => (currentState = state),
}) // getCurrentState,
) // // nodesWithCodeBoundChildren,
// // nodesBoundByProps,
// componentLibraries,
// onScreenSlotRendered,
// setupState: setup,
// })
// )
return { return {
setup, setup,
destroy: () => unsubscribe(), destroy: () => unsubscribe(),
getCurrentState, getCurrentState,
store, store: appStore,
} }
} }
@ -80,16 +88,19 @@ const onStoreStateUpdated = ({
getCurrentState, getCurrentState,
componentLibraries, componentLibraries,
onScreenSlotRendered, onScreenSlotRendered,
setupState, setupState
}) => state => { }) => state => {
setCurrentState(state) // fire the state update event to re-render anything bound to this
attachChildren({ // setCurrentState(state)
componentLibraries,
treeNode: createTreeNode(), // setCurrentState(state)
onScreenSlotRendered, // attachChildren({
setupState, // componentLibraries,
getCurrentState, // treeNode: createTreeNode(),
})(document.querySelector("#app"), { hydrate: true, force: true }) // onScreenSlotRendered,
// setupState,
// getCurrentState,
// })(document.querySelector("#app"), { hydrate: true, force: true })
// // the original array gets changed by components' destroy() // // the original array gets changed by components' destroy()
// // so we make a clone and check if they are still in the original // // so we make a clone and check if they are still in the original
@ -154,32 +165,41 @@ const onStoreStateUpdated = ({
// node.component.$set(newProps) // node.component.$set(newProps)
// } // }
const _setup = ( const _setup = ({
handlerTypes, handlerTypes,
getCurrentState, getCurrentState,
bb bb,
) => node => { store
}) => node => {
console.log(node);
const props = node.props const props = node.props
const context = node.context || {} const context = node.context || {}
const initialProps = { ...props } const initialProps = { ...props }
// const storeBoundProps = [] // const storeBoundProps = []
const currentStoreState = getCurrentState() const currentStoreState = getCurrentState()
console.log("node", node);
// console.log("node", node);
// console.log("nodeComponent", node.component);
for (let propName in props) { for (let propName in props) {
if (isMetaProp(propName)) continue if (isMetaProp(propName)) continue
const propValue = props[propName] const propValue = props[propName]
// const binding = parseBinding(propValue) // 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, { initialProps[propName] = mustache.render(propValue, {
state: currentStoreState, state: currentStoreState,
context context
}) })
if (!node.stateBound) {
node.stateBound = true
}
} }
// if (isBound) binding.propName = propName // if (isBound) binding.propName = propName
@ -254,7 +274,7 @@ const _setup = (
// registerBindings(node, storeBoundProps) // registerBindings(node, storeBoundProps)
const setup = _setup(handlerTypes, getCurrentState, bb) const setup = _setup({ handlerTypes, getCurrentState, bb, store })
initialProps._bb = bb(node, setup) initialProps._bb = bb(node, setup)
return initialProps return initialProps

View File

@ -0,0 +1,16 @@
import { writable } from "svelte/store";
const appStore = writable({});
appStore.actions = {
};
const routerStore = writable({});
routerStore.actions = {
}
export {
appStore,
routerStore
}