@cratis/scene.react 4.14.0 → 4.16.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/core/ScreenplayCoreComponents.tsx +24 -3
- package/dist/cjs/SceneElementView.js +1 -0
- package/dist/cjs/SceneElementView.js.map +1 -1
- package/dist/cjs/core/ScreenplayCoreComponents.js +25 -5
- package/dist/cjs/core/ScreenplayCoreComponents.js.map +1 -1
- package/dist/cjs/index.js +11 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/navigation/createHashSceneHistory.js +43 -0
- package/dist/cjs/navigation/createHashSceneHistory.js.map +1 -0
- package/dist/cjs/packages/PackageHostConfiguration.js +18 -2
- package/dist/cjs/packages/PackageHostConfiguration.js.map +1 -1
- package/dist/cjs/packages/isNetworkAsset.js +18 -0
- package/dist/cjs/packages/isNetworkAsset.js.map +1 -0
- package/dist/cjs/renderer/RenderBindingScopeContext.js +15 -5
- package/dist/cjs/renderer/RenderBindingScopeContext.js.map +1 -1
- package/dist/cjs/renderer/useBoundQuery.js +50 -0
- package/dist/cjs/renderer/useBoundQuery.js.map +1 -0
- package/dist/cjs/renderer/useValueOutput.js +31 -0
- package/dist/cjs/renderer/useValueOutput.js.map +1 -0
- package/dist/cjs/renderer/valueOutputProperty.js +28 -0
- package/dist/cjs/renderer/valueOutputProperty.js.map +1 -0
- package/dist/esm/SceneElementView.js +1 -0
- package/dist/esm/SceneElementView.js.map +1 -1
- package/dist/esm/core/ScreenplayCoreComponents.d.ts.map +1 -1
- package/dist/esm/core/ScreenplayCoreComponents.js +25 -5
- package/dist/esm/core/ScreenplayCoreComponents.js.map +1 -1
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/navigation/createHashSceneHistory.d.ts +4 -0
- package/dist/esm/navigation/createHashSceneHistory.d.ts.map +1 -0
- package/dist/esm/navigation/createHashSceneHistory.js +41 -0
- package/dist/esm/navigation/createHashSceneHistory.js.map +1 -0
- package/dist/esm/navigation/index.d.ts +1 -0
- package/dist/esm/navigation/index.d.ts.map +1 -1
- package/dist/esm/navigation/index.js +1 -0
- package/dist/esm/navigation/index.js.map +1 -1
- package/dist/esm/packages/PackageHostConfiguration.d.ts +2 -0
- package/dist/esm/packages/PackageHostConfiguration.d.ts.map +1 -1
- package/dist/esm/packages/PackageHostConfiguration.js +19 -3
- package/dist/esm/packages/PackageHostConfiguration.js.map +1 -1
- package/dist/esm/packages/index.d.ts +1 -0
- package/dist/esm/packages/index.d.ts.map +1 -1
- package/dist/esm/packages/index.js +1 -0
- package/dist/esm/packages/index.js.map +1 -1
- package/dist/esm/packages/isNetworkAsset.d.ts +2 -0
- package/dist/esm/packages/isNetworkAsset.d.ts.map +1 -0
- package/dist/esm/packages/isNetworkAsset.js +16 -0
- package/dist/esm/packages/isNetworkAsset.js.map +1 -0
- package/dist/esm/renderer/RenderBindingScopeContext.d.ts.map +1 -1
- package/dist/esm/renderer/RenderBindingScopeContext.js +17 -7
- package/dist/esm/renderer/RenderBindingScopeContext.js.map +1 -1
- package/dist/esm/renderer/index.d.ts +3 -0
- package/dist/esm/renderer/index.d.ts.map +1 -1
- package/dist/esm/renderer/index.js +3 -0
- package/dist/esm/renderer/index.js.map +1 -1
- package/dist/esm/renderer/useBoundQuery.d.ts +3 -0
- package/dist/esm/renderer/useBoundQuery.d.ts.map +1 -0
- package/dist/esm/renderer/useBoundQuery.js +48 -0
- package/dist/esm/renderer/useBoundQuery.js.map +1 -0
- package/dist/esm/renderer/useValueOutput.d.ts +4 -0
- package/dist/esm/renderer/useValueOutput.d.ts.map +1 -0
- package/dist/esm/renderer/useValueOutput.js +28 -0
- package/dist/esm/renderer/useValueOutput.js.map +1 -0
- package/dist/esm/renderer/valueOutputProperty.d.ts +3 -0
- package/dist/esm/renderer/valueOutputProperty.d.ts.map +1 -0
- package/dist/esm/renderer/valueOutputProperty.js +26 -0
- package/dist/esm/renderer/valueOutputProperty.js.map +1 -0
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/navigation/createHashSceneHistory.ts +42 -0
- package/navigation/index.ts +1 -0
- package/package.json +5 -5
- package/packages/PackageHostConfiguration.ts +26 -3
- package/packages/index.ts +1 -0
- package/packages/isNetworkAsset.ts +15 -0
- package/renderer/RenderBindingScopeContext.tsx +12 -8
- package/renderer/index.ts +3 -0
- package/renderer/useBoundQuery.ts +50 -0
- package/renderer/useValueOutput.ts +28 -0
- package/renderer/valueOutputProperty.ts +25 -0
|
@@ -0,0 +1,42 @@
|
|
|
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 { SceneBrowserWindow } from './createBrowserSceneHistory';
|
|
5
|
+
import { SceneHistory } from './SceneHistory';
|
|
6
|
+
import { SceneNavigationState } from './SceneNavigationState';
|
|
7
|
+
|
|
8
|
+
const stateKey = 'sceneNavigation';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A history kept in the URL fragment - `index.html#/WorkItemDetails?workItemId=B` - for hosts that are served
|
|
12
|
+
* as a single file or from a path they do not control, such as a generated runtime or a webview. It behaves
|
|
13
|
+
* like `createBrowserSceneHistory`: entries are pushed with their navigation state, back and forward restore
|
|
14
|
+
* them, and a typed or bookmarked fragment is matched by URL alone. A fragment changed by hand, which fires
|
|
15
|
+
* `hashchange` rather than `popstate`, is reported the same way.
|
|
16
|
+
*/
|
|
17
|
+
export function createHashSceneHistory(browser: SceneBrowserWindow = window): SceneHistory {
|
|
18
|
+
const location = () => {
|
|
19
|
+
const fragment = browser.location.hash.replace(/^#\/?/, '');
|
|
20
|
+
return fragment.length ? fragment : undefined;
|
|
21
|
+
};
|
|
22
|
+
const href = (url: string | undefined) => `${browser.location.pathname}${browser.location.search}#/${url ?? ''}`;
|
|
23
|
+
const stored = (value: unknown) => (value as Record<string, SceneNavigationState> | null)?.[stateKey];
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
location,
|
|
27
|
+
state: () => stored(browser.history.state),
|
|
28
|
+
push: state => browser.history.pushState({ [stateKey]: state }, '', href(state.url)),
|
|
29
|
+
replace: state => browser.history.replaceState({ [stateKey]: state }, '', href(state.url)),
|
|
30
|
+
back: () => browser.history.back(),
|
|
31
|
+
listen: listener => {
|
|
32
|
+
const onPopState = (event: PopStateEvent) => listener(location(), stored(event.state));
|
|
33
|
+
const onHashChange = () => listener(location(), stored(browser.history.state));
|
|
34
|
+
browser.addEventListener('popstate', onPopState);
|
|
35
|
+
browser.addEventListener('hashchange', onHashChange);
|
|
36
|
+
return () => {
|
|
37
|
+
browser.removeEventListener('popstate', onPopState);
|
|
38
|
+
browser.removeEventListener('hashchange', onHashChange);
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
package/navigation/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from './SceneNavigationHost';
|
|
|
9
9
|
export * from './SceneNavigationState';
|
|
10
10
|
export * from './SceneRoute';
|
|
11
11
|
export * from './createBrowserSceneHistory';
|
|
12
|
+
export * from './createHashSceneHistory';
|
|
12
13
|
export * from './createMemorySceneHistory';
|
|
13
14
|
export * from './diagnoseRenderedNavigation';
|
|
14
15
|
export * from './identityRoute';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cratis/scene.react",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.16.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.16.0",
|
|
55
|
+
"@cratis/scene.model": "4.16.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@cratis/scene.engine": "4.
|
|
59
|
-
"@cratis/scene.model": "4.
|
|
58
|
+
"@cratis/scene.engine": "4.16.0",
|
|
59
|
+
"@cratis/scene.model": "4.16.0",
|
|
60
60
|
"@storybook/addon-links": "^10.6.1",
|
|
61
61
|
"@storybook/react": "^10.6.1",
|
|
62
62
|
"@storybook/react-vite": "^10.6.1",
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
4
|
import { DialogTemplate, Layout, ScreenTemplate, Theme, UiProfile } from '@cratis/scene.model';
|
|
5
|
-
import { TemplateCatalogEntry, describeTemplateCatalog, resolvePackageDependencies } from '@cratis/scene.engine';
|
|
5
|
+
import { TemplateCatalogEntry, describeTemplateCatalog, incompatiblePackages, resolvePackageDependencies } from '@cratis/scene.engine';
|
|
6
6
|
import { IconAdapterRegistry, createIconAdapterRegistry } from '../icons/IconAdapterRegistry';
|
|
7
7
|
import { ScenePackageBundle, mergePackageRegistries, validatePackageBundle } from './ScenePackageBundle';
|
|
8
|
+
import { isNetworkAsset } from './isNetworkAsset';
|
|
8
9
|
|
|
9
10
|
export interface PackageHostPolicy {
|
|
10
11
|
allowExecutableImports: boolean;
|
|
@@ -31,6 +32,12 @@ export interface ResolvedPackageHost {
|
|
|
31
32
|
dialogTemplates: DialogTemplate[];
|
|
32
33
|
themes: Theme[];
|
|
33
34
|
|
|
35
|
+
/** The layout the UI profile names, from the approved packages. Absent when the profile names none. */
|
|
36
|
+
layout?: Layout;
|
|
37
|
+
|
|
38
|
+
/** The theme the UI profile names, from the approved packages. Absent when the profile names none. */
|
|
39
|
+
theme?: Theme;
|
|
40
|
+
|
|
34
41
|
/**
|
|
35
42
|
* Every template the approved packages provide, with compatibility, attribution and license metadata and
|
|
36
43
|
* whether it can be used with this package set. Template browsers read this rather than the raw lists.
|
|
@@ -82,8 +89,9 @@ export function resolvePackageHost(configuration: PackageHostConfiguration): Res
|
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
const packageAssets = bundle.manifest.assets ?? [];
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
const networkAssets = packageAssets.filter(isNetworkAsset);
|
|
93
|
+
if (!configuration.policy.allowNetworkAssets && networkAssets.length > 0) {
|
|
94
|
+
diagnostics.push(`Package '${packageName}' declares network assets (${networkAssets.join(', ')}), but host policy forbids them`);
|
|
87
95
|
}
|
|
88
96
|
assets.push(...packageAssets);
|
|
89
97
|
|
|
@@ -98,6 +106,19 @@ export function resolvePackageHost(configuration: PackageHostConfiguration): Res
|
|
|
98
106
|
bundles.push(bundle);
|
|
99
107
|
}
|
|
100
108
|
|
|
109
|
+
const layouts = bundles.flatMap(bundle => bundle.layouts ?? []);
|
|
110
|
+
const themes = bundles.flatMap(bundle => bundle.themes ?? []);
|
|
111
|
+
const layout = configuration.profile.layout ? layouts.find(candidate => candidate.name === configuration.profile.layout) : undefined;
|
|
112
|
+
const theme = configuration.profile.theme ? themes.find(candidate => candidate.name === configuration.profile.theme) : undefined;
|
|
113
|
+
if (configuration.profile.layout && !layout) {
|
|
114
|
+
diagnostics.push(`UI profile '${configuration.profile.name}' uses layout '${configuration.profile.layout}', which no approved package provides`);
|
|
115
|
+
}
|
|
116
|
+
if (configuration.profile.theme && !theme) {
|
|
117
|
+
diagnostics.push(`UI profile '${configuration.profile.name}' uses theme '${configuration.profile.theme}', which no approved package provides`);
|
|
118
|
+
} else if (theme && incompatiblePackages(theme, configuration.profile).length > 0) {
|
|
119
|
+
diagnostics.push(`Theme '${theme.name}' is not compatible with ${incompatiblePackages(theme, configuration.profile).join(', ')} in UI profile '${configuration.profile.name}'`);
|
|
120
|
+
}
|
|
121
|
+
|
|
101
122
|
const blocked = diagnostics.length > 0;
|
|
102
123
|
const approved = blocked ? [] : bundles;
|
|
103
124
|
const approvedAssets = blocked ? [] : assets;
|
|
@@ -109,6 +130,8 @@ export function resolvePackageHost(configuration: PackageHostConfiguration): Res
|
|
|
109
130
|
screenTemplates: approved.flatMap(bundle => bundle.screenTemplates ?? []),
|
|
110
131
|
dialogTemplates: approved.flatMap(bundle => bundle.dialogTemplates ?? []),
|
|
111
132
|
themes: approved.flatMap(bundle => bundle.themes ?? []),
|
|
133
|
+
layout: blocked ? undefined : layout,
|
|
134
|
+
theme: blocked ? undefined : theme,
|
|
112
135
|
templates: describeTemplateCatalog(approved, configuration.sceneVersion),
|
|
113
136
|
icons: createIconAdapterRegistry(approved.map(bundle => bundle.iconLibrary?.adapter).filter(adapter => adapter !== undefined)),
|
|
114
137
|
assets: approvedAssets,
|
package/packages/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ export * from './DesignTimeResolution';
|
|
|
16
16
|
export * from '../navigation';
|
|
17
17
|
export * from './EmbeddedPackageHost';
|
|
18
18
|
export * from './loadDesignTimeContributions';
|
|
19
|
+
export * from './isNetworkAsset';
|
|
19
20
|
export * from './PackageHostAssets';
|
|
20
21
|
export * from './PackageHostConfiguration';
|
|
21
22
|
export * from './PackageHostRenderMode';
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
* Whether loading an asset reaches beyond the host: an absolute URL with any scheme other than `data:`
|
|
6
|
+
* or `blob:` (`https://`, `http://`, `ws://`, `ftp://`), or a protocol-relative `//host/...` reference. A
|
|
7
|
+
* package-relative path or a bare module specifier such as `@cratis/components/styles` is resolved by the
|
|
8
|
+
* host from what it bundled, so it is not a network asset.
|
|
9
|
+
*/
|
|
10
|
+
export function isNetworkAsset(asset: string): boolean {
|
|
11
|
+
const trimmed = asset.trim();
|
|
12
|
+
if (trimmed.startsWith('//')) return true;
|
|
13
|
+
const scheme = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(trimmed)?.[1]?.toLowerCase();
|
|
14
|
+
return scheme !== undefined && scheme !== 'data' && scheme !== 'blob';
|
|
15
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
4
|
import { ReactNode, createContext, useCallback, useContext, useMemo, useRef } from 'react';
|
|
5
|
-
import { BindingResolver, BindingScope, resolveBindingExpression } from '@cratis/scene.engine';
|
|
5
|
+
import { BindingResolver, BindingScope, nestBindingScope, resolveBindingExpression } from '@cratis/scene.engine';
|
|
6
6
|
import { BindingExpression, BindingNullBehavior } from '@cratis/scene.model';
|
|
7
7
|
import { BindingOutputProvider, useBindingOutputScope } from './BindingOutputContext';
|
|
8
8
|
|
|
@@ -24,10 +24,15 @@ export interface RenderBindingScopeProviderProps {
|
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Provides the inherited reactive binding scope for a rendered Scene tree.
|
|
27
|
+
*
|
|
28
|
+
* The outermost provider owns the component output scope. A provider rendered inside another - a nested
|
|
29
|
+
* template or region - nests its own data context, query results and component outputs over the inherited
|
|
30
|
+
* scope with `nestBindingScope`, so a detail region bound to the selected row reads that row while still
|
|
31
|
+
* seeing the screen's queries and every component's outputs.
|
|
27
32
|
*/
|
|
28
33
|
export function RenderBindingScopeProvider(props: RenderBindingScopeProviderProps) {
|
|
29
34
|
const current = useContext(Context);
|
|
30
|
-
if (current) return
|
|
35
|
+
if (current) return <RenderBindingScopeInner {...props} parent={current.scope} />;
|
|
31
36
|
|
|
32
37
|
return (
|
|
33
38
|
<BindingOutputProvider onOutputsChanged={props.onComponentOutputsChanged}>
|
|
@@ -36,14 +41,13 @@ export function RenderBindingScopeProvider(props: RenderBindingScopeProviderProp
|
|
|
36
41
|
);
|
|
37
42
|
}
|
|
38
43
|
|
|
39
|
-
function RenderBindingScopeInner({ children, dataContext, queryResults, componentOutputs, resolveBinding }: RenderBindingScopeProviderProps) {
|
|
44
|
+
function RenderBindingScopeInner({ children, dataContext, queryResults, componentOutputs, resolveBinding, parent }: RenderBindingScopeProviderProps & { parent?: BindingScope }) {
|
|
40
45
|
const outputs = useBindingOutputScope();
|
|
41
46
|
const previousValues = useRef(new Map<string, unknown>());
|
|
42
|
-
const scope = useMemo<BindingScope>(() =>
|
|
43
|
-
dataContext,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}), [componentOutputs, dataContext, outputs, queryResults]);
|
|
47
|
+
const scope = useMemo<BindingScope>(() => {
|
|
48
|
+
const own = { dataContext, queryResults, componentOutputs: { ...(componentOutputs ?? {}), ...outputs } };
|
|
49
|
+
return parent ? nestBindingScope(parent, own) : own;
|
|
50
|
+
}, [componentOutputs, dataContext, outputs, parent, queryResults]);
|
|
47
51
|
|
|
48
52
|
const resolver = useCallback<BindingResolver>((binding: BindingExpression) => {
|
|
49
53
|
if (!binding.kind && resolveBinding) return resolveBinding(binding);
|
package/renderer/index.ts
CHANGED
|
@@ -6,5 +6,8 @@ export * from './BindingOutputContext';
|
|
|
6
6
|
export * from './ComponentRegistry';
|
|
7
7
|
export * from './RenderBindingScopeContext';
|
|
8
8
|
export * from './UnresolvedComponent';
|
|
9
|
+
export * from './useBoundQuery';
|
|
10
|
+
export * from './useValueOutput';
|
|
11
|
+
export * from './valueOutputProperty';
|
|
9
12
|
export * from './createReactRenderer';
|
|
10
13
|
export * from './InteractiveComponent';
|
|
@@ -0,0 +1,50 @@
|
|
|
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 { useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
+
import { QueryBindingState } from '@cratis/scene.engine';
|
|
6
|
+
import { QueryPerformer } from './AbortableQueryRunner';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Runs a bound query whenever its arguments change and returns the accepted query results.
|
|
10
|
+
*
|
|
11
|
+
* Each change of arguments rebinds the query: the previous run is aborted and, should its answer still
|
|
12
|
+
* arrive, discarded by `QueryBindingState` - a slow result for a previous selection never replaces the
|
|
13
|
+
* current one. Arguments of `undefined` mean the binding is cleared, as when a selection is cleared, so the
|
|
14
|
+
* result is removed, and so is it when the current run fails - a failed rebind must not leave the previous
|
|
15
|
+
* selection's data on screen. The results are keyed by `query`, ready to pass as a scope's `queryResults`.
|
|
16
|
+
*/
|
|
17
|
+
export function useBoundQuery(query: string, argumentsValue: unknown, perform: QueryPerformer): Record<string, unknown> {
|
|
18
|
+
const state = useMemo(() => new QueryBindingState(), []);
|
|
19
|
+
const [results, setResults] = useState<Record<string, unknown>>({});
|
|
20
|
+
const key = argumentsValue === undefined ? undefined : JSON.stringify(argumentsValue);
|
|
21
|
+
const latestArguments = useRef(argumentsValue);
|
|
22
|
+
latestArguments.current = argumentsValue;
|
|
23
|
+
|
|
24
|
+
// The arguments are compared by value through `key`; the effect reads them from `latestArguments`.
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const argumentsForRun = latestArguments.current;
|
|
27
|
+
if (argumentsForRun === undefined) {
|
|
28
|
+
state.clear(query);
|
|
29
|
+
setResults(state.queryResults);
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const controller = new AbortController();
|
|
34
|
+
const ticket = state.begin(query);
|
|
35
|
+
perform(query, argumentsForRun, controller.signal).then(
|
|
36
|
+
result => {
|
|
37
|
+
if (state.complete(ticket, result)) setResults(state.queryResults);
|
|
38
|
+
},
|
|
39
|
+
() => {
|
|
40
|
+
if (controller.signal.aborted || !state.complete(ticket, undefined)) return;
|
|
41
|
+
state.clear(query);
|
|
42
|
+
setResults(state.queryResults);
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
return () => controller.abort();
|
|
47
|
+
}, [key, perform, query, state]);
|
|
48
|
+
|
|
49
|
+
return results;
|
|
50
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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 { useEffect, useRef } from 'react';
|
|
5
|
+
import { ComponentBindingOutputs } from './BindingOutputContext';
|
|
6
|
+
|
|
7
|
+
/** The output property an input widget publishes its current value under. */
|
|
8
|
+
export const valueOutputName = 'value';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Publishes an input widget's current value as its `value` output: once when it mounts, with the initial
|
|
12
|
+
* value, and again on every change. Other elements bind to it with a `componentProperty` binding, and a
|
|
13
|
+
* command action maps it as `component.<id>.value`. The renderer clears the output when the widget unmounts.
|
|
14
|
+
*
|
|
15
|
+
* Values are published as JSON-compatible data so both engines read the same thing: a `Date` is published
|
|
16
|
+
* as its ISO 8601 string, and `undefined` as `null`.
|
|
17
|
+
*/
|
|
18
|
+
export function useValueOutput(bindingOutputs: ComponentBindingOutputs | undefined, value: unknown): void {
|
|
19
|
+
const published = value instanceof Date ? value.toISOString() : value ?? null;
|
|
20
|
+
const key = JSON.stringify(published);
|
|
21
|
+
const latest = useRef(published);
|
|
22
|
+
latest.current = published;
|
|
23
|
+
|
|
24
|
+
// Published by value through `key`: a new array or object with the same content is not a change.
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
bindingOutputs?.setOutput(valueOutputName, latest.current);
|
|
27
|
+
}, [bindingOutputs, key]);
|
|
28
|
+
}
|
|
@@ -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 { BindingMode, PropertyDescriptor, PropertyValueType } from '@cratis/scene.model';
|
|
5
|
+
import { valueOutputName } from './useValueOutput';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The descriptor entry an input widget declares for the value it publishes with `useValueOutput`, so a
|
|
9
|
+
* designer can offer it as a binding source - for a detail component, a query argument or a command input.
|
|
10
|
+
*
|
|
11
|
+
* @param valueType The type of value the widget publishes.
|
|
12
|
+
* @param description What the value is, when the default sentence does not say enough.
|
|
13
|
+
*/
|
|
14
|
+
export function valueOutputProperty(valueType: PropertyValueType, description = 'What the user has entered, published as the input changes.'): PropertyDescriptor {
|
|
15
|
+
return {
|
|
16
|
+
path: valueOutputName,
|
|
17
|
+
label: 'Value',
|
|
18
|
+
group: 'Outputs',
|
|
19
|
+
valueType,
|
|
20
|
+
readOnly: true,
|
|
21
|
+
output: true,
|
|
22
|
+
bindingMode: BindingMode.OneWay,
|
|
23
|
+
description,
|
|
24
|
+
};
|
|
25
|
+
}
|