Skip to content

Commit 91f752a

Browse files
committed
WIP RC.7
1 parent 421724a commit 91f752a

File tree

15 files changed

+765
-652
lines changed

15 files changed

+765
-652
lines changed

library/src/bundles/datastar-aliased.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export { action, actions, attribute, watcher } from '@engine'
2-
export { morph } from '@engine/morph'
32
export {
43
beginBatch,
54
computed,
@@ -25,11 +24,11 @@ import '@plugins/attributes/class'
2524
import '@plugins/attributes/computed'
2625
import '@plugins/attributes/effect'
2726
import '@plugins/attributes/indicator'
27+
import '@plugins/attributes/init'
2828
import '@plugins/attributes/jsonSignals'
2929
import '@plugins/attributes/on'
3030
import '@plugins/attributes/onIntersect'
3131
import '@plugins/attributes/onInterval'
32-
import '@plugins/attributes/init'
3332
import '@plugins/attributes/onSignalPatch'
3433
import '@plugins/attributes/ref'
3534
import '@plugins/attributes/show'

library/src/bundles/datastar-core.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export { action, actions, attribute, watcher } from '@engine'
2-
export { morph } from '@engine/morph'
32
export {
43
beginBatch,
54
computed,

library/src/bundles/datastar.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export { action, actions, attribute, watcher } from '@engine'
2-
export { morph } from '@engine/morph'
32
export {
43
beginBatch,
54
computed,
@@ -25,11 +24,11 @@ import '@plugins/attributes/class'
2524
import '@plugins/attributes/computed'
2625
import '@plugins/attributes/effect'
2726
import '@plugins/attributes/indicator'
27+
import '@plugins/attributes/init'
2828
import '@plugins/attributes/jsonSignals'
2929
import '@plugins/attributes/on'
3030
import '@plugins/attributes/onIntersect'
3131
import '@plugins/attributes/onInterval'
32-
import '@plugins/attributes/init'
3332
import '@plugins/attributes/onSignalPatch'
3433
import '@plugins/attributes/ref'
3534
import '@plugins/attributes/show'

library/src/engine/engine.ts

Lines changed: 91 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import { DATASTAR_FETCH_EVENT, DSP, DSS } from '@engine/consts'
2-
import { snake } from '@utils/text'
32
import { root } from '@engine/signals'
43
import type {
5-
ActionPlugin,
64
ActionContext,
5+
ActionPlugin,
76
AttributeContext,
87
AttributePlugin,
98
DatastarFetchEvent,
109
HTMLOrSVG,
10+
Modifiers,
1111
Requirement,
1212
WatcherPlugin,
1313
} from '@engine/types'
1414
import { isHTMLOrSVG } from '@utils/dom'
15-
import { aliasify } from '@utils/text'
15+
import { aliasify, snake } from '@utils/text'
1616

1717
const url = 'https://data-star.dev/errors'
1818

@@ -50,11 +50,12 @@ export const actions: Record<
5050
},
5151
)
5252

53-
// Map of cleanups keyed by element and attribute name
54-
const removals = new Map<HTMLOrSVG, Map<string, () => void>>()
53+
// Map of cleanups keyed by element, attribute name, and cleanup name
54+
const removals = new Map<HTMLOrSVG, Map<string, Map<string, () => void>>>()
5555

5656
const queuedAttributes: AttributePlugin[] = []
5757
const queuedAttributeNames = new Set<string>()
58+
const observedRoots = new WeakSet<Node>()
5859
export const attribute = <R extends Requirement, B extends boolean>(
5960
plugin: AttributePlugin<R, B>,
6061
): void => {
@@ -103,13 +104,13 @@ export const watcher = (plugin: WatcherPlugin): void => {
103104

104105
const cleanupEls = (els: Iterable<HTMLOrSVG>): void => {
105106
for (const el of els) {
106-
const cleanups = removals.get(el)
107-
// If removals has el, delete it and run all cleanup functions
108-
if (removals.delete(el)) {
109-
for (const cleanup of cleanups!.values()) {
110-
cleanup()
107+
const elCleanups = removals.get(el)
108+
if (elCleanups && removals.delete(el)) {
109+
for (const attrCleanups of elCleanups.values()) {
110+
for (const cleanup of attrCleanups.values()) {
111+
cleanup()
112+
}
111113
}
112-
cleanups!.clear()
113114
}
114115
}
115116
}
@@ -166,10 +167,15 @@ const observe = (mutations: MutationRecord[]) => {
166167
const key = attributeName!.slice(5)
167168
const value = target.getAttribute(attributeName!)
168169
if (value === null) {
169-
const cleanups = removals.get(target)
170-
if (cleanups) {
171-
cleanups.get(key)?.()
172-
cleanups.delete(key)
170+
const elCleanups = removals.get(target)
171+
if (elCleanups) {
172+
const attrCleanups = elCleanups.get(key)
173+
if (attrCleanups) {
174+
for (const cleanup of attrCleanups.values()) {
175+
cleanup()
176+
}
177+
elCleanups.delete(key)
178+
}
173179
}
174180
} else {
175181
applyAttributePlugin(target, key, value)
@@ -181,19 +187,45 @@ const observe = (mutations: MutationRecord[]) => {
181187
// TODO: mutation observer per root so applying to web component doesnt overwrite main observer
182188
const mutationObserver = new MutationObserver(observe)
183189

190+
export const parseAttributeKey = (
191+
rawKey: string,
192+
): {
193+
pluginName: string
194+
key: string | undefined
195+
mods: Modifiers
196+
} => {
197+
const [namePart, ...rawModifiers] = rawKey.split('__')
198+
const [pluginName, key] = namePart.split(/:(.+)/)
199+
const mods: Modifiers = new Map()
200+
201+
for (const rawMod of rawModifiers) {
202+
const [label, ...mod] = rawMod.split('.')
203+
mods.set(label, new Set(mod))
204+
}
205+
206+
return { pluginName, key, mods }
207+
}
208+
209+
export const isDocumentObserverActive = () =>
210+
observedRoots.has(document.documentElement)
211+
184212
export const apply = (
185213
root: HTMLOrSVG | ShadowRoot = document.documentElement,
214+
observeRoot = true,
186215
): void => {
187216
if (isHTMLOrSVG(root)) {
188217
applyEls([root], true)
189218
}
190219
applyEls(root.querySelectorAll<HTMLOrSVG>('*'), true)
191220

192-
mutationObserver.observe(root, {
193-
subtree: true,
194-
childList: true,
195-
attributes: true,
196-
})
221+
if (observeRoot) {
222+
mutationObserver.observe(root, {
223+
subtree: true,
224+
childList: true,
225+
attributes: true,
226+
})
227+
observedRoots.add(root)
228+
}
197229
}
198230

199231
const applyAttributePlugin = (
@@ -204,21 +236,24 @@ const applyAttributePlugin = (
204236
): void => {
205237
if (!ALIAS || attrKey.startsWith(`${ALIAS}-`)) {
206238
const rawKey = ALIAS ? attrKey.slice(ALIAS.length + 1) : attrKey
207-
const [namePart, ...rawModifiers] = rawKey.split('__')
208-
const [pluginName, key] = namePart.split(/:(.+)/)
239+
const { pluginName, key, mods } = parseAttributeKey(rawKey)
209240
const plugin = attributePlugins.get(pluginName)
210241
if ((!onlyNew || queuedAttributeNames.has(pluginName)) && plugin) {
211242
const ctx = {
212243
el,
213244
rawKey,
214-
mods: new Map(),
245+
mods,
215246
error: error.bind(0, {
216247
plugin: { type: 'attribute', name: plugin.name },
217248
element: { id: el.id, tag: el.tagName },
218249
expression: { rawKey, key, value },
219250
}),
220251
key,
221252
value,
253+
loadedPluginNames: {
254+
actions: new Set(actionPlugins.keys()),
255+
attributes: new Set(attributePlugins.keys()),
256+
},
222257
rx: undefined,
223258
} as AttributeContext
224259

@@ -235,15 +270,19 @@ const applyAttributePlugin = (
235270
: plugin.requirement.value)) ||
236271
'allowed'
237272

238-
if (key) {
273+
const keyProvided = key !== undefined && key !== null && key !== ''
274+
const valueProvided =
275+
value !== undefined && value !== null && value !== ''
276+
277+
if (keyProvided) {
239278
if (keyReq === 'denied') {
240279
throw ctx.error('KeyNotAllowed')
241280
}
242281
} else if (keyReq === 'must') {
243282
throw ctx.error('KeyRequired')
244283
}
245284

246-
if (value) {
285+
if (valueProvided) {
247286
if (valueReq === 'denied') {
248287
throw ctx.error('ValueNotAllowed')
249288
}
@@ -252,57 +291,66 @@ const applyAttributePlugin = (
252291
}
253292

254293
if (keyReq === 'exclusive' || valueReq === 'exclusive') {
255-
if (key && value) {
294+
if (keyProvided && valueProvided) {
256295
throw ctx.error('KeyAndValueProvided')
257296
}
258-
if (!key && !value) {
297+
if (!keyProvided && !valueProvided) {
259298
throw ctx.error('KeyOrValueRequired')
260299
}
261300
}
262301

263-
if (value) {
302+
const cleanups = new Map<string, () => void>()
303+
if (valueProvided) {
264304
let cachedRx: GenRxFn
265305
ctx.rx = (...args: any[]) => {
266306
if (!cachedRx) {
267307
cachedRx = genRx(value, {
268308
returnsValue: plugin.returnsValue,
269309
argNames: plugin.argNames,
310+
cleanups,
270311
})
271312
}
272313
return cachedRx(el, ...args)
273314
}
274315
}
275316

276-
for (const rawMod of rawModifiers) {
277-
const [label, ...mod] = rawMod.split('.')
278-
ctx.mods.set(label, new Set(mod))
279-
}
280-
281317
const cleanup = plugin.apply(ctx)
282318
if (cleanup) {
283-
let cleanups = removals.get(el)
284-
if (cleanups) {
285-
cleanups.get(rawKey)?.()
286-
} else {
287-
cleanups = new Map()
288-
removals.set(el, cleanups)
319+
cleanups.set('attribute', cleanup)
320+
}
321+
322+
let elCleanups = removals.get(el)
323+
if (elCleanups) {
324+
const attrCleanups = elCleanups.get(rawKey)
325+
if (attrCleanups) {
326+
for (const oldCleanup of attrCleanups.values()) {
327+
oldCleanup()
328+
}
289329
}
290-
cleanups.set(rawKey, cleanup)
330+
} else {
331+
elCleanups = new Map()
332+
removals.set(el, elCleanups)
291333
}
334+
elCleanups.set(rawKey, cleanups)
292335
}
293336
}
294337
}
295338

296339
type GenRxOptions = {
297340
returnsValue?: boolean
298341
argNames?: string[]
342+
cleanups?: Map<string, () => void>
299343
}
300344

301345
type GenRxFn = <T>(el: HTMLOrSVG, ...args: any[]) => T
302346

303347
const genRx = (
304348
value: string,
305-
{ returnsValue = false, argNames = [] }: GenRxOptions = {},
349+
{
350+
returnsValue = false,
351+
argNames = [],
352+
cleanups = new Map(),
353+
}: GenRxOptions = {},
306354
): GenRxFn => {
307355
let expr = ''
308356
if (returnsValue) {
@@ -376,13 +424,8 @@ const genRx = (
376424
.split('.')
377425
.reduce((acc: string, part: string) => `${acc}['${part}']`, '$'),
378426
)
379-
// [$x] -> [$['x']] ($ inside brackets)
380-
.replace(
381-
/\[(\$[a-zA-Z_\d]\w*)\]/g,
382-
(_, varName) => `[$['${varName.slice(1)}']]`,
383-
)
384427

385-
expr = expr.replaceAll(/@(\w+)\(/g, '__action("$1",evt,')
428+
expr = expr.replaceAll(/@([A-Za-z_$][\w$]*)\(/g, '__action("$1",evt,')
386429

387430
// Replace any escaped values
388431
for (const [k, v] of escaped) {
@@ -408,6 +451,7 @@ const genRx = (
408451
el,
409452
evt,
410453
error: err,
454+
cleanups,
411455
},
412456
...args,
413457
)

0 commit comments

Comments
 (0)