@fluentui-react-native/framework-base 0.2.0 → 0.3.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.
Files changed (188) hide show
  1. package/CHANGELOG.json +16 -1
  2. package/CHANGELOG.md +43 -2
  3. package/README.md +18 -1
  4. package/babel.config.js +1 -1
  5. package/jsx-runtime.js +1 -0
  6. package/lib/component-patterns/directComponent.d.ts +7 -0
  7. package/lib/component-patterns/directComponent.d.ts.map +1 -0
  8. package/lib/component-patterns/directComponent.js +8 -0
  9. package/lib/component-patterns/directComponent.js.map +1 -0
  10. package/lib/component-patterns/extract.d.ts +22 -0
  11. package/lib/component-patterns/extract.d.ts.map +1 -0
  12. package/lib/component-patterns/extract.js +25 -0
  13. package/lib/component-patterns/extract.js.map +1 -0
  14. package/lib/component-patterns/phasedComponent.d.ts +18 -0
  15. package/lib/component-patterns/phasedComponent.d.ts.map +1 -0
  16. package/lib/component-patterns/phasedComponent.js +51 -0
  17. package/lib/component-patterns/phasedComponent.js.map +1 -0
  18. package/lib/component-patterns/render.d.ts +9 -3
  19. package/lib/component-patterns/render.d.ts.map +1 -1
  20. package/lib/component-patterns/render.js +39 -32
  21. package/lib/component-patterns/render.js.map +1 -1
  22. package/lib/component-patterns/render.types.d.ts +60 -34
  23. package/lib/component-patterns/render.types.d.ts.map +1 -1
  24. package/lib/component-patterns/render.types.js +1 -1
  25. package/lib/component-patterns/stagedComponent.d.ts +3 -8
  26. package/lib/component-patterns/stagedComponent.d.ts.map +1 -1
  27. package/lib/component-patterns/stagedComponent.js +10 -27
  28. package/lib/component-patterns/stagedComponent.js.map +1 -1
  29. package/lib/component-patterns/withSlots.d.ts +6 -2
  30. package/lib/component-patterns/withSlots.d.ts.map +1 -1
  31. package/lib/component-patterns/withSlots.js +3 -3
  32. package/lib/component-patterns/withSlots.js.map +1 -1
  33. package/lib/immutable-merge/Merge.d.ts +6 -3
  34. package/lib/immutable-merge/Merge.js +79 -83
  35. package/lib/immutable-merge/Merge.js.map +1 -1
  36. package/lib/immutable-merge/Merge.test.d.ts +1 -1
  37. package/lib/immutable-merge/Merge.test.js +231 -219
  38. package/lib/immutable-merge/Merge.test.js.map +1 -1
  39. package/lib/index.d.ts +33 -5
  40. package/lib/index.d.ts.map +1 -1
  41. package/lib/index.js +13 -4
  42. package/lib/index.js.map +1 -1
  43. package/lib/jsx-namespace.d.ts +65 -0
  44. package/lib/jsx-namespace.d.ts.map +1 -0
  45. package/lib/jsx-namespace.js +2 -0
  46. package/lib/jsx-namespace.js.map +1 -0
  47. package/lib/jsx-runtime.d.ts +6 -4
  48. package/lib/jsx-runtime.d.ts.map +1 -1
  49. package/lib/jsx-runtime.js +9 -7
  50. package/lib/jsx-runtime.js.map +1 -1
  51. package/lib/memo-cache/getCacheEntry.d.ts +13 -13
  52. package/lib/memo-cache/getCacheEntry.js +20 -22
  53. package/lib/memo-cache/getCacheEntry.js.map +1 -1
  54. package/lib/memo-cache/getCacheEntry.test.d.ts +1 -1
  55. package/lib/memo-cache/getCacheEntry.test.js +90 -90
  56. package/lib/memo-cache/getCacheEntry.test.js.map +1 -1
  57. package/lib/memo-cache/getMemoCache.d.ts +1 -1
  58. package/lib/memo-cache/getMemoCache.js +11 -11
  59. package/lib/memo-cache/getMemoCache.js.map +1 -1
  60. package/lib/memo-cache/getMemoCache.test.d.ts +1 -1
  61. package/lib/memo-cache/getMemoCache.test.js +73 -73
  62. package/lib/memo-cache/getMemoCache.test.js.map +1 -1
  63. package/lib/memo-cache/memoize.d.ts +1 -1
  64. package/lib/memo-cache/memoize.js +9 -9
  65. package/lib/memo-cache/memoize.js.map +1 -1
  66. package/lib/memo-cache/memoize.test.d.ts +1 -1
  67. package/lib/memo-cache/memoize.test.js +36 -38
  68. package/lib/memo-cache/memoize.test.js.map +1 -1
  69. package/lib/merge-props/index.d.ts +1 -1
  70. package/lib/merge-props/index.js +1 -1
  71. package/lib/merge-props/mergeProps.d.ts +1 -1
  72. package/lib/merge-props/mergeProps.js +4 -4
  73. package/lib/merge-props/mergeProps.js.map +1 -1
  74. package/lib/merge-props/mergeStyles.d.ts +33 -4
  75. package/lib/merge-props/mergeStyles.d.ts.map +1 -1
  76. package/lib/merge-props/mergeStyles.js +16 -17
  77. package/lib/merge-props/mergeStyles.js.map +1 -1
  78. package/lib/merge-props/mergeStyles.test.d.ts +1 -1
  79. package/lib/merge-props/mergeStyles.test.js +75 -75
  80. package/lib/merge-props/mergeStyles.test.js.map +1 -1
  81. package/lib/merge-props/mergeStyles.types.d.ts +4 -4
  82. package/lib/merge-props/mergeStyles.types.d.ts.map +1 -1
  83. package/lib/merge-props/mergeStyles.types.js +1 -1
  84. package/lib/utilities/filterProps.d.ts +3 -0
  85. package/lib/utilities/filterProps.d.ts.map +1 -0
  86. package/lib/utilities/filterProps.js +12 -0
  87. package/lib/utilities/filterProps.js.map +1 -0
  88. package/lib-commonjs/component-patterns/directComponent.d.ts +7 -0
  89. package/lib-commonjs/component-patterns/directComponent.d.ts.map +1 -0
  90. package/lib-commonjs/component-patterns/directComponent.js +11 -0
  91. package/lib-commonjs/component-patterns/directComponent.js.map +1 -0
  92. package/lib-commonjs/component-patterns/extract.d.ts +22 -0
  93. package/lib-commonjs/component-patterns/extract.d.ts.map +1 -0
  94. package/lib-commonjs/component-patterns/extract.js +30 -0
  95. package/lib-commonjs/component-patterns/extract.js.map +1 -0
  96. package/lib-commonjs/component-patterns/phasedComponent.d.ts +18 -0
  97. package/lib-commonjs/component-patterns/phasedComponent.d.ts.map +1 -0
  98. package/lib-commonjs/component-patterns/phasedComponent.js +60 -0
  99. package/lib-commonjs/component-patterns/phasedComponent.js.map +1 -0
  100. package/lib-commonjs/component-patterns/render.d.ts +9 -3
  101. package/lib-commonjs/component-patterns/render.d.ts.map +1 -1
  102. package/lib-commonjs/component-patterns/render.js +101 -39
  103. package/lib-commonjs/component-patterns/render.js.map +1 -1
  104. package/lib-commonjs/component-patterns/render.types.d.ts +60 -34
  105. package/lib-commonjs/component-patterns/render.types.d.ts.map +1 -1
  106. package/lib-commonjs/component-patterns/render.types.js +3 -3
  107. package/lib-commonjs/component-patterns/stagedComponent.d.ts +3 -8
  108. package/lib-commonjs/component-patterns/stagedComponent.d.ts.map +1 -1
  109. package/lib-commonjs/component-patterns/stagedComponent.js +64 -34
  110. package/lib-commonjs/component-patterns/stagedComponent.js.map +1 -1
  111. package/lib-commonjs/component-patterns/withSlots.d.ts +6 -2
  112. package/lib-commonjs/component-patterns/withSlots.d.ts.map +1 -1
  113. package/lib-commonjs/component-patterns/withSlots.js +7 -8
  114. package/lib-commonjs/component-patterns/withSlots.js.map +1 -1
  115. package/lib-commonjs/immutable-merge/Merge.d.ts +6 -3
  116. package/lib-commonjs/immutable-merge/Merge.js +85 -90
  117. package/lib-commonjs/immutable-merge/Merge.js.map +1 -1
  118. package/lib-commonjs/immutable-merge/Merge.test.d.ts +1 -1
  119. package/lib-commonjs/immutable-merge/Merge.test.js +234 -222
  120. package/lib-commonjs/immutable-merge/Merge.test.js.map +1 -1
  121. package/lib-commonjs/index.d.ts +33 -5
  122. package/lib-commonjs/index.d.ts.map +1 -1
  123. package/lib-commonjs/index.js +184 -31
  124. package/lib-commonjs/index.js.map +1 -1
  125. package/lib-commonjs/jsx-namespace.d.ts +65 -0
  126. package/lib-commonjs/jsx-namespace.d.ts.map +1 -0
  127. package/lib-commonjs/jsx-namespace.js +3 -0
  128. package/lib-commonjs/jsx-namespace.js.map +1 -0
  129. package/lib-commonjs/jsx-runtime.d.ts +6 -4
  130. package/lib-commonjs/jsx-runtime.d.ts.map +1 -1
  131. package/lib-commonjs/jsx-runtime.js +70 -13
  132. package/lib-commonjs/jsx-runtime.js.map +1 -1
  133. package/lib-commonjs/memo-cache/getCacheEntry.d.ts +13 -13
  134. package/lib-commonjs/memo-cache/getCacheEntry.js +23 -26
  135. package/lib-commonjs/memo-cache/getCacheEntry.js.map +1 -1
  136. package/lib-commonjs/memo-cache/getCacheEntry.test.d.ts +1 -1
  137. package/lib-commonjs/memo-cache/getCacheEntry.test.js +93 -93
  138. package/lib-commonjs/memo-cache/getCacheEntry.test.js.map +1 -1
  139. package/lib-commonjs/memo-cache/getMemoCache.d.ts +1 -1
  140. package/lib-commonjs/memo-cache/getMemoCache.js +16 -17
  141. package/lib-commonjs/memo-cache/getMemoCache.js.map +1 -1
  142. package/lib-commonjs/memo-cache/getMemoCache.test.d.ts +1 -1
  143. package/lib-commonjs/memo-cache/getMemoCache.test.js +76 -76
  144. package/lib-commonjs/memo-cache/getMemoCache.test.js.map +1 -1
  145. package/lib-commonjs/memo-cache/memoize.d.ts +1 -1
  146. package/lib-commonjs/memo-cache/memoize.js +13 -14
  147. package/lib-commonjs/memo-cache/memoize.js.map +1 -1
  148. package/lib-commonjs/memo-cache/memoize.test.d.ts +1 -1
  149. package/lib-commonjs/memo-cache/memoize.test.js +39 -41
  150. package/lib-commonjs/memo-cache/memoize.test.js.map +1 -1
  151. package/lib-commonjs/merge-props/index.d.ts +1 -1
  152. package/lib-commonjs/merge-props/index.js +17 -7
  153. package/lib-commonjs/merge-props/index.js.map +1 -1
  154. package/lib-commonjs/merge-props/mergeProps.d.ts +1 -1
  155. package/lib-commonjs/merge-props/mergeProps.js +9 -10
  156. package/lib-commonjs/merge-props/mergeProps.js.map +1 -1
  157. package/lib-commonjs/merge-props/mergeStyles.d.ts +33 -4
  158. package/lib-commonjs/merge-props/mergeStyles.d.ts.map +1 -1
  159. package/lib-commonjs/merge-props/mergeStyles.js +23 -25
  160. package/lib-commonjs/merge-props/mergeStyles.js.map +1 -1
  161. package/lib-commonjs/merge-props/mergeStyles.test.d.ts +1 -1
  162. package/lib-commonjs/merge-props/mergeStyles.test.js +78 -78
  163. package/lib-commonjs/merge-props/mergeStyles.test.js.map +1 -1
  164. package/lib-commonjs/merge-props/mergeStyles.types.d.ts +4 -4
  165. package/lib-commonjs/merge-props/mergeStyles.types.d.ts.map +1 -1
  166. package/lib-commonjs/merge-props/mergeStyles.types.js +3 -3
  167. package/lib-commonjs/utilities/filterProps.d.ts +3 -0
  168. package/lib-commonjs/utilities/filterProps.d.ts.map +1 -0
  169. package/lib-commonjs/utilities/filterProps.js +15 -0
  170. package/lib-commonjs/utilities/filterProps.js.map +1 -0
  171. package/package.json +48 -20
  172. package/src/component-patterns/README.md +53 -16
  173. package/src/component-patterns/directComponent.ts +9 -0
  174. package/src/component-patterns/extract.ts +32 -0
  175. package/src/component-patterns/phasedComponent.ts +54 -0
  176. package/src/component-patterns/render.ts +21 -13
  177. package/src/component-patterns/render.types.ts +55 -30
  178. package/src/component-patterns/stagedComponent.ts +24 -0
  179. package/src/immutable-merge/Merge.test.ts +5 -1
  180. package/src/index.ts +27 -7
  181. package/src/jsx-namespace.ts +83 -0
  182. package/src/jsx-runtime.ts +8 -4
  183. package/src/memo-cache/README.md +1 -1
  184. package/src/merge-props/mergeStyles.test.ts +3 -2
  185. package/src/merge-props/mergeStyles.ts +52 -4
  186. package/src/merge-props/mergeStyles.types.ts +2 -2
  187. package/src/utilities/filterProps.ts +14 -0
  188. package/src/component-patterns/stagedComponent.tsx +0 -45
