@cratis/scene.react 4.7.0 → 4.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SceneElementView.tsx +10 -3
- package/dist/cjs/SceneElementView.js +4 -2
- package/dist/cjs/SceneElementView.js.map +1 -1
- package/dist/cjs/index.js +11 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/packages/ComponentsUiLibraryContract.js +72 -0
- package/dist/cjs/packages/ComponentsUiLibraryContract.js.map +1 -0
- package/dist/cjs/packages/DesignTimeBundle.js +17 -0
- package/dist/cjs/packages/DesignTimeBundle.js.map +1 -0
- package/dist/cjs/packages/PackageHostConfiguration.js +43 -0
- package/dist/cjs/packages/PackageHostConfiguration.js.map +1 -0
- package/dist/cjs/packages/ScenePackageBundle.js +9 -0
- package/dist/cjs/packages/ScenePackageBundle.js.map +1 -1
- package/dist/cjs/renderer/BindingOutputContext.js +74 -0
- package/dist/cjs/renderer/BindingOutputContext.js.map +1 -0
- package/dist/cjs/renderer/InteractiveComponent.js +7 -1
- package/dist/cjs/renderer/InteractiveComponent.js.map +1 -1
- package/dist/esm/SceneElementView.d.ts +2 -1
- package/dist/esm/SceneElementView.d.ts.map +1 -1
- package/dist/esm/SceneElementView.js +5 -3
- package/dist/esm/SceneElementView.js.map +1 -1
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/packages/ComponentsUiLibraryContract.d.ts +19 -0
- package/dist/esm/packages/ComponentsUiLibraryContract.d.ts.map +1 -0
- package/dist/esm/packages/ComponentsUiLibraryContract.js +69 -0
- package/dist/esm/packages/ComponentsUiLibraryContract.js.map +1 -0
- package/dist/esm/packages/DesignTimeAction.d.ts +23 -0
- package/dist/esm/packages/DesignTimeAction.d.ts.map +1 -0
- package/dist/esm/packages/DesignTimeAction.js +2 -0
- package/dist/esm/packages/DesignTimeAction.js.map +1 -0
- package/dist/esm/packages/DesignTimeBundle.d.ts +17 -0
- package/dist/esm/packages/DesignTimeBundle.d.ts.map +1 -0
- package/dist/esm/packages/DesignTimeBundle.js +15 -0
- package/dist/esm/packages/DesignTimeBundle.js.map +1 -0
- package/dist/esm/packages/PackageHostConfiguration.d.ts +18 -0
- package/dist/esm/packages/PackageHostConfiguration.d.ts.map +1 -0
- package/dist/esm/packages/PackageHostConfiguration.js +41 -0
- package/dist/esm/packages/PackageHostConfiguration.js.map +1 -0
- package/dist/esm/packages/ScenePackageBundle.d.ts +4 -0
- package/dist/esm/packages/ScenePackageBundle.d.ts.map +1 -1
- package/dist/esm/packages/ScenePackageBundle.js +9 -0
- package/dist/esm/packages/ScenePackageBundle.js.map +1 -1
- package/dist/esm/packages/index.d.ts +4 -0
- package/dist/esm/packages/index.d.ts.map +1 -1
- package/dist/esm/packages/index.js +4 -0
- package/dist/esm/packages/index.js.map +1 -1
- package/dist/esm/renderer/BindingOutputContext.d.ts +18 -0
- package/dist/esm/renderer/BindingOutputContext.d.ts.map +1 -0
- package/dist/esm/renderer/BindingOutputContext.js +70 -0
- package/dist/esm/renderer/BindingOutputContext.js.map +1 -0
- package/dist/esm/renderer/ComponentRegistry.d.ts +2 -0
- package/dist/esm/renderer/ComponentRegistry.d.ts.map +1 -1
- package/dist/esm/renderer/InteractiveComponent.d.ts.map +1 -1
- package/dist/esm/renderer/InteractiveComponent.js +8 -2
- package/dist/esm/renderer/InteractiveComponent.js.map +1 -1
- package/dist/esm/renderer/index.d.ts +1 -0
- package/dist/esm/renderer/index.d.ts.map +1 -1
- package/dist/esm/renderer/index.js +1 -0
- package/dist/esm/renderer/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/packages/ComponentsUiLibraryContract.ts +89 -0
- package/packages/DesignTimeAction.ts +25 -0
- package/packages/DesignTimeBundle.ts +27 -0
- package/packages/PackageHostConfiguration.ts +56 -0
- package/packages/ScenePackageBundle.ts +19 -0
- package/packages/index.ts +4 -0
- package/renderer/BindingOutputContext.tsx +69 -0
- package/renderer/ComponentRegistry.ts +4 -0
- package/renderer/InteractiveComponent.tsx +5 -2
- package/renderer/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cratis/scene.react",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.9.0",
|
|
4
4
|
"description": "The React renderer implementing Scene.Engine's Renderer contract against real React/DOM.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cratis",
|
|
@@ -51,12 +51,12 @@
|
|
|
51
51
|
"build-storybook": "storybook build"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@cratis/scene.engine": "4.
|
|
55
|
-
"@cratis/scene.model": "4.
|
|
54
|
+
"@cratis/scene.engine": "4.9.0",
|
|
55
|
+
"@cratis/scene.model": "4.9.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@cratis/scene.engine": "4.
|
|
59
|
-
"@cratis/scene.model": "4.
|
|
58
|
+
"@cratis/scene.engine": "4.9.0",
|
|
59
|
+
"@cratis/scene.model": "4.9.0",
|
|
60
60
|
"@storybook/addon-links": "^10.6.1",
|
|
61
61
|
"@storybook/react": "^10.6.1",
|
|
62
62
|
"@storybook/react-vite": "^10.6.1",
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The web-only mapping between an independently authored Scene catalog and a Components renderer.
|
|
6
|
+
* Primitive slot identities are not Scene component names: composites such as DataTable remain
|
|
7
|
+
* library-independent. This contract belongs to the React bundle, never the platform-neutral model.
|
|
8
|
+
*/
|
|
9
|
+
export interface ComponentsUiLibraryContract {
|
|
10
|
+
/** The npm module the host loads through its explicit package allowlist. */
|
|
11
|
+
module: string;
|
|
12
|
+
/** The named export supplying the Components UiLibrary. */
|
|
13
|
+
exportName: string;
|
|
14
|
+
/** The expected runtime library identity. */
|
|
15
|
+
id: string;
|
|
16
|
+
/** The Components renderer ABI major required by this mapping. */
|
|
17
|
+
abi: number;
|
|
18
|
+
/** The renderer profile promised by the library. */
|
|
19
|
+
profile: string;
|
|
20
|
+
/** Primitive or presentation slots the Scene integration requires. */
|
|
21
|
+
requiredSlots: readonly string[];
|
|
22
|
+
/** Capabilities required by the integration, not inferred from library identity. */
|
|
23
|
+
requiredCapabilities?: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The structural part of Components' UiLibrary needed to check a mapping. The actual library may
|
|
28
|
+
* also contain providers and executable slot declarations; Scene does not load or reinterpret them.
|
|
29
|
+
* Keeping this structural avoids a Components ABI dependency in Scene.Model or Scene.Engine.
|
|
30
|
+
*/
|
|
31
|
+
export interface ComponentsUiLibraryManifest {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly abi: number;
|
|
34
|
+
readonly profile: string;
|
|
35
|
+
readonly slots: object;
|
|
36
|
+
readonly capabilities: readonly string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Checks a mapping before loading executable frontend code. */
|
|
40
|
+
export function validateComponentsUiLibraryContract(contract: ComponentsUiLibraryContract): string[] {
|
|
41
|
+
const problems: string[] = [];
|
|
42
|
+
for (const field of ['module', 'exportName', 'id', 'profile'] as const) {
|
|
43
|
+
if (contract[field].trim().length === 0) problems.push(`Components UI library mapping has an empty '${field}'`);
|
|
44
|
+
}
|
|
45
|
+
if (!Number.isInteger(contract.abi) || contract.abi < 1) {
|
|
46
|
+
problems.push('Components UI library mapping requires a positive integer ABI major');
|
|
47
|
+
}
|
|
48
|
+
for (const [field, values] of [
|
|
49
|
+
['requiredSlots', contract.requiredSlots],
|
|
50
|
+
['requiredCapabilities', contract.requiredCapabilities ?? []],
|
|
51
|
+
] as const) {
|
|
52
|
+
const seen = new Set<string>();
|
|
53
|
+
for (const value of values) {
|
|
54
|
+
if (value.trim().length === 0) problems.push(`Components UI library mapping has an empty '${field}' entry`);
|
|
55
|
+
if (seen.has(value)) problems.push(`Components UI library mapping repeats '${value}' in '${field}'`);
|
|
56
|
+
seen.add(value);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return problems;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Proves that an allowlisted module export satisfies its Scene mapping. Hosts must reject problems,
|
|
64
|
+
* rather than silently falling back to a different library. Additional slots are permitted: a
|
|
65
|
+
* renderer can support more than the bounded profile this integration consumes.
|
|
66
|
+
*/
|
|
67
|
+
export function validateComponentsUiLibraryMapping(
|
|
68
|
+
contract: ComponentsUiLibraryContract,
|
|
69
|
+
library: ComponentsUiLibraryManifest,
|
|
70
|
+
): string[] {
|
|
71
|
+
const problems = validateComponentsUiLibraryContract(contract);
|
|
72
|
+
for (const field of ['id', 'abi', 'profile'] as const) {
|
|
73
|
+
if (library[field] !== contract[field]) {
|
|
74
|
+
problems.push(`Components UI library '${contract.id}' requires ${field} '${contract[field]}' but the loaded library supplies '${library[field]}'`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const slots = new Map(Object.entries(library.slots));
|
|
78
|
+
for (const slot of contract.requiredSlots) {
|
|
79
|
+
if (!slots.has(slot) || slots.get(slot) === undefined || slots.get(slot) === null) {
|
|
80
|
+
problems.push(`Components UI library '${contract.id}' provides no implementation for required slot '${slot}'`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const capability of contract.requiredCapabilities ?? []) {
|
|
84
|
+
if (!library.capabilities.includes(capability)) {
|
|
85
|
+
problems.push(`Components UI library '${contract.id}' does not support required capability '${capability}'`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return problems;
|
|
89
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { ComponentDescriptor, DesignTimeActionDescriptor, ExternalComponent, SceneEdit } from '@cratis/scene.model';
|
|
5
|
+
|
|
6
|
+
export interface DesignTimeActionContext {
|
|
7
|
+
element: ExternalComponent;
|
|
8
|
+
descriptor: ComponentDescriptor;
|
|
9
|
+
commandMetadata?: { properties: { name: string; type: string; label?: string }[] };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DesignTimeActionResult {
|
|
13
|
+
edits: SceneEdit[];
|
|
14
|
+
diagnostics: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Executable package-owned design-time action. Hosts apply the returned edit batch through their own undo/redo.
|
|
19
|
+
*/
|
|
20
|
+
export interface DesignTimeAction {
|
|
21
|
+
descriptor: DesignTimeActionDescriptor;
|
|
22
|
+
isVisible(context: DesignTimeActionContext): boolean;
|
|
23
|
+
isEnabled(context: DesignTimeActionContext): boolean;
|
|
24
|
+
execute(context: DesignTimeActionContext): DesignTimeActionResult;
|
|
25
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { ComponentType, ReactNode } from 'react';
|
|
5
|
+
import { DesignTimeAction } from './DesignTimeAction';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Optional React-only design-time surface a package may publish beside its runtime bundle.
|
|
9
|
+
*/
|
|
10
|
+
export interface DesignTimeBundle {
|
|
11
|
+
previews?: Record<string, ComponentType<{ children?: ReactNode }>>;
|
|
12
|
+
designers?: Record<string, ComponentType<{ children?: ReactNode }>>;
|
|
13
|
+
propertyEditors?: Record<string, ComponentType<Record<string, unknown>>>;
|
|
14
|
+
propertyDisplays?: Record<string, ComponentType<{ value: unknown }>>;
|
|
15
|
+
actions?: Record<string, DesignTimeAction>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function validateDesignTimeBundle(bundle: DesignTimeBundle | undefined): string[] {
|
|
19
|
+
if (!bundle) return [];
|
|
20
|
+
const problems: string[] = [];
|
|
21
|
+
for (const [name, action] of Object.entries(bundle.actions ?? {})) {
|
|
22
|
+
if (action.descriptor.id !== name) {
|
|
23
|
+
problems.push(`registers design-time action '${name}' with descriptor id '${action.descriptor.id}'`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return problems;
|
|
27
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { UiProfile } from '@cratis/scene.model';
|
|
5
|
+
import { ScenePackageBundle, mergePackageRegistries } from './ScenePackageBundle';
|
|
6
|
+
|
|
7
|
+
export interface PackageHostPolicy {
|
|
8
|
+
allowExecutableImports: boolean;
|
|
9
|
+
allowNetworkAssets: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PackageHostConfiguration {
|
|
13
|
+
bundles: ScenePackageBundle[];
|
|
14
|
+
profile: UiProfile;
|
|
15
|
+
policy: PackageHostPolicy;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ResolvedPackageHost {
|
|
19
|
+
bundles: ScenePackageBundle[];
|
|
20
|
+
components: ReturnType<typeof mergePackageRegistries>;
|
|
21
|
+
diagnostics: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolves the approved package set a standalone or embedded React host uses.
|
|
26
|
+
*/
|
|
27
|
+
export function resolvePackageHost(configuration: PackageHostConfiguration): ResolvedPackageHost {
|
|
28
|
+
const diagnostics: string[] = [];
|
|
29
|
+
const byName = new Map(configuration.bundles.map(bundle => [bundle.manifest.name, bundle]));
|
|
30
|
+
const bundles: ScenePackageBundle[] = [];
|
|
31
|
+
const runtimeSingletons = new Map<string, string>();
|
|
32
|
+
|
|
33
|
+
for (const packageName of configuration.profile.packages) {
|
|
34
|
+
const bundle = byName.get(packageName);
|
|
35
|
+
if (!bundle) {
|
|
36
|
+
diagnostics.push(`UI profile asks for '${packageName}' but the host did not approve a bundle for it`);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!configuration.policy.allowNetworkAssets && (bundle.manifest.assets ?? []).some(asset => /^https?:\/\//.test(asset))) {
|
|
41
|
+
diagnostics.push(`Package '${packageName}' declares network assets, but host policy forbids them`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const dependency of bundle.manifest.runtimeSingletons ?? []) {
|
|
45
|
+
const owner = runtimeSingletons.get(dependency);
|
|
46
|
+
if (owner && owner !== packageName) {
|
|
47
|
+
diagnostics.push(`Runtime singleton '${dependency}' is provided by both '${owner}' and '${packageName}'`);
|
|
48
|
+
}
|
|
49
|
+
runtimeSingletons.set(dependency, packageName);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
bundles.push(bundle);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { bundles, components: mergePackageRegistries(bundles), diagnostics };
|
|
56
|
+
}
|
|
@@ -5,6 +5,8 @@ import { ComponentDescriptor, DialogTemplate, Layout, PackageKind, PropertyValue
|
|
|
5
5
|
import { IconAdapterRegistry, createIconAdapterRegistry } from '../icons/IconAdapterRegistry';
|
|
6
6
|
import { IconLibraryBundle } from '../icons/IconLibraryBundle';
|
|
7
7
|
import { ComponentRegistry } from '../renderer';
|
|
8
|
+
import { ComponentsUiLibraryContract, validateComponentsUiLibraryContract } from './ComponentsUiLibraryContract';
|
|
9
|
+
import { DesignTimeBundle, validateDesignTimeBundle } from './DesignTimeBundle';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* What a package actually ships to a React renderer, as opposed to what it *declares*.
|
|
@@ -70,6 +72,15 @@ export interface ScenePackageBundle {
|
|
|
70
72
|
* manifest declares an icon library.
|
|
71
73
|
*/
|
|
72
74
|
iconLibrary?: IconLibraryBundle;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Optional web-renderer mapping to a Components UiLibrary. Scene's catalog remains independently
|
|
78
|
+
* authored; hosts check the loaded export with validateComponentsUiLibraryMapping before rendering.
|
|
79
|
+
*/
|
|
80
|
+
componentsUiLibrary?: ComponentsUiLibraryContract;
|
|
81
|
+
|
|
82
|
+
/** Optional React-only design-time tooling. Runtime hosts can omit it entirely. */
|
|
83
|
+
designTime?: DesignTimeBundle;
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
/**
|
|
@@ -140,8 +151,16 @@ export function validatePackageBundle(bundle: ScenePackageBundle): string[] {
|
|
|
140
151
|
}
|
|
141
152
|
}
|
|
142
153
|
|
|
154
|
+
if (bundle.componentsUiLibrary) {
|
|
155
|
+
if (manifest.kind !== PackageKind.ComponentLibrary) {
|
|
156
|
+
problems.push('declares a Components UI library mapping but is not of kind ComponentLibrary');
|
|
157
|
+
}
|
|
158
|
+
problems.push(...validateComponentsUiLibraryContract(bundle.componentsUiLibrary));
|
|
159
|
+
}
|
|
160
|
+
|
|
143
161
|
problems.push(...validateIconLibrary(bundle));
|
|
144
162
|
problems.push(...validateDescriptors(bundle));
|
|
163
|
+
problems.push(...validateDesignTimeBundle(bundle.designTime));
|
|
145
164
|
|
|
146
165
|
return problems;
|
|
147
166
|
}
|
package/packages/index.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
// Copyright (c) Cratis. All rights reserved.
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
|
+
export * from './DesignTimeAction';
|
|
5
|
+
export * from './DesignTimeBundle';
|
|
6
|
+
export * from './PackageHostConfiguration';
|
|
4
7
|
export * from './ScenePackageBundle';
|
|
8
|
+
export * from './ComponentsUiLibraryContract';
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { ReactNode, createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
export interface ComponentBindingOutputs {
|
|
7
|
+
setOutput(property: string, value: unknown): void;
|
|
8
|
+
clearOutput(property: string): void;
|
|
9
|
+
clearAll(): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface BindingOutputProviderValue {
|
|
13
|
+
outputs: Record<string, Record<string, unknown>>;
|
|
14
|
+
outputFor(elementId: string): ComponentBindingOutputs;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const emptyOutputs: ComponentBindingOutputs = {
|
|
18
|
+
setOutput: () => undefined,
|
|
19
|
+
clearOutput: () => undefined,
|
|
20
|
+
clearAll: () => undefined,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const Context = createContext<BindingOutputProviderValue | undefined>(undefined);
|
|
24
|
+
|
|
25
|
+
export interface BindingOutputProviderProps {
|
|
26
|
+
children: ReactNode;
|
|
27
|
+
onOutputsChanged?: (outputs: Record<string, Record<string, unknown>>) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Tracks component output properties for typed component-property bindings and clears them with component lifecycle.
|
|
32
|
+
*/
|
|
33
|
+
export function BindingOutputProvider({ children, onOutputsChanged }: BindingOutputProviderProps) {
|
|
34
|
+
const [outputs, setOutputs] = useState<Record<string, Record<string, unknown>>>({});
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
onOutputsChanged?.(outputs);
|
|
38
|
+
}, [onOutputsChanged, outputs]);
|
|
39
|
+
|
|
40
|
+
const outputFor = useCallback((elementId: string): ComponentBindingOutputs => ({
|
|
41
|
+
setOutput: (property: string, propertyValue: unknown) => setOutputs(current => ({
|
|
42
|
+
...current,
|
|
43
|
+
[elementId]: { ...(current[elementId] ?? {}), [property]: propertyValue },
|
|
44
|
+
})),
|
|
45
|
+
clearOutput: (property: string) => setOutputs(current => {
|
|
46
|
+
const elementOutputs = { ...(current[elementId] ?? {}) };
|
|
47
|
+
delete elementOutputs[property];
|
|
48
|
+
return { ...current, [elementId]: elementOutputs };
|
|
49
|
+
}),
|
|
50
|
+
clearAll: () => setOutputs(current => {
|
|
51
|
+
const next = { ...current };
|
|
52
|
+
delete next[elementId];
|
|
53
|
+
return next;
|
|
54
|
+
}),
|
|
55
|
+
}), []);
|
|
56
|
+
|
|
57
|
+
const value = useMemo<BindingOutputProviderValue>(() => ({ outputs, outputFor }), [outputs, outputFor]);
|
|
58
|
+
|
|
59
|
+
return <Context.Provider value={value}>{children}</Context.Provider>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function useBindingOutputs(elementId: string): ComponentBindingOutputs {
|
|
63
|
+
const outputFor = useContext(Context)?.outputFor;
|
|
64
|
+
return useMemo(() => outputFor?.(elementId) ?? emptyOutputs, [outputFor, elementId]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function useBindingOutputScope(): Record<string, Record<string, unknown>> {
|
|
68
|
+
return useContext(Context)?.outputs ?? {};
|
|
69
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { ComponentType, ReactNode } from 'react';
|
|
5
5
|
import { ExternalComponent } from '@cratis/scene.model';
|
|
6
6
|
import { InteractionHandlers } from '../interactions';
|
|
7
|
+
import { ComponentBindingOutputs } from './BindingOutputContext';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* The props every registered component receives.
|
|
@@ -21,6 +22,9 @@ export interface RegisteredComponentProps {
|
|
|
21
22
|
* specification, or a gallery. Spreading it is safe either way.
|
|
22
23
|
*/
|
|
23
24
|
interactions?: InteractionHandlers;
|
|
25
|
+
|
|
26
|
+
/** Component output property writer for typed element-to-element bindings. */
|
|
27
|
+
bindingOutputs?: ComponentBindingOutputs;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
/**
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Copyright (c) Cratis. All rights reserved.
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
|
-
import { ComponentType, ReactNode, createElement } from 'react';
|
|
4
|
+
import { ComponentType, ReactNode, createElement, useEffect } from 'react';
|
|
5
5
|
import { ExternalComponent } from '@cratis/scene.model';
|
|
6
6
|
import { useInteractions } from '../interactions';
|
|
7
|
+
import { useBindingOutputs } from './BindingOutputContext';
|
|
7
8
|
import { RegisteredComponentProps } from './ComponentRegistry';
|
|
8
9
|
|
|
9
10
|
export interface InteractiveComponentProps {
|
|
@@ -21,5 +22,7 @@ export interface InteractiveComponentProps {
|
|
|
21
22
|
*/
|
|
22
23
|
export function InteractiveComponent({ component, element, slots }: InteractiveComponentProps) {
|
|
23
24
|
const interactions = useInteractions(element.id, element.behaviors);
|
|
24
|
-
|
|
25
|
+
const bindingOutputs = useBindingOutputs(element.id);
|
|
26
|
+
useEffect(() => () => bindingOutputs.clearAll(), [element.id]);
|
|
27
|
+
return createElement(component, { element, slots, interactions, bindingOutputs });
|
|
25
28
|
}
|
package/renderer/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Cratis. All rights reserved.
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
|
+
export * from './BindingOutputContext';
|
|
4
5
|
export * from './ComponentRegistry';
|
|
5
6
|
export * from './UnresolvedComponent';
|
|
6
7
|
export * from './createReactRenderer';
|