-
Notifications
You must be signed in to change notification settings - Fork 55
Expand file tree
/
Copy pathattr.ts
More file actions
100 lines (94 loc) · 3.57 KB
/
attr.ts
File metadata and controls
100 lines (94 loc) · 3.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import type {CustomElementClass} from './custom-element.js'
import {mustDasherize} from './dasherize.js'
import {meta} from './core.js'
const attrKey = 'attr'
type attrValue = string | number | boolean
/**
* Attr is a decorator which tags a property as one to be initialized via
* `initializeAttrs`.
*
* The signature is typed such that the property must be one of a String,
* Number or Boolean. This matches the behavior of `initializeAttrs`.
*/
export function attr<K extends string>(proto: Record<K, attrValue>, key: K): void {
meta(proto, attrKey).add(key)
}
/**
* initializeAttrs is called with a set of class property names (if omitted, it
* will look for any properties tagged with the `@attr` decorator). With this
* list it defines property descriptors for each property that map to `data-*`
* attributes on the HTMLElement instance.
*
* It works around Native Class Property semantics - which are equivalent to
* calling `Object.defineProperty` on the instance upon creation, but before
* `constructor()` is called.
*
* If a class property is assigned to the class body, it will infer the type
* (using `typeof`) and define an appropriate getter/setter combo that aligns
* to that type. This means class properties assigned to Numbers can only ever
* be Numbers, assigned to Booleans can only ever be Booleans, and assigned to
* Strings can only ever be Strings.
*
* This is automatically called as part of `@controller`. If a class uses the
* `@controller` decorator it should not call this manually.
*/
const initialized = new WeakSet<Element>()
export function initializeAttrs(instance: HTMLElement, names?: Iterable<string>): void {
if (initialized.has(instance)) return
initialized.add(instance)
const proto = Object.getPrototypeOf(instance)
const prefix = proto?.constructor?.attrPrefix ?? 'data-'
if (!names) names = meta(proto, attrKey)
for (const key of names) {
const value = (<Record<PropertyKey, unknown>>(<unknown>instance))[key]
const name = mustDasherize(`${prefix}${key}`)
let descriptor: PropertyDescriptor = {
configurable: true,
get(this: HTMLElement): string {
return this.getAttribute(name) || ''
},
set(this: HTMLElement, newValue: string) {
this.setAttribute(name, newValue || '')
}
}
if (typeof value === 'number') {
descriptor = {
configurable: true,
get(this: HTMLElement): number {
return Number(this.getAttribute(name) || 0)
},
set(this: HTMLElement, newValue: string) {
this.setAttribute(name, newValue)
}
}
} else if (typeof value === 'boolean') {
descriptor = {
configurable: true,
get(this: HTMLElement): boolean {
return this.hasAttribute(name)
},
set(this: HTMLElement, newValue: boolean) {
this.toggleAttribute(name, newValue)
}
}
}
Object.defineProperty(instance, key, descriptor)
if (key in instance && !instance.hasAttribute(name)) {
descriptor.set!.call(instance, value)
}
}
}
export function defineObservedAttributes(classObject: CustomElementClass): void {
let observed = classObject.observedAttributes || []
const prefix = classObject.attrPrefix ?? 'data-'
const attrToAttributeName = (name: string) => mustDasherize(`${prefix}${name}`)
Object.defineProperty(classObject, 'observedAttributes', {
configurable: true,
get() {
return [...meta(classObject.prototype, attrKey)].map(attrToAttributeName).concat(observed)
},
set(attributes: string[]) {
observed = attributes
}
})
}