client lib - new binding
This commit is contained in:
parent
12e20e460d
commit
a82c0dd44e
|
@ -1,5 +1,7 @@
|
||||||
export function uuid() {
|
export function uuid() {
|
||||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => {
|
// always want to make this start with a letter, as this makes it
|
||||||
|
// easier to use with mustache bindings in the client
|
||||||
|
return "cxxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx".replace(/[xy]/g, c => {
|
||||||
const r = (Math.random() * 16) | 0,
|
const r = (Math.random() * 16) | 0,
|
||||||
v = c == "x" ? r : (r & 0x3) | 0x8
|
v = c == "x" ? r : (r & 0x3) | 0x8
|
||||||
return v.toString(16)
|
return v.toString(16)
|
||||||
|
|
|
@ -21,28 +21,24 @@
|
||||||
"\\.(css|less|sass|scss)$": "identity-obj-proxy"
|
"\\.(css|less|sass|scss)$": "identity-obj-proxy"
|
||||||
},
|
},
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
"js"
|
"js",
|
||||||
|
"svelte"
|
||||||
],
|
],
|
||||||
"moduleDirectories": [
|
"moduleDirectories": [
|
||||||
"node_modules"
|
"node_modules"
|
||||||
],
|
],
|
||||||
"transform": {
|
"transform": {
|
||||||
"^.+js$": "babel-jest"
|
"^.+js$": "babel-jest",
|
||||||
|
"^.+.svelte$": "svelte-jester"
|
||||||
},
|
},
|
||||||
"transformIgnorePatterns": [
|
"transformIgnorePatterns": [
|
||||||
"/node_modules/(?!svelte).+\\.js$"
|
"/node_modules/(?!svelte).+\\.js$"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nx-js/compiler-util": "^2.0.0",
|
|
||||||
"bcryptjs": "^2.4.3",
|
|
||||||
"deep-equal": "^2.0.1",
|
"deep-equal": "^2.0.1",
|
||||||
"lodash": "^4.17.15",
|
|
||||||
"lunr": "^2.3.5",
|
|
||||||
"mustache": "^4.0.1",
|
"mustache": "^4.0.1",
|
||||||
"regexparam": "^1.3.0",
|
"regexparam": "^1.3.0"
|
||||||
"shortid": "^2.2.8",
|
|
||||||
"svelte": "^3.9.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.5.5",
|
"@babel/core": "^7.5.5",
|
||||||
|
@ -58,7 +54,9 @@
|
||||||
"rollup-plugin-node-builtins": "^2.1.2",
|
"rollup-plugin-node-builtins": "^2.1.2",
|
||||||
"rollup-plugin-node-globals": "^1.4.0",
|
"rollup-plugin-node-globals": "^1.4.0",
|
||||||
"rollup-plugin-node-resolve": "^5.2.0",
|
"rollup-plugin-node-resolve": "^5.2.0",
|
||||||
"rollup-plugin-terser": "^4.0.4"
|
"rollup-plugin-terser": "^4.0.4",
|
||||||
|
"svelte": "3.23.x",
|
||||||
|
"svelte-jester": "^1.0.6"
|
||||||
},
|
},
|
||||||
"gitHead": "e4e053cb6ff9a0ddc7115b44ccaa24b8ec41fb9a"
|
"gitHead": "e4e053cb6ff9a0ddc7115b44ccaa24b8ec41fb9a"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,74 +3,6 @@ import commonjs from "rollup-plugin-commonjs"
|
||||||
import builtins from "rollup-plugin-node-builtins"
|
import builtins from "rollup-plugin-node-builtins"
|
||||||
import nodeglobals from "rollup-plugin-node-globals"
|
import nodeglobals from "rollup-plugin-node-globals"
|
||||||
|
|
||||||
const lodash_fp_exports = [
|
|
||||||
"find",
|
|
||||||
"compose",
|
|
||||||
"isUndefined",
|
|
||||||
"split",
|
|
||||||
"max",
|
|
||||||
"last",
|
|
||||||
"union",
|
|
||||||
"reduce",
|
|
||||||
"isObject",
|
|
||||||
"cloneDeep",
|
|
||||||
"some",
|
|
||||||
"isArray",
|
|
||||||
"map",
|
|
||||||
"filter",
|
|
||||||
"keys",
|
|
||||||
"isFunction",
|
|
||||||
"isEmpty",
|
|
||||||
"countBy",
|
|
||||||
"join",
|
|
||||||
"includes",
|
|
||||||
"flatten",
|
|
||||||
"constant",
|
|
||||||
"first",
|
|
||||||
"intersection",
|
|
||||||
"take",
|
|
||||||
"has",
|
|
||||||
"mapValues",
|
|
||||||
"isString",
|
|
||||||
"isBoolean",
|
|
||||||
"isNull",
|
|
||||||
"isNumber",
|
|
||||||
"isObjectLike",
|
|
||||||
"isDate",
|
|
||||||
"clone",
|
|
||||||
"values",
|
|
||||||
"keyBy",
|
|
||||||
"isNaN",
|
|
||||||
"isInteger",
|
|
||||||
"toNumber",
|
|
||||||
]
|
|
||||||
|
|
||||||
const lodash_exports = [
|
|
||||||
"flow",
|
|
||||||
"head",
|
|
||||||
"find",
|
|
||||||
"each",
|
|
||||||
"tail",
|
|
||||||
"findIndex",
|
|
||||||
"startsWith",
|
|
||||||
"dropRight",
|
|
||||||
"takeRight",
|
|
||||||
"trim",
|
|
||||||
"split",
|
|
||||||
"replace",
|
|
||||||
"merge",
|
|
||||||
"assign",
|
|
||||||
]
|
|
||||||
|
|
||||||
const coreExternal = [
|
|
||||||
"lodash",
|
|
||||||
"lodash/fp",
|
|
||||||
"lunr",
|
|
||||||
"safe-buffer",
|
|
||||||
"shortid",
|
|
||||||
"@nx-js/compiler-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: "src/index.js",
|
input: "src/index.js",
|
||||||
output: [
|
output: [
|
||||||
|
@ -90,17 +22,8 @@ export default {
|
||||||
resolve({
|
resolve({
|
||||||
preferBuiltins: true,
|
preferBuiltins: true,
|
||||||
browser: true,
|
browser: true,
|
||||||
dedupe: importee => {
|
|
||||||
return coreExternal.includes(importee)
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
commonjs({
|
|
||||||
namedExports: {
|
|
||||||
"lodash/fp": lodash_fp_exports,
|
|
||||||
lodash: lodash_exports,
|
|
||||||
shortid: ["generate"],
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
|
commonjs(),
|
||||||
builtins(),
|
builtins(),
|
||||||
nodeglobals(),
|
nodeglobals(),
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import appStore from "../state/store"
|
||||||
|
|
||||||
export const USER_STATE_PATH = "_bbuser"
|
export const USER_STATE_PATH = "_bbuser"
|
||||||
|
|
||||||
export const authenticate = api => async ({ username, password }) => {
|
export const authenticate = api => async ({ username, password }) => {
|
||||||
|
@ -17,6 +19,10 @@ export const authenticate = api => async ({ username, password }) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// set user even if error - so it is defined at least
|
// set user even if error - so it is defined at least
|
||||||
api.setState(USER_STATE_PATH, user)
|
appStore.update(s => {
|
||||||
|
s[USER_STATE_PATH] = user
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
|
||||||
localStorage.setItem("budibase:user", JSON.stringify(user))
|
localStorage.setItem("budibase:user", JSON.stringify(user))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,62 +1,59 @@
|
||||||
import { authenticate } from "./authenticate"
|
import { authenticate } from "./authenticate"
|
||||||
import { triggerWorkflow } from "./workflow"
|
import { triggerWorkflow } from "./workflow"
|
||||||
|
import appStore from "../state/store"
|
||||||
|
|
||||||
export const createApi = ({ setState, getState }) => {
|
const apiCall = method => async ({ url, body }) => {
|
||||||
const apiCall = method => async ({ url, body }) => {
|
const response = await fetch(url, {
|
||||||
const response = await fetch(url, {
|
method: method,
|
||||||
method: method,
|
headers: {
|
||||||
headers: {
|
"Content-Type": "application/json",
|
||||||
"Content-Type": "application/json",
|
},
|
||||||
},
|
body: body && JSON.stringify(body),
|
||||||
body: body && JSON.stringify(body),
|
credentials: "same-origin",
|
||||||
credentials: "same-origin",
|
})
|
||||||
})
|
|
||||||
|
|
||||||
switch (response.status) {
|
switch (response.status) {
|
||||||
case 200:
|
case 200:
|
||||||
|
return response.json()
|
||||||
|
case 404:
|
||||||
|
return error(`${url} Not found`)
|
||||||
|
case 400:
|
||||||
|
return error(`${url} Bad Request`)
|
||||||
|
case 403:
|
||||||
|
return error(`${url} Forbidden`)
|
||||||
|
default:
|
||||||
|
if (response.status >= 200 && response.status < 400) {
|
||||||
return response.json()
|
return response.json()
|
||||||
case 404:
|
}
|
||||||
return error(`${url} Not found`)
|
|
||||||
case 400:
|
|
||||||
return error(`${url} Bad Request`)
|
|
||||||
case 403:
|
|
||||||
return error(`${url} Forbidden`)
|
|
||||||
default:
|
|
||||||
if (response.status >= 200 && response.status < 400) {
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
|
|
||||||
return error(`${url} - ${response.statusText}`)
|
return error(`${url} - ${response.statusText}`)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const post = apiCall("POST")
|
|
||||||
const get = apiCall("GET")
|
|
||||||
const patch = apiCall("PATCH")
|
|
||||||
const del = apiCall("DELETE")
|
|
||||||
|
|
||||||
const ERROR_MEMBER = "##error"
|
|
||||||
const error = message => {
|
|
||||||
const err = { [ERROR_MEMBER]: message }
|
|
||||||
setState("##error_message", message)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
const isSuccess = obj => !obj || !obj[ERROR_MEMBER]
|
|
||||||
|
|
||||||
const apiOpts = {
|
|
||||||
setState,
|
|
||||||
getState,
|
|
||||||
isSuccess,
|
|
||||||
error,
|
|
||||||
post,
|
|
||||||
get,
|
|
||||||
patch,
|
|
||||||
delete: del,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
authenticate: authenticate(apiOpts),
|
|
||||||
triggerWorkflow: triggerWorkflow(apiOpts),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const post = apiCall("POST")
|
||||||
|
const get = apiCall("GET")
|
||||||
|
const patch = apiCall("PATCH")
|
||||||
|
const del = apiCall("DELETE")
|
||||||
|
|
||||||
|
const ERROR_MEMBER = "##error"
|
||||||
|
const error = message => {
|
||||||
|
const err = { [ERROR_MEMBER]: message }
|
||||||
|
appStore.update(s => s["##error_message"], message)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSuccess = obj => !obj || !obj[ERROR_MEMBER]
|
||||||
|
|
||||||
|
const apiOpts = {
|
||||||
|
isSuccess,
|
||||||
|
error,
|
||||||
|
post,
|
||||||
|
get,
|
||||||
|
patch,
|
||||||
|
delete: del,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
authenticate: authenticate(apiOpts),
|
||||||
|
triggerWorkflow: triggerWorkflow(apiOpts),
|
||||||
|
}
|
||||||
|
|
|
@ -1,16 +1,6 @@
|
||||||
import { setState } from "../../state/setState"
|
|
||||||
|
|
||||||
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
|
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
SET_STATE: ({ context, args, id }) => {
|
|
||||||
setState(...Object.values(args))
|
|
||||||
context = {
|
|
||||||
...context,
|
|
||||||
[id]: args,
|
|
||||||
}
|
|
||||||
return context
|
|
||||||
},
|
|
||||||
NAVIGATE: () => {
|
NAVIGATE: () => {
|
||||||
// TODO client navigation
|
// TODO client navigation
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { get } from "svelte/store"
|
|
||||||
import mustache from "mustache"
|
import mustache from "mustache"
|
||||||
import { appStore } from "../../state/store"
|
import appStore from "../../state/store"
|
||||||
import Orchestrator from "./orchestrator"
|
import Orchestrator from "./orchestrator"
|
||||||
import clientActions from "./actions"
|
import clientActions from "./actions"
|
||||||
|
|
||||||
|
@ -20,7 +19,7 @@ export const clientStrategy = ({ api }) => ({
|
||||||
// Render the string with values from the workflow context and state
|
// Render the string with values from the workflow context and state
|
||||||
mappedArgs[arg] = mustache.render(argValue, {
|
mappedArgs[arg] = mustache.render(argValue, {
|
||||||
context: this.context,
|
context: this.context,
|
||||||
state: get(appStore),
|
state: appStore.get(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { split, last, compose } from "lodash/fp"
|
|
||||||
import { prepareRenderComponent } from "./prepareRenderComponent"
|
import { prepareRenderComponent } from "./prepareRenderComponent"
|
||||||
import { isScreenSlot } from "./builtinComponents"
|
import { isScreenSlot } from "./builtinComponents"
|
||||||
import deepEqual from "deep-equal"
|
import deepEqual from "deep-equal"
|
||||||
|
import appStore from "../state/store"
|
||||||
|
|
||||||
export const attachChildren = initialiseOpts => (htmlElement, options) => {
|
export const attachChildren = initialiseOpts => (htmlElement, options) => {
|
||||||
const {
|
const {
|
||||||
|
@ -30,11 +30,28 @@ export const attachChildren = initialiseOpts => (htmlElement, options) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const contextArray = Array.isArray(context) ? context : [context]
|
const contextStoreKeys = []
|
||||||
|
|
||||||
|
// create new context if supplied
|
||||||
|
if (context) {
|
||||||
|
let childIndex = 0
|
||||||
|
// if context is an array, map to new structure
|
||||||
|
const contextArray = Array.isArray(context) ? context : [context]
|
||||||
|
for (let ctx of contextArray) {
|
||||||
|
const key = appStore.create(
|
||||||
|
ctx,
|
||||||
|
treeNode.props._id,
|
||||||
|
childIndex,
|
||||||
|
treeNode.contextStoreKey
|
||||||
|
)
|
||||||
|
contextStoreKeys.push(key)
|
||||||
|
childIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const childNodes = []
|
const childNodes = []
|
||||||
|
|
||||||
for (let context of contextArray) {
|
const createChildNodes = contextStoreKey => {
|
||||||
for (let childProps of treeNode.props._children) {
|
for (let childProps of treeNode.props._children) {
|
||||||
const { componentName, libName } = splitName(childProps._component)
|
const { componentName, libName } = splitName(childProps._component)
|
||||||
|
|
||||||
|
@ -42,25 +59,33 @@ export const attachChildren = initialiseOpts => (htmlElement, options) => {
|
||||||
|
|
||||||
const ComponentConstructor = componentLibraries[libName][componentName]
|
const ComponentConstructor = componentLibraries[libName][componentName]
|
||||||
|
|
||||||
const prepareNodes = ctx => {
|
const childNode = prepareRenderComponent({
|
||||||
const childNodesThisIteration = prepareRenderComponent({
|
props: childProps,
|
||||||
props: childProps,
|
parentNode: treeNode,
|
||||||
parentNode: treeNode,
|
ComponentConstructor,
|
||||||
ComponentConstructor,
|
htmlElement,
|
||||||
htmlElement,
|
anchor,
|
||||||
anchor,
|
// in same context as parent, unless a new one was supplied
|
||||||
context: ctx,
|
contextStoreKey,
|
||||||
})
|
})
|
||||||
|
|
||||||
for (let childNode of childNodesThisIteration) {
|
childNodes.push(childNode)
|
||||||
childNodes.push(childNode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
prepareNodes(context)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (context) {
|
||||||
|
// if new context(s) is supplied, then create nodes
|
||||||
|
// with keys to new context stores
|
||||||
|
for (let contextStoreKey of contextStoreKeys) {
|
||||||
|
createChildNodes(contextStoreKey)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// otherwise, use same context store as parent
|
||||||
|
// which maybe undefined (therfor using the root state)
|
||||||
|
createChildNodes(treeNode.contextStoreKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if everything is equal, then don't re-render
|
||||||
if (areTreeNodesEqual(treeNode.children, childNodes)) return treeNode.children
|
if (areTreeNodesEqual(treeNode.children, childNodes)) return treeNode.children
|
||||||
|
|
||||||
for (let node of childNodes) {
|
for (let node of childNodes) {
|
||||||
|
@ -81,9 +106,9 @@ export const attachChildren = initialiseOpts => (htmlElement, options) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const splitName = fullname => {
|
const splitName = fullname => {
|
||||||
const getComponentName = compose(last, split("/"))
|
const nameParts = fullname.split("/")
|
||||||
|
|
||||||
const componentName = getComponentName(fullname)
|
const componentName = nameParts[nameParts.length - 1]
|
||||||
|
|
||||||
const libName = fullname.substring(
|
const libName = fullname.substring(
|
||||||
0,
|
0,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { appStore } from "../state/store"
|
|
||||||
import mustache from "mustache"
|
import mustache from "mustache"
|
||||||
|
import appStore from "../state/store"
|
||||||
|
import hasBinding from "../state/hasBinding"
|
||||||
|
|
||||||
export const prepareRenderComponent = ({
|
export const prepareRenderComponent = ({
|
||||||
ComponentConstructor,
|
ComponentConstructor,
|
||||||
|
@ -7,62 +8,54 @@ export const prepareRenderComponent = ({
|
||||||
anchor,
|
anchor,
|
||||||
props,
|
props,
|
||||||
parentNode,
|
parentNode,
|
||||||
context,
|
contextStoreKey,
|
||||||
}) => {
|
}) => {
|
||||||
const parentContext = (parentNode && parentNode.context) || {}
|
const thisNode = createTreeNode()
|
||||||
|
thisNode.parentNode = parentNode
|
||||||
|
thisNode.props = props
|
||||||
|
thisNode.contextStoreKey = contextStoreKey
|
||||||
|
|
||||||
let nodesToRender = []
|
// the treeNode is first created (above), and then this
|
||||||
const createNodeAndRender = () => {
|
// render method is add. The treeNode is returned, and
|
||||||
let componentContext = parentContext
|
// render is called later (in attachChildren)
|
||||||
if (context) {
|
thisNode.render = initialProps => {
|
||||||
componentContext = { ...context }
|
thisNode.component = new ComponentConstructor({
|
||||||
componentContext.$parent = parentContext
|
target: htmlElement,
|
||||||
|
props: initialProps,
|
||||||
|
hydrate: false,
|
||||||
|
anchor,
|
||||||
|
})
|
||||||
|
|
||||||
|
// finds the root element of the component, which was created by the contructor above
|
||||||
|
// we use this later to attach a className to. This is how styles
|
||||||
|
// are applied by the builder
|
||||||
|
thisNode.rootElement = htmlElement.children[htmlElement.children.length - 1]
|
||||||
|
|
||||||
|
let [componentName] = props._component.match(/[a-z]*$/)
|
||||||
|
if (props._id && thisNode.rootElement) {
|
||||||
|
thisNode.rootElement.classList.add(`${componentName}-${props._id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const thisNode = createTreeNode()
|
// make this node listen to the store
|
||||||
thisNode.context = componentContext
|
if (thisNode.stateBound) {
|
||||||
thisNode.parentNode = parentNode
|
const unsubscribe = appStore.subscribe(state => {
|
||||||
thisNode.props = props
|
const storeBoundProps = Object.keys(initialProps._bb.props).filter(p =>
|
||||||
nodesToRender.push(thisNode)
|
hasBinding(initialProps._bb.props[p])
|
||||||
|
)
|
||||||
thisNode.render = initialProps => {
|
if (storeBoundProps.length > 0) {
|
||||||
thisNode.component = new ComponentConstructor({
|
const toSet = {}
|
||||||
target: htmlElement,
|
for (let prop of storeBoundProps) {
|
||||||
props: initialProps,
|
const propValue = initialProps._bb.props[prop]
|
||||||
hydrate: false,
|
toSet[prop] = mustache.render(propValue, state)
|
||||||
anchor,
|
|
||||||
})
|
|
||||||
thisNode.rootElement =
|
|
||||||
htmlElement.children[htmlElement.children.length - 1]
|
|
||||||
|
|
||||||
let [componentName] = props._component.match(/[a-z]*$/)
|
|
||||||
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) {
|
|
||||||
const propValue = storeBoundProps[prop]
|
|
||||||
if (typeof propValue === "string") {
|
|
||||||
storeBoundProps[prop] = mustache.render(propValue, {
|
|
||||||
state,
|
|
||||||
context: componentContext,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
thisNode.component.$set(storeBoundProps)
|
thisNode.component.$set(toSet)
|
||||||
})
|
}
|
||||||
thisNode.unsubscribe = unsubscribe
|
}, thisNode.contextStoreKey)
|
||||||
}
|
thisNode.unsubscribe = unsubscribe
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
createNodeAndRender()
|
return thisNode
|
||||||
|
|
||||||
return nodesToRender
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createTreeNode = () => ({
|
export const createTreeNode = () => ({
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import regexparam from "regexparam"
|
import regexparam from "regexparam"
|
||||||
import { appStore } from "../state/store"
|
import appStore from "../state/store"
|
||||||
import { parseAppIdFromCookie } from "./getAppId"
|
import { parseAppIdFromCookie } from "./getAppId"
|
||||||
|
|
||||||
export const screenRouter = ({ screens, onScreenSelected, window }) => {
|
export const screenRouter = ({ screens, onScreenSelected, window }) => {
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { setState } from "./setState"
|
import setBindableComponentProp from "./setBindableComponentProp"
|
||||||
import { attachChildren } from "../render/attachChildren"
|
import { attachChildren } from "../render/attachChildren"
|
||||||
import { getContext, setContext } from "./getSetContext"
|
|
||||||
|
|
||||||
export const trimSlash = str => str.replace(/^\/+|\/+$/g, "")
|
export const trimSlash = str => str.replace(/^\/+|\/+$/g, "")
|
||||||
|
|
||||||
export const bbFactory = ({
|
export const bbFactory = ({
|
||||||
store,
|
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
getCurrentState,
|
getCurrentState,
|
||||||
|
@ -45,13 +43,9 @@ export const bbFactory = ({
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attachChildren: attachChildren(attachParams),
|
attachChildren: attachChildren(attachParams),
|
||||||
context: treeNode.context,
|
|
||||||
props: treeNode.props,
|
props: treeNode.props,
|
||||||
call: safeCallEvent,
|
call: safeCallEvent,
|
||||||
setState,
|
setBinding: setBindableComponentProp(treeNode),
|
||||||
getContext: getContext(treeNode),
|
|
||||||
setContext: setContext(treeNode),
|
|
||||||
store: store,
|
|
||||||
api,
|
api,
|
||||||
parent,
|
parent,
|
||||||
// these parameters are populated by screenRouter
|
// these parameters are populated by screenRouter
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
import { setState } from "./setState"
|
import api from "../api"
|
||||||
import { getState } from "./getState"
|
|
||||||
import { isArray, isUndefined } from "lodash/fp"
|
|
||||||
|
|
||||||
import { createApi } from "../api"
|
|
||||||
|
|
||||||
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType"
|
||||||
|
|
||||||
|
@ -12,21 +8,13 @@ export const eventHandlers = routeTo => {
|
||||||
parameters,
|
parameters,
|
||||||
})
|
})
|
||||||
|
|
||||||
const api = createApi({
|
|
||||||
setState,
|
|
||||||
getState: (path, fallback) => getState(path, fallback),
|
|
||||||
})
|
|
||||||
|
|
||||||
const setStateHandler = ({ path, value }) => setState(path, value)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"Set State": handler(["path", "value"], setStateHandler),
|
|
||||||
"Navigate To": handler(["url"], param => routeTo(param && param.url)),
|
"Navigate To": handler(["url"], param => routeTo(param && param.url)),
|
||||||
"Trigger Workflow": handler(["workflow"], api.triggerWorkflow),
|
"Trigger Workflow": handler(["workflow"], api.triggerWorkflow),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isEventType = prop =>
|
export const isEventType = prop =>
|
||||||
isArray(prop) &&
|
Array.isArray(prop) &&
|
||||||
prop.length > 0 &&
|
prop.length > 0 &&
|
||||||
!isUndefined(prop[0][EVENT_TYPE_MEMBER_NAME])
|
!prop[0][EVENT_TYPE_MEMBER_NAME] === undefined
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
import { get } from "svelte/store"
|
|
||||||
import getOr from "lodash/fp/getOr"
|
|
||||||
import { appStore } from "./store"
|
|
||||||
|
|
||||||
export const getState = (path, fallback) => {
|
|
||||||
if (!path || path.length === 0) return fallback
|
|
||||||
|
|
||||||
return getOr(fallback, path, get(appStore))
|
|
||||||
}
|
|
|
@ -0,0 +1 @@
|
||||||
|
export default value => typeof value === "string" && value.includes("{{")
|
|
@ -0,0 +1,13 @@
|
||||||
|
import appStore from "./store"
|
||||||
|
|
||||||
|
export default treeNode => (propName, value) => {
|
||||||
|
if (!propName || propName.length === 0) return
|
||||||
|
if (!treeNode) return
|
||||||
|
const componentId = treeNode.props._id
|
||||||
|
|
||||||
|
appStore.update(state => {
|
||||||
|
state[componentId] = state[componentId] || {}
|
||||||
|
state[componentId][propName] = value
|
||||||
|
return state
|
||||||
|
}, treeNode.contextStoreKey)
|
||||||
|
}
|
|
@ -1,11 +0,0 @@
|
||||||
import set from "lodash/fp/set"
|
|
||||||
import { appStore } from "./store"
|
|
||||||
|
|
||||||
export const setState = (path, value) => {
|
|
||||||
if (!path || path.length === 0) return
|
|
||||||
|
|
||||||
appStore.update(state => {
|
|
||||||
state = set(path, value, state)
|
|
||||||
return state
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -5,8 +5,8 @@ import {
|
||||||
} from "./eventHandlers"
|
} from "./eventHandlers"
|
||||||
import { bbFactory } from "./bbComponentApi"
|
import { bbFactory } from "./bbComponentApi"
|
||||||
import mustache from "mustache"
|
import mustache from "mustache"
|
||||||
import { get } from "svelte/store"
|
import appStore from "./store"
|
||||||
import { appStore } from "./store"
|
import hasBinding from "./hasBinding"
|
||||||
|
|
||||||
const doNothing = () => {}
|
const doNothing = () => {}
|
||||||
doNothing.isPlaceholder = true
|
doNothing.isPlaceholder = true
|
||||||
|
@ -37,41 +37,34 @@ export const createStateManager = ({
|
||||||
const getCurrentState = () => currentState
|
const getCurrentState = () => currentState
|
||||||
|
|
||||||
const bb = bbFactory({
|
const bb = bbFactory({
|
||||||
store: appStore,
|
|
||||||
getCurrentState,
|
getCurrentState,
|
||||||
componentLibraries,
|
componentLibraries,
|
||||||
onScreenSlotRendered,
|
onScreenSlotRendered,
|
||||||
})
|
})
|
||||||
|
|
||||||
const setup = _setup({ handlerTypes, getCurrentState, bb, store: appStore })
|
const setup = _setup({ handlerTypes, getCurrentState, bb })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
setup,
|
setup,
|
||||||
destroy: () => {},
|
destroy: () => {},
|
||||||
getCurrentState,
|
getCurrentState,
|
||||||
store: appStore,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const _setup = ({ handlerTypes, getCurrentState, bb, store }) => node => {
|
const _setup = ({ handlerTypes, getCurrentState, bb }) => node => {
|
||||||
const props = node.props
|
const props = node.props
|
||||||
const context = node.context || {}
|
|
||||||
const initialProps = { ...props }
|
const initialProps = { ...props }
|
||||||
const currentStoreState = get(appStore)
|
|
||||||
|
|
||||||
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]
|
||||||
|
|
||||||
// A little bit of a hack - won't bind if the string doesn't start with {{
|
const isBound = hasBinding(propValue)
|
||||||
const isBound = typeof propValue === "string" && propValue.includes("{{")
|
|
||||||
|
|
||||||
if (isBound) {
|
if (isBound) {
|
||||||
initialProps[propName] = mustache.render(propValue, {
|
const state = appStore.getState(node.contextStoreKey)
|
||||||
state: currentStoreState,
|
initialProps[propName] = mustache.render(propValue, state)
|
||||||
context,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!node.stateBound) {
|
if (!node.stateBound) {
|
||||||
node.stateBound = true
|
node.stateBound = true
|
||||||
|
@ -79,6 +72,7 @@ const _setup = ({ handlerTypes, getCurrentState, bb, store }) => node => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEventType(propValue)) {
|
if (isEventType(propValue)) {
|
||||||
|
const state = appStore.getState(node.contextStoreKey)
|
||||||
const handlersInfos = []
|
const handlersInfos = []
|
||||||
for (let event of propValue) {
|
for (let event of propValue) {
|
||||||
const handlerInfo = {
|
const handlerInfo = {
|
||||||
|
@ -89,11 +83,7 @@ const _setup = ({ handlerTypes, getCurrentState, bb, store }) => node => {
|
||||||
const resolvedParams = {}
|
const resolvedParams = {}
|
||||||
for (let paramName in handlerInfo.parameters) {
|
for (let paramName in handlerInfo.parameters) {
|
||||||
const paramValue = handlerInfo.parameters[paramName]
|
const paramValue = handlerInfo.parameters[paramName]
|
||||||
resolvedParams[paramName] = () =>
|
resolvedParams[paramName] = () => mustache.render(paramValue, state)
|
||||||
mustache.render(paramValue, {
|
|
||||||
state: getCurrentState(),
|
|
||||||
context,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handlerInfo.parameters = resolvedParams
|
handlerInfo.parameters = resolvedParams
|
||||||
|
@ -113,7 +103,7 @@ const _setup = ({ handlerTypes, getCurrentState, bb, store }) => node => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const setup = _setup({ handlerTypes, getCurrentState, bb, store })
|
const setup = _setup({ handlerTypes, getCurrentState, bb })
|
||||||
initialProps._bb = bb(node, setup)
|
initialProps._bb = bb(node, setup)
|
||||||
|
|
||||||
return initialProps
|
return initialProps
|
||||||
|
|
|
@ -1,9 +1,104 @@
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
|
|
||||||
const appStore = writable({})
|
// we assume that the reference to this state object
|
||||||
appStore.actions = {}
|
// will remain for the life of the application
|
||||||
|
const rootState = {}
|
||||||
|
const rootStore = writable(rootState)
|
||||||
|
const contextStores = {}
|
||||||
|
|
||||||
const routerStore = writable({})
|
// contextProviderId is the component id that provides the data for the context
|
||||||
routerStore.actions = {}
|
const contextStoreKey = (dataProviderId, childIndex) =>
|
||||||
|
`${dataProviderId}${childIndex >= 0 ? ":" + childIndex : ""}`
|
||||||
|
|
||||||
export { appStore, routerStore }
|
// creates a store for a datacontext (e.g. each item in a list component)
|
||||||
|
const create = (data, dataProviderId, childIndex, parentContextStoreId) => {
|
||||||
|
const key = contextStoreKey(dataProviderId, childIndex)
|
||||||
|
const state = { data }
|
||||||
|
|
||||||
|
// add reference to parent state object,
|
||||||
|
// so we can use bindings like state.parent.parent
|
||||||
|
// (if no parent, then parent is rootState )
|
||||||
|
state.parent = parentContextStoreId
|
||||||
|
? contextStores[parentContextStoreId].state
|
||||||
|
: rootState
|
||||||
|
|
||||||
|
if (!contextStores[key]) {
|
||||||
|
contextStores[key] = {
|
||||||
|
store: writable(state),
|
||||||
|
subscriberCount: 0,
|
||||||
|
state,
|
||||||
|
parentContextStoreId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscribe = (subscription, storeKey) => {
|
||||||
|
if (!storeKey) {
|
||||||
|
return rootStore.subscribe(subscription)
|
||||||
|
}
|
||||||
|
const contextStore = contextStores[storeKey]
|
||||||
|
|
||||||
|
// we are subscribing to multiple stores,
|
||||||
|
// we dont want to each subscription the first time
|
||||||
|
// as this could repeatedly run $set on the same component
|
||||||
|
// ... which already has its initial properties set properly
|
||||||
|
const ignoreFirstSubscription = () => {
|
||||||
|
let hasRunOnce = false
|
||||||
|
return () => {
|
||||||
|
if (hasRunOnce) subscription(contextStore.state)
|
||||||
|
hasRunOnce = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsubscribes = [rootStore.subscribe(ignoreFirstSubscription())]
|
||||||
|
|
||||||
|
// we subscribe to all stores in the hierarchy
|
||||||
|
const ancestorSubscribe = ctxStore => {
|
||||||
|
// unsubscribe func returned by svelte store
|
||||||
|
const svelteUnsub = ctxStore.store.subscribe(ignoreFirstSubscription())
|
||||||
|
|
||||||
|
// we wrap the svelte unsubscribe, so we can
|
||||||
|
// cleanup stores when they are no longer subscribed to
|
||||||
|
const unsub = () => {
|
||||||
|
ctxStore.subscriberCount = contextStore.subscriberCount - 1
|
||||||
|
// when no subscribers left, we delete the store
|
||||||
|
if (ctxStore.subscriberCount === 0) {
|
||||||
|
delete ctxStore[storeKey]
|
||||||
|
}
|
||||||
|
svelteUnsub()
|
||||||
|
}
|
||||||
|
unsubscribes.push(unsub)
|
||||||
|
if (ctxStore.parentContextStoreId) {
|
||||||
|
ancestorSubscribe(contextStores[ctxStore.parentContextStoreId])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ancestorSubscribe(contextStore)
|
||||||
|
|
||||||
|
// our final unsubscribe function calls unsubscribe on all stores
|
||||||
|
return () => unsubscribes.forEach(u => u())
|
||||||
|
}
|
||||||
|
|
||||||
|
const findStore = (dataProviderId, childIndex) =>
|
||||||
|
dataProviderId
|
||||||
|
? contextStores[contextStoreKey(dataProviderId, childIndex)].store
|
||||||
|
: rootStore
|
||||||
|
|
||||||
|
const update = (updatefunc, dataProviderId, childIndex) =>
|
||||||
|
findStore(dataProviderId, childIndex).update(updatefunc)
|
||||||
|
|
||||||
|
const set = (value, dataProviderId, childIndex) =>
|
||||||
|
findStore(dataProviderId, childIndex).set(value)
|
||||||
|
|
||||||
|
const getState = contextStoreKey =>
|
||||||
|
contextStoreKey ? contextStores[contextStoreKey].state : rootState
|
||||||
|
|
||||||
|
export default {
|
||||||
|
subscribe,
|
||||||
|
update,
|
||||||
|
set,
|
||||||
|
getState,
|
||||||
|
create,
|
||||||
|
contextStoreKey,
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,209 @@
|
||||||
|
import { load, makePage, makeScreen } from "./testAppDef"
|
||||||
|
|
||||||
|
describe("binding", () => {
|
||||||
|
|
||||||
|
|
||||||
|
it("should bind to data in context", async () => {
|
||||||
|
const { dom } = await load(
|
||||||
|
makePage({
|
||||||
|
_component: "testlib/div",
|
||||||
|
_children: [
|
||||||
|
{
|
||||||
|
_component: "##builtin/screenslot",
|
||||||
|
text: "header one",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
makeScreen("/", {
|
||||||
|
_component: "testlib/list",
|
||||||
|
data: dataArray,
|
||||||
|
_children: [
|
||||||
|
{
|
||||||
|
_component: "testlib/h1",
|
||||||
|
text: "{{data.name}}",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const rootDiv = dom.window.document.body.children[0]
|
||||||
|
expect(rootDiv.children.length).toBe(1)
|
||||||
|
|
||||||
|
const screenRoot = rootDiv.children[0]
|
||||||
|
|
||||||
|
expect(screenRoot.children[0].children.length).toBe(2)
|
||||||
|
expect(screenRoot.children[0].children[0].innerText).toBe(dataArray[0].name)
|
||||||
|
expect(screenRoot.children[0].children[1].innerText).toBe(dataArray[1].name)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should bind to input in root", async () => {
|
||||||
|
const { dom } = await load(
|
||||||
|
makePage({
|
||||||
|
_component: "testlib/div",
|
||||||
|
_children: [
|
||||||
|
{
|
||||||
|
_component: "##builtin/screenslot",
|
||||||
|
text: "header one",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
makeScreen("/", {
|
||||||
|
_component: "testlib/div",
|
||||||
|
_children: [
|
||||||
|
{
|
||||||
|
_component: "testlib/h1",
|
||||||
|
text: "{{inputid.value}}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "inputid",
|
||||||
|
_component: "testlib/input",
|
||||||
|
value: "hello"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const rootDiv = dom.window.document.body.children[0]
|
||||||
|
expect(rootDiv.children.length).toBe(1)
|
||||||
|
|
||||||
|
const screenRoot = rootDiv.children[0]
|
||||||
|
|
||||||
|
expect(screenRoot.children[0].children.length).toBe(2)
|
||||||
|
expect(screenRoot.children[0].children[0].innerText).toBe("hello")
|
||||||
|
|
||||||
|
// change value of input
|
||||||
|
const input = dom.window.document.getElementsByClassName("input-inputid")[0]
|
||||||
|
|
||||||
|
changeInputValue(dom, input, "new value")
|
||||||
|
expect(screenRoot.children[0].children[0].innerText).toBe("new value")
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should bind to input in context", async () => {
|
||||||
|
const { dom } = await load(
|
||||||
|
makePage({
|
||||||
|
_component: "testlib/div",
|
||||||
|
_children: [
|
||||||
|
{
|
||||||
|
_component: "##builtin/screenslot",
|
||||||
|
text: "header one",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
makeScreen("/", {
|
||||||
|
_component: "testlib/list",
|
||||||
|
data: dataArray,
|
||||||
|
_children: [
|
||||||
|
{
|
||||||
|
_component: "testlib/h1",
|
||||||
|
text: "{{inputid.value}}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: "inputid",
|
||||||
|
_component: "testlib/input",
|
||||||
|
value: "hello"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const rootDiv = dom.window.document.body.children[0]
|
||||||
|
expect(rootDiv.children.length).toBe(1)
|
||||||
|
|
||||||
|
const screenRoot = rootDiv.children[0]
|
||||||
|
expect(screenRoot.children[0].children.length).toBe(4)
|
||||||
|
|
||||||
|
const firstHeader = screenRoot.children[0].children[0]
|
||||||
|
const firstInput = screenRoot.children[0].children[1]
|
||||||
|
const secondHeader = screenRoot.children[0].children[2]
|
||||||
|
const secondInput = screenRoot.children[0].children[3]
|
||||||
|
|
||||||
|
expect(firstHeader.innerText).toBe("hello")
|
||||||
|
expect(secondHeader.innerText).toBe("hello")
|
||||||
|
|
||||||
|
changeInputValue(dom, firstInput, "first input value")
|
||||||
|
expect(firstHeader.innerText).toBe("first input value")
|
||||||
|
|
||||||
|
changeInputValue(dom, secondInput, "second input value")
|
||||||
|
expect(secondHeader.innerText).toBe("second input value")
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should bind contextual component, to input in root context", async () => {
|
||||||
|
const { dom } = await load(
|
||||||
|
makePage({
|
||||||
|
_component: "testlib/div",
|
||||||
|
_children: [
|
||||||
|
{
|
||||||
|
_component: "##builtin/screenslot",
|
||||||
|
text: "header one",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
makeScreen("/", {
|
||||||
|
_component: "testlib/div",
|
||||||
|
_children: [
|
||||||
|
{
|
||||||
|
_id: "inputid",
|
||||||
|
_component: "testlib/input",
|
||||||
|
value: "hello"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_component: "testlib/list",
|
||||||
|
data: dataArray,
|
||||||
|
_children: [
|
||||||
|
{
|
||||||
|
_component: "testlib/h1",
|
||||||
|
text: "{{parent.inputid.value}}",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const rootDiv = dom.window.document.body.children[0]
|
||||||
|
expect(rootDiv.children.length).toBe(1)
|
||||||
|
|
||||||
|
const screenRoot = rootDiv.children[0]
|
||||||
|
expect(screenRoot.children[0].children.length).toBe(2)
|
||||||
|
|
||||||
|
const input = screenRoot.children[0].children[0]
|
||||||
|
|
||||||
|
const firstHeader = screenRoot.children[0].children[1].children[0]
|
||||||
|
const secondHeader = screenRoot.children[0].children[1].children[0]
|
||||||
|
|
||||||
|
expect(firstHeader.innerText).toBe("hello")
|
||||||
|
expect(secondHeader.innerText).toBe("hello")
|
||||||
|
|
||||||
|
changeInputValue(dom, input, "new input value")
|
||||||
|
expect(firstHeader.innerText).toBe("new input value")
|
||||||
|
expect(secondHeader.innerText).toBe("new input value")
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
const changeInputValue = (dom, input, newValue) => {
|
||||||
|
var event = new dom.window.Event("change")
|
||||||
|
input.value = newValue
|
||||||
|
input.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataArray = [
|
||||||
|
{
|
||||||
|
name: "katherine",
|
||||||
|
age: 30,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "steve",
|
||||||
|
age: 41,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
|
@ -135,4 +135,38 @@ describe("initialiseApp", () => {
|
||||||
expect(screenRoot.children[0].children[0].innerText).toBe("header one")
|
expect(screenRoot.children[0].children[0].innerText).toBe("header one")
|
||||||
expect(screenRoot.children[0].children[1].innerText).toBe("header two")
|
expect(screenRoot.children[0].children[1].innerText).toBe("header two")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("should repeat elements that pass an array of contexts", async () => {
|
||||||
|
const { dom } = await load(
|
||||||
|
makePage({
|
||||||
|
_component: "testlib/div",
|
||||||
|
_children: [
|
||||||
|
{
|
||||||
|
_component: "##builtin/screenslot",
|
||||||
|
text: "header one",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
makeScreen("/", {
|
||||||
|
_component: "testlib/list",
|
||||||
|
data: [1,2,3,4],
|
||||||
|
_children: [
|
||||||
|
{
|
||||||
|
_component: "testlib/h1",
|
||||||
|
text: "header",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
const rootDiv = dom.window.document.body.children[0]
|
||||||
|
expect(rootDiv.children.length).toBe(1)
|
||||||
|
|
||||||
|
const screenRoot = rootDiv.children[0]
|
||||||
|
|
||||||
|
expect(screenRoot.children[0].children.length).toBe(4)
|
||||||
|
expect(screenRoot.children[0].children[0].innerText).toBe("header")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -194,4 +194,47 @@ const maketestlib = window => ({
|
||||||
set(opts.props)
|
set(opts.props)
|
||||||
opts.target.appendChild(node)
|
opts.target.appendChild(node)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
list: function(opts) {
|
||||||
|
const node = window.document.createElement("DIV")
|
||||||
|
|
||||||
|
let currentProps = { ...opts.props }
|
||||||
|
|
||||||
|
const set = props => {
|
||||||
|
currentProps = Object.assign(currentProps, props)
|
||||||
|
if (currentProps._children && currentProps._children.length > 0) {
|
||||||
|
currentProps._bb.attachChildren(node, {
|
||||||
|
context: currentProps.data || {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$destroy = () => opts.target.removeChild(node)
|
||||||
|
|
||||||
|
this.$set = set
|
||||||
|
this._element = node
|
||||||
|
set(opts.props)
|
||||||
|
opts.target.appendChild(node)
|
||||||
|
},
|
||||||
|
|
||||||
|
input: function(opts) {
|
||||||
|
const node = window.document.createElement("INPUT")
|
||||||
|
let currentProps = { ...opts.props }
|
||||||
|
|
||||||
|
const set = props => {
|
||||||
|
currentProps = Object.assign(currentProps, props)
|
||||||
|
opts.props._bb.setBinding("value", props.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
node.addEventListener("change", e => {
|
||||||
|
opts.props._bb.setBinding("value", e.target.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$destroy = () => opts.target.removeChild(node)
|
||||||
|
|
||||||
|
this.$set = set
|
||||||
|
this._element = node
|
||||||
|
set(opts.props)
|
||||||
|
opts.target.appendChild(node)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
<script>
|
||||||
|
export let _bb
|
||||||
|
export let className = ""
|
||||||
|
|
||||||
|
let containerElement
|
||||||
|
let hasLoaded
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (containerElement) {
|
||||||
|
_bb.attachChildren(containerElement)
|
||||||
|
hasLoaded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div bind:this={containerElement} class={className} />
|
Loading…
Reference in New Issue