@contember/bindx-react 0.1.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 (194) hide show
  1. package/dist/batching/QueryBatcher.d.ts +31 -0
  2. package/dist/batching/QueryBatcher.d.ts.map +1 -0
  3. package/dist/batching/index.d.ts +2 -0
  4. package/dist/batching/index.d.ts.map +1 -0
  5. package/dist/components/EntityScope.d.ts +28 -0
  6. package/dist/components/EntityScope.d.ts.map +1 -0
  7. package/dist/dataview/DataGrid.d.ts +45 -0
  8. package/dist/dataview/DataGrid.d.ts.map +1 -0
  9. package/dist/dataview/DataGridPagination.d.ts +24 -0
  10. package/dist/dataview/DataGridPagination.d.ts.map +1 -0
  11. package/dist/dataview/DataGridToolbar.d.ts +16 -0
  12. package/dist/dataview/DataGridToolbar.d.ts.map +1 -0
  13. package/dist/dataview/DataViewContext.d.ts +32 -0
  14. package/dist/dataview/DataViewContext.d.ts.map +1 -0
  15. package/dist/dataview/columns.d.ts +115 -0
  16. package/dist/dataview/columns.d.ts.map +1 -0
  17. package/dist/dataview/contextHooks.d.ts +48 -0
  18. package/dist/dataview/contextHooks.d.ts.map +1 -0
  19. package/dist/dataview/dataAttribute.d.ts +6 -0
  20. package/dist/dataview/dataAttribute.d.ts.map +1 -0
  21. package/dist/dataview/export.d.ts +46 -0
  22. package/dist/dataview/export.d.ts.map +1 -0
  23. package/dist/dataview/filterComponents.d.ts +127 -0
  24. package/dist/dataview/filterComponents.d.ts.map +1 -0
  25. package/dist/dataview/filterContext.d.ts +10 -0
  26. package/dist/dataview/filterContext.d.ts.map +1 -0
  27. package/dist/dataview/filterHooks.d.ts +74 -0
  28. package/dist/dataview/filterHooks.d.ts.map +1 -0
  29. package/dist/dataview/filterScope.d.ts +54 -0
  30. package/dist/dataview/filterScope.d.ts.map +1 -0
  31. package/dist/dataview/filterWrappers.d.ts +65 -0
  32. package/dist/dataview/filterWrappers.d.ts.map +1 -0
  33. package/dist/dataview/index.d.ts +21 -0
  34. package/dist/dataview/index.d.ts.map +1 -0
  35. package/dist/dataview/infiniteLoad.d.ts +32 -0
  36. package/dist/dataview/infiniteLoad.d.ts.map +1 -0
  37. package/dist/dataview/loaderComponents.d.ts +80 -0
  38. package/dist/dataview/loaderComponents.d.ts.map +1 -0
  39. package/dist/dataview/pagingComponents.d.ts +50 -0
  40. package/dist/dataview/pagingComponents.d.ts.map +1 -0
  41. package/dist/dataview/renderCellValue.d.ts +9 -0
  42. package/dist/dataview/renderCellValue.d.ts.map +1 -0
  43. package/dist/dataview/selectionComponents.d.ts +62 -0
  44. package/dist/dataview/selectionComponents.d.ts.map +1 -0
  45. package/dist/dataview/sortingComponents.d.ts +39 -0
  46. package/dist/dataview/sortingComponents.d.ts.map +1 -0
  47. package/dist/dataview/stateStorage.d.ts +19 -0
  48. package/dist/dataview/stateStorage.d.ts.map +1 -0
  49. package/dist/dataview/useDataView.d.ts +58 -0
  50. package/dist/dataview/useDataView.d.ts.map +1 -0
  51. package/dist/dataview/useDataViewState.d.ts +89 -0
  52. package/dist/dataview/useDataViewState.d.ts.map +1 -0
  53. package/dist/hooks/BackendAdapterContext.d.ts +110 -0
  54. package/dist/hooks/BackendAdapterContext.d.ts.map +1 -0
  55. package/dist/hooks/ContemberBindxProvider.d.ts +67 -0
  56. package/dist/hooks/ContemberBindxProvider.d.ts.map +1 -0
  57. package/dist/hooks/createBindx.d.ts +39 -0
  58. package/dist/hooks/createBindx.d.ts.map +1 -0
  59. package/dist/hooks/index.d.ts +10 -0
  60. package/dist/hooks/index.d.ts.map +1 -0
  61. package/dist/hooks/useBindxEvents.d.ts +83 -0
  62. package/dist/hooks/useBindxEvents.d.ts.map +1 -0
  63. package/dist/hooks/useEntity.d.ts +123 -0
  64. package/dist/hooks/useEntity.d.ts.map +1 -0
  65. package/dist/hooks/useEntityBeforePersist.d.ts +22 -0
  66. package/dist/hooks/useEntityBeforePersist.d.ts.map +1 -0
  67. package/dist/hooks/useEntityCore.d.ts +52 -0
  68. package/dist/hooks/useEntityCore.d.ts.map +1 -0
  69. package/dist/hooks/useEntityImpl.d.ts +105 -0
  70. package/dist/hooks/useEntityImpl.d.ts.map +1 -0
  71. package/dist/hooks/useEntityList.d.ts +102 -0
  72. package/dist/hooks/useEntityList.d.ts.map +1 -0
  73. package/dist/hooks/useEntityListCore.d.ts +49 -0
  74. package/dist/hooks/useEntityListCore.d.ts.map +1 -0
  75. package/dist/hooks/useEntityListImpl.d.ts +70 -0
  76. package/dist/hooks/useEntityListImpl.d.ts.map +1 -0
  77. package/dist/hooks/useErrors.d.ts +72 -0
  78. package/dist/hooks/useErrors.d.ts.map +1 -0
  79. package/dist/hooks/usePersist.d.ts +115 -0
  80. package/dist/hooks/usePersist.d.ts.map +1 -0
  81. package/dist/hooks/useSelectionCollection.d.ts +41 -0
  82. package/dist/hooks/useSelectionCollection.d.ts.map +1 -0
  83. package/dist/hooks/useSelectionCollectionForList.d.ts +40 -0
  84. package/dist/hooks/useSelectionCollectionForList.d.ts.map +1 -0
  85. package/dist/hooks/useStableSelectionMeta.d.ts +11 -0
  86. package/dist/hooks/useStableSelectionMeta.d.ts.map +1 -0
  87. package/dist/hooks/useStoreSubscription.d.ts +19 -0
  88. package/dist/hooks/useStoreSubscription.d.ts.map +1 -0
  89. package/dist/hooks/useUndo.d.ts +45 -0
  90. package/dist/hooks/useUndo.d.ts.map +1 -0
  91. package/dist/index.d.ts +17 -0
  92. package/dist/index.d.ts.map +1 -0
  93. package/dist/jsx/SelectionMeta.d.ts +2 -0
  94. package/dist/jsx/SelectionMeta.d.ts.map +1 -0
  95. package/dist/jsx/analyzer.d.ts +22 -0
  96. package/dist/jsx/analyzer.d.ts.map +1 -0
  97. package/dist/jsx/collectorProxy.d.ts +14 -0
  98. package/dist/jsx/collectorProxy.d.ts.map +1 -0
  99. package/dist/jsx/componentBuilder.d.ts +51 -0
  100. package/dist/jsx/componentBuilder.d.ts.map +1 -0
  101. package/dist/jsx/componentBuilder.types.d.ts +268 -0
  102. package/dist/jsx/componentBuilder.types.d.ts.map +1 -0
  103. package/dist/jsx/componentBuilderCompat.d.ts +17 -0
  104. package/dist/jsx/componentBuilderCompat.d.ts.map +1 -0
  105. package/dist/jsx/componentFactory.d.ts +39 -0
  106. package/dist/jsx/componentFactory.d.ts.map +1 -0
  107. package/dist/jsx/components/Entity.d.ts +82 -0
  108. package/dist/jsx/components/Entity.d.ts.map +1 -0
  109. package/dist/jsx/components/EntityList.d.ts +48 -0
  110. package/dist/jsx/components/EntityList.d.ts.map +1 -0
  111. package/dist/jsx/components/Field.d.ts +27 -0
  112. package/dist/jsx/components/Field.d.ts.map +1 -0
  113. package/dist/jsx/components/HasMany.d.ts +34 -0
  114. package/dist/jsx/components/HasMany.d.ts.map +1 -0
  115. package/dist/jsx/components/HasOne.d.ts +25 -0
  116. package/dist/jsx/components/HasOne.d.ts.map +1 -0
  117. package/dist/jsx/components/HasRole.d.ts +51 -0
  118. package/dist/jsx/components/HasRole.d.ts.map +1 -0
  119. package/dist/jsx/components/If.d.ts +51 -0
  120. package/dist/jsx/components/If.d.ts.map +1 -0
  121. package/dist/jsx/components/Show.d.ts +33 -0
  122. package/dist/jsx/components/Show.d.ts.map +1 -0
  123. package/dist/jsx/components/index.d.ts +8 -0
  124. package/dist/jsx/components/index.d.ts.map +1 -0
  125. package/dist/jsx/conditions.d.ts +310 -0
  126. package/dist/jsx/conditions.d.ts.map +1 -0
  127. package/dist/jsx/createComponent.d.ts +44 -0
  128. package/dist/jsx/createComponent.d.ts.map +1 -0
  129. package/dist/jsx/index.d.ts +19 -0
  130. package/dist/jsx/index.d.ts.map +1 -0
  131. package/dist/jsx/inlineProxy.d.ts +14 -0
  132. package/dist/jsx/inlineProxy.d.ts.map +1 -0
  133. package/dist/jsx/legacyTypes.d.ts +26 -0
  134. package/dist/jsx/legacyTypes.d.ts.map +1 -0
  135. package/dist/jsx/proxy.d.ts +2 -0
  136. package/dist/jsx/proxy.d.ts.map +1 -0
  137. package/dist/jsx/proxyShared.d.ts +14 -0
  138. package/dist/jsx/proxyShared.d.ts.map +1 -0
  139. package/dist/jsx/runtimeProxy.d.ts +11 -0
  140. package/dist/jsx/runtimeProxy.d.ts.map +1 -0
  141. package/dist/jsx/standaloneCreateComponent.d.ts +30 -0
  142. package/dist/jsx/standaloneCreateComponent.d.ts.map +1 -0
  143. package/dist/jsx/types.d.ts +109 -0
  144. package/dist/jsx/types.d.ts.map +1 -0
  145. package/dist/jsx/withCollector.d.ts +39 -0
  146. package/dist/jsx/withCollector.d.ts.map +1 -0
  147. package/dist/roles/RoleContext.d.ts +31 -0
  148. package/dist/roles/RoleContext.d.ts.map +1 -0
  149. package/dist/roles/index.d.ts +2 -0
  150. package/dist/roles/index.d.ts.map +1 -0
  151. package/package.json +34 -0
  152. package/src/batching/QueryBatcher.ts +114 -0
  153. package/src/batching/index.ts +1 -0
  154. package/src/components/EntityScope.tsx +43 -0
  155. package/src/hooks/BackendAdapterContext.tsx +214 -0
  156. package/src/hooks/ContemberBindxProvider.tsx +158 -0
  157. package/src/hooks/index.ts +61 -0
  158. package/src/hooks/useBindxEvents.ts +229 -0
  159. package/src/hooks/useEntity.ts +489 -0
  160. package/src/hooks/useEntityBeforePersist.ts +32 -0
  161. package/src/hooks/useEntityList.ts +444 -0
  162. package/src/hooks/useErrors.ts +150 -0
  163. package/src/hooks/usePersist.ts +377 -0
  164. package/src/hooks/useSelectionCollection.ts +122 -0
  165. package/src/hooks/useStoreSubscription.ts +51 -0
  166. package/src/hooks/useUndo.ts +83 -0
  167. package/src/index.ts +357 -0
  168. package/src/jsx/SelectionMeta.ts +2 -0
  169. package/src/jsx/analyzer.ts +261 -0
  170. package/src/jsx/collectorProxy.ts +282 -0
  171. package/src/jsx/componentBuilder.ts +180 -0
  172. package/src/jsx/componentBuilder.types.ts +463 -0
  173. package/src/jsx/componentBuilderCompat.ts +39 -0
  174. package/src/jsx/componentFactory.ts +400 -0
  175. package/src/jsx/components/Entity.tsx +383 -0
  176. package/src/jsx/components/EntityList.tsx +147 -0
  177. package/src/jsx/components/Field.tsx +79 -0
  178. package/src/jsx/components/HasMany.tsx +109 -0
  179. package/src/jsx/components/HasOne.tsx +78 -0
  180. package/src/jsx/components/HasRole.tsx +95 -0
  181. package/src/jsx/components/If.tsx +127 -0
  182. package/src/jsx/components/Show.tsx +56 -0
  183. package/src/jsx/components/index.ts +20 -0
  184. package/src/jsx/conditions.ts +472 -0
  185. package/src/jsx/createComponent.ts +175 -0
  186. package/src/jsx/index.ts +135 -0
  187. package/src/jsx/legacyTypes.ts +30 -0
  188. package/src/jsx/proxy.ts +1 -0
  189. package/src/jsx/proxyShared.ts +42 -0
  190. package/src/jsx/standaloneCreateComponent.ts +47 -0
  191. package/src/jsx/types.ts +192 -0
  192. package/src/jsx/withCollector.ts +45 -0
  193. package/src/roles/RoleContext.tsx +57 -0
  194. package/src/roles/index.ts +1 -0
