@plastic-js/plastic 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +442 -0
- package/package.json +78 -0
- package/src/computation-context.js +11 -0
- package/src/control-flow.js +367 -0
- package/src/index.js +87 -0
- package/src/jsx-runtime.js +1058 -0
- package/src/merge-props.js +245 -0
- package/src/reactivity.js +408 -0
- package/src/router.js +919 -0
- package/src/split-props.js +42 -0
- package/src/utils.js +51 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { createSignal, runUntracked } from './reactivity.js'
|
|
2
|
+
import { getCurrentComputation, setCurrentComputation } from './computation-context.js'
|
|
3
|
+
|
|
4
|
+
const createControlFlow = ({
|
|
5
|
+
createOwner,
|
|
6
|
+
runOwnerMounts,
|
|
7
|
+
runWithOwner,
|
|
8
|
+
disposeOwner,
|
|
9
|
+
createBindingEffect,
|
|
10
|
+
renderInOwner,
|
|
11
|
+
getCurrentOwner,
|
|
12
|
+
registerCleanup,
|
|
13
|
+
appendChild,
|
|
14
|
+
flushPendingDescriptors,
|
|
15
|
+
})=> {
|
|
16
|
+
// Reactively replaces DOM content after a comment anchor.
|
|
17
|
+
// getContent() is called inside a binding effect - any signals it reads
|
|
18
|
+
// will trigger a branch switch when they change.
|
|
19
|
+
// On each switch: the previous branch owner is disposed (cleaning up all its
|
|
20
|
+
// effects and event listeners), its DOM nodes are removed, and the new branch
|
|
21
|
+
// is rendered in a fresh child owner.
|
|
22
|
+
const mountDynamic = (anchor, getContent)=> {
|
|
23
|
+
let prevNodes = []
|
|
24
|
+
let prevOwner = null
|
|
25
|
+
// Capture the Either component's owner at call time so branch owners are always
|
|
26
|
+
// parented correctly even when the effect re-runs outside component context.
|
|
27
|
+
const hostOwner = getCurrentOwner()
|
|
28
|
+
|
|
29
|
+
const update = ()=> {
|
|
30
|
+
if (prevOwner){
|
|
31
|
+
disposeOwner(prevOwner)
|
|
32
|
+
prevOwner = null
|
|
33
|
+
}
|
|
34
|
+
prevNodes.forEach(n=> n.remove())
|
|
35
|
+
prevNodes = []
|
|
36
|
+
|
|
37
|
+
const owner = createOwner(hostOwner)
|
|
38
|
+
const prevComp = getCurrentComputation()
|
|
39
|
+
setCurrentComputation(null)
|
|
40
|
+
// NOTE: unlike materializeComponentDescriptor / Loop's renderRow, we do
|
|
41
|
+
// NOT wrap these in runUntracked. getContent reads tracking signals
|
|
42
|
+
// (e.g. the condition for Either/Match) AND invokes lazy branch
|
|
43
|
+
// factories that materialize JSX in the same call. Detaching activeSub
|
|
44
|
+
// here would silence the condition subscription. The auto-parent-link
|
|
45
|
+
// pattern that bites Loop's reused rows is harmless here because
|
|
46
|
+
// disposeOwner(prevOwner) above pre-emptively severs the old branch's
|
|
47
|
+
// effects before each re-run, so purgeDeps has nothing stale to walk.
|
|
48
|
+
const result = runWithOwner(owner, getContent)
|
|
49
|
+
const node = renderInOwner(owner, result ?? null)
|
|
50
|
+
setCurrentComputation(prevComp)
|
|
51
|
+
|
|
52
|
+
// Collect child refs before insertion: DocumentFragment drains on append.
|
|
53
|
+
if (node instanceof DocumentFragment){
|
|
54
|
+
prevNodes = [...node.childNodes]
|
|
55
|
+
} else {
|
|
56
|
+
prevNodes = [node]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
anchor.after(node)
|
|
60
|
+
prevOwner = owner
|
|
61
|
+
|
|
62
|
+
// For reactive updates (anchor already in live DOM), trigger mount hooks now.
|
|
63
|
+
// For the initial render, renderApp will call runOwnerMounts on the full tree.
|
|
64
|
+
if (anchor.isConnected){
|
|
65
|
+
runOwnerMounts(owner)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const stop = createBindingEffect(update)
|
|
70
|
+
|
|
71
|
+
return ()=> {
|
|
72
|
+
if (typeof stop === 'function'){
|
|
73
|
+
stop()
|
|
74
|
+
}
|
|
75
|
+
if (prevOwner){
|
|
76
|
+
disposeOwner(prevOwner)
|
|
77
|
+
prevOwner = null
|
|
78
|
+
}
|
|
79
|
+
prevNodes.forEach(n=> n.remove())
|
|
80
|
+
prevNodes = []
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// <True> and <False> are transparent slot markers - they pass children through.
|
|
85
|
+
// The Babel plugin wraps them in lazy arrow functions before Either ever sees them,
|
|
86
|
+
// so the inactive branch is never evaluated until needed.
|
|
87
|
+
const True = ({ children })=> children
|
|
88
|
+
const False = ({ children })=> children
|
|
89
|
+
const Case = ({ children })=> children
|
|
90
|
+
const Default = ({ children })=> children
|
|
91
|
+
|
|
92
|
+
// <Either condition={...}>
|
|
93
|
+
// <True>...</True>
|
|
94
|
+
// <False>...</False>
|
|
95
|
+
// </Either>
|
|
96
|
+
//
|
|
97
|
+
// The Babel plugin transforms this into:
|
|
98
|
+
// <Either condition={...} trueBranch={() => <True>...</True>} falseBranch={() => <False>...</False>} />
|
|
99
|
+
// so branches are only rendered when active.
|
|
100
|
+
const Either = ({
|
|
101
|
+
condition, trueBranch, falseBranch,
|
|
102
|
+
})=> {
|
|
103
|
+
const anchor = document.createComment('if')
|
|
104
|
+
// Return a fragment so the anchor and initial branch content land in the
|
|
105
|
+
// parent as siblings. anchor.after() keeps working once in the live DOM.
|
|
106
|
+
const fragment = document.createDocumentFragment()
|
|
107
|
+
fragment.appendChild(anchor)
|
|
108
|
+
const activeTrue = trueBranch
|
|
109
|
+
const activeFalse = falseBranch
|
|
110
|
+
|
|
111
|
+
mountDynamic(anchor, ()=> {
|
|
112
|
+
const cond = typeof condition === 'function' ? condition() : condition
|
|
113
|
+
const branch = cond ? activeTrue : activeFalse
|
|
114
|
+
return branch ? branch() : null
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
return fragment
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// <Match value={...}>
|
|
121
|
+
// <Case when={...}>...</Case>
|
|
122
|
+
// <Default>...</Default>
|
|
123
|
+
// </Match>
|
|
124
|
+
//
|
|
125
|
+
// The Babel plugin rewrites Case/Default children into lazy factory props:
|
|
126
|
+
// cases=[{ when, branch: () => <Case>...</Case> }, ...]
|
|
127
|
+
// defaultBranch={() => <Default>...</Default>}
|
|
128
|
+
const Match = ({
|
|
129
|
+
value,
|
|
130
|
+
cases = [],
|
|
131
|
+
defaultBranch,
|
|
132
|
+
})=> {
|
|
133
|
+
const anchor = document.createComment('match')
|
|
134
|
+
const fragment = document.createDocumentFragment()
|
|
135
|
+
fragment.appendChild(anchor)
|
|
136
|
+
|
|
137
|
+
const resolve = (source)=> {
|
|
138
|
+
return typeof source === 'function' ? source() : source
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
mountDynamic(anchor, ()=> {
|
|
142
|
+
const activeCases = Array.isArray(cases) ? cases : []
|
|
143
|
+
const valueToMatch = resolve(value)
|
|
144
|
+
|
|
145
|
+
for (const slot of activeCases){
|
|
146
|
+
if (!slot || typeof slot !== 'object'){
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let matched = false
|
|
151
|
+
if (Object.prototype.hasOwnProperty.call(slot, 'when')){
|
|
152
|
+
matched = Object.is(valueToMatch, resolve(slot.when))
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!matched){
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return typeof slot.branch === 'function' ? slot.branch() : null
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return typeof defaultBranch === 'function' ? defaultBranch() : null
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
return fragment
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// <Loop each={items}>{(item, index) => ...}</Loop>
|
|
169
|
+
//
|
|
170
|
+
// Rows are tracked by identity (object reference / primitive value).
|
|
171
|
+
const Loop = ({
|
|
172
|
+
each,
|
|
173
|
+
children,
|
|
174
|
+
})=> {
|
|
175
|
+
const anchor = document.createComment('for')
|
|
176
|
+
const fragment = document.createDocumentFragment()
|
|
177
|
+
fragment.appendChild(anchor)
|
|
178
|
+
|
|
179
|
+
const hostOwner = getCurrentOwner()
|
|
180
|
+
let rows = []
|
|
181
|
+
|
|
182
|
+
const resolveList = ()=> {
|
|
183
|
+
const list = typeof each === 'function' ? each() : each
|
|
184
|
+
return Array.isArray(list) ? list : []
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const renderRow = (item, indexValue)=> {
|
|
188
|
+
const owner = createOwner(hostOwner)
|
|
189
|
+
const indexSignal = createSignal(indexValue)
|
|
190
|
+
const prevComp = getCurrentComputation()
|
|
191
|
+
setCurrentComputation(null)
|
|
192
|
+
// runUntracked mirrors materializeComponentDescriptor: the row's binding
|
|
193
|
+
// effects belong to its own owner, so they must not auto-link as deps of
|
|
194
|
+
// the surrounding Loop binding effect. Otherwise Loop's purgeDeps on re-run
|
|
195
|
+
// would unwatch them and sever their signal subscriptions.
|
|
196
|
+
const result = runUntracked(()=> runWithOwner(owner, ()=> {
|
|
197
|
+
if (typeof children !== 'function'){
|
|
198
|
+
return null
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return children(item, indexSignal)
|
|
202
|
+
}))
|
|
203
|
+
const node = runUntracked(()=> renderInOwner(owner, result))
|
|
204
|
+
setCurrentComputation(prevComp)
|
|
205
|
+
const nodes = node instanceof DocumentFragment ? [...node.childNodes] : [node]
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
owner,
|
|
209
|
+
indexSignal,
|
|
210
|
+
nodes,
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const mountRows = (nextRows)=> {
|
|
215
|
+
const nodeFragment = document.createDocumentFragment()
|
|
216
|
+
nextRows.forEach((row)=> {
|
|
217
|
+
row.nodes.forEach((node)=> {
|
|
218
|
+
nodeFragment.appendChild(node)
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
anchor.before(nodeFragment)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const reconcileRows = (nextItems)=> {
|
|
225
|
+
const prevRows = rows
|
|
226
|
+
const nextRows = new Array(nextItems.length)
|
|
227
|
+
const createdRows = []
|
|
228
|
+
const reusedPrevIndices = new Set()
|
|
229
|
+
|
|
230
|
+
// Build lookup: identity -> [prevIndex, ...] (buckets handle duplicates).
|
|
231
|
+
const prevRowsByIdentity = new Map()
|
|
232
|
+
prevRows.forEach((row, prevIndex)=> {
|
|
233
|
+
const bucket = prevRowsByIdentity.get(row.identity)
|
|
234
|
+
if (bucket){
|
|
235
|
+
bucket.push(prevIndex)
|
|
236
|
+
} else {
|
|
237
|
+
prevRowsByIdentity.set(row.identity, [prevIndex])
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// Match existing rows by identity.
|
|
242
|
+
nextItems.forEach((item, nextIndex)=> {
|
|
243
|
+
const bucket = prevRowsByIdentity.get(item)
|
|
244
|
+
if (!bucket || bucket.length === 0){
|
|
245
|
+
return
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const prevIndex = bucket.shift()
|
|
249
|
+
nextRows[nextIndex] = prevRows[prevIndex]
|
|
250
|
+
reusedPrevIndices.add(prevIndex)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// Create missing rows.
|
|
254
|
+
for (let nextIndex = 0; nextIndex < nextRows.length; nextIndex++){
|
|
255
|
+
const row = nextRows[nextIndex]
|
|
256
|
+
if (row){
|
|
257
|
+
row.indexSignal(nextIndex)
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const created = renderRow(nextItems[nextIndex], nextIndex)
|
|
262
|
+
created.identity = nextItems[nextIndex]
|
|
263
|
+
nextRows[nextIndex] = created
|
|
264
|
+
createdRows.push(created)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Dispose rows that were not reused.
|
|
268
|
+
prevRows.forEach((row, prevIndex)=> {
|
|
269
|
+
if (reusedPrevIndices.has(prevIndex)){
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
disposeOwner(row.owner)
|
|
274
|
+
row.nodes.forEach(node=> node.remove())
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
rows = nextRows
|
|
278
|
+
mountRows(rows)
|
|
279
|
+
|
|
280
|
+
if (anchor.isConnected){
|
|
281
|
+
createdRows.forEach((row)=> {
|
|
282
|
+
runOwnerMounts(row.owner)
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const stop = createBindingEffect(()=> {
|
|
288
|
+
const nextItems = resolveList()
|
|
289
|
+
reconcileRows(nextItems)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
registerCleanup(()=> {
|
|
293
|
+
if (typeof stop === 'function'){
|
|
294
|
+
stop()
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
rows.forEach((row)=> {
|
|
298
|
+
disposeOwner(row.owner)
|
|
299
|
+
row.nodes.forEach(node=> node.remove())
|
|
300
|
+
})
|
|
301
|
+
rows = []
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
return fragment
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// <Portal container={el}>...</Portal>
|
|
308
|
+
//
|
|
309
|
+
// Renders children into `container` (defaults to document.body) instead of
|
|
310
|
+
// the component's position in the tree. A comment node is left as a placeholder
|
|
311
|
+
// so the surrounding component tree is not disturbed.
|
|
312
|
+
// All child owners, effects, and event listeners are disposed when the host
|
|
313
|
+
// component unmounts via the registerCleanup call below.
|
|
314
|
+
const Portal = ({ container, children })=> {
|
|
315
|
+
const target = (typeof container === 'function' ? container() : container) ?? document.body
|
|
316
|
+
|
|
317
|
+
const hostOwner = getCurrentOwner()
|
|
318
|
+
const owner = createOwner(hostOwner)
|
|
319
|
+
|
|
320
|
+
const prevComp = getCurrentComputation()
|
|
321
|
+
setCurrentComputation(null)
|
|
322
|
+
const result = runWithOwner(owner, ()=> typeof children === 'function' ? children() : children)
|
|
323
|
+
const node = renderInOwner(owner, result ?? null)
|
|
324
|
+
setCurrentComputation(prevComp)
|
|
325
|
+
|
|
326
|
+
const childCountBefore = target.childNodes.length
|
|
327
|
+
|
|
328
|
+
if (node instanceof DocumentFragment && appendChild){
|
|
329
|
+
appendChild(target, node)
|
|
330
|
+
} else {
|
|
331
|
+
target.appendChild(node)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (flushPendingDescriptors){
|
|
335
|
+
flushPendingDescriptors(target)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const portalNodes = [...target.childNodes].slice(childCountBefore)
|
|
339
|
+
|
|
340
|
+
if (target.isConnected){
|
|
341
|
+
runOwnerMounts(owner)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
registerCleanup(()=> {
|
|
345
|
+
disposeOwner(owner)
|
|
346
|
+
portalNodes.forEach(n=> n.remove())
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
return document.createComment('portal')
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
mountDynamic,
|
|
354
|
+
Either,
|
|
355
|
+
True,
|
|
356
|
+
False,
|
|
357
|
+
Match,
|
|
358
|
+
Case,
|
|
359
|
+
Default,
|
|
360
|
+
Loop,
|
|
361
|
+
Portal,
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export {
|
|
366
|
+
createControlFlow,
|
|
367
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Case,
|
|
3
|
+
Default,
|
|
4
|
+
Dynamic,
|
|
5
|
+
Either,
|
|
6
|
+
False,
|
|
7
|
+
Fragment,
|
|
8
|
+
Loop,
|
|
9
|
+
Match,
|
|
10
|
+
Portal,
|
|
11
|
+
True,
|
|
12
|
+
createBindingEffect,
|
|
13
|
+
createComputed,
|
|
14
|
+
createContext,
|
|
15
|
+
createSignal,
|
|
16
|
+
createTree,
|
|
17
|
+
h,
|
|
18
|
+
jsx,
|
|
19
|
+
jsxs,
|
|
20
|
+
onMount,
|
|
21
|
+
registerCleanup,
|
|
22
|
+
renderApp,
|
|
23
|
+
useContext,
|
|
24
|
+
} from './jsx-runtime.js'
|
|
25
|
+
import {
|
|
26
|
+
Link,
|
|
27
|
+
NavLink,
|
|
28
|
+
Outlet,
|
|
29
|
+
Route,
|
|
30
|
+
Router,
|
|
31
|
+
lazy,
|
|
32
|
+
navigate,
|
|
33
|
+
useLocation,
|
|
34
|
+
useMatch,
|
|
35
|
+
useNavigate,
|
|
36
|
+
useNavigationState,
|
|
37
|
+
useParams,
|
|
38
|
+
useRoute,
|
|
39
|
+
useSearchParams,
|
|
40
|
+
} from './router.js'
|
|
41
|
+
import { batch } from './reactivity.js'
|
|
42
|
+
import { mergeProps } from './merge-props.js'
|
|
43
|
+
import { createSplitProps, splitProps } from './split-props.js'
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
Case,
|
|
47
|
+
Default,
|
|
48
|
+
Dynamic,
|
|
49
|
+
False,
|
|
50
|
+
createTree,
|
|
51
|
+
lazy,
|
|
52
|
+
Link,
|
|
53
|
+
NavLink,
|
|
54
|
+
navigate,
|
|
55
|
+
Loop,
|
|
56
|
+
Outlet,
|
|
57
|
+
Fragment,
|
|
58
|
+
Match,
|
|
59
|
+
Either,
|
|
60
|
+
Portal,
|
|
61
|
+
Route,
|
|
62
|
+
Router,
|
|
63
|
+
True,
|
|
64
|
+
useLocation,
|
|
65
|
+
useMatch,
|
|
66
|
+
useNavigate,
|
|
67
|
+
useNavigationState,
|
|
68
|
+
useParams,
|
|
69
|
+
useRoute,
|
|
70
|
+
useSearchParams,
|
|
71
|
+
h,
|
|
72
|
+
jsx,
|
|
73
|
+
jsxs,
|
|
74
|
+
onMount,
|
|
75
|
+
registerCleanup as onCleanup,
|
|
76
|
+
renderApp,
|
|
77
|
+
createBindingEffect as createEffect,
|
|
78
|
+
createComputed,
|
|
79
|
+
createSignal,
|
|
80
|
+
createContext,
|
|
81
|
+
useContext,
|
|
82
|
+
batch,
|
|
83
|
+
mergeProps,
|
|
84
|
+
splitProps,
|
|
85
|
+
createSplitProps,
|
|
86
|
+
}
|
|
87
|
+
|