@@ -2,38 +2,75 @@
2
2
 
3
3
  These are the base component patterns shared across the deprecated or v0 framework (found under packages/deprecated), and the newer framework (found under packages/framework). This also includes the custom JSX handlers required to render them properly.
4
4
 
5
- There are two main patterns exposed here: direct rendering and staged rendering.
5
+ There are two main patterns exposed here: direct rendering and phased rendering.
6
6
 
7
7
  ## Direct Rendering
8
8
 
9
- The direct rendering pattern allows a component to be called directly, rather than creating a new entry in the DOM.
9
+ The direct rendering pattern allows a component to be called directly, rather than creating a new entry in the render tree.
10
10
 
11
- As an example, if you want to create a wrapper around a component called `MyText` that has `italicize` as one of its props, that always wants to set that value to true. You could define:
11
+ As an example, if you want to create a wrapper around a component called `MyText` that has `italicize` as one of its props, that always wants to set that value to true, you could define:
12
12
 
13
13
  ```ts
14
14
  const MyNewText: React.FunctionComponent<MyTextProps> = (props) => {
15
- return <MyText {...props, italicize: true} />;
16
- }
15
+ return <MyText {...props} italicize={true} />;
16
+ };
17
17
  ```
18
18
 
19
- When this is rendered, there is an entry for `MyNewText` which contains a `MyText` (another entry), which might contains `Text` (for react-native usage). The direct rendering pattern is one where a component can denote that it is safe to be called directly as a function, instead operating as a prop transform that gets applied to the underlying component.
19
+ When this is rendered, there is an entry for `MyNewText` which contains a `MyText` (another entry), which might contain `Text` (for react-native usage). The direct rendering pattern is one where a component can denote that it is safe to be called directly as a function, instead operating as a prop transform that gets applied to the underlying component.
20
20
 
