@genesislcap/foundation-react-utils 14.416.1-alpha-c3adbe7.0 → 14.417.0-FUI-0-react-renderers.1

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.
@@ -2,6 +2,58 @@
2
2
  "schemaVersion": "1.0.0",
3
3
  "readme": "",
4
4
  "modules": [
5
+ {
6
+ "kind": "javascript-module",
7
+ "path": "src/create-react-renderer.ts",
8
+ "declarations": [
9
+ {
10
+ "kind": "function",
11
+ "name": "createReactRenderer",
12
+ "return": {
13
+ "type": {
14
+ "text": "RendererEntry"
15
+ }
16
+ },
17
+ "parameters": [
18
+ {
19
+ "name": "Component",
20
+ "type": {
21
+ "text": "ComponentType<RendererControlProps>"
22
+ }
23
+ },
24
+ {
25
+ "name": "options",
26
+ "type": {
27
+ "text": "{\n /**\n * The custom element tag name to register. Must be unique and contain a hyphen.\n * e.g. 'my-price-renderer'\n */\n name: string;\n /**\n * RankedTester from @jsonforms/core — determines when this renderer applies.\n * Use rankWith(rank, tester) — rank 5+ recommended to override built-ins.\n */\n tester: RankedTester;\n /**\n * Custom mapper if you need to transform state differently.\n * Defaults to mapStateToControlProps from @jsonforms/core.\n */\n mapper?: (state: JsonFormsState, ownProps: OwnPropsOfControl) => StatePropsOfControl;\n /**\n * Whether to wrap the React component in the platform's <control-wrapper>,\n * which provides consistent label rendering, error display, and accessibility.\n * Defaults to true. Set to false if your React component handles its own labels/errors.\n *\n * Note: if your component has layout conflicts with control-wrapper's internal flex styles\n * (e.g. a block-level combobox that shrinks to content width), prefer keeping this true\n * and overriding via the exposed CSS part instead:\n *\n * control-wrapper::part(wrapper) { display: block; }\n *\n * This avoids losing built-in label/error/accessibility while still allowing external styling.\n * The `part=\"wrapper\"` is exposed on the inner slot-hosting div of <control-wrapper>.\n */\n wrapWithControlWrapper?: boolean;\n }"
28
+ }
29
+ }
30
+ ],
31
+ "description": "Converts a React component into a `RendererEntry` for use with `foundation-form`'s\n`additionalRenderers` property.\n\nThe component is registered as a custom element via `r2wc` and wrapped in a FAST\n`html` template so it integrates with the JSON Forms dispatch renderer.\n\n**Important — use platform web components for inputs, not native HTML elements.**\nThe React component runs inside an `r2wc` custom element inside a FAST template,\ncreating a three-layer shadow DOM boundary. React's synthetic event system does not\ncross shadow boundaries, so `onChange` on a plain `<input>` will never fire.\nUse `rapid-number-field`, `rapid-text-field`, etc. and attach listeners via\n`addEventListener` in a `useEffect` instead.",
32
+ "privacy": "public"
33
+ },
34
+ {
35
+ "kind": "variable",
36
+ "name": "WebComponent"
37
+ }
38
+ ],
39
+ "exports": [
40
+ {
41
+ "kind": "js",
42
+ "name": "createReactRenderer",
43
+ "declaration": {
44
+ "name": "createReactRenderer",
45
+ "module": "src/create-react-renderer.ts"
46
+ }
47
+ },
48
+ {
49
+ "kind": "custom-element-definition",
50
+ "declaration": {
51
+ "name": "WebComponent",
52
+ "module": "src/create-react-renderer.ts"
53
+ }
54
+ }
55
+ ]
56
+ },
5
57
  {
6
58
  "kind": "javascript-module",
7
59
  "path": "src/index.ts",
@@ -22,6 +74,22 @@
22
74
  "name": "reactFactoryWithProvider",
23
75
  "module": "./react-layout-factory"
24
76
  }
77
+ },
78
+ {
79
+ "kind": "js",
80
+ "name": "createReactRenderer",
81
+ "declaration": {
82
+ "name": "createReactRenderer",
83
+ "module": "./create-react-renderer"
84
+ }
85
+ },
86
+ {
87
+ "kind": "js",
88
+ "name": "RendererControlProps",
89
+ "declaration": {
90
+ "name": "RendererControlProps",
91
+ "package": "@genesislcap/foundation-forms"
92
+ }
25
93
  }
