Support both global and local state simultaneously

This commit is contained in:
Andrew Kingston 2023-09-14 15:31:51 +01:00
parent 1b47fb2308
commit 49948df9cd
5 changed files with 85 additions and 22 deletions

View File

@ -10,7 +10,7 @@
export let vAlign
export let gap
const { Provider } = getContext("sdk")
const { Provider, ContextScopes } = getContext("sdk")
const component = getContext("component")
$: rows = dataProvider?.rows ?? []
@ -22,7 +22,7 @@
<Placeholder />
{:else if rows.length > 0}
{#each rows as row, index}
<Provider data={{ ...row, index }}>
<Provider data={{ ...row, index }} scope={ContextScopes.Local}>
<slot />
</Provider>
{/each}

View File

@ -3,15 +3,23 @@
import { dataSourceStore, createContextStore } from "stores"
import { ActionTypes } from "constants"
import { generate } from "shortid"
import { ContextScopes } from "constants"
export let data
export let actions
export let key
export let scope = ContextScopes.Global
const context = getContext("context")
let context = getContext("context")
const component = getContext("component")
const providerKey = key || $component.id
// Create a new layer of context if we are only locally scoped
if (scope === ContextScopes.Local) {
context = createContextStore(context)
setContext("context", context)
}
// Generate a permanent unique ID for this component and use it to register
// any datasource actions
const instanceId = generate()
@ -26,7 +34,7 @@
const provideData = newData => {
const dataKey = JSON.stringify(newData)
if (dataKey !== lastDataKey) {
context.actions.provideData(providerKey, newData)
context.actions.provideData(providerKey, newData, scope)
lastDataKey = dataKey
}
}
@ -36,7 +44,7 @@
if (actionsKey !== lastActionsKey) {
lastActionsKey = actionsKey
newActions?.forEach(({ type, callback, metadata }) => {
context.actions.provideAction(providerKey, type, callback)
context.actions.provideAction(providerKey, type, callback, scope)
// Register any "refresh datasource" actions with a singleton store
// so we can easily refresh data at all levels for any datasource

View File

@ -32,5 +32,10 @@ export const ActionTypes = {
ScrollTo: "ScrollTo",
}
export const ContextScopes = {
Local: "local",
Global: "global",
}
export const DNDPlaceholderID = "dnd-placeholder"
export const ScreenslotType = "screenslot"

View File

@ -20,7 +20,7 @@ import { getAction } from "utils/getAction"
import Provider from "components/context/Provider.svelte"
import Block from "components/Block.svelte"
import BlockComponent from "components/BlockComponent.svelte"
import { ActionTypes } from "./constants"
import { ActionTypes, ContextScopes } from "./constants"
import { fetchDatasourceSchema } from "./utils/schema.js"
import { getAPIKey } from "./utils/api.js"
@ -44,6 +44,7 @@ export default {
getAction,
fetchDatasourceSchema,
Provider,
ContextScopes,
ActionTypes,
getAPIKey,
Block,

View File

@ -1,30 +1,79 @@
import { writable } from "svelte/store"
import { writable, derived } from "svelte/store"
import { ContextScopes } from "constants"
export const createContextStore = () => {
export const createContextStore = parentContext => {
const context = writable({})
let observers = []
// Adds a data context layer to the tree
const provideData = (providerId, data) => {
// Derive the total context state at this point in the tree
const contexts = parentContext ? [parentContext, context] : [context]
const totalContext = derived(contexts, $contexts => {
return $contexts.reduce((total, context) => ({ ...total, ...context }), {})
})
// Subscribe to updates in the parent context, so that we can proxy on any
// change messages to our own subscribers
if (parentContext) {
parentContext.actions.observeChanges(key => {
broadcastChange(key)
})
}
// Provide some data in context
const provideData = (providerId, data, scope = ContextScopes.Global) => {
if (!providerId || data === undefined) {
return
}
context.update(state => {
state[providerId] = data
return state
})
broadcastChange(providerId)
// Proxy message up the chain if we have a parent and are providing global
// context
if (scope === ContextScopes.Global && parentContext) {
parentContext.actions.provideData(providerId, data, scope)
}
// Otherwise this is either the context root, or we're providing a local
// context override, so we need to update the local context instead
else {
context.update(state => {
state[providerId] = data
return state
})
broadcastChange(providerId)
}
}
// Adds an action context layer to the tree
const provideAction = (providerId, actionType, callback) => {
// Provides some action in context
const provideAction = (
providerId,
actionType,
callback,
scope = ContextScopes.Global
) => {
if (!providerId || !actionType) {
return
}
context.update(state => {
state[`${providerId}_${actionType}`] = callback
return state
})
// Proxy message up the chain if we have a parent and are providing global
// context
if (scope === ContextScopes.Global && parentContext) {
parentContext.actions.provideAction(
providerId,
actionType,
callback,
scope
)
}
// Otherwise this is either the context root, or we're providing a local
// context override, so we need to update the local context instead
else {
const key = `${providerId}_${actionType}`
context.update(state => {
state[key] = callback
return state
})
broadcastChange(key)
}
}
const observeChanges = callback => {
@ -39,7 +88,7 @@ export const createContextStore = () => {
}
return {
subscribe: context.subscribe,
subscribe: totalContext.subscribe,
actions: {
provideData,
provideAction,