21
- - For the above to be safe, `MyNewText` should NOT use hooks. In the case of any conditional rendering logic this will break the rule of hooks.
21
+ - For the above to be safe, `MyNewText` should NOT use hooks. In the case of any conditional rendering logic this will break the rules of hooks.
22
22
 
23
23
  There are two types of implementations in this folder:
24
24
 
25
- - `DirectComponent` - a functional component that marks itself as direct with a `_callDirect: true` attached property. This will then be called as a normal function component, with children included as part of props.
26
- - `LegacyDirectComponent` - the pattern currently used in this library that should be moved away from. In this case `_canCompose: true` is set as an attached property, and the function component will be called with children split from props.
25
+ - `DirectComponent` - a functional component that marks itself as direct with a `_callDirect: true` attached property. This will then be called as a normal function component, with children included as part of props. Use the `directComponent()` helper to create these.
26
+ - `LegacyDirectComponent` - the pattern currently used in legacy code that should be moved away from. In this case `_canCompose: true` is set as an attached property, and the function component will be called with children split from props.
27
27
 
28
- The internal logic of the JSX rendering helpers will handle both patterns. In the case of the newer `DirectComponent` pattern, the component will still work, even without any jsx hooks, whereas the `LegacyDirectComponent` pattern will have a somewhat undefined behavior with regards to children.
28
+ The internal logic of the JSX rendering helpers (`renderForJsxRuntime` and `renderForClassicRuntime`) will handle both patterns. In the case of the newer `DirectComponent` pattern, the component will still work, even without any jsx hooks, whereas the `LegacyDirectComponent` pattern will have somewhat undefined behavior with regards to children.
29
29
 
30
- ## Staged Rendering
30
+ ### Example: Using directComponent
31
31
 