26
94
  ]
27
95
  }
@@ -0,0 +1,84 @@
1
+ import type { RendererControlProps, RendererEntry } from '@genesislcap/foundation-forms';
2
+ import { type JsonFormsState, type OwnPropsOfControl, type RankedTester, type StatePropsOfControl } from '@jsonforms/core';
3
+ import type { ComponentType } from 'react';
4
+ /**
5
+ * Converts a React component into a `RendererEntry` for use with `foundation-form`'s
6
+ * `additionalRenderers` property.
7
+ *
8
+ * The component is registered as a custom element via `r2wc` and wrapped in a FAST
9
+ * `html` template so it integrates with the JSON Forms dispatch renderer.
10
+ *
11
+ * **Important — use platform web components for inputs, not native HTML elements.**
12
+ * The React component runs inside an `r2wc` custom element inside a FAST template,
13
+ * creating a three-layer shadow DOM boundary. React's synthetic event system does not
14
+ * cross shadow boundaries, so `onChange` on a plain `<input>` will never fire.
15
+ * Use `rapid-number-field`, `rapid-text-field`, etc. and attach listeners via
16
+ * `addEventListener` in a `useEffect` instead.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * import { useEffect, useRef } from 'react';
21
+ * import { createReactRenderer, type RendererControlProps } from '@genesislcap/foundation-react-utils';
22
+ * import { isNumberControl, rankWith } from '@jsonforms/core';
23
+ *
24
+ * function PriceRenderer({ data, path, enabled, handleChange }: RendererControlProps) {
25
+ * const ref = useRef<HTMLElement>(null);
26
+ *
27
+ * useEffect(() => {
28
+ * const el = ref.current as any;
29
+ * if (!el) return;
30
+ * const onChange = (e: CustomEvent) => {
31
+ * const val = (e.target as any).value;
32
+ * handleChange(path, val === '' ? undefined : Number(val));
33
+ * };
34
+ * el.addEventListener('change', onChange);
35
+ * return () => el.removeEventListener('change', onChange);
36
+ * }, [path, handleChange]);
37
+ *
38
+ * return <rapid-number-field ref={ref} value={data ?? ''} disabled={!enabled || undefined} />;
39
+ * }
40
+ *
41
+ * export const priceRendererEntry = createReactRenderer(PriceRenderer, {
42
+ * name: 'my-price-renderer',
43
+ * tester: rankWith(6, isNumberControl),
44
+ * });
45
+ *
46
+ * // On the form element:
47
+ * // form.additionalRenderers = [priceRendererEntry];
48
+ * ```
49
+ *
50
+ * @public
51
+ */
52
+ export declare function createReactRenderer(Component: ComponentType<RendererControlProps>, options: {
53
+ /**
54
+ * The custom element tag name to register. Must be unique and contain a hyphen.
55
+ * e.g. 'my-price-renderer'
56
+ */
57
+ name: string;
58
+ /**
59
+ * RankedTester from @jsonforms/core — determines when this renderer applies.
60
+ * Use rankWith(rank, tester) — rank 5+ recommended to override built-ins.
61
+ */
62
+ tester: RankedTester;
63
+ /**
64
+ * Custom mapper if you need to transform state differently.
65
+ * Defaults to mapStateToControlProps from @jsonforms/core.
66
+ */
67
+ mapper?: (state: JsonFormsState, ownProps: OwnPropsOfControl) => StatePropsOfControl;
68
+ /**
69
+ * Whether to wrap the React component in the platform's <control-wrapper>,
70
+ * which provides consistent label rendering, error display, and accessibility.
71
+ * Defaults to true. Set to false if your React component handles its own labels/errors.
72
+ *
73
+ * Note: if your component has layout conflicts with control-wrapper's internal flex styles
74
+ * (e.g. a block-level combobox that shrinks to content width), prefer keeping this true
75
+ * and overriding via the exposed CSS part instead:
76
+ *
77
+ * control-wrapper::part(wrapper) { display: block; }
78
+ *
79
+ * This avoids losing built-in label/error/accessibility while still allowing external styling.
80
+ * The `part="wrapper"` is exposed on the inner slot-hosting div of <control-wrapper>.
81
+ */
82
+ wrapWithControlWrapper?: boolean;
83
+ }): RendererEntry;
84
+ //# sourceMappingURL=create-react-renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create-react-renderer.d.ts","sourceRoot":"","sources":["../../src/create-react-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAEzF,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACzB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAC;AAE3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,aAAa,CAAC,oBAAoB,CAAC,EAC9C,OAAO,EAAE;IACP;;;OAGG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,MAAM,EAAE,YAAY,CAAC;IACrB;;;OAGG;IACH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,iBAAiB,KAAK,mBAAmB,CAAC;IACrF;;;;;;;;;;;;;OAaG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC,GACA,aAAa,CAmDf"}
@@ -7,4 +7,6 @@
7
7
  * Exports will be added here as utilities are developed.
