@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,383 @@
1
+ import React, { memo, useCallback, useEffect, useMemo, useRef, useSyncExternalStore, type ReactElement } from 'react'
2
+ import { useBindxContext, useSchemaRegistry } from '../../hooks/BackendAdapterContext.js'
3
+ import { useEntity } from '../../hooks/useEntity.js'
4
+ import { useSelectionCollection } from '../../hooks/useSelectionCollection.js'
5
+ import type { EntityAccessor, SelectionMeta } from '../types.js'
6
+ import { type EntityDef, type EntityUniqueWhere, type AnyBrand, type FieldError, EntityHandle, type SnapshotStore, type ActionDispatcher, type SchemaRegistry, type CommonEntity } from '@contember/bindx'
7
+
8
+ // ==================== Props Types ====================
9
+
10
+ /**
11
+ * Base props shared by both edit and create modes.
12
+ * TRoleMap is the role map from EntityDef — entity type resolved as CommonEntity<TRoleMap>.
13
+ */
14
+ interface EntityBaseProps<TRoleMap extends Record<string, object>> {
15
+ /** Entity definition reference */
16
+ entity: EntityDef<TRoleMap>
17
+ /** Render function receiving typed entity accessor with direct field access */
18
+ children: (entity: EntityAccessor<CommonEntity<TRoleMap>>) => React.ReactNode
19
+ /** Error fallback */
20
+ error?: (error: FieldError) => React.ReactNode
21
+ }
22
+
23
+ /**
24
+ * Props for editing an existing entity (fetched by unique field)
25
+ */
26
+ interface EntityByProps<TRoleMap extends Record<string, object>> extends EntityBaseProps<TRoleMap> {
27
+ /** Unique field(s) to identify the entity (e.g., { id: '...' } or { slug: '...' }) */
28
+ by: EntityUniqueWhere
29
+ create?: never
30
+ onPersisted?: never
31
+ /** Loading fallback */
32
+ loading?: React.ReactNode
33
+ /** Not found fallback */
34
+ notFound?: React.ReactNode
35
+ }
36
+
37
+ /**
38
+ * Props for creating a new entity
39
+ */
40
+ interface EntityCreateProps<TRoleMap extends Record<string, object>> extends EntityBaseProps<TRoleMap> {
41
+ by?: never
42
+ /** Create a new entity instead of fetching an existing one */
43
+ create: true
44
+ /** Callback when entity is persisted and receives server-assigned ID */
45
+ onPersisted?: (id: string) => void
46
+ loading?: never
47
+ notFound?: never
48
+ }
49
+
50
+ /**
51
+ * Props for Entity component - union of edit mode (by) and create mode
52
+ */
53
+ export type EntityProps<TRoleMap extends Record<string, object> = Record<string, object>> =
54
+ | EntityByProps<TRoleMap>
55
+ | EntityCreateProps<TRoleMap>
56
+
57
+ // ==================== Internal Props Types ====================
58
+
59
+ interface EntityByModeProps {
60
+ entityType: string
61
+ by: EntityUniqueWhere
62
+ children: (entity: EntityAccessor<unknown>) => React.ReactNode
63
+ loading?: React.ReactNode
64
+ error?: (error: FieldError) => React.ReactNode
65
+ notFound?: React.ReactNode
66
+ }
67
+
68
+ interface EntityCreateModeProps {
69
+ entityType: string
70
+ children: (entity: EntityAccessor<unknown>) => React.ReactNode
71
+ error?: (error: FieldError) => React.ReactNode
72
+ onPersisted?: (id: string) => void
73
+ }
74
+
75
+ // ==================== EntityByMode Component ====================
76
+
77
+ /**
78
+ * Internal component for edit mode (fetching existing entity by unique field)
79
+ */
80
+ function EntityByMode({
81
+ entityType,
82
+ by,
83
+ children,
84
+ loading,
85
+ error: errorFallback,
86
+ notFound,
87
+ }: EntityByModeProps): ReactElement | null {
88
+ const { store, dispatcher } = useBindxContext()
89
+ const schemaRegistry = useSchemaRegistry()
90
+
91
+ // Stable key for the 'by' clause
92
+ const byKey = useMemo(() => JSON.stringify(by), [by])
93
+
94
+ // Phase 1: Collect JSX selection
95
+ const { selection, queryKey } = useSelectionCollection({
96
+ entityType,
97
+ depsKey: byKey,
98
+ collect: collector => children(collector as EntityAccessor<unknown>),
99
+ })
100
+
101
+ // Phase 2: Load data using unified hook
102
+ const result = useEntity({ $name: entityType } as EntityDef, {
103
+ by,
104
+ selection,
105
+ queryKey,
106
+ })
107
+
108
+ // Render based on status
109
+ if (result.status === 'loading') {
110
+ return <>{loading ?? <DefaultLoading />}</>
111
+ }
112
+
113
+ if (result.status === 'error') {
114
+ if (errorFallback) {
115
+ return <>{errorFallback(result.error)}</>
116
+ }
117
+ return <DefaultError error={result.error} />
118
+ }
119
+
120
+ if (result.status === 'not_found') {
121
+ return <>{notFound ?? <DefaultNotFound entityType={entityType} by={by} />}</>
122
+ }
123
+
124
+ // Phase 3: Runtime render with EntityHandle (selection-aware)
125
+ return (
126
+ <EntityHandleRenderer
127
+ entityId={result.id}
128
+ entityType={entityType}
129
+ store={store}
130
+ dispatcher={dispatcher}
131
+ schemaRegistry={schemaRegistry}
132
+ selection={selection}
133
+ children={children}
134
+ />
135
+ )
136
+ }
137
+
138
+ // ==================== EntityCreateMode Component ====================
139
+
140
+ /**
141
+ * Snapshot type for create mode subscription
142
+ */
143
+ interface CreateModeSnapshot {
144
+ version: number
145
+ persistedId: string | null
146
+ }
147
+
148
+ /**
149
+ * Internal component for create mode (creating a new entity)
150
+ */
151
+ function EntityCreateMode({
152
+ entityType,
153
+ children,
154
+ error: errorFallback,
155
+ onPersisted,
156
+ }: EntityCreateModeProps): ReactElement {
157
+ const { store, dispatcher } = useBindxContext()
158
+ const schemaRegistry = useSchemaRegistry()
159
+ const tempIdRef = useRef<string | null>(null)
160
+
161
+ // Create entity once on mount (using ref to ensure only one creation)
162
+ const tempId = useMemo(() => {
163
+ if (tempIdRef.current) {
164
+ return tempIdRef.current
165
+ }
166
+ const id = store.createEntity(entityType)
167
+ tempIdRef.current = id
168
+ return id
169
+ }, [entityType, store])
170
+
171
+ // Subscribe to store changes for this entity
172
+ const subscribe = useCallback(
173
+ (callback: () => void) => store.subscribeToEntity(entityType, tempId, callback),
174
+ [store, entityType, tempId],
175
+ )
176
+
177
+ // Cache ref for snapshot stability
178
+ const snapshotCacheRef = useRef<CreateModeSnapshot | null>(null)
179
+
180
+ const getSnapshot = useCallback((): CreateModeSnapshot => {
181
+ const entitySnapshot = store.getEntitySnapshot(entityType, tempId)
182
+ const persistedId = store.getPersistedId(entityType, tempId)
183
+ const version = entitySnapshot?.version ?? 0
184
+
185
+ // Return cached snapshot if values haven't changed
186
+ const cached = snapshotCacheRef.current
187
+ if (cached && cached.version === version && cached.persistedId === persistedId) {
188
+ return cached
189
+ }
190
+
191
+ // Create new snapshot and cache it
192
+ const newSnapshot: CreateModeSnapshot = { version, persistedId }
193
+ snapshotCacheRef.current = newSnapshot
194
+ return newSnapshot
195
+ }, [store, entityType, tempId])
196
+
197
+ const { persistedId } = useSyncExternalStore(
198
+ subscribe,
199
+ getSnapshot,
200
+ getSnapshot, // Server snapshot same as client for create mode
201
+ )
202
+
203
+ // Track previous persistedId to call onPersisted only once
204
+ const prevPersistedIdRef = useRef<string | null>(null)
205
+
206
+ useEffect(() => {
207
+ if (persistedId && persistedId !== prevPersistedIdRef.current) {
208
+ prevPersistedIdRef.current = persistedId
209
+ onPersisted?.(persistedId)
210
+ }
211
+ }, [persistedId, onPersisted])
212
+
213
+ // Selection collection still works for building mutations
214
+ useSelectionCollection({
215
+ entityType,
216
+ depsKey: tempId,
217
+ collect: collector => children(collector as EntityAccessor<unknown>),
218
+ })
219
+
220
+ // Create EntityHandle (no selection for create mode - all fields accessible)
221
+ return (
222
+ <EntityHandleRenderer
223
+ entityId={tempId}
224
+ entityType={entityType}
225
+ store={store}
226
+ dispatcher={dispatcher}
227
+ schemaRegistry={schemaRegistry}
228
+ children={children}
229
+ />
230
+ )
231
+ }
232
+
233
+ // ==================== EntityHandle Renderer ====================
234
+
235
+ interface EntityHandleRendererProps {
236
+ entityId: string
237
+ entityType: string
238
+ store: SnapshotStore
239
+ dispatcher: ActionDispatcher
240
+ schemaRegistry: SchemaRegistry
241
+ selection?: SelectionMeta
242
+ children: (entity: EntityAccessor<unknown>) => React.ReactNode
243
+ }
244
+
245
+ /**
246
+ * Shared component that creates an EntityHandle and renders children with it.
247
+ * Used by both EntityByMode and EntityCreateMode.
248
+ *
249
+ * Subscribes to entity snapshot changes so that the handle reference changes
250
+ * when entity data changes. This is necessary because children may be wrapped
251
+ * in React.memo and need a new handle reference to trigger re-renders.
252
+ */
253
+ function EntityHandleRenderer({
254
+ entityId,
255
+ entityType,
256
+ store,
257
+ dispatcher,
258
+ schemaRegistry,
259
+ selection,
260
+ children,
261
+ }: EntityHandleRendererProps): ReactElement {
262
+ const subscribe = useCallback(
263
+ (callback: () => void) => store.subscribeToEntity(entityType, entityId, callback),
264
+ [store, entityType, entityId],
265
+ )
266
+
267
+ const getSnapshot = useCallback(
268
+ () => store.getEntitySnapshot(entityType, entityId)?.version ?? 0,
269
+ [store, entityType, entityId],
270
+ )
271
+
272
+ const version = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
273
+
274
+ const handle = useMemo(
275
+ () => EntityHandle.create(entityId, entityType, store, dispatcher, schemaRegistry, undefined, selection),
276
+ [entityId, entityType, store, dispatcher, schemaRegistry, selection, version],
277
+ )
278
+
279
+ useEffect(() => () => { handle.dispose() }, [handle])
280
+
281
+ return <>{children(handle as unknown as EntityAccessor<unknown>)}</>
282
+ }
283
+
284
+ // ==================== Main Entity Component ====================
285
+
286
+ /**
287
+ * Entity component - orchestrates the two-pass rendering approach.
288
+ *
289
+ * Supports two modes:
290
+ * - Edit mode: Fetches an existing entity by unique field(s)
291
+ * - Create mode: Creates a new entity locally
292
+ *
293
+ * @example Edit mode
294
+ * ```tsx
295
+ * <Entity entity={schema.Author} by={{ id: 'author-1' }}>
296
+ * {author => (
297
+ * <>
298
+ * <Field field={author.fields.name} />
299
+ * <HasMany field={author.fields.articles}>
300
+ * {article => <Field field={article.fields.title} />}
301
+ * </HasMany>
302
+ * </>
303
+ * )}
304
+ * </Entity>
305
+ * ```
306
+ *
307
+ * @example Create mode
308
+ * ```tsx
309
+ * <Entity entity={schema.Author} create onPersisted={id => navigate(`/authors/${id}`)}>
310
+ * {author => (
311
+ * <>
312
+ * <Field field={author.fields.name} />
313
+ * {author.isNew && <span>New author</span>}
314
+ * {author.persistedId && <span>Saved as {author.persistedId}</span>}
315
+ * </>
316
+ * )}
317
+ * </Entity>
318
+ * ```
319
+ */
320
+ function EntityImpl<TRoleMap extends Record<string, object>>(
321
+ props: EntityProps<TRoleMap>,
322
+ ): ReactElement | null {
323
+ const isCreateMode = 'create' in props && props.create === true
324
+
325
+ if (isCreateMode) {
326
+ const createProps = props as EntityCreateProps<TRoleMap>
327
+ return (
328
+ <EntityCreateMode
329
+ entityType={createProps.entity.$name}
330
+ children={createProps.children as (entity: EntityAccessor<unknown>) => React.ReactNode}
331
+ error={createProps.error}
332
+ onPersisted={createProps.onPersisted}
333
+ />
334
+ )
335
+ }
336
+
337
+ const byProps = props as EntityByProps<TRoleMap>
338
+ return (
339
+ <EntityByMode
340
+ entityType={byProps.entity.$name}
341
+ by={byProps.by}
342
+ children={byProps.children as (entity: EntityAccessor<unknown>) => React.ReactNode}
343
+ loading={byProps.loading}
344
+ error={byProps.error}
345
+ notFound={byProps.notFound}
346
+ />
347
+ )
348
+ }
349
+
350
+ // Note: Using type assertion for generic memo component
351
+ export const Entity = memo(EntityImpl) as unknown as typeof EntityImpl
352
+
353
+ // ==================== Default Fallback Components ====================
354
+
355
+ /**
356
+ * Default loading component
357
+ */
358
+ function DefaultLoading(): ReactElement {
359
+ return <div className="bindx-loading">Loading...</div>
360
+ }
361
+
362
+ /**
363
+ * Default error component
364
+ */
365
+ function DefaultError({ error }: { error: FieldError }): ReactElement {
366
+ return (
367
+ <div className="bindx-error">
368
+ <strong>Error:</strong> {error.message}
369
+ </div>
370
+ )
371
+ }
372
+
373
+ /**
374
+ * Default not found component
375
+ */
376
+ function DefaultNotFound({ entityType, by }: { entityType: string; by: EntityUniqueWhere }): ReactElement {
377
+ const byDescription = Object.entries(by).map(([k, v]) => `${k}="${v}"`).join(', ')
378
+ return (
379
+ <div className="bindx-not-found">
380
+ {entityType} with {byDescription} not found
381
+ </div>
382
+ )
383
+ }
@@ -0,0 +1,147 @@
1
+ import React, { memo, type ReactElement } from 'react'
2
+ import { type EntityDef, type EntityWhere, type EntityOrderBy, type FieldError, type CommonEntity } from '@contember/bindx'
3
+ import { useEntityList } from '../../hooks/useEntityList.js'
4
+ import { useSelectionCollection } from '../../hooks/useSelectionCollection.js'
5
+ import type { EntityAccessor } from '../types.js'
6
+
7
+ /**
8
+ * Props for EntityList component
9
+ */
10
+ export interface EntityListProps<TRoleMap extends Record<string, object> = Record<string, object>> {
11
+ /** Entity definition reference */
12
+ entity: EntityDef<TRoleMap>
13
+ /** Optional filter criteria - type-safe based on entity schema */
14
+ filter?: EntityWhere<CommonEntity<TRoleMap>>
15
+ /** Optional ordering - type-safe based on entity schema */
16
+ orderBy?: readonly EntityOrderBy<CommonEntity<TRoleMap>>[]
17
+ /** Optional limit */
18
+ limit?: number
19
+ /** Optional offset */
20
+ offset?: number
21
+ /** Render function receiving typed entity accessor with direct field access */
22
+ children: (entity: EntityAccessor<CommonEntity<TRoleMap>>, index: number) => React.ReactNode
23
+ /** Loading fallback */
24
+ loading?: React.ReactNode
25
+ /** Error fallback */
26
+ error?: (error: FieldError) => React.ReactNode
27
+ /** Empty state fallback (when list has no items) */
28
+ empty?: React.ReactNode
29
+ }
30
+
31
+ /**
32
+ * EntityList component - orchestrates the two-pass rendering approach for lists.
33
+ *
34
+ * Phase 1 (Collection): Renders children once with collector proxy to determine which fields are needed
35
+ * Phase 2 (Loading): Fetches data based on collected selection
36
+ * Phase 3 (Runtime): Renders children for each item with real data accessors
37
+ *
38
+ * @example
39
+ * ```tsx
40
+ * <EntityList entity={schema.Article} filter={{ published: true }} orderBy={[{ createdAt: 'desc' }]} limit={10}>
41
+ * {(article, index) => (
42
+ * <div key={article.id}>
43
+ * <Field field={article.fields.title} />
44
+ * </div>
45
+ * )}
46
+ * </EntityList>
47
+ * ```
48
+ */
49
+ function EntityListComponent<TRoleMap extends Record<string, object>>({
50
+ entity,
51
+ filter,
52
+ orderBy,
53
+ limit,
54
+ offset,
55
+ children,
56
+ loading,
57
+ error: errorFallback,
58
+ empty,
59
+ }: EntityListProps<TRoleMap>): ReactElement | null {
60
+ const entityType = entity.$name
61
+
62
+ // Stable options key for dependency tracking
63
+ const optionsKey = JSON.stringify({
64
+ filter: filter ?? {},
65
+ orderBy: orderBy ?? [],
66
+ limit,
67
+ offset,
68
+ })
69
+
70
+ // Phase 1: Collect JSX selection
71
+ const { selection, queryKey } = useSelectionCollection({
72
+ entityType,
73
+ depsKey: optionsKey,
74
+ collect: collector => children(collector as unknown as EntityAccessor<CommonEntity<TRoleMap>>, 0),
75
+ queryKeyExtra: { filter, orderBy, limit, offset },
76
+ })
77
+
78
+ // Phase 2: Load data using unified hook
79
+ const result = useEntityList(entity, {
80
+ filter,
81
+ orderBy,
82
+ limit,
83
+ offset,
84
+ selection,
85
+ queryKey,
86
+ })
87
+
88
+ // Render based on status
89
+ if (result.status === 'loading') {
90
+ return <>{loading ?? <DefaultLoading />}</>
91
+ }
92
+
93
+ if (result.status === 'error') {
94
+ if (errorFallback) {
95
+ return <>{errorFallback(result.error)}</>
96
+ }
97
+ return <DefaultError error={result.error} />
98
+ }
99
+
100
+ // Empty state
101
+ if (result.items.length === 0) {
102
+ return <>{empty ?? <DefaultEmpty entityType={entityType} />}</>
103
+ }
104
+
105
+ // Phase 3: Runtime render — items are already EntityAccessors from the hook
106
+ const items = result.items.map((item, index) => {
107
+ return (
108
+ <React.Fragment key={item.id}>
109
+ {children(item as unknown as EntityAccessor<CommonEntity<TRoleMap>>, index)}
110
+ </React.Fragment>
111
+ )
112
+ })
113
+
114
+ return <>{items}</>
115
+ }
116
+
117
+ // Note: Using type assertion for generic memo component
118
+ export const EntityList = memo(EntityListComponent) as unknown as typeof EntityListComponent
119
+
120
+ /**
121
+ * Default loading component
122
+ */
123
+ function DefaultLoading(): ReactElement {
124
+ return <div className="bindx-loading">Loading...</div>
125
+ }
126
+
127
+ /**
128
+ * Default error component
129
+ */
130
+ function DefaultError({ error }: { error: FieldError }): ReactElement {
131
+ return (
132
+ <div className="bindx-error">
133
+ <strong>Error:</strong> {error.message}
134
+ </div>
135
+ )
136
+ }
137
+
138
+ /**
139
+ * Default empty component
140
+ */
141
+ function DefaultEmpty({ entityType }: { entityType: string }): ReactElement {
142
+ return (
143
+ <div className="bindx-empty">
144
+ No {entityType} items found
145
+ </div>
146
+ )
147
+ }
@@ -0,0 +1,79 @@
1
+ import { memo, type ReactElement } from 'react'
2
+ import type { FieldProps, SelectionFieldMeta, SelectionProvider, FieldRef } from '../types.js'
3
+ import { FIELD_REF_META, BINDX_COMPONENT } from '../types.js'
4
+
5
+ /**
6
+ * Field component - renders a scalar field value
7
+ *
8
+ * @example
9
+ * ```tsx
10
+ * // Basic usage - renders value as string
11
+ * <Field field={entity.fields.name} />
12
+ *
13
+ * // With custom render function
14
+ * <Field field={entity.fields.email}>
15
+ * {field => <a href={`mailto:${field.value}`}>{field.value}</a>}
16
+ * </Field>
17
+ *
18
+ * // With format function
19
+ * <Field field={entity.fields.date} format={d => d?.toLocaleDateString()} />
20
+ * ```
21
+ */
22
+ function FieldImpl<T>({ field, children, format }: FieldProps<T>): ReactElement | null {
23
+ // Handle undefined field (e.g., when accessing field on disconnected has-one relation)
24
+ if (field === undefined || field === null) {
25
+ return null
26
+ }
27
+
28
+ // At runtime, field is always a full FieldRef (proxy provides all properties)
29
+ // Props accept FieldRefBase for type compatibility with both implicit and explicit modes
30
+ const fullField = field as FieldRef<T>
31
+
32
+ if (children) {
33
+ return <>{children(fullField)}</>
34
+ }
35
+
36
+ if (format) {
37
+ return <>{format(fullField.value)}</>
38
+ }
39
+
40
+ // Default: render value as string
41
+ if (fullField.value === null || fullField.value === undefined) {
42
+ return null
43
+ }
44
+
45
+ return <>{String(fullField.value)}</>
46
+ }
47
+
48
+ export const Field = memo(FieldImpl) as typeof FieldImpl
49
+
50
+ // Static method for selection extraction
51
+ const fieldWithSelection = Field as typeof Field & SelectionProvider & { [BINDX_COMPONENT]: true }
52
+
53
+ fieldWithSelection.getSelection = (props: FieldProps<unknown>): SelectionFieldMeta | null => {
54
+ // Handle undefined field (e.g., when accessing field on disconnected has-one relation)
55
+ if (props.field === undefined || props.field === null) {
56
+ return null
57
+ }
58
+ const meta = props.field[FIELD_REF_META]
59
+ if (!meta) {
60
+ return null
61
+ }
62
+ // During collection phase, field accesses are already tracked in the scope tree via proxy.
63
+ // Field.getSelection should not duplicate them as flat entries which causes incorrect queries.
64
+ // Collection-phase refs have empty entityId.
65
+ if (meta.entityId === '') {
66
+ return null
67
+ }
68
+ return {
69
+ fieldName: meta.fieldName,
70
+ alias: meta.fieldName,
71
+ path: meta.path,
72
+ isArray: false,
73
+ isRelation: false,
74
+ }
75
+ }
76
+
77
+ fieldWithSelection[BINDX_COMPONENT] = true
78
+
79
+ export { fieldWithSelection as FieldWithMeta }