Merge pull request #121 from shogunpurple/state-management-v2

State management v2
This commit is contained in:
Michael Shanks 2020-02-23 22:16:56 +00:00 committed by GitHub
commit 1a1a9e81e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1349 additions and 1445 deletions

View File

@ -5,12 +5,11 @@
"eslint": "^6.8.0", "eslint": "^6.8.0",
"eslint-plugin-prettier": "^3.1.2", "eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-svelte3": "^2.7.3", "eslint-plugin-svelte3": "^2.7.3",
"lerna": "^3.14.1", "lerna": "3.14.1",
"prettier": "^1.19.1", "prettier": "^1.19.1",
"prettier-plugin-svelte": "^0.7.0", "prettier-plugin-svelte": "^0.7.0",
"svelte": "^3.18.1" "svelte": "^3.18.1"
}, },
"dependencies": {},
"scripts": { "scripts": {
"bootstrap": "lerna bootstrap", "bootstrap": "lerna bootstrap",
"build": "lerna run build", "build": "lerna run build",

File diff suppressed because one or more lines are too long

View File

@ -680,14 +680,14 @@ const _savePage = async s => {
}) })
} }
const saveBackend = async s => { const saveBackend = async state => {
await api.post(`/_builder/api/${appname}/backend`, { await api.post(`/_builder/api/${appname}/backend`, {
appDefinition: { appDefinition: {
hierarchy: s.hierarchy, hierarchy: state.hierarchy,
actions: s.actions, actions: state.actions,
triggers: s.triggers, triggers: state.triggers,
}, },
accessLevels: s.accessLevels, accessLevels: state.accessLevels,
}) })
} }

View File

@ -22,6 +22,6 @@
appearance: none; appearance: none;
background: #fff; background: #fff;
border: 1px solid #ccc; border: 1px solid #ccc;
height: 50px; height: 35px;
} }
</style> </style>

View File

@ -23,11 +23,9 @@
select { select {
display: block; display: block;
font-size: 14px;
font-family: sans-serif; font-family: sans-serif;
font-weight: 500; font-weight: 500;
color: #163057; color: #163057;
line-height: 1.3;
padding: 1em 2.6em 0.9em 1.4em; padding: 1em 2.6em 0.9em 1.4em;
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
@ -38,7 +36,7 @@
appearance: none; appearance: none;
background: #fff; background: #fff;
border: 1px solid #ccc; border: 1px solid #ccc;
height: 50px; height: 35px;
} }
.arrow { .arrow {

View File

@ -70,6 +70,7 @@
hierarchy: $store.hierarchy, hierarchy: $store.hierarchy,
} }
$: selectedComponentId = $store.currentComponentInfo ? $store.currentComponentInfo._id : ""
</script> </script>
<div class="component-container"> <div class="component-container">
@ -84,6 +85,11 @@
<style> <style>
${styles || ''} ${styles || ''}
.pos-${selectedComponentId} {
border: 2px solid #0055ff;
}
body, html { body, html {
height: 100%!important; height: 100%!important;
} }

View File

