@barefootjs/client 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.
- package/dist/build.d.ts +56 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +76 -0
- package/dist/context.d.ts +25 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/csr-adapter.d.ts +26 -0
- package/dist/csr-adapter.d.ts.map +1 -0
- package/dist/forward-props.d.ts +17 -0
- package/dist/forward-props.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +154 -0
- package/dist/reactive.d.ts +150 -0
- package/dist/reactive.d.ts.map +1 -0
- package/dist/reactive.js +215 -0
- package/dist/runtime/apply-rest-attrs.d.ts +16 -0
- package/dist/runtime/apply-rest-attrs.d.ts.map +1 -0
- package/dist/runtime/branch-slot.d.ts +22 -0
- package/dist/runtime/branch-slot.d.ts.map +1 -0
- package/dist/runtime/client-marker.d.ts +21 -0
- package/dist/runtime/client-marker.d.ts.map +1 -0
- package/dist/runtime/component.d.ts +99 -0
- package/dist/runtime/component.d.ts.map +1 -0
- package/dist/runtime/context.d.ts +40 -0
- package/dist/runtime/context.d.ts.map +1 -0
- package/dist/runtime/hydrate.d.ts +100 -0
- package/dist/runtime/hydrate.d.ts.map +1 -0
- package/dist/runtime/hydration-state.d.ts +13 -0
- package/dist/runtime/hydration-state.d.ts.map +1 -0
- package/dist/runtime/index.d.ts +27 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +2093 -0
- package/dist/runtime/insert.d.ts +75 -0
- package/dist/runtime/insert.d.ts.map +1 -0
- package/dist/runtime/list.d.ts +21 -0
- package/dist/runtime/list.d.ts.map +1 -0
- package/dist/runtime/map-array.d.ts +32 -0
- package/dist/runtime/map-array.d.ts.map +1 -0
- package/dist/runtime/portal.d.ts +96 -0
- package/dist/runtime/portal.d.ts.map +1 -0
- package/dist/runtime/qsa-item.d.ts +52 -0
- package/dist/runtime/qsa-item.d.ts.map +1 -0
- package/dist/runtime/query.d.ts +86 -0
- package/dist/runtime/query.d.ts.map +1 -0
- package/dist/runtime/reconcile-elements.d.ts +44 -0
- package/dist/runtime/reconcile-elements.d.ts.map +1 -0
- package/dist/runtime/registry.d.ts +53 -0
- package/dist/runtime/registry.d.ts.map +1 -0
- package/dist/runtime/render.d.ts +35 -0
- package/dist/runtime/render.d.ts.map +1 -0
- package/dist/runtime/scope.d.ts +28 -0
- package/dist/runtime/scope.d.ts.map +1 -0
- package/dist/runtime/slot-resolver.d.ts +36 -0
- package/dist/runtime/slot-resolver.d.ts.map +1 -0
- package/dist/runtime/spread-attrs.d.ts +19 -0
- package/dist/runtime/spread-attrs.d.ts.map +1 -0
- package/dist/runtime/standalone.js +2278 -0
- package/dist/runtime/streaming.d.ts +36 -0
- package/dist/runtime/streaming.d.ts.map +1 -0
- package/dist/runtime/style.d.ts +17 -0
- package/dist/runtime/style.d.ts.map +1 -0
- package/dist/runtime/template.d.ts +39 -0
- package/dist/runtime/template.d.ts.map +1 -0
- package/dist/runtime/types.d.ts +26 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/shims.d.ts +21 -0
- package/dist/shims.d.ts.map +1 -0
- package/dist/slot.d.ts +14 -0
- package/dist/slot.d.ts.map +1 -0
- package/dist/split-props.d.ts +26 -0
- package/dist/split-props.d.ts.map +1 -0
- package/dist/unwrap.d.ts +16 -0
- package/dist/unwrap.d.ts.map +1 -0
- package/package.json +71 -0
- package/src/build.ts +92 -0
- package/src/context.ts +33 -0
- package/src/csr-adapter.ts +134 -0
- package/src/forward-props.ts +43 -0
- package/src/index.ts +42 -0
- package/src/reactive.ts +411 -0
- package/src/runtime/apply-rest-attrs.ts +109 -0
- package/src/runtime/branch-slot.ts +32 -0
- package/src/runtime/client-marker.ts +46 -0
- package/src/runtime/component.ts +501 -0
- package/src/runtime/context.ts +111 -0
- package/src/runtime/hydrate.ts +311 -0
- package/src/runtime/hydration-state.ts +13 -0
- package/src/runtime/index.ts +96 -0
- package/src/runtime/insert.ts +407 -0
- package/src/runtime/list.ts +47 -0
- package/src/runtime/map-array.ts +381 -0
- package/src/runtime/portal.ts +174 -0
- package/src/runtime/qsa-item.ts +128 -0
- package/src/runtime/query.ts +632 -0
- package/src/runtime/reconcile-elements.ts +391 -0
- package/src/runtime/registry.ts +160 -0
- package/src/runtime/render.ts +105 -0
- package/src/runtime/scope.ts +46 -0
- package/src/runtime/slot-resolver.ts +66 -0
- package/src/runtime/spread-attrs.ts +88 -0
- package/src/runtime/streaming.ts +65 -0
- package/src/runtime/style.ts +27 -0
- package/src/runtime/template.ts +53 -0
- package/src/runtime/types.ts +27 -0
- package/src/shims.ts +54 -0
- package/src/slot.ts +23 -0
- package/src/split-props.ts +86 -0
- package/src/unwrap.ts +18 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BarefootJS - Component Creation
|
|
3
|
+
*
|
|
4
|
+
* Functions for dynamically creating component instances at runtime.
|
|
5
|
+
* Used by reconcileList() when rendering components in loops.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getTemplate } from './template'
|
|
9
|
+
import { getComponentInit } from './registry'
|
|
10
|
+
import { getRegisteredDef } from './hydrate'
|
|
11
|
+
import { hydratedScopes } from './hydration-state'
|
|
12
|
+
import { untrack } from '@barefootjs/client/reactive'
|
|
13
|
+
import { setCurrentScope } from './context'
|
|
14
|
+
import { BF_SCOPE, BF_KEY, BF_HOST, BF_AT, BF_PARENT_SCOPE_PLACEHOLDER } from '@barefootjs/shared'
|
|
15
|
+
import type { ComponentDef } from './types'
|
|
16
|
+
|
|
17
|
+
// Parent scope ID context for renderChild() inside insert() branch templates.
|
|
18
|
+
// When set, renderChild uses the parent's scope ID as prefix instead of a random ID,
|
|
19
|
+
// producing scope IDs consistent with SSR (e.g., "ParentName_abc_s5" instead of
|
|
20
|
+
// "Button_random_s5"). This enables $cSingle's getDualScopeIds check to pass.
|
|
21
|
+
// Set by insert() before calling branch.template(), cleared after.
|
|
22
|
+
let _parentScopeId: string | null = null
|
|
23
|
+
|
|
24
|
+
export function setParentScopeId(id: string | null): void {
|
|
25
|
+
_parentScopeId = id
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// WeakMap to store props update functions for each component element
|
|
29
|
+
// This allows reconcileList to update props when an element is reused
|
|
30
|
+
const propsUpdateMap = new WeakMap<HTMLElement, (props: Record<string, unknown>) => void>()
|
|
31
|
+
|
|
32
|
+
// WeakMap to store the current props for each component element
|
|
33
|
+
// Used to pass props to existing elements when they are reused
|
|
34
|
+
const propsMap = new WeakMap<HTMLElement, Record<string, unknown>>()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a component instance with DOM element and initialized state.
|
|
39
|
+
*
|
|
40
|
+
* This function:
|
|
41
|
+
* 1. Gets the template function for the component
|
|
42
|
+
* 2. Generates HTML from props using the template
|
|
43
|
+
* 3. Creates DOM element from HTML
|
|
44
|
+
* 4. Sets scope ID and key attributes
|
|
45
|
+
* 5. Initializes the component (attaches event handlers, sets up effects)
|
|
46
|
+
*
|
|
47
|
+
* @param name - Component name (e.g., 'TodoItem')
|
|
48
|
+
* @param props - Props to pass to the component
|
|
49
|
+
* @param key - Optional key for list reconciliation
|
|
50
|
+
* @returns Created DOM element
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* const el = createComponent('TodoItem', {
|
|
54
|
+
* todo: { id: 1, text: 'Buy milk', done: false },
|
|
55
|
+
* onDelete: () => handleDelete(1)
|
|
56
|
+
* }, 1)
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* Create a component instance from a string name (SSR mode, uses registry)
|
|
60
|
+
* or from a ComponentDef (CSR mode, no registry needed).
|
|
61
|
+
*/
|
|
62
|
+
/**
|
|
63
|
+
* Slot-relationship metadata stamped onto a freshly-created component as
|
|
64
|
+
* `bf-h` / `bf-m`. Top-level CSR mounts pass no `slot` — they own their
|
|
65
|
+
* own hydration lifecycle and `initChild` re-binds callbacks freely on
|
|
66
|
+
* each reconcile.
|
|
67
|
+
*/
|
|
68
|
+
export interface CreateComponentSlotInfo {
|
|
69
|
+
/** Host scope id (this child's `bf-h` value). */
|
|
70
|
+
parent: string
|
|
71
|
+
/** Slot id in the host (this child's `bf-m` value). */
|
|
72
|
+
mount: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createComponent(
|
|
76
|
+
nameOrDef: string | ComponentDef,
|
|
77
|
+
props: Record<string, unknown>,
|
|
78
|
+
key?: string | number,
|
|
79
|
+
slot?: CreateComponentSlotInfo,
|
|
80
|
+
): HTMLElement {
|
|
81
|
+
// ComponentDef mode: use def directly instead of registry lookup
|
|
82
|
+
if (typeof nameOrDef !== 'string') {
|
|
83
|
+
return createComponentFromDef(nameOrDef, props, key)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const name = nameOrDef
|
|
87
|
+
|
|
88
|
+
// 1. Get template function
|
|
89
|
+
const templateFn = getTemplate(name)
|
|
90
|
+
if (!templateFn) {
|
|
91
|
+
console.warn(`[BarefootJS] Template not found for component: ${name}`)
|
|
92
|
+
return createPlaceholder(name, key)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 2. Check for getter children.
|
|
96
|
+
// Children defined via a getter are evaluated AFTER initFn so that context
|
|
97
|
+
// providers set up by the parent are available when children are created.
|
|
98
|
+
const childrenDescriptor = Object.getOwnPropertyDescriptor(props, 'children')
|
|
99
|
+
const childrenIsGetter = childrenDescriptor != null && typeof childrenDescriptor.get === 'function'
|
|
100
|
+
|
|
101
|
+
// 3. Evaluate props for template HTML generation, skipping the children getter.
|
|
102
|
+
// Use untrack() so signal reads don't contaminate the caller's effect tracking.
|
|
103
|
+
const unwrappedProps = untrack(() => {
|
|
104
|
+
const result: Record<string, unknown> = {}
|
|
105
|
+
for (const k of Object.keys(props)) {
|
|
106
|
+
if (k === 'children' && childrenIsGetter) {
|
|
107
|
+
result.children = '' // Deferred — will be inserted after initFn
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
const descriptor = Object.getOwnPropertyDescriptor(props, k)
|
|
111
|
+
if (descriptor && typeof descriptor.get === 'function') {
|
|
112
|
+
result[k] = descriptor.get()
|
|
113
|
+
} else {
|
|
114
|
+
result[k] = props[k]
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Template functions expect children as an HTML string, not an array.
|
|
118
|
+
if (Array.isArray(result.children) && !hasDomElements(result.children)) {
|
|
119
|
+
result.children = (result.children as unknown[])
|
|
120
|
+
.flat()
|
|
121
|
+
.map(c => c == null ? '' : String(c))
|
|
122
|
+
.join('')
|
|
123
|
+
}
|
|
124
|
+
return result
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// 4. Generate HTML from props.
|
|
128
|
+
//
|
|
129
|
+
// Thread `slot.parent` into `_parentScopeId` so any hoisted-children
|
|
130
|
+
// placeholder (#1320) resolves to the calling site's scope.
|
|
131
|
+
const prevParentScopeId = _parentScopeId
|
|
132
|
+
if (slot?.parent) {
|
|
133
|
+
_parentScopeId = slot.parent
|
|
134
|
+
}
|
|
135
|
+
let html: string
|
|
136
|
+
try {
|
|
137
|
+
html = templateFn(unwrappedProps)
|
|
138
|
+
} finally {
|
|
139
|
+
_parentScopeId = prevParentScopeId
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 5. Create DOM element
|
|
143
|
+
const element = parseHTML(html.trim()).firstChild as HTMLElement
|
|
144
|
+
|
|
145
|
+
if (!element) {
|
|
146
|
+
console.warn(`[BarefootJS] Template returned empty HTML for component: ${name}`)
|
|
147
|
+
return createPlaceholder(name, key)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 6. Set scope ID and key attributes.
|
|
151
|
+
//
|
|
152
|
+
// `comment: true` components (synthesized inline-JSX-callback wrappers
|
|
153
|
+
// from #1211) render as transparent shells — the parsed `firstChild` is
|
|
154
|
+
// already the inner component's root with its own bf-s. Don't overwrite
|
|
155
|
+
// it, or `$c(__scope, 's0')` from the wrapper's init resolves to null.
|
|
156
|
+
const def = getRegisteredDef(name)
|
|
157
|
+
const isCommentWrapper = def?.comment === true
|
|
158
|
+
if (!isCommentWrapper) {
|
|
159
|
+
element.setAttribute(BF_SCOPE, `${name}_${generateId()}`)
|
|
160
|
+
}
|
|
161
|
+
if (slot) {
|
|
162
|
+
if (slot.parent) element.setAttribute(BF_HOST, slot.parent)
|
|
163
|
+
element.setAttribute(BF_AT, slot.mount)
|
|
164
|
+
}
|
|
165
|
+
if (key !== undefined) {
|
|
166
|
+
element.setAttribute(BF_KEY, String(key))
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// 7. Set currentScope so provideContext/useContext are element-scoped.
|
|
170
|
+
// This allows context providers in initFn to store context on this element.
|
|
171
|
+
const prevScope = setCurrentScope(element)
|
|
172
|
+
|
|
173
|
+
// 8. Initialize the component (context providers set up here).
|
|
174
|
+
const initFn = getComponentInit(name)
|
|
175
|
+
if (initFn) {
|
|
176
|
+
// Pass original props (with getters) for reactivity
|
|
177
|
+
initFn(element, props)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 9. Evaluate getter children and insert them.
|
|
181
|
+
// Children are evaluated NOW (after initFn) so that context provided by
|
|
182
|
+
// the parent is in the global store when children call useContext().
|
|
183
|
+
if (childrenIsGetter) {
|
|
184
|
+
const children = untrack(() => childrenDescriptor!.get!())
|
|
185
|
+
if (children != null) {
|
|
186
|
+
insertGetterChildren(element, children)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 10. Restore previous scope
|
|
191
|
+
setCurrentScope(prevScope)
|
|
192
|
+
|
|
193
|
+
// 11. Mark element as initialized
|
|
194
|
+
hydratedScopes.add(element)
|
|
195
|
+
|
|
196
|
+
// 12. Store props and register update function for element reuse in reconcileList
|
|
197
|
+
propsMap.set(element, props)
|
|
198
|
+
registerPropsUpdate(element, name, props)
|
|
199
|
+
|
|
200
|
+
return element
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get the props stored for a component element.
|
|
205
|
+
* Used by reconcileList to pass props to an existing element.
|
|
206
|
+
*/
|
|
207
|
+
export function getComponentProps(element: HTMLElement): Record<string, unknown> | undefined {
|
|
208
|
+
return propsMap.get(element)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Register a props update function for a component element.
|
|
213
|
+
* When called, this function re-initializes the component with new props.
|
|
214
|
+
*/
|
|
215
|
+
function registerPropsUpdate(
|
|
216
|
+
element: HTMLElement,
|
|
217
|
+
name: string,
|
|
218
|
+
_initialProps: Record<string, unknown>
|
|
219
|
+
): void {
|
|
220
|
+
// Register update function that will be called by reconcileList
|
|
221
|
+
propsUpdateMap.set(element, (newProps: Record<string, unknown>) => {
|
|
222
|
+
// Re-initialize the component with new props
|
|
223
|
+
// This allows the component to capture new values (e.g., todo with editing: true)
|
|
224
|
+
// and set up new effects that reference the new values
|
|
225
|
+
const init = getComponentInit(name)
|
|
226
|
+
if (init) {
|
|
227
|
+
init(element, newProps)
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Get the props update function for an element.
|
|
234
|
+
* Used by reconcileList to update props when reusing an element.
|
|
235
|
+
*/
|
|
236
|
+
export function getPropsUpdateFn(element: HTMLElement): ((props: Record<string, unknown>) => void) | undefined {
|
|
237
|
+
return propsUpdateMap.get(element)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Render a child component's template to an HTML string.
|
|
243
|
+
* Used by compiler-generated template functions when a stateless component
|
|
244
|
+
* appears inside a conditional branch or loop template.
|
|
245
|
+
*
|
|
246
|
+
* If the component has a registered template, it renders the HTML and injects
|
|
247
|
+
* a bf-s scope attribute. Otherwise, falls back to an empty placeholder.
|
|
248
|
+
*
|
|
249
|
+
* @param name - Component name (e.g., 'Spinner')
|
|
250
|
+
* @param props - Props to pass to the template
|
|
251
|
+
* @param key - Optional key for list reconciliation
|
|
252
|
+
* @returns HTML string with scope marker
|
|
253
|
+
*/
|
|
254
|
+
export function renderChild(
|
|
255
|
+
name: string,
|
|
256
|
+
props: Record<string, unknown>,
|
|
257
|
+
key?: string | number,
|
|
258
|
+
slotSuffix?: string
|
|
259
|
+
): string {
|
|
260
|
+
const templateFn = getTemplate(name)
|
|
261
|
+
const suffix = slotSuffix ? `_${slotSuffix}` : ''
|
|
262
|
+
// When inside an insert() branch template with a known parent scope,
|
|
263
|
+
// use the parent scope ID so child scope IDs match the SSR convention
|
|
264
|
+
// (e.g., ~ParentName_parentHash_s5 instead of ~Button_randomHash_s5).
|
|
265
|
+
// This enables $cSingle's getDualScopeIds verification to pass.
|
|
266
|
+
const scopePrefix = (_parentScopeId && slotSuffix)
|
|
267
|
+
? _parentScopeId
|
|
268
|
+
: `${name}_${generateId()}`
|
|
269
|
+
const keyAttr = key !== undefined ? ` ${BF_KEY}="${key}"` : ''
|
|
270
|
+
// Slot-relationship markers — only emitted when both host and slot are
|
|
271
|
+
// known; top-level renders without parent context omit them.
|
|
272
|
+
const slotAttrs = (_parentScopeId && slotSuffix)
|
|
273
|
+
? ` ${BF_HOST}="${_parentScopeId}" ${BF_AT}="${slotSuffix}"`
|
|
274
|
+
: ''
|
|
275
|
+
const bfsAttr = `${BF_SCOPE}="${scopePrefix}${suffix}"`
|
|
276
|
+
|
|
277
|
+
if (!templateFn) {
|
|
278
|
+
return `<div ${bfsAttr}${slotAttrs}${keyAttr}></div>`
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// The placeholder substitution is anchored to the exact `bf-s="…"`
|
|
282
|
+
// shape so user content that contains the sentinel as text survives
|
|
283
|
+
// unchanged. When `_parentScopeId` is null (top-level render) the
|
|
284
|
+
// attribute strips rather than emitting `bf-s=""`. (#1320)
|
|
285
|
+
let html = templateFn(props).trim().replace(
|
|
286
|
+
PLACEHOLDER_ATTR_PATTERN,
|
|
287
|
+
_parentScopeId ? ` bf-s="${_parentScopeId}"` : '',
|
|
288
|
+
)
|
|
289
|
+
// Templates may start with comment markers (e.g. <!--bf-cond-start:...-->)
|
|
290
|
+
// so we find the first element tag rather than assuming index 0.
|
|
291
|
+
const firstElMatch = html.match(/<(\w+)/)
|
|
292
|
+
if (!firstElMatch) return html
|
|
293
|
+
const insertPos = firstElMatch.index!
|
|
294
|
+
// Dedupe `bf-s` only when the template body's root already carries
|
|
295
|
+
// one (the body was itself a renderChild call). Still inject
|
|
296
|
+
// `slotAttrs` / `keyAttr` — `data-key` is the reconciliation
|
|
297
|
+
// contract `mapArray` reads, and `bf-h` / `bf-m` mark child
|
|
298
|
+
// membership in the parent scope. (#1320)
|
|
299
|
+
const afterInsert = html.slice(insertPos)
|
|
300
|
+
const extraAttrs = `${slotAttrs}${keyAttr}`
|
|
301
|
+
if (ROOT_HAS_BFS_PATTERN.test(afterInsert)) {
|
|
302
|
+
if (!extraAttrs) return html
|
|
303
|
+
return html.slice(0, insertPos) +
|
|
304
|
+
afterInsert.replace(/^(<\w+)/, `$1${extraAttrs}`)
|
|
305
|
+
}
|
|
306
|
+
return html.slice(0, insertPos) +
|
|
307
|
+
afterInsert.replace(/^(<\w+)/, `$1 ${bfsAttr}${extraAttrs}`)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// The leading `\s+` is part of the match so dropping the attribute
|
|
311
|
+
// doesn't leave a dangling space; the compiler always emits the
|
|
312
|
+
// placeholder preceded by whitespace from an enclosing tag.
|
|
313
|
+
const PLACEHOLDER_ATTR_PATTERN = new RegExp(`\\s+bf-s="${BF_PARENT_SCOPE_PLACEHOLDER}"`, 'g')
|
|
314
|
+
const ROOT_HAS_BFS_PATTERN = /^<\w+[^>]*\sbf-s="/
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Generate a random ID for scope identification
|
|
318
|
+
*/
|
|
319
|
+
function generateId(): string {
|
|
320
|
+
return Math.random().toString(36).slice(2, 8)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Create a placeholder element when template is not found
|
|
325
|
+
*/
|
|
326
|
+
function createPlaceholder(name: string, key?: string | number): HTMLElement {
|
|
327
|
+
const el = document.createElement('div')
|
|
328
|
+
el.setAttribute(BF_SCOPE, `${name}_placeholder`)
|
|
329
|
+
if (key !== undefined) {
|
|
330
|
+
el.setAttribute(BF_KEY, String(key))
|
|
331
|
+
}
|
|
332
|
+
el.textContent = `[${name}]`
|
|
333
|
+
el.style.cssText = 'color: red; border: 1px dashed red; padding: 4px;'
|
|
334
|
+
return el
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Unwrap getter props to plain values for template rendering.
|
|
339
|
+
* Template functions need actual values, not getter functions.
|
|
340
|
+
*
|
|
341
|
+
* @param props - Props object (may contain getters)
|
|
342
|
+
* @returns Plain object with unwrapped values
|
|
343
|
+
*/
|
|
344
|
+
function unwrapPropsForTemplate(props: Record<string, unknown>): Record<string, unknown> {
|
|
345
|
+
const result: Record<string, unknown> = {}
|
|
346
|
+
|
|
347
|
+
for (const key of Object.keys(props)) {
|
|
348
|
+
const descriptor = Object.getOwnPropertyDescriptor(props, key)
|
|
349
|
+
|
|
350
|
+
if (descriptor && typeof descriptor.get === 'function') {
|
|
351
|
+
// It's a getter - call it to get the value
|
|
352
|
+
result[key] = descriptor.get()
|
|
353
|
+
} else {
|
|
354
|
+
// Regular property
|
|
355
|
+
result[key] = props[key]
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Template functions expect children as an HTML string, not an array.
|
|
360
|
+
// Join non-DOM array children to avoid Array.toString() inserting commas.
|
|
361
|
+
if (Array.isArray(result.children) && !hasDomElements(result.children)) {
|
|
362
|
+
result.children = (result.children as unknown[])
|
|
363
|
+
.flat()
|
|
364
|
+
.map(c => c == null ? '' : String(c))
|
|
365
|
+
.join('')
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return result
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Escape ">" inside HTML attribute values to prevent broken parsing.
|
|
373
|
+
* UnoCSS classes like has-[>svg]:shrink-0 contain ">" which terminates
|
|
374
|
+
* the opening tag when parsed via innerHTML. The browser decodes >
|
|
375
|
+
* back to ">" in the DOM attribute value, preserving CSS matching.
|
|
376
|
+
*/
|
|
377
|
+
/**
|
|
378
|
+
* Escape ">" inside HTML attribute values to prevent broken parsing.
|
|
379
|
+
* UnoCSS classes like has-[>svg]:shrink-0 contain ">" which terminates
|
|
380
|
+
* the opening tag when parsed via innerHTML. The browser decodes >
|
|
381
|
+
* back to ">" in the DOM attribute value, preserving CSS matching.
|
|
382
|
+
*/
|
|
383
|
+
export function escapeAttrGt(html: string): string {
|
|
384
|
+
return html.replace(/"[^"]*"/g, match => match.replace(/>/g, '>'))
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const SVG_NS = 'http://www.w3.org/2000/svg'
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Parse an HTML string into a DocumentFragment, safely escaping ">" in
|
|
391
|
+
* attribute values. All code that sets innerHTML on dynamic HTML should
|
|
392
|
+
* use this instead of raw innerHTML assignment.
|
|
393
|
+
*
|
|
394
|
+
* When `parent` is provided and lives in the SVG namespace, the markup
|
|
395
|
+
* is parsed under SVG foreign-content context by wrapping it in
|
|
396
|
+
* `<svg>...</svg>`; the wrapper's children are moved into the returned
|
|
397
|
+
* fragment so callers see the same shape as the HTML path. Without
|
|
398
|
+
* this, dynamically-inserted SVG elements (e.g., a `<path>` in a
|
|
399
|
+
* conditional drag preview) end up as `HTMLUnknownElement` in the
|
|
400
|
+
* xhtml namespace and the SVG renderer ignores them. Surfaced by the
|
|
401
|
+
* Graph/DAG Editor block (#135).
|
|
402
|
+
*/
|
|
403
|
+
export function parseHTML(html: string, parent?: Element | null): DocumentFragment {
|
|
404
|
+
const tpl = document.createElement('template')
|
|
405
|
+
const escaped = escapeAttrGt(html)
|
|
406
|
+
if (parent && parent.namespaceURI === SVG_NS) {
|
|
407
|
+
tpl.innerHTML = `<svg>${escaped}</svg>`
|
|
408
|
+
const wrapper = tpl.content.firstElementChild
|
|
409
|
+
const frag = document.createDocumentFragment()
|
|
410
|
+
if (wrapper) {
|
|
411
|
+
while (wrapper.firstChild) frag.appendChild(wrapper.firstChild)
|
|
412
|
+
}
|
|
413
|
+
return frag
|
|
414
|
+
}
|
|
415
|
+
tpl.innerHTML = escaped
|
|
416
|
+
return tpl.content
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Check if a value contains DOM elements (HTMLElement instances).
|
|
421
|
+
*/
|
|
422
|
+
function hasDomElements(value: unknown): boolean {
|
|
423
|
+
if (value instanceof Element) return true
|
|
424
|
+
if (Array.isArray(value)) return value.some(hasDomElements)
|
|
425
|
+
return false
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Insert getter children into an element.
|
|
431
|
+
* Unlike insertDomChildren, strings are parsed as HTML (not text nodes) because
|
|
432
|
+
* getter children may return HTML strings from compiler-generated template literals
|
|
433
|
+
* (e.g. `<span class="...">Required</span>`).
|
|
434
|
+
* Arrays may contain a mix of DOM elements and HTML strings.
|
|
435
|
+
*/
|
|
436
|
+
function insertGetterChildren(element: HTMLElement, children: unknown): void {
|
|
437
|
+
if (children instanceof Element) {
|
|
438
|
+
element.appendChild(children)
|
|
439
|
+
} else if (Array.isArray(children)) {
|
|
440
|
+
for (const child of (children as unknown[]).flat()) {
|
|
441
|
+
if (child instanceof Element) {
|
|
442
|
+
element.appendChild(child)
|
|
443
|
+
} else if (typeof child === 'string' && child.length > 0) {
|
|
444
|
+
element.appendChild(parseHTML(child.trim()))
|
|
445
|
+
} else if (typeof child === 'number') {
|
|
446
|
+
element.appendChild(document.createTextNode(String(child)))
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
} else if (typeof children === 'string' && (children as string).length > 0) {
|
|
450
|
+
element.appendChild(parseHTML((children as string).trim()))
|
|
451
|
+
} else if (typeof children === 'number') {
|
|
452
|
+
element.appendChild(document.createTextNode(String(children)))
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Create a component instance from a ComponentDef (CSR mode).
|
|
458
|
+
* Does not use the component registry — the def is passed directly.
|
|
459
|
+
*/
|
|
460
|
+
function createComponentFromDef(
|
|
461
|
+
def: ComponentDef,
|
|
462
|
+
props: Record<string, unknown>,
|
|
463
|
+
key?: string | number
|
|
464
|
+
): HTMLElement {
|
|
465
|
+
if (!def.template) {
|
|
466
|
+
throw new Error('[BarefootJS] createComponent with ComponentDef requires a template function')
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Generate HTML from template
|
|
470
|
+
const unwrappedProps = unwrapPropsForTemplate(props)
|
|
471
|
+
const html = def.template(unwrappedProps)
|
|
472
|
+
|
|
473
|
+
// Create DOM element
|
|
474
|
+
const element = parseHTML(html.trim()).firstChild as HTMLElement
|
|
475
|
+
|
|
476
|
+
if (!element) {
|
|
477
|
+
const el = document.createElement('div')
|
|
478
|
+
el.textContent = '[ComponentDef]'
|
|
479
|
+
el.style.cssText = 'color: red; border: 1px dashed red; padding: 4px;'
|
|
480
|
+
return el
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Set scope ID and key
|
|
484
|
+
const name = def.name || def.init.name?.replace(/^init/, '') || 'Component'
|
|
485
|
+
const scopeId = `${name}_${generateId()}`
|
|
486
|
+
element.setAttribute(BF_SCOPE, scopeId)
|
|
487
|
+
if (key !== undefined) {
|
|
488
|
+
element.setAttribute(BF_KEY, String(key))
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Initialize
|
|
492
|
+
def.init(element, props)
|
|
493
|
+
|
|
494
|
+
// Mark as initialized
|
|
495
|
+
hydratedScopes.add(element)
|
|
496
|
+
|
|
497
|
+
// Store props for element reuse
|
|
498
|
+
propsMap.set(element, props)
|
|
499
|
+
|
|
500
|
+
return element
|
|
501
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context API: DOM-bound runtime portion.
|
|
3
|
+
*
|
|
4
|
+
* `useContext` and `provideContext` walk the DOM (scope-based) to locate
|
|
5
|
+
* the nearest provider. Portal elements (with bf-po attribute) follow
|
|
6
|
+
* the logical owner chain.
|
|
7
|
+
*
|
|
8
|
+
* A global store is kept as a fallback for non-scoped usage.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { BF_PORTAL_OWNER, BF_SCOPE } from '@barefootjs/shared'
|
|
12
|
+
import type { Context } from '../context'
|
|
13
|
+
|
|
14
|
+
export { createContext, type Context } from '../context'
|
|
15
|
+
|
|
16
|
+
/** Global fallback store for contexts without a DOM scope. */
|
|
17
|
+
const contextStore = new Map<symbol, unknown>()
|
|
18
|
+
|
|
19
|
+
/** Property key for context data stored on DOM elements. */
|
|
20
|
+
const CONTEXT_KEY = '__bfCtx'
|
|
21
|
+
|
|
22
|
+
/** Current scope element, set by initChild during component initialization. */
|
|
23
|
+
let currentScope: Element | null = null
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Set the current scope element for context operations.
|
|
27
|
+
* Called by initChild to scope provideContext/useContext to the correct element.
|
|
28
|
+
* Returns the previous scope for restoration.
|
|
29
|
+
*/
|
|
30
|
+
export function setCurrentScope(scope: Element | null): Element | null {
|
|
31
|
+
const prev = currentScope
|
|
32
|
+
currentScope = scope
|
|
33
|
+
return prev
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Read the current value of a context.
|
|
38
|
+
*
|
|
39
|
+
* Walks up the DOM tree from the current scope element to find
|
|
40
|
+
* the nearest ancestor that provided this context. Falls back to
|
|
41
|
+
* the global store, then to the context's default value, then to
|
|
42
|
+
* `undefined`.
|
|
43
|
+
*
|
|
44
|
+
* Returning `undefined` (rather than throwing) when no provider is
|
|
45
|
+
* available lets templates evaluate safely before init has run
|
|
46
|
+
* `provideContext` — init's `createEffect` repaints once the
|
|
47
|
+
* provider is set up. See piconic-ai/barefootjs#1156.
|
|
48
|
+
*/
|
|
49
|
+
export function useContext<T>(context: Context<T>): T {
|
|
50
|
+
// Walk DOM ancestors from current scope to find nearest provider.
|
|
51
|
+
// For portal elements (bf-po attribute), follow the logical owner
|
|
52
|
+
// chain back to the original parent scope.
|
|
53
|
+
if (currentScope) {
|
|
54
|
+
let el: Element | null = currentScope
|
|
55
|
+
while (el) {
|
|
56
|
+
const ctxMap = (el as any)[CONTEXT_KEY] as Map<symbol, unknown> | undefined
|
|
57
|
+
if (ctxMap?.has(context.id)) {
|
|
58
|
+
return ctxMap.get(context.id) as T
|
|
59
|
+
}
|
|
60
|
+
// Follow portal owner chain: if this element has bf-po, jump to the owner scope
|
|
61
|
+
const portalOwnerId: string | null = el.getAttribute(BF_PORTAL_OWNER)
|
|
62
|
+
if (portalOwnerId) {
|
|
63
|
+
const ownerEl: Element | null = document.querySelector(`[${BF_SCOPE}="${portalOwnerId}"]`)
|
|
64
|
+
if (ownerEl && ownerEl !== el) {
|
|
65
|
+
el = ownerEl
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
el = el.parentElement
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (contextStore.has(context.id)) {
|
|
73
|
+
return contextStore.get(context.id) as T
|
|
74
|
+
}
|
|
75
|
+
return context.defaultValue as T
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Provide a value for a context.
|
|
80
|
+
*
|
|
81
|
+
* Stores the value on the current scope DOM element so that child
|
|
82
|
+
* components can find it via useContext's DOM ancestor walk.
|
|
83
|
+
* Also sets the global store as fallback.
|
|
84
|
+
*/
|
|
85
|
+
export function provideContext<T>(context: Context<T>, value: T): void {
|
|
86
|
+
if (currentScope) {
|
|
87
|
+
let ctxMap = (currentScope as any)[CONTEXT_KEY] as Map<symbol, unknown> | undefined
|
|
88
|
+
if (!ctxMap) {
|
|
89
|
+
ctxMap = new Map()
|
|
90
|
+
;(currentScope as any)[CONTEXT_KEY] = ctxMap
|
|
91
|
+
}
|
|
92
|
+
ctxMap.set(context.id, value)
|
|
93
|
+
|
|
94
|
+
// Propagate context to child scope elements so portal-moved children
|
|
95
|
+
// can find it via DOM ancestor walk. At provideContext time, children
|
|
96
|
+
// are still in their original SSR positions (portals haven't moved them yet).
|
|
97
|
+
const childScopes = currentScope.querySelectorAll(`[${BF_SCOPE}]`)
|
|
98
|
+
for (const child of childScopes) {
|
|
99
|
+
let childCtxMap = (child as any)[CONTEXT_KEY] as Map<symbol, unknown> | undefined
|
|
100
|
+
if (!childCtxMap) {
|
|
101
|
+
childCtxMap = new Map()
|
|
102
|
+
;(child as any)[CONTEXT_KEY] = childCtxMap
|
|
103
|
+
}
|
|
104
|
+
// Only set if not already provided (don't override nested providers)
|
|
105
|
+
if (!childCtxMap.has(context.id)) {
|
|
106
|
+
childCtxMap.set(context.id, value)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
contextStore.set(context.id, value)
|
|
111
|
+
}
|