8
8
  */
9
9
  export { reactFactory, reactFactoryWithProvider } from './react-layout-factory';
10
+ export { createReactRenderer } from './create-react-renderer';
11
+ export type { RendererControlProps } from '@genesislcap/foundation-forms';
10
12
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,YAAY,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,YAAY,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAChF,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,YAAY,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC"}
@@ -0,0 +1,99 @@
1
+ import { html } from '@genesislcap/web-core';
2
+ import { mapStateToControlProps, } from '@jsonforms/core';
3
+ import r2wc from '@r2wc/react-to-web-component';
4
+ /**
5
+ * Converts a React component into a `RendererEntry` for use with `foundation-form`'s
6
+ * `additionalRenderers` property.
7
+ *
8
+ * The component is registered as a custom element via `r2wc` and wrapped in a FAST
9
+ * `html` template so it integrates with the JSON Forms dispatch renderer.
10
+ *
11
+ * **Important — use platform web components for inputs, not native HTML elements.**
12
+ * The React component runs inside an `r2wc` custom element inside a FAST template,
13
+ * creating a three-layer shadow DOM boundary. React's synthetic event system does not
14
+ * cross shadow boundaries, so `onChange` on a plain `<input>` will never fire.
15
+ * Use `rapid-number-field`, `rapid-text-field`, etc. and attach listeners via
16
+ * `addEventListener` in a `useEffect` instead.
17
+ *
18
+ * @example
19
+ * ```tsx
20
+ * import { useEffect, useRef } from 'react';
21
+ * import { createReactRenderer, type RendererControlProps } from '@genesislcap/foundation-react-utils';
22
+ * import { isNumberControl, rankWith } from '@jsonforms/core';
23
+ *
24
+ * function PriceRenderer({ data, path, enabled, handleChange }: RendererControlProps) {
25
+ * const ref = useRef<HTMLElement>(null);
26
+ *
27
+ * useEffect(() => {
28
+ * const el = ref.current as any;
29
+ * if (!el) return;
30
+ * const onChange = (e: CustomEvent) => {
31
+ * const val = (e.target as any).value;
32
+ * handleChange(path, val === '' ? undefined : Number(val));
33
+ * };
34
+ * el.addEventListener('change', onChange);
35
+ * return () => el.removeEventListener('change', onChange);
36
+ * }, [path, handleChange]);
37
+ *
38
+ * return <rapid-number-field ref={ref} value={data ?? ''} disabled={!enabled || undefined} />;
39
+ * }
40
+ *
41
+ * export const priceRendererEntry = createReactRenderer(PriceRenderer, {
42
+ * name: 'my-price-renderer',
43
+ * tester: rankWith(6, isNumberControl),
44
+ * });
45
+ *
46
+ * // On the form element:
47
+ * // form.additionalRenderers = [priceRendererEntry];
48
+ * ```
49
+ *
50
+ * @public
51
+ */
52
+ export function createReactRenderer(Component, options) {
53
+ var _a;
54
+ const WebComponent = r2wc(Component, {
55
+ props: {
56
+ data: 'json',
57
+ path: 'string',
58
+ label: 'string',
59
+ errors: 'string',
60
+ enabled: 'boolean',
61
+ required: 'boolean',
62
+ handleChange: 'function',
63
+ uischema: 'json',
64
+ },
65
+ });
66
+ if (!customElements.get(options.name)) {
67
+ customElements.define(options.name, WebComponent);
68
+ }
69
+ const { name } = options;
70
+ const useWrapper = options.wrapWithControlWrapper !== false;
71
+ const innerTemplate = (elementName) => html `
72
+ <${elementName}
73
+ :data=${(x) => x.control.data}
74
+ :path=${(x) => x.control.path}
75
+ :label=${(x) => x.control.label}
76
+ :errors=${(x) => x.control.errors}
77
+ :enabled=${(x) => x.control.enabled}
78
+ :required=${(x) => x.control.required}
79
+ :handleChange=${(x) => x.control.handleChange}
80
+ :uischema=${(x) => x.control.uischema}
81
+ ></${elementName}>
82
+ `;
83
+ const template = useWrapper
84
+ ? html `
85
+ <template>
86
+ <control-wrapper :control=${(x) => x.control} :json-forms=${(x) => x.jsonforms}>
87
+ ${innerTemplate(name)}
88
+ </control-wrapper>
89
+ </template>
90
+ `
91
+ : html `
92
+ <template>${innerTemplate(name)}</template>
93
+ `;
94
+ return {
95
+ renderer: template,
96
+ tester: options.tester,
97
+ mapper: (_a = options.mapper) !== null && _a !== void 0 ? _a : mapStateToControlProps,
98
+ };
99
+ }
package/dist/esm/index.js CHANGED
@@ -7,3 +7,4 @@
7
7
  * Exports will be added here as utilities are developed.
