Skip to content

Commit

Permalink
Observe DOM only after loading core plugins (#629)
Browse files Browse the repository at this point in the history
  • Loading branch information
bencroker authored Feb 8, 2025
1 parent 305b8ec commit 91f5f07
Show file tree
Hide file tree
Showing 8 changed files with 120 additions and 113 deletions.
16 changes: 8 additions & 8 deletions bundles/datastar-aliased.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions bundles/datastar-aliased.js.map

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions bundles/datastar-core.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions bundles/datastar-core.js.map

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions bundles/datastar.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions bundles/datastar.js.map

Large diffs are not rendered by default.

169 changes: 88 additions & 81 deletions library/src/engine/engine.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Hash, elUniqId } from '../utils/dom'
import { camel } from '../utils/text'
import { debounce } from '../utils/timing'
import { delay } from '../utils/timing'
import { effect } from '../vendored/preact-core'
import { DSP, DSS } from './consts'
import { initErr, runtimeErr } from './errors'
Expand Down Expand Up @@ -29,94 +29,18 @@ export class Engine {
#plugins: AttributePlugin[] = []
#actions: ActionPlugins = {}
#watchers: WatcherPlugin[] = []
#mutationObserver: MutationObserver | null = null

// Map of cleanup functions by element, keyed by the dataset key and value
#removals = new Map<Element, Map<string, OnRemovalFn>>()

constructor() {
const datasetPrefix = 'data-'

const ob = new MutationObserver((mutations) => {
for (const {
target,
type,
attributeName,
oldValue,
addedNodes,
removedNodes,
} of mutations) {
switch (type) {
case 'childList':
{
for (const node of removedNodes) {
const el = node as HTMLorSVGElement
const elRemovals = this.#removals.get(el)
if (!elRemovals) continue

for (const [_, removalFn] of elRemovals) {
removalFn()
}
this.#removals.delete(el)
}

for (const node of addedNodes) {
const el = node as HTMLorSVGElement
this.#apply(el)
}
}
break
case 'attributes': {
{
const requiredPrefix =
datasetPrefix + (this.aliasPrefix ? `${this.aliasPrefix}-` : '')
if (!attributeName?.startsWith(requiredPrefix)) {
break
}

const el = target as HTMLorSVGElement
const datasetKey = camel(
attributeName.slice(datasetPrefix.length),
)

// If the value has changed, clean up the old value
if (oldValue !== null && el.dataset[datasetKey] !== oldValue) {
const elRemovals = this.#removals.get(el)
if (elRemovals) {
const rk = removalKey(datasetKey, oldValue)
const removalFn = elRemovals.get(rk)
if (removalFn) {
removalFn()
elRemovals.delete(rk)
}
}
}

// Apply the plugin only if the dataset key exists
if (datasetKey in el.dataset) {
this.#applyAttributePlugin(el, datasetKey)
}
}
break
}
}
}
})

ob.observe(document.body, {
attributes: true,
attributeOldValue: true,
childList: true,
subtree: true,
})
}

get signals() {
return this.#signals
}

public load(...pluginsToLoad: DatastarPlugin[]) {
const that = this // I hate javascript
for (const plugin of pluginsToLoad) {
const that = this // I hate javascript
const ctx: InitContext = {
get signals() {
return that.#signals
Expand Down Expand Up @@ -161,11 +85,13 @@ export class Engine {
return a.name.localeCompare(b.name)
})

this.#debouncedApply()
this.#delayedApply()
}

#debouncedApply = debounce(() => {
// Delay applying plugins to give them time to load
#delayedApply = delay(() => {
this.#apply(document.body)
this.#observe()
}, 1)

// Apply all plugins to the element and its children
Expand All @@ -188,6 +114,87 @@ export class Engine {
})
}

// Set up a mutation observer to run plugin removal and apply functions
#observe() {
if (this.#mutationObserver) {
return
}

this.#mutationObserver = new MutationObserver((mutations) => {
for (const {
target,
type,
attributeName,
oldValue,
addedNodes,
removedNodes,
} of mutations) {
switch (type) {
case 'childList':
{
for (const node of removedNodes) {
const el = node as HTMLorSVGElement
const elRemovals = this.#removals.get(el)
if (!elRemovals) continue

for (const [_, removalFn] of elRemovals) {
removalFn()
}
this.#removals.delete(el)
}

for (const node of addedNodes) {
const el = node as HTMLorSVGElement
this.#apply(el)
}
}
break
case 'attributes': {
{
const datasetPrefix = 'data-'
const requiredPrefix =
datasetPrefix + (this.aliasPrefix ? `${this.aliasPrefix}-` : '')
if (!attributeName?.startsWith(requiredPrefix)) {
break
}

const el = target as HTMLorSVGElement
const datasetKey = camel(
attributeName.slice(datasetPrefix.length),
)

// If the value has changed, clean up the old value
if (oldValue !== null && el.dataset[datasetKey] !== oldValue) {
const elRemovals = this.#removals.get(el)
if (elRemovals) {
const rk = removalKey(datasetKey, oldValue)
const removalFn = elRemovals.get(rk)
if (removalFn) {
removalFn()
elRemovals.delete(rk)
}
}
}

// Apply the plugin only if the dataset key exists
if (datasetKey in el.dataset) {
this.#applyAttributePlugin(el, datasetKey)
}
}
break
}
}
}
})

this.#mutationObserver.observe(document.body, {
attributes: true,
attributeOldValue: true,
childList: true,
subtree: true,
})
}

#applyAttributePlugin(el: HTMLorSVGElement, camelCasedKey: string) {
// Extract the raw key from the dataset
const rawKey = camelCasedKey.slice(this.aliasPrefix.length)
Expand Down
4 changes: 2 additions & 2 deletions sdk/go/consts.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 91f5f07

Please sign in to comment.