budibase/packages/client/src/components/Component.svelte

144 lines
4.3 KiB
Svelte

<script>
import { getContext, setContext } from "svelte"
import { writable, get } from "svelte/store"
import * as ComponentLibrary from "@budibase/standard-components"
import Router from "./Router.svelte"
import { enrichProps, propsAreSame } from "../utils/componentProps"
import { builderStore } from "../store"
import { hashString } from "../utils/hash"
import Manifest from "@budibase/standard-components/manifest.json"
import { Placeholder } from "@budibase/standard-components"
export let instance = {}
// Props that will be passed to the component instance
let componentProps
// Props are hashed when inside the builder preview and used as a key, so that
// components fully remount whenever any props change
let propsHash = 0
// Latest timestamp that we started a props update.
// Due to enrichment now being async, we need to avoid overwriting newer
// props with old ones, depending on how long enrichment takes.
let latestUpdateTime
// Get contexts
const context = getContext("context")
const insideScreenslot = !!getContext("screenslot")
// Create component context
const componentStore = writable({})
setContext("component", componentStore)
// Extract component instance info
$: constructor = getComponentConstructor(instance._component)
$: definition = getComponentDefinition(instance._component)
$: children = instance._children || []
$: id = instance._id
$: name = instance._instanceName
$: empty =
!children.length &&
definition?.hasChildren &&
definition?.showEmptyState !== false &&
$builderStore.inBuilder
$: updateComponentProps(instance, $context)
$: selected =
$builderStore.inBuilder &&
$builderStore.selectedComponentId === instance._id
$: interactive = $builderStore.previewType === "layout" || insideScreenslot
// Update component context
$: componentStore.set({
id,
children: children.length,
styles: { ...instance._styles, id, empty, interactive },
empty,
selected,
props: componentProps,
name,
})
// Gets the component constructor for the specified component
const getComponentConstructor = component => {
const split = component?.split("/")
const name = split?.[split.length - 1]
if (name === "screenslot" && $builderStore.previewType !== "layout") {
return Router
}
return ComponentLibrary[name]
}
const getComponentDefinition = component => {
const prefix = "@budibase/standard-components/"
const type = component?.replace(prefix, "")
return type ? Manifest[type] : null
}
// Enriches any string component props using handlebars
const updateComponentProps = (instance, context) => {
// Record the timestamp so we can reference it after enrichment
latestUpdateTime = Date.now()
const enrichmentTime = latestUpdateTime
// Enrich props with context
const enrichedProps = enrichProps(instance, context)
// Abandon this update if a newer update has started
if (enrichmentTime !== latestUpdateTime) {
return
}
// Update the component props.
// Most props are deeply compared so that svelte will only trigger reactive
// statements on props that have actually changed.
if (!enrichedProps) {
return
}
let propsChanged = false
if (!componentProps) {
componentProps = {}
propsChanged = true
}
Object.keys(enrichedProps).forEach(key => {
if (!propsAreSame(enrichedProps[key], componentProps[key])) {
propsChanged = true
componentProps[key] = enrichedProps[key]
}
})
// Update the hash if we're in the builder so we can fully remount this
// component
if (get(builderStore).inBuilder && propsChanged) {
propsHash = hashString(JSON.stringify(componentProps))
}
}
</script>
<div
class={`component ${id}`}
data-type={interactive ? "component" : ""}
data-id={id}
data-name={name}
>
{#key propsHash}
{#if constructor && componentProps}
<svelte:component this={constructor} {...componentProps}>
{#if children.length}
{#each children as child (child._id)}
<svelte:self instance={child} />
{/each}
{:else if empty}
<Placeholder />
{/if}
</svelte:component>
{/if}
{/key}
</div>
<style>
.component {
display: contents;
}
</style>