Merge pull request #121 from shogunpurple/state-management-v2
State management v2
This commit is contained in:
commit
1a1a9e81e0
|
@ -5,12 +5,11 @@
|
|||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-prettier": "^3.1.2",
|
||||
"eslint-plugin-svelte3": "^2.7.3",
|
||||
"lerna": "^3.14.1",
|
||||
"lerna": "3.14.1",
|
||||
"prettier": "^1.19.1",
|
||||
"prettier-plugin-svelte": "^0.7.0",
|
||||
"svelte": "^3.18.1"
|
||||
},
|
||||
"dependencies": {},
|
||||
"scripts": {
|
||||
"bootstrap": "lerna bootstrap",
|
||||
"build": "lerna run build",
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -680,14 +680,14 @@ const _savePage = async s => {
|
|||
})
|
||||
}
|
||||
|
||||
const saveBackend = async s => {
|
||||
const saveBackend = async state => {
|
||||
await api.post(`/_builder/api/${appname}/backend`, {
|
||||
appDefinition: {
|
||||
hierarchy: s.hierarchy,
|
||||
actions: s.actions,
|
||||
triggers: s.triggers,
|
||||
hierarchy: state.hierarchy,
|
||||
actions: state.actions,
|
||||
triggers: state.triggers,
|
||||
},
|
||||
accessLevels: s.accessLevels,
|
||||
accessLevels: state.accessLevels,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,6 @@
|
|||
appearance: none;
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
height: 50px;
|
||||
height: 35px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -23,11 +23,9 @@
|
|||
|
||||
select {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-family: sans-serif;
|
||||
font-weight: 500;
|
||||
color: #163057;
|
||||
line-height: 1.3;
|
||||
padding: 1em 2.6em 0.9em 1.4em;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
|
@ -38,7 +36,7 @@
|
|||
appearance: none;
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
height: 50px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
|
|
|
@ -70,6 +70,7 @@
|
|||
hierarchy: $store.hierarchy,
|
||||
}
|
||||
|
||||
$: selectedComponentId = $store.currentComponentInfo ? $store.currentComponentInfo._id : ""
|
||||
</script>
|
||||
|
||||
<div class="component-container">
|
||||
|
@ -84,6 +85,11 @@
|
|||
|
||||
<style>
|
||||
${styles || ''}
|
||||
|
||||
.pos-${selectedComponentId} {
|
||||
border: 2px solid #0055ff;
|
||||
}
|
||||
|
||||
body, html {
|
||||
height: 100%!important;
|
||||
}
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
<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 IconButton from "../../common/IconButton.svelte"
|
||||
import PlusButton from "../../common/PlusButton.svelte"
|
||||
import Select from "../../common/Select.svelte"
|
||||
import StateBindingCascader from "./StateBindingCascader.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";
|
||||
} from "../../common/eventHandlers"
|
||||
import { store } from "../../builderStore"
|
||||
|
||||
export let handler;
|
||||
export let onCreate;
|
||||
export let onChanged;
|
||||
export let onRemoved;
|
||||
export let handler
|
||||
export let onCreate
|
||||
export let onChanged
|
||||
export let onRemoved
|
||||
|
||||
export let index;
|
||||
export let newHandler;
|
||||
export let index
|
||||
export let newHandler
|
||||
|
||||
let eventOptions;
|
||||
let handlerType;
|
||||
let parameters = [];
|
||||
let eventOptions
|
||||
let handlerType
|
||||
let parameters = []
|
||||
|
||||
$: eventOptions = allHandlers(
|
||||
{ hierarchy: $store.hierarchy },
|
||||
|
@ -30,53 +30,54 @@
|
|||
hierarchy: $store.hierarchy,
|
||||
actions: keyBy("name")($store.actions),
|
||||
})
|
||||
);
|
||||
)
|
||||
|
||||
$: {
|
||||
if (handler) {
|
||||
handlerType = handler[EVENT_TYPE_MEMBER_NAME];
|
||||
handlerType = handler[EVENT_TYPE_MEMBER_NAME]
|
||||
parameters = Object.entries(handler.parameters).map(([name, value]) => ({
|
||||
name,
|
||||
value,
|
||||
}));
|
||||
}))
|
||||
} else {
|
||||
// Empty Handler
|
||||
handlerType = "";
|
||||
parameters = [];
|
||||
handlerType = ""
|
||||
parameters = []
|
||||
}
|
||||
}
|
||||
|
||||
const handlerChanged = (type, params) => {
|
||||
const handlerParams = {};
|
||||
const handlerParams = {}
|
||||
for (let param of params) {
|
||||
handlerParams[param.name] = param.value;
|
||||
handlerParams[param.name] = param.value
|
||||
}
|
||||
|
||||
const updatedHandler = {
|
||||
[EVENT_TYPE_MEMBER_NAME]: type,
|
||||
parameters: handlerParams,
|
||||
};
|
||||
}
|
||||
|
||||
onChanged(updatedHandler, index);
|
||||
};
|
||||
onChanged(updatedHandler, index)
|
||||
}
|
||||
|
||||
const handlerTypeChanged = e => {
|
||||
const handlerType = eventOptions.find(
|
||||
handler => handler.name === e.target.value
|
||||
);
|
||||
)
|
||||
const defaultParams = handlerType.parameters.map(param => ({
|
||||
name: param,
|
||||
value: "",
|
||||
}));
|
||||
}))
|
||||
|
||||
handlerChanged(handlerType.name, defaultParams);
|
||||
};
|
||||
handlerChanged(handlerType.name, defaultParams)
|
||||
}
|
||||
|
||||
const onParameterChanged = index => e => {
|
||||
const newParams = [...parameters];
|
||||
newParams[index].value = e.target.value;
|
||||
handlerChanged(handlerType, newParams);
|
||||
};
|
||||
const value = e.target ? e.target.value : e
|
||||
const newParams = [...parameters]
|
||||
newParams[index].value = value
|
||||
handlerChanged(handlerType, newParams)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="type-selector-container {newHandler && 'new-handler'}">
|
||||
|
@ -91,11 +92,8 @@
|
|||
</Select>
|
||||
</div>
|
||||
{#if parameters}
|
||||
{#each parameters as param, idx}
|
||||
<div class="handler-option">
|
||||
<span>{param.name}</span>
|
||||
<Input on:change={onParameterChanged(idx)} value={param.value} />
|
||||
</div>
|
||||
{#each parameters as parameter, idx}
|
||||
<StateBindingCascader onChange={onParameterChanged(idx)} {parameter} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
@ -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>
|
|
@ -1,8 +1,9 @@
|
|||
<script>
|
||||
import { ArrowDownIcon } from "../common/Icons/"
|
||||
import { store } from "../builderStore"
|
||||
import { buildStateOrigins } from "../builderStore/buildStateOrigins"
|
||||
import { isBinding, getBinding, setBinding } from "../common/binding"
|
||||
import { ArrowDownIcon } from "../../common/Icons/"
|
||||
import { store } from "../../builderStore"
|
||||
import { buildStateOrigins } from "../../builderStore/buildStateOrigins"
|
||||
import { isBinding, getBinding, setBinding } from "../../common/binding"
|
||||
import StateBindingOptions from "./StateBindingOptions.svelte";
|
||||
|
||||
export let onChanged = () => {}
|
||||
export let value = ""
|
||||
|
@ -15,7 +16,7 @@
|
|||
let bindingSource = "store"
|
||||
let bindingValue = ""
|
||||
|
||||
const bind = (path, fallback, source) => {
|
||||
const bindValueToSource = (path, fallback, source) => {
|
||||
if (!path) {
|
||||
onChanged(fallback)
|
||||
return
|
||||
|
@ -25,12 +26,12 @@
|
|||
}
|
||||
|
||||
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 =>
|
||||
bind(bindingPath, bindingFallbackValue, value)
|
||||
const setBindingSource = source =>
|
||||
bindValueToSource(bindingPath, bindingFallbackValue, source)
|
||||
|
||||
$: {
|
||||
const binding = getBinding(value)
|
||||
|
@ -58,29 +59,20 @@
|
|||
setBindingFallback(e.target.value)
|
||||
onChanged(e.target.value)
|
||||
}} />
|
||||
{#if stateBindings.length}
|
||||
<button on:click={() => (isOpen = !isOpen)}>
|
||||
<div
|
||||
class="icon"
|
||||
class:highlighted={bindingPath}
|
||||
style={`transform: rotate(${isOpen ? 0 : 90}deg);`}>
|
||||
<ArrowDownIcon size={36} />
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
<button on:click={() => (isOpen = !isOpen)}>
|
||||
<div
|
||||
class="icon"
|
||||
class:highlighted={bindingPath}
|
||||
style={`transform: rotate(${isOpen ? 0 : 90}deg);`}>
|
||||
<ArrowDownIcon size={36} />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{#if isOpen}
|
||||
<ul class="options">
|
||||
{#each stateBindings as stateBinding}
|
||||
<li
|
||||
class:bold={stateBinding === bindingPath}
|
||||
on:click={() => {
|
||||
setBindingPath(stateBinding === bindingPath ? null : stateBinding)
|
||||
}}>
|
||||
{stateBinding}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<StateBindingOptions onSelect={option => {
|
||||
onChanged(option);
|
||||
isOpen = false;
|
||||
}} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
@ -115,27 +107,6 @@
|
|||
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 {
|
||||
margin-right: 5px;
|
||||
border: 1px solid #dbdbdb;
|
||||
|
@ -143,4 +114,8 @@
|
|||
opacity: 0.5;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 24px;
|
||||
}
|
||||
</style>
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./PropertyCascader.svelte"
|
|
@ -1,7 +1,7 @@
|
|||
<script>
|
||||
import IconButton from "../common/IconButton.svelte"
|
||||
import Input from "../common/Input.svelte"
|
||||
import PropertyCascader from "./PropertyCascader.svelte"
|
||||
import PropertyCascader from "./PropertyCascader"
|
||||
import { isBinding, getBinding, setBinding } from "../common/binding"
|
||||
|
||||
export let value = ""
|
||||
|
|
|
@ -4,7 +4,7 @@ import { listRecords } from "./listRecords"
|
|||
import { authenticate } from "./authenticate"
|
||||
import { saveRecord } from "./saveRecord"
|
||||
|
||||
export const createApi = ({ rootPath, setState, getState }) => {
|
||||
export const createApi = ({ rootPath = "", setState, getState }) => {
|
||||
const apiCall = method => ({
|
||||
url,
|
||||
body,
|
||||
|
|
|
@ -2,6 +2,10 @@ import { createApp } from "./createApp"
|
|||
import { trimSlash } from "./common/trimSlash"
|
||||
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) => {
|
||||
|
||||
let componentLibraries = opts && opts.componentLibraries
|
||||
|
|
|
@ -21,7 +21,7 @@ export const eventHandlers = (store, coreApi, rootPath, routeTo) => {
|
|||
})
|
||||
|
||||
const api = createApi({
|
||||
rootPath: rootPath,
|
||||
rootPath,
|
||||
setState: setStateWithStore,
|
||||
getState: (path, fallback) => getState(currentState, path, fallback),
|
||||
})
|
||||
|
|
|
@ -4,8 +4,14 @@ export const BB_STATE_FALLBACK = "##bbstatefallback"
|
|||
|
||||
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
|
||||
|
||||
if (isBindingExpression(prop)) {
|
||||
return parseBindingExpression(prop)
|
||||
}
|
||||
|
@ -38,21 +44,22 @@ const isAlreadyBinding = prop => typeof prop === "object" && prop.path
|
|||
|
||||
const isBindingExpression = prop =>
|
||||
typeof prop === "string" &&
|
||||
(prop.startsWith("store.") ||
|
||||
(prop.startsWith("state.") ||
|
||||
prop.startsWith("context.") ||
|
||||
prop.startsWith("event.") ||
|
||||
prop.startsWith("route."))
|
||||
|
||||
const parseBindingExpression = prop => {
|
||||
let source = prop.split(".")[0]
|
||||
let path = prop.replace(`${source}.`, "")
|
||||
let [source, ...rest] = prop.split(".");
|
||||
let path = rest.join(".")
|
||||
|
||||
if (source === "route") {
|
||||
source = "store"
|
||||
source = "state"
|
||||
path = `##routeParams.${path}`
|
||||
}
|
||||
const fallback = ""
|
||||
|
||||
return {
|
||||
fallback,
|
||||
fallback: "", // TODO: provide fallback support
|
||||
source,
|
||||
path,
|
||||
}
|
||||
|
|
|
@ -5,28 +5,29 @@ export const setState = (store, path, value) => {
|
|||
if (!path || path.length === 0) return
|
||||
|
||||
const pathParts = path.split(".")
|
||||
const safeSetPath = (obj, currentPartIndex = 0) => {
|
||||
|
||||
const safeSetPath = (state, currentPartIndex = 0) => {
|
||||
const currentKey = pathParts[currentPartIndex]
|
||||
|
||||
if (pathParts.length - 1 == currentPartIndex) {
|
||||
obj[currentKey] = value
|
||||
state[currentKey] = value
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
obj[currentKey] === null ||
|
||||
obj[currentKey] === undefined ||
|
||||
state[currentKey] === null ||
|
||||
state[currentKey] === undefined ||
|
||||
!isObject(obj[currentKey])
|
||||
) {
|
||||
obj[currentKey] = {}
|
||||
state[currentKey] = {}
|
||||
}
|
||||
|
||||
safeSetPath(obj[currentKey], currentPartIndex + 1)
|
||||
safeSetPath(state[currentKey], currentPartIndex + 1)
|
||||
}
|
||||
|
||||
store.update(s => {
|
||||
safeSetPath(s)
|
||||
return s
|
||||
store.update(state => {
|
||||
safeSetPath(state)
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -171,13 +171,14 @@ const _setup = (
|
|||
for (let propName in props) {
|
||||
if (isMetaProp(propName)) continue
|
||||
|
||||
const val = props[propName]
|
||||
const propValue = props[propName]
|
||||
|
||||
const binding = parseBinding(val)
|
||||
const binding = parseBinding(propValue)
|
||||
const isBound = !!binding
|
||||
|
||||
if (isBound) binding.propName = propName
|
||||
|
||||
if (isBound && binding.source === "store") {
|
||||
if (isBound && binding.source === "state") {
|
||||
storeBoundProps.push(binding)
|
||||
|
||||
initialProps[propName] = !currentStoreState
|
||||
|
@ -188,16 +189,20 @@ const _setup = (
|
|||
binding.fallback,
|
||||
binding.source
|
||||
)
|
||||
} else if (isBound && binding.source === "context") {
|
||||
}
|
||||
|
||||
if (isBound && binding.source === "context") {
|
||||
initialProps[propName] = !context
|
||||
? val
|
||||
? propValue
|
||||
: getState(context, binding.path, binding.fallback, binding.source)
|
||||
} else if (isEventType(val)) {
|
||||
}
|
||||
|
||||
if (isEventType(propValue)) {
|
||||
const handlersInfos = []
|
||||
for (let e of val) {
|
||||
for (let event of propValue) {
|
||||
const handlerInfo = {
|
||||
handlerType: e[EVENT_TYPE_MEMBER_NAME],
|
||||
parameters: e.parameters,
|
||||
handlerType: event[EVENT_TYPE_MEMBER_NAME],
|
||||
parameters: event.parameters,
|
||||
}
|
||||
const resolvedParams = {}
|
||||
for (let paramName in handlerInfo.parameters) {
|
||||
|
@ -206,26 +211,20 @@ const _setup = (
|
|||
if (!paramBinding) {
|
||||
resolvedParams[paramName] = () => paramValue
|
||||
continue
|
||||
} else if (paramBinding.source === "context") {
|
||||
const val = getState(
|
||||
context,
|
||||
paramBinding.path,
|
||||
paramBinding.fallback
|
||||
)
|
||||
resolvedParams[paramName] = () => val
|
||||
} else if (paramBinding.source === "store") {
|
||||
resolvedParams[paramName] = () =>
|
||||
getState(
|
||||
getCurrentState(),
|
||||
paramBinding.path,
|
||||
paramBinding.fallback
|
||||
)
|
||||
continue
|
||||
} else if (paramBinding.source === "event") {
|
||||
resolvedParams[paramName] = eventContext => {
|
||||
getState(eventContext, paramBinding.path, paramBinding.fallback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let paramValueSource;
|
||||
|
||||
if (paramBinding.source === "context") paramValueSource = context;
|
||||
if (paramBinding.source === "state") paramValueSource = getCurrentState();
|
||||
if (paramBinding.source === "context") paramValueSource = context;
|
||||
|
||||
// The new dynamic event parameter bound to the relevant source
|
||||
resolvedParams[paramName] = () => getState(
|
||||
paramValueSource,
|
||||
paramBinding.path,
|
||||
paramBinding.fallback
|
||||
);
|
||||
}
|
||||
|
||||
handlerInfo.parameters = resolvedParams
|
||||
|
@ -256,9 +255,9 @@ 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)
|
||||
for (let paramName in handlerInfo.parameters) {
|
||||
parameters[paramName] = handlerInfo.parameters[paramName](context)
|
||||
}
|
||||
handlerType.execute(parameters)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -74,6 +74,7 @@
|
|||
"tel", "time", "week"],
|
||||
"default":"text"
|
||||
},
|
||||
"onChange": "event",
|
||||
"className": "string"
|
||||
},
|
||||
"tags": ["form"]
|
||||
|
|
Loading…
Reference in New Issue