Skip to content

Commit 86e4eeb

Browse files
committed
refactor attr into attrable
1 parent 1366c00 commit 86e4eeb

8 files changed

Lines changed: 415 additions & 294 deletions

File tree

src/attr.ts

Lines changed: 0 additions & 96 deletions
This file was deleted.

src/attrable.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import type {CustomElementClass} from './custom-element.js'
2+
import type {ControllableClass} from './controllable.js'
3+
import {controllable} from './controllable.js'
4+
import {dasherize, mustDasherize} from './dasherize.js'
5+
import {createMark} from './mark.js'
6+
import {createAbility} from './ability.js'
7+
8+
const attrChangedCallback = Symbol()
9+
10+
export interface Attrable {
11+
[key: PropertyKey]: unknown
12+
[attrChangedCallback](changed: Map<PropertyKey, unknown>): void
13+
}
14+
15+
export interface AttrableClass {
16+
new (): Attrable
17+
}
18+
19+
const Identity = (v: unknown) => v
20+
let setFromMutation = false
21+
const attrs = new WeakMap<Element, Map<string, PropertyKey>>()
22+
23+
const handleMutations = (mutations: MutationRecord[]) => {
24+
for (const mutation of mutations) {
25+
if (mutation.type === 'attributes') {
26+
const name = mutation.attributeName!
27+
const el = mutation.target as Element & {[key: PropertyKey]: unknown}
28+
const key = attrs.get(el)?.get(name)
29+
if (key) {
30+
setFromMutation = true
31+
el[key] = el.getAttribute(name)
32+
setFromMutation = false
33+
}
34+
}
35+
}
36+
}
37+
const observer = new MutationObserver(handleMutations)
38+
39+
const [attr, getAttr, initializeAttrs] = createMark<Element & Attrable>(
40+
({name}) => mustDasherize(name, '@attr'),
41+
(instance: Element & Attrable, {name, kind, access}) => {
42+
let cast: typeof Identity | typeof Boolean | typeof Number | typeof String = Identity
43+
let initialValue: unknown
44+
if (access.get) {
45+
initialValue = access.get.call(instance)
46+
} else if ('value' in access && kind !== 'method') {
47+
initialValue = access.value
48+
}
49+
let value = initialValue
50+
const attributeName = dasherize(name)
51+
const setCallback = (kind === 'method' ? access.value : access.set) || Identity
52+
const getCallback = access.get || (() => value)
53+
if (!attrs.get(instance)) attrs.set(instance, new Map())
54+
attrs.get(instance)!.set(attributeName, name)
55+
if (typeof value === 'number') {
56+
cast = Number
57+
} else if (typeof value === 'boolean') {
58+
cast = Boolean
59+
} else if (typeof value === 'string') {
60+
cast = String
61+
}
62+
const queue = new Map()
63+
const requestAttrChanged = async (newValue: unknown) => {
64+
queue.set(name, newValue)
65+
if (queue.size > 1) return
66+
await Promise.resolve()
67+
const changed = new Map(queue)
68+
queue.clear()
69+
instance[attrChangedCallback](changed)
70+
}
71+
return {
72+
get() {
73+
const has = instance.hasAttribute(attributeName)
74+
if (has) {
75+
return cast === Boolean ? has : cast(instance.getAttribute(attributeName))
76+
}
77+
return cast(getCallback.call(instance))
78+
},
79+
set(newValue: unknown) {
80+
const isInitial = newValue === null
81+
if (isInitial) newValue = initialValue
82+
const same = Object.is(value, newValue)
83+
value = newValue
84+
setCallback.call(instance, value)
85+
if (setFromMutation || same || isInitial) return
86+
requestAttrChanged(newValue)
87+
}
88+
}
89+
}
90+
)
91+
92+
export {attr, getAttr, attrChangedCallback}
93+
export const attrable = createAbility(
94+
<T extends CustomElementClass>(Class: T): T & ControllableClass & AttrableClass =>
95+
class extends controllable(Class) {
96+
[key: PropertyKey]: unknown
97+
constructor() {
98+
super()
99+
initializeAttrs(this)
100+
observer.observe(this, {attributeFilter: Array.from(getAttr(this)).map(dasherize)})
101+
}
102+
103+
[attrChangedCallback](changed: Map<PropertyKey, unknown>) {
104+
if (!this.isConnected) return
105+
for (const [name, value] of changed) {
106+
if (typeof value === 'boolean') {
107+
this.toggleAttribute(dasherize(name), value)
108+
} else {
109+
this.setAttribute(dasherize(name), String(value))
110+
}
111+
}
112+
}
113+
}
114+
)

src/controller.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import {CatalystDelegate} from './core.js'
22
import type {CustomElementClass} from './custom-element.js'
3+
import {attrable} from './attrable.js'
34
/**
45
* Controller is a decorator to be used over a class that extends HTMLElement.
56
* It will automatically `register()` the component in the customElement
67
* registry, as well as ensuring `bind(this)` is called on `connectedCallback`,
78
* wrapping the classes `connectedCallback` method if needed.
89
*/
910
export function controller(classObject: CustomElementClass): void {
10-
new CatalystDelegate(classObject)
11+
new CatalystDelegate(attrable(classObject))
1112
}

src/core.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import {register} from './register.js'
2-
import {bind, bindShadow} from './bind.js'
3-
import {defineObservedAttributes, initializeAttrs} from './attr.js'
42
import type {CustomElementClass} from './custom-element.js'
53

64
const symbol = Symbol.for('catalyst')
@@ -41,7 +39,6 @@ export class CatalystDelegate {
4139
}
4240
})
4341

44-
defineObservedAttributes(classObject)
4542
register(classObject)
4643
}
4744

@@ -52,7 +49,6 @@ export class CatalystDelegate {
5249
connectedCallback(instance: HTMLElement, connectedCallback: () => void) {
5350
instance.toggleAttribute('data-catalyst', true)
5451
customElements.upgrade(instance)
55-
initializeAttrs(instance)
5652
bind(instance)
5753
connectedCallback?.call(instance)
5854
if (instance.shadowRoot) bindShadow(instance.shadowRoot)
@@ -69,7 +65,6 @@ export class CatalystDelegate {
6965
newValue: string | null,
7066
attributeChangedCallback: (...args: unknown[]) => void
7167
) {
72-
initializeAttrs(instance)
7368
if (name !== 'data-catalyst' && attributeChangedCallback) {
7469
attributeChangedCallback.call(instance, name, oldValue, newValue)
7570
}

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ export {register} from './register.js'
33
export {findTarget, findTargets} from './findtarget.js'
44
export {target, targets} from './target.js'
55
export {controller} from './controller.js'
6-
export {attr, initializeAttrs, defineObservedAttributes} from './attr.js'
6+
export {attr, getAttr, attrable, attrChangedCallback} from './attrable.js'

0 commit comments

Comments
 (0)