mirror of https://github.com/Budibase/budibase.git
23 changed files with 30 additions and 821 deletions
@ -1,88 +0,0 @@ |
|||
import { attachChildren } from "./render/attachChildren" |
|||
import { createTreeNode } from "./render/prepareRenderComponent" |
|||
import { screenRouter } from "./render/screenRouter" |
|||
import { createStateManager } from "./state/stateManager" |
|||
import { getAppId } from "@budibase/component-sdk" |
|||
|
|||
export const createApp = ({ |
|||
componentLibraries, |
|||
frontendDefinition, |
|||
window, |
|||
}) => { |
|||
let routeTo |
|||
let currentUrl |
|||
let screenStateManager |
|||
|
|||
const onScreenSlotRendered = screenSlotNode => { |
|||
const onScreenSelected = (screen, url) => { |
|||
const stateManager = createStateManager({ |
|||
componentLibraries, |
|||
onScreenSlotRendered: () => {}, |
|||
routeTo, |
|||
}) |
|||
const getAttachChildrenParams = attachChildrenParams(stateManager) |
|||
screenSlotNode.props._children = [screen.props] |
|||
const initialiseChildParams = getAttachChildrenParams(screenSlotNode) |
|||
attachChildren(initialiseChildParams)(screenSlotNode.rootElement, { |
|||
hydrate: true, |
|||
force: true, |
|||
}) |
|||
if (screenStateManager) screenStateManager.destroy() |
|||
screenStateManager = stateManager |
|||
currentUrl = url |
|||
} |
|||
|
|||
routeTo = screenRouter({ |
|||
screens: frontendDefinition.screens, |
|||
onScreenSelected, |
|||
window, |
|||
}) |
|||
const fallbackPath = window.location.pathname.replace(getAppId(), "") |
|||
routeTo(currentUrl || fallbackPath) |
|||
} |
|||
|
|||
const attachChildrenParams = stateManager => { |
|||
const getInitialiseParams = treeNode => ({ |
|||
componentLibraries, |
|||
treeNode, |
|||
onScreenSlotRendered, |
|||
setupState: stateManager.setup, |
|||
}) |
|||
|
|||
return getInitialiseParams |
|||
} |
|||
|
|||
let rootTreeNode |
|||
const pageStateManager = createStateManager({ |
|||
componentLibraries, |
|||
onScreenSlotRendered, |
|||
// seems weird, but the routeTo variable may not be available at this point
|
|||
routeTo: url => routeTo(url), |
|||
}) |
|||
|
|||
const initialisePage = (page, target, urlPath) => { |
|||
currentUrl = urlPath |
|||
|
|||
rootTreeNode = createTreeNode() |
|||
rootTreeNode.props = { |
|||
_children: [page.props], |
|||
} |
|||
const getInitialiseParams = attachChildrenParams(pageStateManager) |
|||
const initChildParams = getInitialiseParams(rootTreeNode) |
|||
|
|||
attachChildren(initChildParams)(target, { |
|||
hydrate: true, |
|||
force: true, |
|||
}) |
|||
|
|||
return rootTreeNode |
|||
} |
|||
|
|||
return { |
|||
initialisePage, |
|||
screenStore: () => screenStateManager.store, |
|||
pageStore: () => pageStateManager.store, |
|||
routeTo: () => routeTo, |
|||
rootNode: () => rootTreeNode, |
|||
} |
|||
} |
|||
@ -1,59 +0,0 @@ |
|||
import { createApp } from "./createApp" |
|||
import { builtins, builtinLibName } from "./render/builtinComponents" |
|||
import { getAppId } from "@budibase/component-sdk" |
|||
|
|||
/** |
|||
* create a web application from static budibase definition files. |
|||
* @param {object} opts - configuration options for budibase client libary |
|||
*/ |
|||
export const loadBudibase = async opts => { |
|||
const _window = (opts && opts.window) || window |
|||
// const _localStorage = (opts && opts.localStorage) || localStorage
|
|||
const appId = getAppId(window.document.cookie) |
|||
const frontendDefinition = _window["##BUDIBASE_FRONTEND_DEFINITION##"] |
|||
|
|||
const user = {} |
|||
|
|||
const componentLibraryModules = (opts && opts.componentLibraries) || {} |
|||
|
|||
const libraries = frontendDefinition.libraries || [] |
|||
|
|||
for (let library of libraries) { |
|||
// fetch the JavaScript for the component libraries from the server
|
|||
componentLibraryModules[library] = await import( |
|||
`/componentlibrary?library=${encodeURI(library)}&appId=${appId}` |
|||
) |
|||
} |
|||
|
|||
componentLibraryModules[builtinLibName] = builtins(_window) |
|||
|
|||
const { |
|||
initialisePage, |
|||
screenStore, |
|||
pageStore, |
|||
routeTo, |
|||
rootNode, |
|||
} = createApp({ |
|||
componentLibraries: componentLibraryModules, |
|||
frontendDefinition, |
|||
user, |
|||
window: _window, |
|||
}) |
|||
|
|||
const route = _window.location |
|||
? _window.location.pathname.replace(`${appId}/`, "").replace(appId, "") |
|||
: "" |
|||
|
|||
initialisePage(frontendDefinition.page, _window.document.body, route) |
|||
|
|||
return { |
|||
screenStore, |
|||
pageStore, |
|||
routeTo, |
|||
rootNode, |
|||
} |
|||
} |
|||
|
|||
if (window) { |
|||
window.loadBudibase = loadBudibase |
|||
} |
|||
@ -1,138 +0,0 @@ |
|||
import { prepareRenderComponent } from "./prepareRenderComponent" |
|||
import { isScreenSlot } from "./builtinComponents" |
|||
import deepEqual from "deep-equal" |
|||
import appStore from "../state/store" |
|||
|
|||
export const attachChildren = initialiseOpts => (htmlElement, options) => { |
|||
const { |
|||
componentLibraries, |
|||
treeNode, |
|||
onScreenSlotRendered, |
|||
setupState, |
|||
} = initialiseOpts |
|||
|
|||
const anchor = options && options.anchor ? options.anchor : null |
|||
const force = options ? options.force : false |
|||
const hydrate = options ? options.hydrate : true |
|||
const context = options && options.context |
|||
|
|||
if (!force && treeNode.children.length > 0) return treeNode.children |
|||
|
|||
for (let childNode of treeNode.children) { |
|||
childNode.destroy() |
|||
} |
|||
|
|||
if (!htmlElement) return |
|||
|
|||
if (hydrate) { |
|||
while (htmlElement.firstChild) { |
|||
htmlElement.removeChild(htmlElement.firstChild) |
|||
} |
|||
} |
|||
|
|||
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 createChildNodes = contextStoreKey => { |
|||
for (let childProps of treeNode.props._children) { |
|||
const { componentName, libName } = splitName(childProps._component) |
|||
|
|||
if (!componentName || !libName) return |
|||
|
|||
const ComponentConstructor = componentLibraries[libName][componentName] |
|||
|
|||
const childNode = prepareRenderComponent({ |
|||
props: childProps, |
|||
parentNode: treeNode, |
|||
ComponentConstructor, |
|||
htmlElement, |
|||
anchor, |
|||
// in same context as parent, unless a new one was supplied
|
|||
contextStoreKey, |
|||
}) |
|||
|
|||
childNodes.push(childNode) |
|||
} |
|||
} |
|||
|
|||
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 |
|||
|
|||
for (let node of childNodes) { |
|||
const initialProps = setupState(node) |
|||
node.render(initialProps) |
|||
} |
|||
|
|||
const screenSlot = childNodes.find(n => isScreenSlot(n.props._component)) |
|||
|
|||
if (onScreenSlotRendered && screenSlot) { |
|||
// assuming there is only ever one screen slot
|
|||
onScreenSlotRendered(screenSlot) |
|||
} |
|||
|
|||
treeNode.children = childNodes |
|||
|
|||
return childNodes |
|||
} |
|||
|
|||
const splitName = fullname => { |
|||
const nameParts = fullname.split("/") |
|||
|
|||
const componentName = nameParts[nameParts.length - 1] |
|||
|
|||
const libName = fullname.substring( |
|||
0, |
|||
fullname.length - componentName.length - 1 |
|||
) |
|||
|
|||
return { libName, componentName } |
|||
} |
|||
|
|||
const areTreeNodesEqual = (children1, children2) => { |
|||
if (children1.length !== children2.length) return false |
|||
if (children1 === children2) return true |
|||
|
|||
let isEqual = false |
|||
for (let i = 0; i < children1.length; i++) { |
|||
// same context and same children, then nothing has changed
|
|||
isEqual = |
|||
deepEqual(children1[i].context, children2[i].context) && |
|||
areTreeNodesEqual(children1[i].children, children2[i].children) |
|||
if (!isEqual) return false |
|||
if (isScreenSlot(children1[i].parentNode.props._component)) { |
|||
isEqual = deepEqual(children1[i].props, children2[i].props) |
|||
} |
|||
if (!isEqual) return false |
|||
} |
|||
return true |
|||
} |
|||
@ -1,10 +0,0 @@ |
|||
import { screenSlotComponent } from "./screenSlotComponent" |
|||
|
|||
export const builtinLibName = "##builtin" |
|||
|
|||
export const isScreenSlot = componentName => |
|||
componentName === "##builtin/screenslot" |
|||
|
|||
export const builtins = window => ({ |
|||
screenslot: screenSlotComponent(window), |
|||
}) |
|||
@ -1,88 +0,0 @@ |
|||
import renderTemplateString from "../state/renderTemplateString" |
|||
import appStore from "../state/store" |
|||
import hasBinding from "../state/hasBinding" |
|||
|
|||
export const prepareRenderComponent = ({ |
|||
ComponentConstructor, |
|||
htmlElement, |
|||
anchor, |
|||
props, |
|||
parentNode, |
|||
contextStoreKey, |
|||
}) => { |
|||
const thisNode = createTreeNode() |
|||
thisNode.parentNode = parentNode |
|||
thisNode.props = props |
|||
thisNode.contextStoreKey = contextStoreKey |
|||
|
|||
// the treeNode is first created (above), and then this
|
|||
// render method is add. The treeNode is returned, and
|
|||
// render is called later (in attachChildren)
|
|||
thisNode.render = initialProps => { |
|||
thisNode.component = new ComponentConstructor({ |
|||
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}`) |
|||
} |
|||
|
|||
// make this node listen to the store
|
|||
if (thisNode.stateBound) { |
|||
const unsubscribe = appStore.subscribe(state => { |
|||
const storeBoundProps = Object.keys(initialProps._bb.props).filter(p => |
|||
hasBinding(initialProps._bb.props[p]) |
|||
) |
|||
if (storeBoundProps.length > 0) { |
|||
const toSet = {} |
|||
for (let prop of storeBoundProps) { |
|||
const propValue = initialProps._bb.props[prop] |
|||
toSet[prop] = renderTemplateString(propValue, state) |
|||
} |
|||
thisNode.component.$set(toSet) |
|||
} |
|||
}, thisNode.contextStoreKey) |
|||
thisNode.unsubscribe = unsubscribe |
|||
} |
|||
} |
|||
|
|||
return thisNode |
|||
} |
|||
|
|||
export const createTreeNode = () => ({ |
|||
context: {}, |
|||
props: {}, |
|||
rootElement: null, |
|||
parentNode: null, |
|||
children: [], |
|||
bindings: [], |
|||
component: null, |
|||
unsubscribe: () => {}, |
|||
render: () => {}, |
|||
get destroy() { |
|||
const node = this |
|||
return () => { |
|||
if (node.children) { |
|||
// destroy children first - from leaf nodes up
|
|||
for (let child of node.children) { |
|||
child.destroy() |
|||
} |
|||
} |
|||
if (node.unsubscribe) node.unsubscribe() |
|||
if (node.component && node.component.$destroy) node.component.$destroy() |
|||
for (let onDestroyItem of node.onDestroy) { |
|||
onDestroyItem() |
|||
} |
|||
} |
|||
}, |
|||
onDestroy: [], |
|||
}) |
|||
@ -1,122 +0,0 @@ |
|||
import regexparam from "regexparam" |
|||
import appStore from "../state/store" |
|||
import { getAppId } from "@budibase/component-sdk" |
|||
|
|||
export const screenRouter = ({ screens, onScreenSelected, window }) => { |
|||
function sanitize(url) { |
|||
if (!url) return url |
|||
return url |
|||
.split("/") |
|||
.map(part => { |
|||
// if parameter, then use as is
|
|||
if (part.startsWith(":")) return part |
|||
return encodeURIComponent(part) |
|||
}) |
|||
.join("/") |
|||
.toLowerCase() |
|||
} |
|||
|
|||
const isRunningLocally = () => { |
|||
const hostname = (window.location && window.location.hostname) || "" |
|||
return ( |
|||
hostname === "localhost" || |
|||
hostname === "127.0.0.1" || |
|||
hostname.startsWith("192.168") |
|||
) |
|||
} |
|||
|
|||
const makeRootedPath = url => { |
|||
if (isRunningLocally()) { |
|||
const appId = getAppId() |
|||
if (url) { |
|||
url = sanitize(url) |
|||
if (!url.startsWith("/")) { |
|||
url = `/${url}` |
|||
} |
|||
if (url.startsWith(`/${appId}`)) { |
|||
return url |
|||
} |
|||
return `/${appId}${url}` |
|||
} |
|||
return `/${appId}` |
|||
} |
|||
return sanitize(url) |
|||
} |
|||
|
|||
const routes = screens.map(s => makeRootedPath(s.routing?.route)) |
|||
let fallback = routes.findIndex(([p]) => p === makeRootedPath("*")) |
|||
if (fallback < 0) fallback = 0 |
|||
|
|||
let current |
|||
|
|||
function route(url) { |
|||
const _url = makeRootedPath(url.state || url) |
|||
current = routes.findIndex( |
|||
p => |
|||
p !== makeRootedPath("*") && |
|||
new RegExp("^" + p.toLowerCase() + "$").test(_url.toLowerCase()) |
|||
) |
|||
|
|||
const params = {} |
|||
|
|||
if (current === -1) { |
|||
routes.forEach((p, i) => { |
|||
// ignore home - which matched everything
|
|||
if (p === makeRootedPath("*")) return |
|||
const pm = regexparam(p) |
|||
const matches = pm.pattern.exec(_url) |
|||
|
|||
if (!matches) return |
|||
|
|||
let j = 0 |
|||
while (j < pm.keys.length) { |
|||
params[pm.keys[j]] = matches[++j] || null |
|||
} |
|||
|
|||
current = i |
|||
}) |
|||
} |
|||
|
|||
appStore.update(state => { |
|||
state["##routeParams"] = params |
|||
return state |
|||
}) |
|||
|
|||
const screenIndex = current !== -1 ? current : fallback |
|||
|
|||
try { |
|||
!url.state && history.pushState(_url, null, _url) |
|||
} catch (_) { |
|||
// ignoring an exception here as the builder runs an iframe, which does not like this
|
|||
} |
|||
|
|||
onScreenSelected(screens[screenIndex], _url) |
|||
} |
|||
|
|||
function click(e) { |
|||
const x = e.target.closest("a") |
|||
const y = x && x.getAttribute("href") |
|||
|
|||
if ( |
|||
e.ctrlKey || |
|||
e.metaKey || |
|||
e.altKey || |
|||
e.shiftKey || |
|||
e.button || |
|||
e.defaultPrevented |
|||
) |
|||
return |
|||
|
|||
const target = (x && x.target) || "_self" |
|||
if (!y || target !== "_self" || x.host !== location.host) return |
|||
|
|||
e.preventDefault() |
|||
route(y) |
|||
} |
|||
|
|||
addEventListener("popstate", route) |
|||
addEventListener("pushstate", route) |
|||
addEventListener("click", click) |
|||
|
|||
return route |
|||
} |
|||
@ -1,14 +0,0 @@ |
|||
export const screenSlotComponent = window => { |
|||
return function(opts) { |
|||
const node = window.document.createElement("DIV") |
|||
const $set = props => { |
|||
props._bb.attachChildren(node) |
|||
} |
|||
const $destroy = () => { |
|||
if (opts.target && node) opts.target.removeChild(node) |
|||
} |
|||
this.$set = $set |
|||
this.$destroy = $destroy |
|||
opts.target.appendChild(node) |
|||
} |
|||
} |
|||
@ -1,34 +0,0 @@ |
|||
import setBindableComponentProp from "./setBindableComponentProp" |
|||
import { attachChildren } from "../render/attachChildren" |
|||
import store from "./store" |
|||
|
|||
export const bbFactory = ({ |
|||
componentLibraries, |
|||
onScreenSlotRendered, |
|||
runEventActions, |
|||
}) => { |
|||
return (treeNode, setupState) => { |
|||
const attachParams = { |
|||
componentLibraries, |
|||
treeNode, |
|||
onScreenSlotRendered, |
|||
setupState, |
|||
} |
|||
|
|||
return { |
|||
attachChildren: attachChildren(attachParams), |
|||
props: treeNode.props, |
|||
call: async eventName => |
|||
eventName && |
|||
(await runEventActions( |
|||
treeNode.props[eventName], |
|||
store.getState(treeNode.contextStoreKey) |
|||
)), |
|||
setBinding: setBindableComponentProp(treeNode), |
|||
parent, |
|||
store: store.getStore(treeNode.contextStoreKey), |
|||
// these parameters are populated by screenRouter
|
|||
routeParams: () => store.getState()["##routeParams"], |
|||
} |
|||
} |
|||
} |
|||
@ -1,42 +0,0 @@ |
|||
import renderTemplateString from "./renderTemplateString" |
|||
import { updateRow, saveRow, deleteRow } from "@budibase/component-sdk" |
|||
|
|||
export const EVENT_TYPE_MEMBER_NAME = "##eventHandlerType" |
|||
|
|||
export const eventHandlers = routeTo => { |
|||
const handlers = { |
|||
"Navigate To": param => routeTo(param && param.url), |
|||
"Update Row": updateRow, |
|||
"Save Row": saveRow, |
|||
"Delete Row": deleteRow, |
|||
} |
|||
|
|||
// when an event is called, this is what gets run
|
|||
const runEventActions = async (actions, state) => { |
|||
if (!actions) return |
|||
// calls event handlers sequentially
|
|||
for (let action of actions) { |
|||
const handler = handlers[action[EVENT_TYPE_MEMBER_NAME]] |
|||
const parameters = createParameters(action.parameters, state) |
|||
if (handler) { |
|||
await handler(parameters, state) |
|||
} |
|||
} |
|||
} |
|||
|
|||
return runEventActions |
|||
} |
|||
|
|||
// this will take a parameters obj, iterate all keys, and do a mustache render
|
|||
// for every string. It will work recursively if it encounnters an {}
|
|||
const createParameters = (parameterTemplateObj, state) => { |
|||
const parameters = {} |
|||
for (let key in parameterTemplateObj) { |
|||
if (typeof parameterTemplateObj[key] === "string") { |
|||
parameters[key] = renderTemplateString(parameterTemplateObj[key], state) |
|||
} else if (typeof parameterTemplateObj[key] === "object") { |
|||
parameters[key] = createParameters(parameterTemplateObj[key], state) |
|||
} |
|||
} |
|||
return parameters |
|||
} |
|||
@ -1,11 +0,0 @@ |
|||
export const setContext = treeNode => (key, value) => |
|||
(treeNode.context[key] = value) |
|||
|
|||
export const getContext = treeNode => key => { |
|||
if (treeNode.context && treeNode.context[key] !== undefined) |
|||
return treeNode.context[key] |
|||
|
|||
if (!treeNode.context.$parent) return |
|||
|
|||
return getContext(treeNode.parentNode)(key) |
|||
} |
|||
@ -1 +0,0 @@ |
|||
export default value => typeof value === "string" && value.includes("{{") |
|||
@ -1,17 +0,0 @@ |
|||
import mustache from "mustache" |
|||
|
|||
// this is a much more liberal version of mustache's escape function
|
|||
// ...just ignoring < and > to prevent tags from user input
|
|||
// original version here https://github.com/janl/mustache.js/blob/4b7908f5c9fec469a11cfaed2f2bed23c84e1c5c/mustache.js#L78
|
|||
|
|||
const entityMap = { |
|||
"<": "<", |
|||
">": ">", |
|||
} |
|||
|
|||
mustache.escape = text => |
|||
String(text).replace(/[&<>"'`=/]/g, function fromEntityMap(s) { |
|||
return entityMap[s] || s |
|||
}) |
|||
|
|||
export default mustache.render |
|||
@ -1,13 +0,0 @@ |
|||
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,65 +0,0 @@ |
|||
import { eventHandlers } from "./eventHandlers" |
|||
import { bbFactory } from "./bbComponentApi" |
|||
import renderTemplateString from "./renderTemplateString" |
|||
import appStore from "./store" |
|||
import hasBinding from "./hasBinding" |
|||
|
|||
const doNothing = () => {} |
|||
doNothing.isPlaceholder = true |
|||
|
|||
const isMetaProp = propName => |
|||
propName === "_component" || |
|||
propName === "_children" || |
|||
propName === "_id" || |
|||
propName === "_style" || |
|||
propName === "_code" || |
|||
propName === "_codeMeta" || |
|||
propName === "_styles" |
|||
|
|||
export const createStateManager = ({ |
|||
componentLibraries, |
|||
onScreenSlotRendered, |
|||
routeTo, |
|||
}) => { |
|||
let runEventActions = eventHandlers(routeTo) |
|||
|
|||
const bb = bbFactory({ |
|||
componentLibraries, |
|||
onScreenSlotRendered, |
|||
runEventActions, |
|||
}) |
|||
|
|||
const setup = _setup(bb) |
|||
|
|||
return { |
|||
setup, |
|||
destroy: () => {}, |
|||
} |
|||
} |
|||
|
|||
const _setup = bb => node => { |
|||
const props = node.props |
|||
const initialProps = { ...props } |
|||
|
|||
for (let propName in props) { |
|||
if (isMetaProp(propName)) continue |
|||
|
|||
const propValue = props[propName] |
|||
|
|||
const isBound = hasBinding(propValue) |
|||
|
|||
if (isBound) { |
|||
const state = appStore.getState(node.contextStoreKey) |
|||
initialProps[propName] = renderTemplateString(propValue, state) |
|||
|
|||
if (!node.stateBound) { |
|||
node.stateBound = true |
|||
} |
|||
} |
|||
} |
|||
|
|||
const setup = _setup(bb) |
|||
initialProps._bb = bb(node, setup) |
|||
|
|||
return initialProps |
|||
} |
|||
@ -1,108 +0,0 @@ |
|||
import { writable } from "svelte/store" |
|||
|
|||
// we assume that the reference to this state object
|
|||
// will remain for the life of the application
|
|||
const rootState = {} |
|||
const rootStore = writable(rootState) |
|||
const contextStores = {} |
|||
|
|||
// contextProviderId is the component id that provides the data for the context
|
|||
const contextStoreKey = (dataProviderId, childIndex) => |
|||
`${dataProviderId}${childIndex >= 0 ? ":" + childIndex : ""}` |
|||
|
|||
// creates a store for a datacontext (e.g. each item in a list component)
|
|||
// overrides store if already exists
|
|||
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 |
|||
|
|||
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 run our listener for every 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 |
|||
|
|||
const getStore = contextStoreKey => |
|||
contextStoreKey ? contextStores[contextStoreKey].store : rootStore |
|||
|
|||
export default { |
|||
subscribe, |
|||
update, |
|||
set, |
|||
getState, |
|||
create, |
|||
contextStoreKey, |
|||
getStore, |
|||
} |
|||
Loading…
Reference in new issue