@ -1,28 +1,28 @@
<script> <script>
import IconButton from "../../common/IconButton.svelte"; import IconButton from "../../common/IconButton.svelte"
import PlusButton from "../../common/PlusButton.svelte"; import PlusButton from "../../common/PlusButton.svelte"
import Select from "../../common/Select.svelte"; import Select from "../../common/Select.svelte"
import Input from "../../common/Input.svelte"; import StateBindingCascader from "./StateBindingCascader.svelte"
import StateBindingControl from "../StateBindingControl.svelte"; import StateBindingControl from "../StateBindingControl.svelte"
import { find, map, keys, reduce, keyBy } from "lodash/fp"; import { find, map, keys, reduce, keyBy } from "lodash/fp"
import { pipe, userWithFullAccess } from "../../common/core"; import { pipe, userWithFullAccess } from "../../common/core"
import { import {
EVENT_TYPE_MEMBER_NAME, EVENT_TYPE_MEMBER_NAME,
allHandlers, allHandlers,
} from "../../common/eventHandlers"; } from "../../common/eventHandlers"
import { store } from "../../builderStore"; import { store } from "../../builderStore"
export let handler; export let handler
export let onCreate; export let onCreate
export let onChanged; export let onChanged
export let onRemoved; export let onRemoved
export let index; export let index
export let newHandler; export let newHandler
let eventOptions; let eventOptions
let handlerType; let handlerType
let parameters = []; let parameters = []
$: eventOptions = allHandlers( $: eventOptions = allHandlers(
{ hierarchy: $store.hierarchy }, { hierarchy: $store.hierarchy },
@ -30,53 +30,54 @@
hierarchy: $store.hierarchy, hierarchy: $store.hierarchy,
actions: keyBy("name")($store.actions), actions: keyBy("name")($store.actions),
}) })
); )
$: { $: {
if (handler) { if (handler) {
handlerType = handler[EVENT_TYPE_MEMBER_NAME]; handlerType = handler[EVENT_TYPE_MEMBER_NAME]
parameters = Object.entries(handler.parameters).map(([name, value]) => ({ parameters = Object.entries(handler.parameters).map(([name, value]) => ({
name, name,
value, value,
})); }))
} else { } else {
// Empty Handler // Empty Handler
handlerType = ""; handlerType = ""
parameters = []; parameters = []
} }
} }
const handlerChanged = (type, params) => { const handlerChanged = (type, params) => {
const handlerParams = {}; const handlerParams = {}
for (let param of params) { for (let param of params) {
handlerParams[param.name] = param.value; handlerParams[param.name] = param.value
} }
const updatedHandler = { const updatedHandler = {
[EVENT_TYPE_MEMBER_NAME]: type, [EVENT_TYPE_MEMBER_NAME]: type,
parameters: handlerParams, parameters: handlerParams,
}; }
onChanged(updatedHandler, index); onChanged(updatedHandler, index)
}; }
const handlerTypeChanged = e => { const handlerTypeChanged = e => {
const handlerType = eventOptions.find( const handlerType = eventOptions.find(
handler => handler.name === e.target.value handler => handler.name === e.target.value
); )
const defaultParams = handlerType.parameters.map(param => ({ const defaultParams = handlerType.parameters.map(param => ({
name: param, name: param,
value: "", value: "",
})); }))
handlerChanged(handlerType.name, defaultParams); handlerChanged(handlerType.name, defaultParams)
}; }
const onParameterChanged = index => e => { const onParameterChanged = index => e => {
const newParams = [...parameters]; const value = e.target ? e.target.value : e
newParams[index].value = e.target.value; const newParams = [...parameters]
handlerChanged(handlerType, newParams); newParams[index].value = value
}; handlerChanged(handlerType, newParams)
}
</script> </script>
<div class="type-selector-container {newHandler && 'new-handler'}"> <div class="type-selector-container {newHandler && 'new-handler'}">
@ -91,11 +92,8 @@
</Select> </Select>
</div> </div>
{#if parameters} {#if parameters}
{#each parameters as param, idx} {#each parameters as parameter, idx}
<div class="handler-option"> <StateBindingCascader onChange={onParameterChanged(idx)} {parameter} />
<span>{param.name}</span>
<Input on:change={onParameterChanged(idx)} value={param.value} />
</div>
{/each} {/each}
{/if} {/if}
</div> </div>

View File

@ -0,0 +1,74 @@
<script>
import IconButton from "../../common/IconButton.svelte"
import PlusButton from "../../common/PlusButton.svelte"
import Select from "../../common/Select.svelte"
import Input from "../../common/Input.svelte"
import StateBindingControl from "../StateBindingControl.svelte"
import { find, map, keys, reduce, keyBy } from "lodash/fp"
import { pipe, userWithFullAccess } from "../../common/core"
import {
EVENT_TYPE_MEMBER_NAME,
allHandlers,
} from "../../common/eventHandlers"
import { store } from "../../builderStore"
import StateBindingOptions from "../PropertyCascader/StateBindingOptions.svelte"
import { ArrowDownIcon } from "../../common/Icons/"
export let parameter
export let onChange
let isOpen = false
</script>
<div class="handler-option">
<span>{parameter.name}</span>
<div class="handler-input">
<Input on:change={onChange} value={parameter.value} />
<button on:click={() => (isOpen = !isOpen)}>
<div class="icon" style={`transform: rotate(${isOpen ? 0 : 90}deg);`}>
<ArrowDownIcon size={36} />
</div>
</button>
{#if isOpen}
<StateBindingOptions
onSelect={option => {
onChange(option)
isOpen = false
}} />
{/if}
</div>
</div>
<style>
button {
cursor: pointer;
outline: none;
border: none;
border-radius: 5px;
background: rgba(249, 249, 249, 1);
font-size: 1.6rem;
font-weight: 700;
color: rgba(22, 48, 87, 1);
margin-left: 5px;
}
.icon {
width: 24px;
}
.handler-option {
display: flex;
flex-direction: column;
}
.handler-input {
position: relative;
display: flex;
}
span {
font-size: 12px;
margin-bottom: 5px;
}
</style>

View File

@ -1,8 +1,9 @@
<script> <script>
import { ArrowDownIcon } from "../common/Icons/" import { ArrowDownIcon } from "../../common/Icons/"
import { store } from "../builderStore" import { store } from "../../builderStore"
import { buildStateOrigins } from "../builderStore/buildStateOrigins" import { buildStateOrigins } from "../../builderStore/buildStateOrigins"
import { isBinding, getBinding, setBinding } from "../common/binding" import { isBinding, getBinding, setBinding } from "../../common/binding"
import StateBindingOptions from "./StateBindingOptions.svelte";
export let onChanged = () => {} export let onChanged = () => {}
export let value = "" export let value = ""
@ -15,7 +16,7 @@
let bindingSource = "store" let bindingSource = "store"
let bindingValue = "" let bindingValue = ""
const bind = (path, fallback, source) => { const bindValueToSource = (path, fallback, source) => {
if (!path) { if (!path) {
onChanged(fallback) onChanged(fallback)
return return
@ -25,12 +26,12 @@
} }
const setBindingPath = value => const setBindingPath = value =>
bind(value, bindingFallbackValue, bindingSource) bindValueToSource(value, bindingFallbackValue, bindingSource)
const setBindingFallback = value => bind(bindingPath, value, bindingSource) const setBindingFallback = value => bindValueToSource(bindingPath, value, bindingSource)
const setBindingSource = value => const setBindingSource = source =>
bind(bindingPath, bindingFallbackValue, value) bindValueToSource(bindingPath, bindingFallbackValue, source)
$: { $: {
const binding = getBinding(value) const binding = getBinding(value)
@ -58,29 +59,20 @@
setBindingFallback(e.target.value) setBindingFallback(e.target.value)
onChanged(e.target.value) onChanged(e.target.value)
}} /> }} />
{#if stateBindings.length} <button on:click={() => (isOpen = !isOpen)}>
<button on:click={() => (isOpen = !isOpen)}> <div
<div class="icon"
class="icon" class:highlighted={bindingPath}
class:highlighted={bindingPath} style={`transform: rotate(${isOpen ? 0 : 90}deg);`}>
style={`transform: rotate(${isOpen ? 0 : 90}deg);`}> <ArrowDownIcon size={36} />
<ArrowDownIcon size={36} /> </div>
</div> </button>
</button>
{/if}
</div> </div>
{#if isOpen} {#if isOpen}
<ul class="options"> <StateBindingOptions onSelect={option => {
{#each stateBindings as stateBinding} onChanged(option);
<li isOpen = false;
class:bold={stateBinding === bindingPath} }} />
on:click={() => {
setBindingPath(stateBinding === bindingPath ? null : stateBinding)
}}>
{stateBinding}
</li>
{/each}
</ul>
{/if} {/if}
</div> </div>
@ -115,27 +107,6 @@
align-items: center; align-items: center;
} }
.options {
width: 172px;
margin: 0;
position: absolute;
top: 35px;
padding: 10px;
z-index: 1;
background: rgba(249, 249, 249, 1);
min-height: 50px;
border-radius: 2px;
}
li {
list-style-type: none;
}
li:hover {
cursor: pointer;
font-weight: bold;
}
input { input {
margin-right: 5px; margin-right: 5px;
border: 1px solid #dbdbdb; border: 1px solid #dbdbdb;
@ -143,4 +114,8 @@
opacity: 0.5; opacity: 0.5;
height: 40px; height: 40px;
} }
.icon {
width: 24px;
}
</style> </style>

View File

@ -0,0 +1,63 @@
<script>
export let onSelect = () => {}
let options = [
{
name: "state",
description: "Front-end client state.",
},
{
name: "context",
description: "The component context object.",
},
{
name: "event",
description: "DOM event handler arguments.",
},
]
</script>
<ul class="options">
{#each options as option}
<li on:click={() => onSelect(`${option.name}.`)}>
<span class="name">{option.name}</span>
<span class="description">{option.description}</span>
</li>
{/each}
</ul>
<style>
.options {
width: 172px;
margin: 0;
position: absolute;
top: 35px;
padding: 10px;
z-index: 1;
background: rgba(249, 249, 249, 1);
min-height: 50px;
border-radius: 2px;
}
.description {
font-size: 0.8em;
}
.name {
color: rgba(22, 48, 87, 0.6);
font-size: 12px;
font-weight: bold;
text-transform: uppercase;
margin-top: 5px;
display: block;
}
.name:hover {
cursor: pointer;
font-weight: 800;
}
li {
list-style-type: none;
}
</style>

View File

@ -0,0 +1 @@
export { default } from "./PropertyCascader.svelte"

View File

@ -1,7 +1,7 @@
<script> <script>
import IconButton from "../common/IconButton.svelte" import IconButton from "../common/IconButton.svelte"
import Input from "../common/Input.svelte" import Input from "../common/Input.svelte"
import PropertyCascader from "./PropertyCascader.svelte" import PropertyCascader from "./PropertyCascader"
import { isBinding, getBinding, setBinding } from "../common/binding" import { isBinding, getBinding, setBinding } from "../common/binding"
export let value = "" export let value = ""

View File

@ -4,7 +4,7 @@ import { listRecords } from "./listRecords"
import { authenticate } from "./authenticate" import { authenticate } from "./authenticate"
import { saveRecord } from "./saveRecord" import { saveRecord } from "./saveRecord"
export const createApi = ({ rootPath, setState, getState }) => { export const createApi = ({ rootPath = "", setState, getState }) => {
const apiCall = method => ({ const apiCall = method => ({
url, url,
body, body,

View File

@ -2,6 +2,10 @@ import { createApp } from "./createApp"
import { trimSlash } from "./common/trimSlash" import { trimSlash } from "./common/trimSlash"
import { builtins, builtinLibName } from "./render/builtinComponents" import { builtins, builtinLibName } from "./render/builtinComponents"
/**
* create a web application from static budibase definition files.
* @param {object} opts - configuration options for budibase client libary
*/
export const loadBudibase = async (opts) => { export const loadBudibase = async (opts) => {
let componentLibraries = opts && opts.componentLibraries let componentLibraries = opts && opts.componentLibraries

View File

@ -21,7 +21,7 @@ export const eventHandlers = (store, coreApi, rootPath, routeTo) => {
}) })
const api = createApi({ const api = createApi({
rootPath: rootPath, rootPath,
setState: setStateWithStore, setState: setStateWithStore,
getState: (path, fallback) => getState(currentState, path, fallback), getState: (path, fallback) => getState(currentState, path, fallback),
}) })

View File

@ -4,8 +4,14 @@ 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 => { export const parseBinding = prop => {
if (!prop) return false if (!prop) return false
if (isBindingExpression(prop)) { if (isBindingExpression(prop)) {
return parseBindingExpression(prop) return parseBindingExpression(prop)
} }
@ -38,21 +44,22 @@ const isAlreadyBinding = prop => typeof prop === "object" && prop.path
const isBindingExpression = prop => const isBindingExpression = prop =>
typeof prop === "string" && typeof prop === "string" &&
(prop.startsWith("store.") || (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 = prop.split(".")[0] let [source, ...rest] = prop.split(".");
let path = prop.replace(`${source}.`, "") let path = rest.join(".")
if (source === "route") { if (source === "route") {
source = "store" source = "state"
path = `##routeParams.${path}` path = `##routeParams.${path}`
} }
const fallback = ""
return { return {
fallback, fallback: "", // TODO: provide fallback support
source, source,
path, path,
} }

View File

@ -5,28 +5,29 @@ export const setState = (store, path, value) => {
if (!path || path.length === 0) return if (!path || path.length === 0) return
const pathParts = path.split(".") const pathParts = path.split(".")
const safeSetPath = (obj, currentPartIndex = 0) => {
const safeSetPath = (state, currentPartIndex = 0) => {
const currentKey = pathParts[currentPartIndex] const currentKey = pathParts[currentPartIndex]
if (pathParts.length - 1 == currentPartIndex) { if (pathParts.length - 1 == currentPartIndex) {
obj[currentKey] = value state[currentKey] = value
return return
} }
if ( if (
obj[currentKey] === null || state[currentKey] === null ||
obj[currentKey] === undefined || state[currentKey] === undefined ||
!isObject(obj[currentKey]) !isObject(obj[currentKey])
) { ) {
obj[currentKey] = {} state[currentKey] = {}
} }
safeSetPath(obj[currentKey], currentPartIndex + 1) safeSetPath(state[currentKey], currentPartIndex + 1)
} }
store.update(s => { store.update(state => {
safeSetPath(s) safeSetPath(state)
return s return state
}) })
} }

View File

@ -171,13 +171,14 @@ const _setup = (
for (let propName in props) { for (let propName in props) {
if (isMetaProp(propName)) continue if (isMetaProp(propName)) continue
const val = props[propName] const propValue = props[propName]
const binding = parseBinding(val) const binding = parseBinding(propValue)
const isBound = !!binding const isBound = !!binding
if (isBound) binding.propName = propName if (isBound) binding.propName = propName
if (isBound && binding.source === "store") { if (isBound && binding.source === "state") {
storeBoundProps.push(binding) storeBoundProps.push(binding)
initialProps[propName] = !currentStoreState initialProps[propName] = !currentStoreState
@ -188,16 +189,20 @@ const _setup = (
binding.fallback, binding.fallback,
binding.source binding.source
) )
} else if (isBound && binding.source === "context") { }
if (isBound && binding.source === "context") {
initialProps[propName] = !context initialProps[propName] = !context
? val ? propValue
: getState(context, binding.path, binding.fallback, binding.source) : getState(context, binding.path, binding.fallback, binding.source)
} else if (isEventType(val)) { }
if (isEventType(propValue)) {
const handlersInfos = [] const handlersInfos = []
for (let e of val) { for (let event of propValue) {
const handlerInfo = { const handlerInfo = {
handlerType: e[EVENT_TYPE_MEMBER_NAME], handlerType: event[EVENT_TYPE_MEMBER_NAME],
parameters: e.parameters, parameters: event.parameters,
} }
const resolvedParams = {} const resolvedParams = {}
for (let paramName in handlerInfo.parameters) { for (let paramName in handlerInfo.parameters) {
@ -206,26 +211,20 @@ const _setup = (
if (!paramBinding) { if (!paramBinding) {
resolvedParams[paramName] = () => paramValue resolvedParams[paramName] = () => paramValue
continue continue
} else if (paramBinding.source === "context") { }
const val = getState(
context, let paramValueSource;
paramBinding.path,
paramBinding.fallback if (paramBinding.source === "context") paramValueSource = context;
) if (paramBinding.source === "state") paramValueSource = getCurrentState();
resolvedParams[paramName] = () => val if (paramBinding.source === "context") paramValueSource = context;
} else if (paramBinding.source === "store") {
resolvedParams[paramName] = () => // The new dynamic event parameter bound to the relevant source
getState( resolvedParams[paramName] = () => getState(
getCurrentState(), paramValueSource,
paramBinding.path, paramBinding.path,
paramBinding.fallback paramBinding.fallback
) );
continue
} else if (paramBinding.source === "event") {
resolvedParams[paramName] = eventContext => {
getState(eventContext, paramBinding.path, paramBinding.fallback)
}
}
} }
handlerInfo.parameters = resolvedParams handlerInfo.parameters = resolvedParams
@ -256,9 +255,9 @@ const makeHandler = (handlerTypes, handlerInfo) => {
const handlerType = handlerTypes[handlerInfo.handlerType] const handlerType = handlerTypes[handlerInfo.handlerType]
return context => { return context => {
const parameters = {} const parameters = {}
for (let p in handlerInfo.parameters) { for (let paramName in handlerInfo.parameters) {
parameters[p] = handlerInfo.parameters[p](context) parameters[paramName] = handlerInfo.parameters[paramName](context)
} }
handlerType.execute(parameters) handlerType.execute(parameters)
} }
} }

View File

@ -0,0 +1,274 @@
import {
isEventType,
eventHandlers,
EVENT_TYPE_MEMBER_NAME,
} from "./eventHandlers"
import { bbFactory } from "./bbComponentApi"
import { getState } from "./getState"
import { attachChildren } from "../render/attachChildren"
import { parseBinding } from "./parseBinding"
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,
routeTo,
}) => {
let handlerTypes = eventHandlers(store, coreApi, rootPath, routeTo)
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)
}
/**
* Bind a components event handler parameters to state, context or the event itself.
* @param {Array} eventHandlerProp - event handler array from component definition
*/
function bindComponentEventHandlers(eventHandlerProp) {
const boundEventHandlers = []
for (let event of eventHandlerProp) {
const boundEventHandler = {
handlerType: event[EVENT_TYPE_MEMBER_NAME],
parameters: event.parameters,
}
const boundParameters = {}
for (let paramName in boundEventHandler.parameters) {
const paramValue = boundEventHandler.parameters[paramName]
const paramBinding = parseBinding(paramValue)
if (!paramBinding) {
boundParameters[paramName] = () => paramValue
continue
}
let paramValueSource;
if (paramBinding.source === "context") paramValueSource = context;
if (paramBinding.source === "state") paramValueSource = getCurrentState();
// The new dynamic event parameter bound to the relevant source
boundParameters[paramName] = eventContext => getState(
paramBinding.source === "event" ? eventContext : paramValueSource,
paramBinding.path,
paramBinding.fallback
);
}
boundEventHandler.parameters = boundParameters
boundEventHandlers.push(boundEventHandlers)
return boundEventHandlers;
}
}
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 propValue = props[propName]
const binding = parseBinding(propValue)
const isBound = !!binding
if (isBound) binding.propName = propName
if (isBound && binding.source === "state") {
storeBoundProps.push(binding)
initialProps[propName] = !currentStoreState
? binding.fallback
: getState(
currentStoreState,
binding.path,
binding.fallback,
binding.source
)
}
if (isBound && binding.source === "context") {
initialProps[propName] = !context
? propValue
: getState(context, binding.path, binding.fallback, binding.source)
}
if (isEventType(propValue)) {
const boundEventHandlers = bindComponentEventHandlers(propValue);
if (boundEventHandlers.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 paramName in handlerInfo.parameters) {
parameters[paramName] = handlerInfo.parameters[paramName](context)
}
handlerType.execute(parameters)
}
}

View File

@ -74,6 +74,7 @@
"tel", "time", "week"], "tel", "time", "week"],
"default":"text" "default":"text"
}, },
"onChange": "event",
"className": "string" "className": "string"
}, },
"tags": ["form"] "tags": ["form"]

1643
yarn.lock

File diff suppressed because it is too large Load Diff