8
8
  */
9
9
  export { reactFactory, reactFactoryWithProvider } from './react-layout-factory';
10
+ export { createReactRenderer } from './create-react-renderer';
@@ -172,6 +172,118 @@
172
172
  "name": "",
173
173
  "preserveMemberOrder": false,
174
174
  "members": [
175
+ {
176
+ "kind": "Function",
177
+ "canonicalReference": "@genesislcap/foundation-react-utils!createReactRenderer:function(1)",
178
+ "docComment": "/**\n * Converts a React component into a `RendererEntry` for use with `foundation-form`'s `additionalRenderers` property.\n *\n * The component is registered as a custom element via `r2wc` and wrapped in a FAST `html` template so it integrates with the JSON Forms dispatch renderer.\n *\n * **Important — use platform web components for inputs, not native HTML elements.** The React component runs inside an `r2wc` custom element inside a FAST template, creating a three-layer shadow DOM boundary. React's synthetic event system does not cross shadow boundaries, so `onChange` on a plain `<input>` will never fire. Use `rapid-number-field`, `rapid-text-field`, etc. and attach listeners via `addEventListener` in a `useEffect` instead.\n *\n * @example\n * ```tsx\n * import { useEffect, useRef } from 'react';\n * import { createReactRenderer, type RendererControlProps } from '@genesislcap/foundation-react-utils';\n * import { isNumberControl, rankWith } from '@jsonforms/core';\n *\n * function PriceRenderer({ data, path, enabled, handleChange }: RendererControlProps) {\n * const ref = useRef<HTMLElement>(null);\n *\n * useEffect(() => {\n * const el = ref.current as any;\n * if (!el) return;\n * const onChange = (e: CustomEvent) => {\n * const val = (e.target as any).value;\n * handleChange(path, val === '' ? undefined : Number(val));\n * };\n * el.addEventListener('change', onChange);\n * return () => el.removeEventListener('change', onChange);\n * }, [path, handleChange]);\n *\n * return <rapid-number-field ref={ref} value={data ?? ''} disabled={!enabled || undefined} />;\n * }\n *\n * export const priceRendererEntry = createReactRenderer(PriceRenderer, {\n * name: 'my-price-renderer',\n * tester: rankWith(6, isNumberControl),\n * });\n *\n * // On the form element:\n * // form.additionalRenderers = [priceRendererEntry];\n * ```\n *\n * @public\n */\n",
179
+ "excerptTokens": [
180
+ {
181
+ "kind": "Content",
182
+ "text": "export declare function createReactRenderer(Component: "
183
+ },
184
+ {
185
+ "kind": "Reference",
186
+ "text": "ComponentType",
187
+ "canonicalReference": "@types/react!React.ComponentType:type"
188
+ },
189
+ {
190
+ "kind": "Content",
191
+ "text": "<"
192
+ },
193
+ {
194
+ "kind": "Reference",
195
+ "text": "RendererControlProps",
196
+ "canonicalReference": "@genesislcap/foundation-forms!RendererControlProps:interface"
197
+ },
198
+ {
199
+ "kind": "Content",
200
+ "text": ">"
201
+ },
202
+ {
203
+ "kind": "Content",
204
+ "text": ", options: "
205
+ },
206
+ {
207
+ "kind": "Content",
208
+ "text": "{\n name: string;\n tester: "
209
+ },
210
+ {
211
+ "kind": "Reference",
212
+ "text": "RankedTester",
213
+ "canonicalReference": "@jsonforms/core!RankedTester:type"
214
+ },
215
+ {
216
+ "kind": "Content",
217
+ "text": ";\n mapper?: (state: "
218
+ },
219
+ {
220
+ "kind": "Reference",
221
+ "text": "JsonFormsState",
222
+ "canonicalReference": "@jsonforms/core!JsonFormsState:interface"
223
+ },
224
+ {
225
+ "kind": "Content",
226
+ "text": ", ownProps: "
227
+ },
228
+ {
229
+ "kind": "Reference",
230
+ "text": "OwnPropsOfControl",
231
+ "canonicalReference": "@jsonforms/core!OwnPropsOfControl:interface"
232
+ },
233
+ {
234
+ "kind": "Content",
235
+ "text": ") => "
236
+ },
237
+ {
238
+ "kind": "Reference",
239
+ "text": "StatePropsOfControl",
240
+ "canonicalReference": "@jsonforms/core!StatePropsOfControl:interface"
241
+ },
242
+ {
243
+ "kind": "Content",
244
+ "text": ";\n wrapWithControlWrapper?: boolean;\n}"
245
+ },
246
+ {
247
+ "kind": "Content",
248
+ "text": "): "
249
+ },
250
+ {
251
+ "kind": "Reference",
252
+ "text": "RendererEntry",
253
+ "canonicalReference": "@genesislcap/foundation-forms!RendererEntry:type"
254
+ },
255
+ {
256
+ "kind": "Content",
257
+ "text": ";"
258
+ }
259
+ ],
260
+ "fileUrlPath": "src/create-react-renderer.ts",
261
+ "returnTypeTokenRange": {
262
+ "startIndex": 16,
263
+ "endIndex": 17
264
+ },
265
+ "releaseTag": "Public",
266
+ "overloadIndex": 1,
267
+ "parameters": [
268
+ {
269
+ "parameterName": "Component",
270
+ "parameterTypeTokenRange": {
271
+ "startIndex": 1,
272
+ "endIndex": 5
273
+ },
274
+ "isOptional": false
275
+ },
276
+ {
277
+ "parameterName": "options",
278
+ "parameterTypeTokenRange": {
279
+ "startIndex": 6,
280
+ "endIndex": 15
281
+ },
282
+ "isOptional": false
283
+ }
284
+ ],
285
+ "name": "createReactRenderer"
286
+ },
175
287
  {
176
288
  "kind": "Function",
177
289
  "canonicalReference": "@genesislcap/foundation-react-utils!reactFactory:function(1)",
@@ -1,5 +1,93 @@
1
1
  import type { ComponentFactory } from '@genesislcap/foundation-layout';
2
+ import type { ComponentType } from 'react';
3
+ import { JsonFormsState } from '@jsonforms/core';
4
+ import { OwnPropsOfControl } from '@jsonforms/core';
5
+ import { RankedTester } from '@jsonforms/core';
2
6
  import * as React_2 from 'react';
7
+ import { RendererControlProps } from '@genesislcap/foundation-forms';
8
+ import type { RendererEntry } from '@genesislcap/foundation-forms';
9
+ import { StatePropsOfControl } from '@jsonforms/core';
10
+
11
+ /**
12
+ * Converts a React component into a `RendererEntry` for use with `foundation-form`'s
13
+ * `additionalRenderers` property.
14
+ *
15
+ * The component is registered as a custom element via `r2wc` and wrapped in a FAST
16
+ * `html` template so it integrates with the JSON Forms dispatch renderer.
17
+ *
18
+ * **Important — use platform web components for inputs, not native HTML elements.**
19
+ * The React component runs inside an `r2wc` custom element inside a FAST template,
20
+ * creating a three-layer shadow DOM boundary. React's synthetic event system does not
21
+ * cross shadow boundaries, so `onChange` on a plain `<input>` will never fire.
22
+ * Use `rapid-number-field`, `rapid-text-field`, etc. and attach listeners via
23
+ * `addEventListener` in a `useEffect` instead.
24
+ *
25
+ * @example
26
+ * ```tsx
27
+ * import { useEffect, useRef } from 'react';
28
+ * import { createReactRenderer, type RendererControlProps } from '@genesislcap/foundation-react-utils';
29
+ * import { isNumberControl, rankWith } from '@jsonforms/core';
30
+ *
31
+ * function PriceRenderer({ data, path, enabled, handleChange }: RendererControlProps) {
32
+ * const ref = useRef<HTMLElement>(null);
33
+ *
34
+ * useEffect(() => {
35
+ * const el = ref.current as any;
36
+ * if (!el) return;
37
+ * const onChange = (e: CustomEvent) => {
38
+ * const val = (e.target as any).value;
39
+ * handleChange(path, val === '' ? undefined : Number(val));
40
+ * };
41
+ * el.addEventListener('change', onChange);
42
+ * return () => el.removeEventListener('change', onChange);
43
+ * }, [path, handleChange]);
44
+ *
45
+ * return <rapid-number-field ref={ref} value={data ?? ''} disabled={!enabled || undefined} />;
46
+ * }
47
+ *
48
+ * export const priceRendererEntry = createReactRenderer(PriceRenderer, {
49
+ * name: 'my-price-renderer',
50
+ * tester: rankWith(6, isNumberControl),
51
+ * });
52
+ *
53
+ * // On the form element:
54
+ * // form.additionalRenderers = [priceRendererEntry];
55
+ * ```
56
+ *
57
+ * @public
58
+ */
59
+ export declare function createReactRenderer(Component: ComponentType<RendererControlProps>, options: {
60
+ /**
61
+ * The custom element tag name to register. Must be unique and contain a hyphen.
62
+ * e.g. 'my-price-renderer'
63
+ */
64
+ name: string;
65
+ /**
66
+ * RankedTester from @jsonforms/core — determines when this renderer applies.
67
+ * Use rankWith(rank, tester) — rank 5+ recommended to override built-ins.
68
+ */
69
+ tester: RankedTester;
70
+ /**
71
+ * Custom mapper if you need to transform state differently.
72
+ * Defaults to mapStateToControlProps from @jsonforms/core.
73
+ */
74
+ mapper?: (state: JsonFormsState, ownProps: OwnPropsOfControl) => StatePropsOfControl;
75
+ /**
76
+ * Whether to wrap the React component in the platform's <control-wrapper>,
77
+ * which provides consistent label rendering, error display, and accessibility.
78
+ * Defaults to true. Set to false if your React component handles its own labels/errors.
79
+ *
80
+ * Note: if your component has layout conflicts with control-wrapper's internal flex styles
81
+ * (e.g. a block-level combobox that shrinks to content width), prefer keeping this true
82
+ * and overriding via the exposed CSS part instead:
83
+ *
84
+ * control-wrapper::part(wrapper) { display: block; }
85
+ *
86
+ * This avoids losing built-in label/error/accessibility while still allowing external styling.
87
+ * The `part="wrapper"` is exposed on the inner slot-hosting div of <control-wrapper>.
88
+ */
89
+ wrapWithControlWrapper?: boolean;
90
+ }): RendererEntry;
3
91
 
4
92
  /**
5
93
  * Creates a factory function for rendering React components in layout items.
@@ -49,4 +137,6 @@ export declare function reactFactoryWithProvider<CP = {}, WP = {}>(Component: Re
49
137
  children: React_2.ReactNode;
50
138
  }>, wrapperProps: WP, componentProps?: CP): ComponentFactory;
51
139
 
140
+ export { RendererControlProps }
141
+
52
142
  export { }
@@ -1 +1 @@
1
- {"root":["../src/index.ts","../src/react-layout-factory.tsx"],"version":"5.9.2"}
1
+ {"root":["../src/create-react-renderer.ts","../src/index.ts","../src/react-layout-factory.tsx"],"version":"5.9.2"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@genesislcap/foundation-react-utils",
3
3
  "description": "Genesis Foundation React Utils",
4
- "version": "14.416.1-alpha-c3adbe7.0",
4
+ "version": "14.417.0-FUI-0-react-renderers.1",
5
5
  "sideEffects": false,
6
6
  "license": "SEE LICENSE IN license.txt",
7
7
  "main": "dist/esm/index.js",
@@ -29,21 +29,25 @@
29
29
  }
30
30
  },
31
31
  "devDependencies": {
32
- "@genesislcap/foundation-testing": "14.416.1-alpha-c3adbe7.0",
33
- "@genesislcap/genx": "14.416.1-alpha-c3adbe7.0",
34
- "@genesislcap/rollup-builder": "14.416.1-alpha-c3adbe7.0",
35
- "@genesislcap/ts-builder": "14.416.1-alpha-c3adbe7.0",
36
- "@genesislcap/uvu-playwright-builder": "14.416.1-alpha-c3adbe7.0",
37
- "@genesislcap/vite-builder": "14.416.1-alpha-c3adbe7.0",
38
- "@genesislcap/webpack-builder": "14.416.1-alpha-c3adbe7.0"
32
+ "@genesislcap/foundation-testing": "14.417.0-FUI-0-react-renderers.1",
33
+ "@genesislcap/genx": "14.417.0-FUI-0-react-renderers.1",
34
+ "@genesislcap/rollup-builder": "14.417.0-FUI-0-react-renderers.1",
35
+ "@genesislcap/ts-builder": "14.417.0-FUI-0-react-renderers.1",
36
+ "@genesislcap/uvu-playwright-builder": "14.417.0-FUI-0-react-renderers.1",
37
+ "@genesislcap/vite-builder": "14.417.0-FUI-0-react-renderers.1",
38
+ "@genesislcap/webpack-builder": "14.417.0-FUI-0-react-renderers.1"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "react": "^19.0.0",
42
42
  "react-dom": "^19.0.0"
43
43
  },
44
44
  "dependencies": {
45
- "@genesislcap/foundation-layout": "14.416.1-alpha-c3adbe7.0",
46
- "@genesislcap/foundation-logger": "14.416.1-alpha-c3adbe7.0"
45
+ "@genesislcap/foundation-forms": "14.417.0-FUI-0-react-renderers.1",
46
+ "@genesislcap/foundation-layout": "14.417.0-FUI-0-react-renderers.1",
47
+ "@genesislcap/foundation-logger": "14.417.0-FUI-0-react-renderers.1",
48
+ "@genesislcap/web-core": "14.417.0-FUI-0-react-renderers.1",
49
+ "@jsonforms/core": "^3.2.1",
50
+ "@r2wc/react-to-web-component": "^2.0.2"
47
51
  },
48
52
  "repository": {
49
53
  "type": "git",
@@ -54,5 +58,5 @@
54
58
  "access": "public"
55
59
  },
56
60
  "customElements": "dist/custom-elements.json",
57
- "gitHead": "b642cca2132b2e27c94aa89821d267a8bce6a180"
61
+ "gitHead": "41516c5593cc64a42b33da71ea45e3e3a8beab88"
58
62
  }
@@ -0,0 +1,146 @@
1
+ import type { RendererControlProps, RendererEntry } from '@genesislcap/foundation-forms';
2
+ import { html } from '@genesislcap/web-core';
3
+ import {
4
+ mapStateToControlProps,
5
+ type JsonFormsState,
6
+ type OwnPropsOfControl,
7
+ type RankedTester,
8
+ type StatePropsOfControl,
9
+ } from '@jsonforms/core';
10
+ import r2wc from '@r2wc/react-to-web-component';
11
+ import type { ComponentType } from 'react';
12
+
13
+ /**
14
+ * Converts a React component into a `RendererEntry` for use with `foundation-form`'s
15
+ * `additionalRenderers` property.
16
+ *
17
+ * The component is registered as a custom element via `r2wc` and wrapped in a FAST
18
+ * `html` template so it integrates with the JSON Forms dispatch renderer.
19
+ *
20
+ * **Important — use platform web components for inputs, not native HTML elements.**
21
+ * The React component runs inside an `r2wc` custom element inside a FAST template,
22
+ * creating a three-layer shadow DOM boundary. React's synthetic event system does not
23
+ * cross shadow boundaries, so `onChange` on a plain `<input>` will never fire.
24
+ * Use `rapid-number-field`, `rapid-text-field`, etc. and attach listeners via
25
+ * `addEventListener` in a `useEffect` instead.
26
+ *
27
+ * @example
28
+ * ```tsx
29
+ * import { useEffect, useRef } from 'react';
30
+ * import { createReactRenderer, type RendererControlProps } from '@genesislcap/foundation-react-utils';
31
+ * import { isNumberControl, rankWith } from '@jsonforms/core';
32
+ *
33
+ * function PriceRenderer({ data, path, enabled, handleChange }: RendererControlProps) {
34
+ * const ref = useRef<HTMLElement>(null);
35
+ *
36
+ * useEffect(() => {
37
+ * const el = ref.current as any;
38
+ * if (!el) return;
39
+ * const onChange = (e: CustomEvent) => {
40
+ * const val = (e.target as any).value;
41
+ * handleChange(path, val === '' ? undefined : Number(val));
42
+ * };
43
+ * el.addEventListener('change', onChange);
44
+ * return () => el.removeEventListener('change', onChange);
45
+ * }, [path, handleChange]);
46
+ *
47
+ * return <rapid-number-field ref={ref} value={data ?? ''} disabled={!enabled || undefined} />;
48
+ * }
49
+ *
50
+ * export const priceRendererEntry = createReactRenderer(PriceRenderer, {
51
+ * name: 'my-price-renderer',
52
+ * tester: rankWith(6, isNumberControl),
53
+ * });
54
+ *
55
+ * // On the form element:
56
+ * // form.additionalRenderers = [priceRendererEntry];
57
+ * ```
58
+ *
59
+ * @public
60
+ */
61
+ export function createReactRenderer(
62
+ Component: ComponentType<RendererControlProps>,
63
+ options: {
64
+ /**
65
+ * The custom element tag name to register. Must be unique and contain a hyphen.
66
+ * e.g. 'my-price-renderer'
67
+ */
68
+ name: string;
69
+ /**
70
+ * RankedTester from @jsonforms/core — determines when this renderer applies.
71
+ * Use rankWith(rank, tester) — rank 5+ recommended to override built-ins.
72
+ */
73
+ tester: RankedTester;
74
+ /**
75
+ * Custom mapper if you need to transform state differently.
76
+ * Defaults to mapStateToControlProps from @jsonforms/core.
77
+ */
78
+ mapper?: (state: JsonFormsState, ownProps: OwnPropsOfControl) => StatePropsOfControl;
79
+ /**
80
+ * Whether to wrap the React component in the platform's <control-wrapper>,
81
+ * which provides consistent label rendering, error display, and accessibility.
82
+ * Defaults to true. Set to false if your React component handles its own labels/errors.
83
+ *
84
+ * Note: if your component has layout conflicts with control-wrapper's internal flex styles
85
+ * (e.g. a block-level combobox that shrinks to content width), prefer keeping this true
86
+ * and overriding via the exposed CSS part instead:
87
+ *
88
+ * control-wrapper::part(wrapper) { display: block; }
89
+ *
90
+ * This avoids losing built-in label/error/accessibility while still allowing external styling.
91
+ * The `part="wrapper"` is exposed on the inner slot-hosting div of <control-wrapper>.
92
+ */
93
+ wrapWithControlWrapper?: boolean;
94
+ },
95
+ ): RendererEntry {
96
+ const WebComponent = r2wc(Component, {
97
+ props: {
98
+ data: 'json',
99
+ path: 'string',
100
+ label: 'string',
101
+ errors: 'string',
102
+ enabled: 'boolean',
103
+ required: 'boolean',
104
+ handleChange: 'function',
105
+ uischema: 'json',
106
+ },
107
+ });
108
+
109
+ if (!customElements.get(options.name)) {
110
+ customElements.define(options.name, WebComponent);
111
+ }
112
+
113
+ const { name } = options;
114
+ const useWrapper = options.wrapWithControlWrapper !== false;
115
+
116
+ const innerTemplate = (elementName: string) => html`
117
+ <${elementName}
118
+ :data=${(x: any) => x.control.data}
119
+ :path=${(x: any) => x.control.path}
120
+ :label=${(x: any) => x.control.label}
121
+ :errors=${(x: any) => x.control.errors}
122
+ :enabled=${(x: any) => x.control.enabled}
123
+ :required=${(x: any) => x.control.required}
124
+ :handleChange=${(x: any) => x.control.handleChange}
125
+ :uischema=${(x: any) => x.control.uischema}
126
+ ></${elementName}>
127
+ `;
128
+
129
+ const template = useWrapper
130
+ ? html`
131
+ <template>
132
+ <control-wrapper :control=${(x: any) => x.control} :json-forms=${(x: any) => x.jsonforms}>
133
+ ${innerTemplate(name)}
134
+ </control-wrapper>
135
+ </template>
136
+ `
137
+ : html`
138
+ <template>${innerTemplate(name)}</template>
139
+ `;
140
+
141
+ return {
142
+ renderer: template,
143
+ tester: options.tester,
144
+ mapper: options.mapper ?? mapStateToControlProps,
145
+ };
146
+ }
package/src/index.ts CHANGED
@@ -8,3 +8,5 @@
8
8
  */
9
9
 
10
10
  export { reactFactory, reactFactoryWithProvider } from './react-layout-factory';
11
+ export { createReactRenderer } from './create-react-renderer';
12
+ export type { RendererControlProps } from '@genesislcap/foundation-forms';