32
- The issue with the direct component pattern above, is that hooks are integral to writing functional components. The staged rendering pattern is designed to help with this. In this case a component is implemented in two stages, the prep stage where hooks are called, and the rendering stage where the tree is emitted.
32
+ ```ts
33
+ import { directComponent } from '@fluentui-react-native/framework-base';
34
+
35
+ const MyNewText = directComponent<MyTextProps>((props) => {
36
+ return <MyText {...props} italicize={true} />;
37
+ });
38
+ ```
39
+
40
+ ## Phased Rendering
41
+
42
+ The issue with the direct component pattern above is that hooks are integral to writing functional components. The phased rendering pattern is designed to help with this. In this case a component is implemented in two phases: the prep phase where hooks are called, and the rendering phase where the tree is emitted.
43
+
44
+ As above there is a newer and older version of the pattern:
45
+
46
+ - `PhasedComponent` - the newer version of the pattern, where the returned component function expects children as part of props. Create these using `phasedComponent()`. The attached property is `_phasedRender`.
47
+ - `ComposableFunction` (deprecated) - the older "staged" version, where children are split out and JSX hooks are required to render correctly. Create these using the deprecated `stagedComponent()`. The attached property is `_staged`.
48
+
49
+ Note that while the newer patterns work without any JSX hooks, the hooks will enable element flattening.
50
+
51
+ ### Example: Using phasedComponent
52
+
53
+ ```ts
54
+ import { phasedComponent } from '@fluentui-react-native/framework-base';
55
+
56
+ const MyComponent = phasedComponent<MyComponentProps>((props) => {
57
+ // Phase 1: Hooks and logic
58
+ const theme = useTheme();
59
+ const styles = useStyles(theme, props);
60
+
61
+ // Phase 2: Return a component that renders
62
+ return (innerProps) => {
63
+ return <View style={styles}>{innerProps.children}</View>;
64
+ };
65
+ });
66
+ ```
67
+
68
+ ## JSX Runtime
33
69
 
34
- As above there is a newer and older version of the pattern.
70
+ This package provides a custom JSX runtime (`@fluentui-react-native/framework-base/jsx-runtime`) that automatically handles both direct and phased rendering patterns. When you use the `@jsxImportSource @fluentui-react-native/framework-base` pragma, the custom runtime will:
35
71
 
36
- - `StagedComponent` - the newer version of the pattern, where the returned component function expects children as part of props.
37
- - `StagedRender` - the older version, where children are split out and JSX hooks are required to render correctly.
72
+ 1. Detect components marked with `_callDirect` or `_canCompose` and call them directly
73
+ 2. Handle the different children patterns (props vs. rest args)
74
+ 3. Fall back to standard React rendering for normal components
38
75
 
39
- Note that while the newer patterns work without any JSX hooks, the hooks will enable the element flattening.
76
+ This enables element flattening without requiring explicit calls to helper functions.
@@ -0,0 +1,9 @@
1
+ import type { FunctionComponent } from './render.types';
2
+
3
+ /**
4
+ * @param component functional component, usually a closure, to make into a direct component
5
+ * @return the same component with the direct component flag set, return type is a pure function component
6
+ */
7
+ export function directComponent<TProps>(component: FunctionComponent<TProps>): FunctionComponent<TProps> {
8
+ return Object.assign(component, { _callDirect: true });
9
+ }
@@ -0,0 +1,32 @@
1
+ import type { ViewStyle, TextStyle, ImageStyle } from 'react-native';
2
+
3
+ type StyleTypes = ViewStyle | TextStyle | ImageStyle;
4
+ type PropsWithStyle = { style?: ViewStyle | TextStyle | ImageStyle };
5
+ type PropsWithChildren = { children?: React.ReactNode };
6
+
7
+ /**
8
+ * Extract the props from a React element. If the element is undefined, undefined will be returned.
9
+ * @param element The React element from which to extract the props.
10
+ * @returns The extracted props or undefined if the element is undefined.
11
+ */
12
+ export function extractProps<T extends object>(element?: React.ReactElement): T | undefined {
13
+ return element ? (element.props as T) : undefined;
14
+ }
15
+
16
+ /**
17
+ * Extract the children from a React element. If the element is undefined, undefined will be returned.
18
+ * @param element The React element from which to extract the children.
19
+ * @returns The extracted children or undefined if the element is undefined.
20
+ */
21
+ export function extractChildren(element?: React.ReactElement): React.ReactNode | undefined {
22
+ return extractProps<PropsWithChildren>(element)?.children;
23
+ }
24
+
25
+ /**
26
+ * Extract a style object from a React element. If the element is undefined or does not have a style prop, an empty object will be returned.
27
+ * @param element The React element from which to extract the style.
28
+ * @returns The extracted style object or an empty object if not available.
29
+ */
30
+ export function extractStyle<T extends StyleTypes = ViewStyle>(element?: React.ReactElement): T {
31
+ return (extractProps<PropsWithStyle>(element)?.style as T) ?? ({} as T);
32
+ }
@@ -0,0 +1,54 @@
1
+ import React from 'react';
2
+ import type { ComposableFunction, PhasedComponent, PhasedRender, FunctionComponent } from './render.types';
3
+ import { renderForJsxRuntime } from './render';
4
+ import type { LegacyDirectComponent } from './render.types';
5
+
6
+ /**
7
+ * Extract the phased render function from a component, if it has one.
8
+ * Handles both the newer PhasedComponent pattern (_phasedRender) and the legacy
9
+ * ComposableFunction pattern (_staged) for backward compatibility.
10
+ *
11
+ * @param component - The component to extract the phased render from
12
+ * @returns The phased render function if present, undefined otherwise
13
+ */
14
+ export function getPhasedRender<TProps>(component: React.ComponentType<TProps>): PhasedRender<TProps> | undefined {
15
+ // only a function component can have a phased render
16
+ if (typeof component === 'function') {
17
+ // if this has a phased render function, return it
18
+ if ((component as PhasedComponent<TProps>)._phasedRender) {
19
+ return (component as PhasedComponent<TProps>)._phasedRender;
20
+ } else if ((component as ComposableFunction<TProps>)._staged) {
21
+ // for backward compatibility check for staged render and return a wrapper that maps the signature
22
+ const staged = (component as ComposableFunction<TProps>)._staged;
23
+ return (props: TProps) => {
24
+ const { children, ...rest } = props as React.PropsWithChildren<TProps>;
25
+ const inner = staged(rest as TProps, ...React.Children.toArray(children));
26
+ // staged render functions were not consistently marking contents as composable, though they were treated
27
+ // as such in useHook. To maintain compatibility we mark the returned function as composable here. This was
28
+ // dangerous, but this shim is necessary for backward compatibility. The newer pattern is explicit about this.
29
+ if (typeof inner === 'function' && !(inner as LegacyDirectComponent<TProps>)._canCompose) {
30
+ return Object.assign(inner, { _canCompose: true });
31
+ }
32
+ return inner;
33
+ };
34
+ }
35
+ }
36
+ return undefined;
37
+ }
38
+
39
+ /**
40
+ * Take a phased render function and make a real component out of it, attaching the phased render function
41
+ * so it can be split if used in that manner.
42
+ * @param getInnerPhase - phased render function to wrap into a staged component
43
+ */
44
+ export function phasedComponent<TProps>(getInnerPhase: PhasedRender<TProps>): FunctionComponent<TProps> {
45
+ return Object.assign(
46
+ (props: React.PropsWithChildren<TProps>) => {
47
+ // pull out children from props
48
+ const { children, ...outerProps } = props;
49
+ const Inner = getInnerPhase(outerProps as TProps);
50
+ return renderForJsxRuntime(Inner, { children });
51
+ },
52
+ { _phasedRender: getInnerPhase },
53
+ );
54
+ }
@@ -4,7 +4,7 @@ import type { RenderType, RenderResult, DirectComponent, LegacyDirectComponent }
4
4
 