@@ -0,0 +1,39 @@
1
+ import type { ReactNode } from 'react';
2
+ /**
3
+ * Attaches a static render function to a component for selection collection.
4
+ *
5
+ * During the collection phase, the analyzer calls `staticRender(props)` instead of
6
+ * analyzing runtime children. Props contain collector proxies, so field accesses
7
+ * are tracked automatically. The returned JSX is analyzed for nested components.
8
+ *
9
+ * @example
10
+ * ```tsx
11
+ * // Simple: children is a render prop receiving has-one entity
12
+ * export const SelectField = withCollector(
13
+ * function SelectField({ field, children, ... }) { ... },
14
+ * (props) => props.children(props.field.$entity)
15
+ * )
16
+ *
17
+ * // Composing with HasMany for iteration
18
+ * export const DefaultRepeater = withCollector(
19
+ * function DefaultRepeater({ field, children, ... }) { ... },
20
+ * (props) => (
21
+ * <HasMany field={props.field}>
22
+ * {item => props.children(item, collectionItemInfo)}
23
+ * </HasMany>
24
+ * )
25
+ * )
26
+ *
27
+ * // Programmatic field access (no JSX needed)
28
+ * export const Uploader = withCollector(
29
+ * function Uploader({ field, fileType }) { ... },
30
+ * (props) => {
31
+ * const entity = props.field.$entity
32
+ * for (const ext of props.fileType.extractors) entity[ext.fieldName]
33
+ * return null
34
+ * }
35
+ * )
36
+ * ```
37
+ */
38
+ export declare function withCollector<TComponent extends (...args: never[]) => ReactNode>(component: TComponent, staticRender: (props: Parameters<TComponent>[0]) => ReactNode): TComponent;
39
+ //# sourceMappingURL=withCollector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withCollector.d.ts","sourceRoot":"","sources":["../../src/jsx/withCollector.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,aAAa,CAAC,UAAU,SAAS,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,SAAS,EAC/E,SAAS,EAAE,UAAU,EACrB,YAAY,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,KAAK,SAAS,GAC3D,UAAU,CAGZ"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Role provider context for runtime role checking.
3
+ *
4
+ * HasRole uses this to determine if the current user has the required roles.
5
+ * Without a RoleProvider, HasRole always renders (no gating).
6
+ */
7
+ import { type ReactNode, type ReactElement } from 'react';
8
+ type HasRoleFn = (role: string) => boolean;
9
+ export interface RoleProviderProps {
10
+ children: ReactNode;
11
+ /** Function that checks if the current user has a given role */
12
+ hasRole: HasRoleFn;
13
+ }
14
+ /**
15
+ * Provides runtime role checking for HasRole components.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * <RoleProvider hasRole={role => currentUser.roles.includes(role)}>
20
+ * <App />
21
+ * </RoleProvider>
22
+ * ```
23
+ */
24
+ export declare function RoleProvider({ children, hasRole }: RoleProviderProps): ReactElement;
25
+ /**
26
+ * Returns a function to check if the current user has a given role.
27
+ * If no RoleProvider is present, returns a function that always returns true.
28
+ */
29
+ export declare function useHasRole(): HasRoleFn;
30
+ export {};
31
+ //# sourceMappingURL=RoleContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RoleContext.d.ts","sourceRoot":"","sources":["../../src/roles/RoleContext.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAc,EAA6B,KAAK,SAAS,EAAE,KAAK,YAAY,EAAE,MAAM,OAAO,CAAA;AAM3F,KAAK,SAAS,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;AAQ1C,MAAM,WAAW,iBAAiB;IACjC,QAAQ,EAAE,SAAS,CAAA;IACnB,gEAAgE;IAChE,OAAO,EAAE,SAAS,CAAA;CAClB;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,EAAE,iBAAiB,GAAG,YAAY,CAEnF;AAMD;;;GAGG;AACH,wBAAgB,UAAU,IAAI,SAAS,CAGtC"}
@@ -0,0 +1,2 @@
1
+ export { RoleProvider, useHasRole, type RoleProviderProps } from './RoleContext.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/roles/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAA"}
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@contember/bindx-react",
3
+ "version": "0.1.0",
4
+ "description": "Type-safe React data binding framework",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts"
9
+ },
10
+ "scripts": {
11
+ "build": "tsc --build",
12
+ "typecheck": "tsc --build"
13
+ },
14
+ "peerDependencies": {
15
+ "react": ">=18.0.0"
16
+ },
17
+ "dependencies": {
18
+ "@contember/bindx": "0.1.0",
19
+ "@contember/client-content": "^2.1.0-alpha.30",
20
+ "@contember/graphql-client": "^2.1.0-alpha.30",
21
+ "@radix-ui/primitive": "^1.1.3",
22
+ "@radix-ui/react-slot": "^1.2.4"
23
+ },
24
+ "files": [
25
+ "dist",
26
+ "src"
27
+ ],
28
+ "license": "MIT",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/contember/bindx.git",
32
+ "directory": "packages/bindx-react"
33
+ }
34
+ }
@@ -0,0 +1,114 @@
1
+ import type { BackendAdapter, Query, QueryResult, QueryOptions } from '@contember/bindx'
2
+
3
+ /**
4
+ * Pending query with its resolution callbacks
5
+ */
6
+ interface PendingQuery {
7
+ readonly query: Query
8
+ readonly resolve: (result: QueryResult) => void
9
+ readonly reject: (error: Error) => void
10
+ readonly signal?: AbortSignal
11
+ }
12
+
13
+ /**
14
+ * Batches queries from multiple components into single adapter calls.
15
+ *
16
+ * Uses queueMicrotask to collect all queries during a React render cycle
17
+ * and execute them together in a single batch.
18
+ */
19
+ export class QueryBatcher {
20
+ private pendingQueries: PendingQuery[] = []
21
+ private flushScheduled = false
22
+
23
+ constructor(private readonly adapter: BackendAdapter) {}
24
+
25
+ /**
26
+ * Queue a query for batched execution.
27
+ * Returns a promise that resolves when the query completes.
28
+ */
29
+ enqueue(query: Query, options?: QueryOptions): Promise<QueryResult> {
30
+ return new Promise((resolve, reject) => {
31
+ // If provided signal is already aborted, reject immediately
32
+ if (options?.signal?.aborted) {
33
+ reject(new DOMException('Aborted', 'AbortError'))
34
+ return
35
+ }
36
+
37
+ const pending: PendingQuery = { query, resolve, reject, signal: options?.signal }
38
+
39
+ // Handle abort for this specific query
40
+ if (options?.signal) {
41
+ const abortHandler = (): void => {
42
+ const index = this.pendingQueries.indexOf(pending)
43
+ if (index >= 0) {
44
+ this.pendingQueries.splice(index, 1)
45
+ reject(new DOMException('Aborted', 'AbortError'))
46
+ }
47
+ }
48
+ options.signal.addEventListener('abort', abortHandler, { once: true })
49
+ }
50
+
51
+ this.pendingQueries.push(pending)
52
+ this.scheduleFlush()
53
+ })
54
+ }
55
+
56
+ /**
57
+ * Schedule flush using microtask to batch within render cycle
58
+ */
59
+ private scheduleFlush(): void {
60
+ if (this.flushScheduled) return
61
+ this.flushScheduled = true
62
+
63
+ queueMicrotask(() => {
64
+ this.flush()
65
+ })
66
+ }
67
+
68
+ /**
69
+ * Execute all pending queries in a batch
70
+ */
71
+ private async flush(): Promise<void> {
72
+ this.flushScheduled = false
73
+
74
+ if (this.pendingQueries.length === 0) return
75
+
76
+ // Take all pending queries
77
+ const batch = this.pendingQueries.splice(0)
78
+
79
+ // Find queries that aren't aborted
80
+ const activeBatch = batch.filter(p => !p.signal?.aborted)
81
+
82
+ if (activeBatch.length === 0) return
83
+
84
+ try {
85
+ const queries = activeBatch.map(p => p.query)
86
+ const results = await this.adapter.query(queries)
87
+
88
+ // Resolve each query with its result
89
+ for (let i = 0; i < activeBatch.length; i++) {
90
+ const pending = activeBatch[i]
91
+ const result = results[i]
92
+ if (pending && result) {
93
+ pending.resolve(result)
94
+ }
95
+ }
96
+ } catch (error) {
97
+ // Reject all queries in batch
98
+ for (const pending of activeBatch) {
99
+ pending.reject(error instanceof Error ? error : new Error(String(error)))
100
+ }
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Cancel all pending queries
106
+ */
107
+ cancelAll(): void {
108
+ const error = new DOMException('Cancelled', 'AbortError')
109
+ for (const pending of this.pendingQueries) {
110
+ pending.reject(error)
111
+ }
112
+ this.pendingQueries = []
113
+ }
114
+ }
@@ -0,0 +1 @@
1
+ export { QueryBatcher } from './QueryBatcher.js'
@@ -0,0 +1,43 @@
1
+ import { createContext, useContext, type ReactNode } from 'react'
2
+ import type { EntityHandle } from '@contember/bindx'
3
+
4
+ const EntityScopeContext = createContext<EntityHandle | null>(null)
5
+
6
+ export interface EntityScopeProps {
7
+ entity: EntityHandle
8
+ children: ReactNode
9
+ }
10
+
11
+ /**
12
+ * Provides an EntityHandle into React context for nested components.
13
+ * Useful for scoping entity access in editors, repeaters, selects, etc.
14
+ *
15
+ * @example
16
+ * ```tsx
17
+ * <EntityScope entity={handle}>
18
+ * <MyComponent /> // can use useEntityScope() inside
19
+ * </EntityScope>
20
+ * ```
21
+ */
22
+ export function EntityScope({ entity, children }: EntityScopeProps): ReactNode {
23
+ return <EntityScopeContext.Provider value={entity}>{children}</EntityScopeContext.Provider>
24
+ }
25
+
26
+ /**
27
+ * Reads the closest EntityHandle from EntityScope context.
28
+ * Throws if used outside of an EntityScope.
29
+ */
30
+ export function useEntityScope(): EntityHandle {
31
+ const entity = useContext(EntityScopeContext)
32
+ if (!entity) {
33
+ throw new Error('useEntityScope must be used within an EntityScope')
34
+ }
35
+ return entity
36
+ }
37
+
38
+ /**
39
+ * Reads the closest EntityHandle from EntityScope context, or null if not in scope.
40
+ */
41
+ export function useOptionalEntityScope(): EntityHandle | null {
42
+ return useContext(EntityScopeContext)
43
+ }
@@ -0,0 +1,214 @@
1
+ import { createContext, useContext, useMemo, type ReactNode } from 'react'
2
+ import type { BackendAdapter, MutationDataCollector, SchemaDefinition, UndoManagerConfig, UpdateMode } from '@contember/bindx'
3
+ import { SnapshotStore } from '@contember/bindx'
4
+ import { ActionDispatcher } from '@contember/bindx'
5
+ import { BatchPersister } from '@contember/bindx'
6
+ import { MutationCollector } from '@contember/bindx'
7
+ import { SchemaRegistry } from '@contember/bindx'
8
+ import { UndoManager } from '@contember/bindx'
9
+ import { QueryBatcher } from '../batching/QueryBatcher.js'
10
+
11
+ /**
12
+ * Context value containing all bindx services
13
+ */
14
+ export interface BindxContextValue {
15
+ /** Backend adapter for data fetching/persistence */
16
+ adapter: BackendAdapter
17
+ /** Query batcher for combining multiple queries into single requests */
18
+ batcher: QueryBatcher
19
+ /** Immutable snapshot store */
20
+ store: SnapshotStore
21
+ /** Action dispatcher for mutations */
22
+ dispatcher: ActionDispatcher
23
+ /** Batch persister for multi-entity persistence */
24
+ batchPersister: BatchPersister
25
+ /** Schema registry for entity metadata */
26
+ schema: SchemaRegistry
27
+ /** Undo manager (if enabled) */
28
+ undoManager: UndoManager | null
29
+ /** Whether debug logging is enabled */
30
+ debug: boolean
31
+ }
32
+
33
+ export const BindxContext = createContext<BindxContextValue | null>(null)
34
+
35
+ /**
36
+ * Props for BindxProvider
37
+ */
38
+ export interface BindxProviderProps {
39
+ /** The backend adapter to use for data fetching/persistence */
40
+ adapter: BackendAdapter
41
+ /** Optional custom snapshot store (useful for testing) */
42
+ store?: SnapshotStore
43
+ /** Schema definition for entity metadata and mutation collection */
44
+ schema: SchemaDefinition<Record<string, object>>
45
+ /** Optional custom mutation collector (overrides default behavior) */
46
+ mutationCollector?: MutationDataCollector
47
+ /** Enable undo/redo functionality */
48
+ enableUndo?: boolean
49
+ /** Configuration for undo manager */
50
+ undoConfig?: UndoManagerConfig
51
+ /**
52
+ * Default update mode for persistence operations.
53
+ * - 'optimistic': Update UI immediately, revert on failure (default)
54
+ * - 'pessimistic': Wait for server confirmation before updating UI
55
+ */
56
+ defaultUpdateMode?: UpdateMode
57
+ /** Enable debug logging (e.g., selection collection output) */
58
+ debug?: boolean
59
+ children: ReactNode
60
+ }
61
+
62
+ /**
63
+ * Provider component that supplies the bindx services
64
+ * to all child components using bindx hooks.
65
+ *
66
+ * @example
67
+ * ```tsx
68
+ * const adapter = new MockAdapter(initialData)
69
+ *
70
+ * function App() {
71
+ * return (
72
+ * <BindxProvider adapter={adapter} schema={testSchema}>
73
+ * <ArticleEditor id="123" />
74
+ * </BindxProvider>
75
+ * )
76
+ * }
77
+ * ```
78
+ */
79
+ export function BindxProvider({
80
+ adapter,
81
+ store: customStore,
82
+ schema: schemaDefinition,
83
+ mutationCollector: customMutationCollector,
84
+ enableUndo = false,
85
+ undoConfig,
86
+ defaultUpdateMode,
87
+ debug = false,
88
+ children,
89
+ }: BindxProviderProps) {
90
+ // Create services - memoized to maintain stable references
91
+ const services = useMemo(() => {
92
+ const store = customStore ?? new SnapshotStore()
93
+ const dispatcher = new ActionDispatcher(store)
94
+ const batcher = new QueryBatcher(adapter)
95
+
96
+ // Create undo manager if enabled
97
+ const undoManager = enableUndo ? new UndoManager(store, undoConfig) : null
98
+ if (undoManager) {
99
+ dispatcher.addMiddleware(undoManager.createMiddleware())
100
+ }
101
+
102
+ // Create schema registry from definition
103
+ const schemaRegistry = new SchemaRegistry(schemaDefinition)
104
+
105
+ // Create mutation collector - use custom, or auto-create from schema
106
+ // SchemaRegistry implements MutationSchemaProvider interface
107
+ const mutationCollector =
108
+ customMutationCollector ?? new MutationCollector(store, schemaRegistry)
109
+
110
+ const batchPersister = new BatchPersister(adapter, store, dispatcher, {
111
+ mutationCollector,
112
+ undoManager: undoManager ?? undefined,
113
+ schema: schemaRegistry,
114
+ defaultUpdateMode,
115
+ })
116
+
117
+ return {
118
+ adapter,
119
+ batcher,
120
+ store,
121
+ dispatcher,
122
+ batchPersister,
123
+ schema: schemaRegistry,
124
+ undoManager,
125
+ debug,
126
+ }
127
+ }, [adapter, customStore, schemaDefinition, customMutationCollector, enableUndo, undoConfig, defaultUpdateMode, debug])
128
+
129
+ return <BindxContext.Provider value={services}>{children}</BindxContext.Provider>
130
+ }
131
+
132
+ /**
133
+ * Hook to access the schema registry.
134
+ * Must be used within a BindxProvider or ContemberBindxProvider.
135
+ */
136
+ export function useSchemaRegistry(): SchemaRegistry {
137
+ const context = useContext(BindxContext)
138
+ if (!context) {
139
+ throw new Error('useSchemaRegistry must be used within a BindxProvider')
140
+ }
141
+ return context.schema
142
+ }
143
+
144
+ /**
145
+ * Hook to access the backend adapter.
146
+ * Must be used within a BindxProvider.
147
+ */
148
+ export function useBackendAdapter(): BackendAdapter {
149
+ const context = useContext(BindxContext)
150
+ if (!context) {
151
+ throw new Error('useBackendAdapter must be used within a BindxProvider')
152
+ }
153
+ return context.adapter
154
+ }
155
+
156
+ /**
157
+ * Hook to access the snapshot store.
158
+ * Must be used within a BindxProvider.
159
+ */
160
+ export function useSnapshotStore(): SnapshotStore {
161
+ const context = useContext(BindxContext)
162
+ if (!context) {
163
+ throw new Error('useSnapshotStore must be used within a BindxProvider')
164
+ }
165
+ return context.store
166
+ }
167
+
168
+ /**
169
+ * Hook to access the action dispatcher.
170
+ * Must be used within a BindxProvider.
171
+ */
172
+ export function useDispatcher(): ActionDispatcher {
173
+ const context = useContext(BindxContext)
174
+ if (!context) {
175
+ throw new Error('useDispatcher must be used within a BindxProvider')
176
+ }
177
+ return context.dispatcher
178
+ }
179
+
180
+ /**
181
+ * Hook to access the batch persister.
182
+ * Must be used within a BindxProvider.
183
+ */
184
+ export function useBatchPersister(): BatchPersister {
185
+ const context = useContext(BindxContext)
186
+ if (!context) {
187
+ throw new Error('useBatchPersister must be used within a BindxProvider')
188
+ }
189
+ return context.batchPersister
190
+ }
191
+
192
+ /**
193
+ * Hook to access all bindx services.
194
+ * Must be used within a BindxProvider.
195
+ */
196
+ export function useBindxContext(): BindxContextValue {
197
+ const context = useContext(BindxContext)
198
+ if (!context) {
199
+ throw new Error('useBindxContext must be used within a BindxProvider')
200
+ }
201
+ return context
202
+ }
203
+
204
+ /**
205
+ * Hook to access the query batcher.
206
+ * Must be used within a BindxProvider.
207
+ */
208
+ export function useQueryBatcher(): QueryBatcher {
209
+ const context = useContext(BindxContext)
210
+ if (!context) {
211
+ throw new Error('useQueryBatcher must be used within a BindxProvider')
212
+ }
213
+ return context.batcher
214
+ }
@@ -0,0 +1,158 @@
1
+ import { memo, useMemo, type ReactNode } from 'react'
2
+ import { GraphQlClient } from '@contember/graphql-client'
3
+ import { ContentClient, ContentQueryBuilder, type SchemaNames } from '@contember/client-content'
4
+ import { ContemberAdapter, SnapshotStore, ActionDispatcher, BatchPersister, MutationCollector, ContemberSchemaMutationAdapter, UndoManager, SchemaRegistry, type SchemaDefinition, type FieldDef, type UndoManagerConfig, type UpdateMode } from '@contember/bindx'
5
+ import { BindxContext, type BindxContextValue } from './BackendAdapterContext.js'
6
+ import { QueryBatcher } from '../batching/QueryBatcher.js'
7
+
8
+ /**
9
+ * Converts SchemaNames (Contember format) to SchemaDefinition (bindx format)
10
+ * so that a SchemaRegistry can be created for standalone hooks.
11
+ */
12
+ export function schemaNamesToDef(schemaNames: SchemaNames): SchemaDefinition<Record<string, object>> {
13
+ const enumsMap = (schemaNames as { enums?: Record<string, readonly string[]> }).enums ?? {}
14
+ const entities: Record<string, { fields: Record<string, FieldDef> }> = {}
15
+ for (const [entityName, entity] of Object.entries(schemaNames.entities)) {
16
+ const fields: Record<string, FieldDef> = {}
17
+ for (const [fieldName, fieldDef] of Object.entries(entity.fields)) {
18
+ if (fieldDef.type === 'column') {
19
+ const enumName = (fieldDef as { enumName?: string }).enumName
20
+ const columnType = (fieldDef as { columnType?: string }).columnType
21
+ const enumValues = enumName ? enumsMap[enumName] : undefined
22
+ if (enumName && enumValues) {
23
+ fields[fieldName] = { type: 'enum', enumName, values: enumValues }
24
+ } else {
25
+ fields[fieldName] = { type: 'scalar', columnType }
26
+ }
27
+ } else if (fieldDef.type === 'one') {
28
+ fields[fieldName] = { type: 'hasOne', target: fieldDef.entity }
29
+ } else if (fieldDef.type === 'many') {
30
+ fields[fieldName] = { type: 'hasMany', target: fieldDef.entity }
31
+ }
32
+ }
33
+ entities[entityName] = { fields }
34
+ }
35
+ return { entities }
36
+ }
37
+
38
+ /**
39
+ * Props for ContemberBindxProvider
40
+ */
41
+ export interface ContemberBindxProviderProps {
42
+ /** Contember schema names for query building */
43
+ schema: SchemaNames
44
+ /** Optional custom snapshot store (useful for testing) */
45
+ store?: SnapshotStore
46
+ /** Children */
47
+ children: ReactNode
48
+
49
+ client: GraphQlClient
50
+
51
+ /** Enable undo/redo functionality. Pass true to auto-create an UndoManager, or pass an UndoManager instance. */
52
+ undoManager?: UndoManager | boolean
53
+ /** Configuration for auto-created undo manager (only used when undoManager={true}) */
54
+ undoConfig?: UndoManagerConfig
55
+ /**
56
+ * Default update mode for persistence operations.
57
+ * - 'optimistic': Update UI immediately, revert on failure (default)
58
+ * - 'pessimistic': Wait for server confirmation before updating UI
59
+ */
60
+ defaultUpdateMode?: UpdateMode
61
+ /** Enable debug logging (e.g., selection collection output) */
62
+ debug?: boolean
63
+ }
64
+
65
+ /**
66
+ * Provider component that combines Contember authentication with bindx data binding.
67
+ *
68
+ * @example
69
+ * ```tsx
70
+ * import { ContemberBindxProvider, useUndo } from '@contember/bindx-react'
71
+ * import { schema } from './generated/schema'
72
+ *
73
+ * function App() {
74
+ * return (
75
+ * <ContemberBindxProvider
76
+ * client={graphQlClient}
77
+ * schema={schema}
78
+ * undoManager={true}
79
+ * >
80
+ * <ArticleEditor id="123" />
81
+ * </ContemberBindxProvider>
82
+ * )
83
+ * }
84
+ *
85
+ * // Access undo/redo in child components
86
+ * function UndoControls() {
87
+ * const { canUndo, canRedo, undo, redo } = useUndo()
88
+ * return (
89
+ * <div>
90
+ * <button onClick={undo} disabled={!canUndo}>Undo</button>
91
+ * <button onClick={redo} disabled={!canRedo}>Redo</button>
92
+ * </div>
93
+ * )
94
+ * }
95
+ * ```
96
+ */
97
+ export const ContemberBindxProvider = memo(function ContemberBindxProvider({
98
+ schema,
99
+ store: customStore,
100
+ children,
101
+ client: graphQlClient,
102
+ undoManager: undoManagerProp,
103
+ undoConfig,
104
+ defaultUpdateMode,
105
+ debug = false,
106
+ }: ContemberBindxProviderProps) {
107
+
108
+ // Create GraphQL client and adapter
109
+ const bindxValue = useMemo((): BindxContextValue => {
110
+
111
+ const adapter = new ContemberAdapter(new ContentClient(graphQlClient), new ContentQueryBuilder(schema))
112
+ const batcher = new QueryBatcher(adapter)
113
+
114
+ const store = customStore ?? new SnapshotStore()
115
+ const dispatcher = new ActionDispatcher(store)
116
+
117
+ // Create undo manager if enabled
118
+ const undoManager = undoManagerProp === true
119
+ ? new UndoManager(store, undoConfig)
120
+ : undoManagerProp instanceof UndoManager
121
+ ? undoManagerProp
122
+ : null
123
+ if (undoManager) {
124
+ dispatcher.addMiddleware(undoManager.createMiddleware())
125
+ }
126
+
127
+ // Create schema registry from SchemaNames for standalone hooks
128
+ const schemaRegistry = new SchemaRegistry(schemaNamesToDef(schema))
129
+
130
+ // Create mutation collector for proper nested operations
131
+ // Use ContemberSchemaMutationAdapter to wrap SchemaNames
132
+ const schemaAdapter = new ContemberSchemaMutationAdapter(schema)
133
+ const mutationCollector = new MutationCollector(store, schemaAdapter)
134
+
135
+ const batchPersister = new BatchPersister(adapter, store, dispatcher, {
136
+ mutationCollector,
137
+ undoManager: undoManager ?? undefined,
138
+ defaultUpdateMode,
139
+ })
140
+
141
+ return {
142
+ adapter,
143
+ batcher,
144
+ store,
145
+ dispatcher,
146
+ batchPersister,
147
+ schema: schemaRegistry,
148
+ undoManager,
149
+ debug,
150
+ }
151
+ }, [schema, customStore, undoManagerProp, undoConfig, defaultUpdateMode, debug])
152
+
153
+ return (
154
+ <BindxContext.Provider value={bindxValue}>
155
+ {children}
156
+ </BindxContext.Provider>
157
+ )
158
+ })