5
5
  export type CustomRender = () => RenderResult;
6
6
 
7
- function asDirectComponent<TProps>(type: RenderType): DirectComponent<TProps> | undefined {
7
+ export function asDirectComponent<TProps>(type: RenderType): DirectComponent<TProps> | undefined {
8
8
  if (typeof type === 'function' && (type as DirectComponent<TProps>)._callDirect) {
9
9
  return type as DirectComponent<TProps>;
10
10
  }
@@ -22,7 +22,7 @@ export function renderForJsxRuntime<TProps>(
22
22
  type: React.ElementType,
23
23
  props: React.PropsWithChildren<TProps>,
24
24
  key?: React.Key,
25
- jsxFn: typeof ReactJSX.jsx = ReactJSX.jsx,
25
+ jsxFn: typeof ReactJSX.jsx = undefined,
26
26
  ): RenderResult {
27
27
  const legacyDirect = asLegacyDirectComponent(type);
28
28
  if (legacyDirect) {
@@ -35,20 +35,28 @@ export function renderForJsxRuntime<TProps>(
35
35
  const newProps = { ...props, key };
36
36
  return directComponent(newProps);
37
37
  }
38
- return jsxFn(type, props, key);
38
+
39
+ // auto-detect whether to use jsx or jsxs based on number of children, 0 or 1 = jsx, more than 1 = jsxs
40
+ if (!jsxFn) {
41
+ if (React.Children.count(props.children) > 1) {
42
+ jsxFn = ReactJSX.jsxs;
43
+ } else {
44
+ jsxFn = ReactJSX.jsx;
45
+ }
46
+ }
47
+ // Extract key from props to avoid React 19 warning about spreading key prop
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
+ const { key: propsKey, ...propsWithoutKey } = props as any;
50
+ // Use explicitly passed key, or fall back to key from props
51
+ const finalKey = key ?? propsKey;
52
+ // now call the appropriate jsx function to render the component
53
+ return jsxFn(type, propsWithoutKey, finalKey);
39
54
  }
40
55
 
41
56
  export function renderForClassicRuntime<TProps>(type: RenderType, props: TProps, ...children: React.ReactNode[]): RenderResult {
42
- const legacyDirect = asLegacyDirectComponent(type);
43
- if (legacyDirect) {
44
- return legacyDirect(props, ...children) as RenderResult;
45
- }
46
- const directComponent = asDirectComponent(type);
47
- if (directComponent) {
48
- const newProps = { ...props, children };
49
- return directComponent(newProps);
50
- }
51
- return React.createElement(type, props, ...children);
57
+ // if it is a non-string type with _canCompose set just call the function directly, otherwise call createElement as normal
58
+ const propsWithChildren = { children, ...props };
59
+ return renderForJsxRuntime(type as React.ElementType, propsWithChildren);
52
60
  }
53
61
 
54
62
  export const renderSlot = renderForClassicRuntime;
@@ -1,11 +1,14 @@
1
1
  import type React from 'react';
2
- import type ReactJSX from 'react/jsx-runtime';
2
+ //import type ReactJSX from 'react/jsx-runtime';
3
+ import type { FurnJSX } from '../jsx-namespace';
3
4
 
4
5
  /**
5
6
  * Base types for rendering components in a react application, extracted from react types
6
7
  */
7
- export type RenderResult = ReturnType<typeof ReactJSX.jsx>;
8
- export type RenderType = Parameters<typeof ReactJSX.jsx>[0] | string;
8
+ export type RenderResult = FurnJSX.Element;
9
+ //export type RenderResult = ReturnType<typeof ReactJSX.jsx>;
10
+ export type RenderType = FurnJSX.ElementType;
11
+ //export type RenderType = Parameters<typeof ReactJSX.jsx>[0] | string;
9
12
 
10
13
  /**
11
14
  * The standard element type inputs for react and react-native. This might be View or Button, or it might be 'div' in web. Effectively
@@ -13,6 +16,11 @@ export type RenderType = Parameters<typeof ReactJSX.jsx>[0] | string;
13
16
  */
14
17
  export type NativeReactType = RenderType;
15
18
 
19
+ /**
20
+ * Get the props from a react component type
21
+ */
22
+ export type PropsOf<TComponent> = TComponent extends React.JSXElementConstructor<infer P> ? P : never;
23
+
16
24
  /**
17
25
  * DIRECT RENDERING
18
26
  *
@@ -29,78 +37,95 @@ export type NativeReactType = RenderType;
29
37
  /**
30
38
  * type of the render function, not a FunctionComponent to help prevent hook usage
31
39
  */
32
- export type DirectComponentFunction<TProps> = (props: TProps) => RenderResult;
40
+ export type FunctionComponentCore<TProps> = (props: TProps) => RenderResult;
41
+
42
+ /**
43
+ * A function component that returns an element type. This allows for the empty call props usage for native
44
+ * components, as well as handles the returns of React components.
45
+ */
46
+ export type FunctionComponent<TProps> = FunctionComponentCore<TProps> & {
47
+ displayName?: string;
48
+ };
33
49
 
34
50
  /**
35
51
  * The full component definition that has the attached properties to allow the jsx handlers to render it directly.
36
52
  */
37
- export type DirectComponent<TProps> = DirectComponentFunction<TProps> & {
53
+ export type DirectComponent<TProps> = FunctionComponentCore<TProps> & {
38
54
  displayName?: string;
39
55
  _callDirect?: boolean;
40
56
  };
41
57
 
58
+ type LegacyComponentFunction<TProps> = (props: TProps, ...children: React.ReactNode[]) => RenderResult;
59
+
42
60
  /**
43
61
  * Legacy slot function type, this allows the rendering handlers to bypass the normal JSX rendering and call the function
44
62
  * directly. This expects the function to have children as the last argument of the call which isn't consistent with standard
45
63
  * react usage, where children are passed as a prop. If writing new components use the DirectComponent type instead.
46
64
  * @deprecated use DirectComponent instead
47
65
  */
48
- export type LegacyDirectComponent<TProps> = React.FunctionComponent<TProps> & {
66
+ export type LegacyDirectComponent<TProps> = LegacyComponentFunction<TProps> & {
49
67
  _canCompose?: boolean;
50
68
  };
51
69
 
52
70
  /**
53
- * Legacy type name used for consistency with old rendering patterns.
71
+ * Slot function type used in the composition framework. Slot functions return React elements (not arbitrary ReactNode values)
72
+ * since they always either call staged render functions or React.createElement.
54
73
  */
55
- export type SlotFn<TProps> = LegacyDirectComponent<TProps>;
74
+ export type SlotFn<TProps> = {
75
+ (props: TProps, ...children: React.ReactNode[]): React.ReactElement | null;
76
+ _canCompose?: boolean;
77
+ };
56
78
 
57
79
  /**
58
- * MULTI-STAGE RENDERING
80
+ * PHASED RENDERING (formerly called "staged" or "two-stage" rendering)
59
81
  *
60
- * The above direct rendering pattern is useful for simple components, but it does not allow for hooks or complex logic. The staged render pattern allows
61
- * for a component to be rendered in two stages, allowing for hooks to be used in the first stage and then the second stage to be a simple render function that can
82
+ * The above direct rendering pattern is useful for simple components, but it does not allow for hooks or complex logic. The phased render pattern allows
83
+ * for a component to be rendered in two phases, allowing for hooks to be used in the first phase and then the second phase to be a simple render function that can
62
84
  * be called directly.
63
85
  *
64
- * In code that respects the pattern the first stage will be called with props (though children will not be present) and will return a function that will be called
65
- * with additional props, this time with children present. This allows for the first stage to handle all the logic and hooks, while the second stage can be a simple render function
86
+ * In code that respects the pattern, the first phase will be called with props (though children will not be present) and will return a function that will be called
87
+ * with additional props, this time with children present. This allows for the first phase to handle all the logic and hooks, while the second phase can be a simple render function
66
88
  * that can leverage direct rendering if supported.
67
89
  *
68
- * The component itself will be a FunctionComponent, but it will have an attached property that is the staged render function. This allows the component to be used in two
90
+ * The component itself will be a FunctionComponent, but it will have an attached property that is the phased render function. This allows the component to be used in two
69
91
  * parts via the useSlot hook, or to be used directly in JSX/TSX as a normal component.
70
92
  */
71
93
 
72
94
  /**
73
- * This is an updated version of the staged render that handles children and types more consistently. Generally children
74
- * will be passed as part of the props for component rendering, it is inconsistent to have them as a variable argument.
95
+ * Phased render function signature. This is the recommended pattern for components that need hooks.
96
+ *
97
+ * Phase 1 receives props (without children) and can use hooks to compute derived state.
98
+ * Phase 2 returns a component that will be called with props including children.
75
99
  *
76
- * The `children` prop will be automatically inferred and typed correctly by the prop type. Hooks are still expected
100
+ * Children will be passed as part of the props for component rendering. The `children` prop will be
101
+ * automatically inferred and typed correctly by the prop type.
77
102
  */
78
- export type TwoStageRender<TProps> = (props: TProps) => React.ComponentType<React.PropsWithChildren<TProps>>;
103
+ export type PhasedRender<TProps> = (props: TProps) => React.ComponentType<React.PropsWithChildren<TProps>>;
79
104
 
80
105
  /**
81
- * Component type for a component that can be rendered in two stages, with the attached render function.
106
+ * Component type for a component that can be rendered in two phases, with the attached phased render function.
107
+ * Use phasedComponent() to create these.
82
108
  */
83
- export type StagedComponent<TProps> = React.FunctionComponent<TProps> & {
84
- _twoStageRender?: TwoStageRender<TProps>;
109
+ export type PhasedComponent<TProps> = FunctionComponent<TProps> & {
110
+ _phasedRender?: PhasedRender<TProps>;
85
111
  };
86
-
87
112
  /**
88
- * The final rendering of the props in a staged render. This is the function component signature that matches that of
113
+ * The final rendering of the props in a phased render. This is the function component signature that matches that of
89
114
  * React.createElement, children (if present) will be part of the variable args at the end.
90
115
  */
91
- export type FinalRender<TProps> = (props: TProps, ...children: React.ReactNode[]) => JSX.Element | null;
116
+ export type FinalRender<TProps> = (props: TProps, ...children: React.ReactNode[]) => FurnJSX.Element | null;
92
117
 
93
118
  /**
94
- * Signature for a staged render function.
95
- * @deprecated Use TwoStageRender instead
119
+ * Legacy staged render function signature.
120
+ * @deprecated Use PhasedRender instead. This older pattern splits children from props which is inconsistent with React conventions.
96
121
  */
97
122
  export type StagedRender<TProps> = (props: TProps, ...args: any[]) => FinalRender<TProps>;
98
123
 
99
124
  /**
100
- * Signature for a component that uses the staged render pattern.
101
- * @deprecated Use TwoStageRender instead
125
+ * Legacy component type that uses the staged render pattern.
126
+ * @deprecated Use PhasedComponent instead. Create with phasedComponent() rather than stagedComponent().
102
127
  */
103
- export type ComposableFunction<TProps> = React.FunctionComponent<TProps> & { _staged?: StagedRender<TProps> };
128
+ export type ComposableFunction<TProps> = FunctionComponent<TProps> & { _staged?: StagedRender<TProps> };
104
129
 
105
130
  /**
106
131
  * A type aggregating all the custom types that can be used in the render process.
@@ -109,6 +134,6 @@ export type ComposableFunction<TProps> = React.FunctionComponent<TProps> & { _st
109
134
  export type AnyCustomType<TProps> =
110
135
  | React.FunctionComponent<TProps>
111
136
  | DirectComponent<TProps>
112
- | StagedComponent<TProps>
137
+ | PhasedComponent<TProps>
113
138
  | ComposableFunction<TProps>
114
139
  | LegacyDirectComponent<TProps>;
@@ -0,0 +1,24 @@
1
+ import * as React from 'react';
2
+
3
+ import type { StagedRender, ComposableFunction } from './render.types';
4
+
5
+ function asArray<T>(val: T | T[]): T[] {
6
+ return Array.isArray(val) ? val : [val];
7
+ }
8
+
9
+ /**
10
+ * Take a staged render function and make a real component out of it
11
+ *
12
+ * @param staged - staged render function to wrap into a staged component
13
+ * @param memo - optional flag to enable wrapping the created component in a React.memo HOC
14
+ * @deprecated Use phasedComponent from phasedComponent.ts instead
15
+ */
16
+ export function stagedComponent<TProps>(staged: StagedRender<TProps>, memo?: boolean): ComposableFunction<TProps> {
17
+ const component = (props: React.PropsWithChildren<TProps>) => {
18
+ const { children, ...rest } = props;
19
+ return staged(rest as TProps)({} as React.PropsWithChildren<TProps>, asArray(children));
20
+ };
21
+ const stagedComponent = memo ? React.memo(component) : component;
22
+ Object.assign(stagedComponent, { _staged: staged });
23
+ return stagedComponent as ComposableFunction<TProps>;
24
+ }
@@ -305,7 +305,11 @@ describe('Immutable merge unit tests', () => {
305
305
  const arrayMerger = (...targets: any[]) => {
306
306
  const arrays = targets.filter((t) => Array.isArray(t));
307
307
  let result = [];
308
- arrays.forEach((v) => (result = result.concat(...v)));
308
+ for (const v of arrays) {
309
+ if (v.length > 0) {
310
+ result = result.concat(...v);
311
+ }
312
+ }
309
313
  return result;
310
314
  };
311
315
 
package/src/index.ts CHANGED
@@ -19,22 +19,42 @@ export type { StyleProp } from './merge-props/mergeStyles.types';
19
19
  export { mergeStyles } from './merge-props/mergeStyles';
20
20
  export { mergeProps } from './merge-props/mergeProps';
21
21
 
22
- // component pattern exports
23
- export { renderForClassicRuntime, renderForJsxRuntime, renderSlot } from './component-patterns/render';
22
+ // component pattern exports - extracting from elements
23
+ export { extractChildren, extractProps, extractStyle } from './component-patterns/extract';
24
+
25
+ // component pattern exports - rendering utilities
26
+ export { renderForJsxRuntime, renderSlot, asDirectComponent } from './component-patterns/render';
27
+
28
+ // component pattern exports - core types
24
29
  export type {
25
30
  DirectComponent,
26
- DirectComponentFunction,
31
+ FunctionComponent,
32
+ FunctionComponentCore,
27
33
  LegacyDirectComponent,
28
- StagedComponent,
29
- StagedRender,
30
- TwoStageRender,
34
+ PhasedComponent,
35
+ PhasedRender,
36
+ PropsOf,
31
37
  RenderType,
32
38
  RenderResult,
39
+ StagedRender,
33
40
  ComposableFunction,
34
41
  FinalRender,
35
42
  SlotFn,
36
43
  NativeReactType,
37
44
  } from './component-patterns/render.types';
45
+
46
+ // component pattern exports - component builders
47
+ export { directComponent } from './component-patterns/directComponent';
48
+ export { getPhasedRender, phasedComponent } from './component-patterns/phasedComponent';
49
+ export { stagedComponent } from './component-patterns/stagedComponent';
50
+
51
+ // component pattern exports - legacy JSX handlers
38
52
  export { withSlots } from './component-patterns/withSlots';
39
- export { stagedComponent, twoStageComponent } from './component-patterns/stagedComponent';
53
+
54
+ // jsx runtime exports
40
55
  export { jsx, jsxs } from './jsx-runtime';
56
+ export type { FurnJSX } from './jsx-namespace';
57
+
58
+ // general utilities
59
+ export { filterProps } from './utilities/filterProps';
60
+ export type { PropsFilter } from './utilities/filterProps';
@@ -0,0 +1,83 @@
1
+ import type React from 'react';
2
+
3
+ /**
4
+ * This file defines a custom JSX namespace that re-exports React's JSX types, but also allows us to add our own custom behavior to the JSX runtime.
5
+ * The main reason we need this is to support our "direct component" pattern, which allows certain components to bypass React's createElement and
6
+ * return their own React elements directly from the JSX runtime.
7
+ *
8
+ * Exporting this custom namespace is required to make intrinsic attributes like key and ref work correctly with our custom JSX functions. The normal
9
+ * fallback behavior if not defined is to not allow any attributes on intrinsic elements, which breaks a lot of React functionality.
10
+ *
11
+ * The custom behavior is implemented in the jsx and jsxs functions, which first check if the type being rendered is a direct component and if so, call it directly.
12
+ *
13
+ * Thanks to the emotion library's jsx-namespace for providing a reference implementation of how to do this while handling both React 18 and React 19's
14
+ * changes to the JSX types.
15
+ */
16
+
17
+ type IsPreReact19 = 2 extends Parameters<React.FunctionComponent<any>>['length'] ? true : false;
18
+
19
+ /**
20
+ * The following types are intrinsic types for React, but handled in a way that allows them to be correctly resolved in both
21
+ * React 18 and React 19.
22
+ *
23
+ * React 18:
24
+ * - JSX.Element, JSX.ElementClass, etc.
25
+ * - no React.JSX namespace
26
+ *
27
+ * React 19:
28
+ * - React.JSX.Element, React.JSX.ElementClass, etc.
29
+ * - no global JSX namespace
30
+ *
31
+ * In both cases, we want to be able to reference these types in our custom JSX namespace, so we conditionally define them based on whether we're in
32
+ * a pre-React 19 environment or not. The @ts-expect-error comments are used because both can't be valid at the same time so there will always be
33
+ * an error.
34
+ */
35
+
36
+ /** @ts-expect-error references types for both react 18 and react 19, only one can be valid at a time */
37
+ type ReactJSXElement = true extends IsPreReact19 ? JSX.Element : React.JSX.Element;
38
+
39
+ /** @ts-expect-error references types for both react 18 and react 19, only one can be valid at a time */
40
+ type ReactJSXElementClass = true extends IsPreReact19 ? JSX.ElementClass : React.JSX.ElementClass;
41
+
42
+ /** @ts-expect-error references types for both react 18 and react 19, only one can be valid at a time */
43
+ type ReactJSXElementAttributesProperty = true extends IsPreReact19 ? JSX.ElementAttributesProperty : React.JSX.ElementAttributesProperty;
44
+
45
+ /** @ts-expect-error references types for both react 18 and react 19, only one can be valid at a time */
46
+ type ReactJSXElementChildrenAttribute = true extends IsPreReact19 ? JSX.ElementChildrenAttribute : React.JSX.ElementChildrenAttribute;
47
+
48
+ // prettier-ignore
49
+ /** @ts-expect-error references types for both react 18 and react 19, only one can be valid at a time */
50
+ type ReactJSXLibraryManagedAttributes<C, P> = true extends IsPreReact19 ? JSX.LibraryManagedAttributes<C, P> : React.JSX.LibraryManagedAttributes<C, P>;
51
+
52
+ /** @ts-expect-error references types for both react 18 and react 19, only one can be valid at a time */
53
+ type ReactJSXIntrinsicAttributes = true extends IsPreReact19 ? JSX.IntrinsicAttributes : React.JSX.IntrinsicAttributes;
54
+
55
+ // prettier-ignore
56
+ /** @ts-expect-error references types for both react 18 and react 19, only one can be valid at a time */
57
+ type ReactJSXIntrinsicClassAttributes<T> = true extends IsPreReact19 ? JSX.IntrinsicClassAttributes<T> : React.JSX.IntrinsicClassAttributes<T>;
58
+
59
+ /** @ts-expect-error references types for both react 18 and react 19, only one can be valid at a time */
60
+ type ReactJSXIntrinsicElements = true extends IsPreReact19 ? JSX.IntrinsicElements : React.JSX.IntrinsicElements;
61
+
62
+ // based on the code from @types/react@18.2.8
63
+ // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/3197efc097d522c4bf02b94e1a0766d007d6cdeb/types/react/index.d.ts#LL3204C13-L3204C13
64
+ type ReactJSXElementType = true extends IsPreReact19 ? string | React.JSXElementConstructor<any> : React.JSX.ElementType;
65
+
66
+ // eslint-disable-next-line @typescript-eslint/no-namespace
67
+ export declare namespace FurnJSX {
68
+ export type ElementType = ReactJSXElementType;
69
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
70
+ export interface Element extends ReactJSXElement {}
71
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
72
+ export interface ElementClass extends ReactJSXElementClass {}
73
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
74
+ export interface ElementAttributesProperty extends ReactJSXElementAttributesProperty {}
75
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
76
+ export interface ElementChildrenAttribute extends ReactJSXElementChildrenAttribute {}
77
+ export type LibraryManagedAttributes<C, P> = ReactJSXLibraryManagedAttributes<C, P>;
78
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
79
+ export interface IntrinsicAttributes extends ReactJSXIntrinsicAttributes {}
80
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
81
+ export interface IntrinsicClassAttributes<T> extends ReactJSXIntrinsicClassAttributes<T> {}
82
+ export type IntrinsicElements = ReactJSXIntrinsicElements;
83
+ }
@@ -1,11 +1,15 @@
1
1
  import type React from 'react';
2
2
  import * as ReactJSX from 'react/jsx-runtime';
3
3
  import { renderForJsxRuntime } from './component-patterns/render';
4
+ export type { FurnJSX as JSX } from './jsx-namespace';
4
5
 
5
- export function jsx(type: React.ElementType, props: React.PropsWithChildren<unknown>, key?: React.Key): React.ReactElement {
6
+ export const jsx: typeof ReactJSX.jsx = (type, props, key?: React.Key) => {
6
7
  return renderForJsxRuntime(type, props, key, ReactJSX.jsx);
7
- }
8
+ };
8
9
 
9
- export function jsxs(type: React.ElementType, props: React.PropsWithChildren<unknown>, key?: React.Key): React.ReactElement {
10
+ export const jsxs: typeof ReactJSX.jsxs = (type, props, key?: React.Key) => {
10
11
  return renderForJsxRuntime(type, props, key, ReactJSX.jsxs);
11
- }
12
+ };
13
+
14
+ // Re-export Fragment for <></> syntax
15
+ export { Fragment } from 'react/jsx-runtime';
@@ -134,7 +134,7 @@ export const MyComponent = (props: IMyComponentProps) => {
134
134
  }, [theme]);
135
135
 
136
136
  // merge the styles if a style is passed in via props, caching the union to ensure consistent object identity
137
- newProps.style = newProps.style ? themeLocalCache(() => mergeStyles(style, newProps.style), [newProps.style])[0] : style;
137
+ newProps.style = newProps.style ? themeLocalCache(() => mergeStyles<MyStyleType>(style, newProps.style), [newProps.style])[0] : style;
138
138
 
139
139
  return <InnerControl {...newProps} />;
140
140
  };
@@ -1,8 +1,9 @@
1
- import type { ColorValue } from 'react-native';
2
-
3
1
  import { flattenStyle, mergeAndFlattenStyles, mergeStyles } from './mergeStyles';
4
2
  import type { StyleProp } from './mergeStyles.types';
5
3
 
4
+ type OpaqueColorValue = symbol & { __TYPE__: 'Color' };
5
+ type ColorValue = string | OpaqueColorValue;
6
+
6
7
  interface IFakeStyle {
7
8
  backgroundColor?: ColorValue;
8
9
  color?: ColorValue;