@joist/element 4.0.0-next.4 → 4.0.0-next.41
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/README.md +102 -15
- package/package.json +7 -9
- package/src/lib/attr-changed.test.ts +34 -0
- package/src/lib/attr-changed.ts +15 -0
- package/src/lib/attr.test.ts +159 -97
- package/src/lib/attr.ts +26 -33
- package/src/lib/element.test.ts +95 -71
- package/src/lib/element.ts +107 -51
- package/src/lib/lifecycle.test.ts +31 -0
- package/src/lib/lifecycle.ts +9 -0
- package/src/lib/listen.test.ts +104 -0
- package/src/lib/listen.ts +27 -7
- package/src/lib/metadata.ts +21 -8
- package/src/lib/query.test.ts +21 -47
- package/src/lib/query.ts +2 -8
- package/src/lib/result.ts +1 -21
- package/src/lib/tags.ts +27 -12
- package/src/lib/template.test.ts +123 -0
- package/src/lib/template.ts +118 -0
- package/src/lib.ts +3 -2
- package/target/lib/attr-changed.d.ts +1 -0
- package/target/lib/attr-changed.js +10 -0
- package/target/lib/attr-changed.js.map +1 -0
- package/target/lib/attr-changed.test.d.ts +1 -0
- package/target/lib/attr-changed.test.js +54 -0
- package/target/lib/attr-changed.test.js.map +1 -0
- package/target/lib/attr.d.ts +3 -1
- package/target/lib/attr.js +23 -25
- package/target/lib/attr.js.map +1 -1
- package/target/lib/attr.test.js +373 -251
- package/target/lib/attr.test.js.map +1 -1
- package/target/lib/element.d.ts +8 -339
- package/target/lib/element.js +81 -39
- package/target/lib/element.js.map +1 -1
- package/target/lib/element.test.js +154 -178
- package/target/lib/element.test.js.map +1 -1
- package/target/lib/lifecycle.d.ts +1 -0
- package/target/lib/lifecycle.js +8 -0
- package/target/lib/lifecycle.js.map +1 -0
- package/target/lib/lifecycle.test.d.ts +1 -0
- package/target/lib/lifecycle.test.js +48 -0
- package/target/lib/lifecycle.test.js.map +1 -0
- package/target/lib/listen.d.ts +2 -2
- package/target/lib/listen.js +18 -3
- package/target/lib/listen.js.map +1 -1
- package/target/lib/listen.test.d.ts +1 -0
- package/target/lib/listen.test.js +167 -0
- package/target/lib/listen.test.js.map +1 -0
- package/target/lib/metadata.d.ts +20 -10
- package/target/lib/metadata.js +8 -2
- package/target/lib/metadata.js.map +1 -1
- package/target/lib/query.d.ts +1 -1
- package/target/lib/query.js +1 -6
- package/target/lib/query.js.map +1 -1
- package/target/lib/query.test.js +35 -74
- package/target/lib/query.test.js.map +1 -1
- package/target/lib/result.d.ts +1 -8
- package/target/lib/result.js +1 -14
- package/target/lib/result.js.map +1 -1
- package/target/lib/tags.d.ts +10 -6
- package/target/lib/tags.js +20 -11
- package/target/lib/tags.js.map +1 -1
- package/target/lib/template.d.ts +11 -0
- package/target/lib/template.js +87 -0
- package/target/lib/template.js.map +1 -0
- package/target/lib/template.test.d.ts +1 -0
- package/target/lib/template.test.js +91 -0
- package/target/lib/template.test.js.map +1 -0
- package/target/lib.d.ts +3 -2
- package/target/lib.js +3 -2
- package/target/lib.js.map +1 -1
package/src/lib/element.test.ts
CHANGED
|
@@ -1,93 +1,117 @@
|
|
|
1
|
-
import { expect,
|
|
1
|
+
import { expect, assert } from 'chai';
|
|
2
2
|
|
|
3
3
|
import { attr } from './attr.js';
|
|
4
4
|
import { element } from './element.js';
|
|
5
5
|
import { css, html } from './tags.js';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
accessor value1 = 'hello'; // no attribute
|
|
7
|
+
it('should write default value to attribute', async () => {
|
|
8
|
+
@element({
|
|
9
|
+
tagName: 'element-1'
|
|
10
|
+
})
|
|
11
|
+
class MyElement extends HTMLElement {
|
|
12
|
+
@attr()
|
|
13
|
+
accessor value1 = 'hello'; // no attribute
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
@attr()
|
|
16
|
+
accessor value2 = 0; // number
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
18
|
+
@attr()
|
|
19
|
+
accessor value3 = true; // boolean
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
@attr({ reflect: false })
|
|
22
|
+
accessor value4 = 'foo';
|
|
23
|
+
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
expect(el.getAttribute('value2')).to.equal('0');
|
|
27
|
-
expect(el.getAttribute('value3')).to.equal('');
|
|
28
|
-
});
|
|
25
|
+
const el = new MyElement();
|
|
29
26
|
|
|
30
|
-
|
|
31
|
-
@element({
|
|
32
|
-
tagName: 'element-2'
|
|
33
|
-
})
|
|
34
|
-
class MyElement extends HTMLElement {
|
|
35
|
-
@attr()
|
|
36
|
-
accessor value1 = 'hello'; // no attribute
|
|
27
|
+
document.body.append(el);
|
|
37
28
|
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
expect(el.getAttribute('value1')).to.equal('hello');
|
|
30
|
+
expect(el.getAttribute('value2')).to.equal('0');
|
|
31
|
+
expect(el.getAttribute('value3')).to.equal('');
|
|
32
|
+
expect(el.getAttribute('value4')).to.equal(null);
|
|
40
33
|
|
|
41
|
-
|
|
42
|
-
|
|
34
|
+
el.remove();
|
|
35
|
+
});
|
|
43
36
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
// TODO: Figure out test
|
|
38
|
+
// it('should register attributes', async () => {
|
|
39
|
+
// @element({
|
|
40
|
+
// tagName: 'element-2'
|
|
41
|
+
// })
|
|
42
|
+
// class MyElement extends HTMLElement {
|
|
43
|
+
// @attr()
|
|
44
|
+
// accessor value1 = 'hello'; // no attribute
|
|
45
|
+
|
|
46
|
+
// @attr()
|
|
47
|
+
// accessor value2 = 0; // number
|
|
48
|
+
|
|
49
|
+
// @attr()
|
|
50
|
+
// accessor value3 = true; // boolean
|
|
51
|
+
|
|
52
|
+
// @attr({ observed: false }) // should be filtered out
|
|
53
|
+
// accessor value4 = 'hello world';
|
|
54
|
+
// }
|
|
55
|
+
|
|
56
|
+
// expect(Reflect.get(MyElement, 'observedAttributes')).to.deep.equal([
|
|
57
|
+
// 'value1',
|
|
58
|
+
// 'value2',
|
|
59
|
+
// 'value3'
|
|
60
|
+
// ]);
|
|
61
|
+
// });
|
|
62
|
+
|
|
63
|
+
it('should attach shadow root when the shadow property exists', async () => {
|
|
64
|
+
@element({
|
|
65
|
+
tagName: 'element-3',
|
|
66
|
+
shadowDom: []
|
|
67
|
+
})
|
|
68
|
+
class MyElement extends HTMLElement {}
|
|
69
|
+
|
|
70
|
+
const el = new MyElement();
|
|
71
|
+
|
|
72
|
+
expect(el.shadowRoot).to.be.instanceOf(ShadowRoot);
|
|
73
|
+
});
|
|
47
74
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class MyElement extends HTMLElement {}
|
|
61
|
-
|
|
62
|
-
const el = new MyElement();
|
|
63
|
-
|
|
64
|
-
expect(el.shadowRoot).to.be.instanceOf(ShadowRoot);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('should apply html and css', async () => {
|
|
68
|
-
@element({
|
|
69
|
-
tagName: 'element-4',
|
|
70
|
-
shadow: [
|
|
71
|
-
css`
|
|
72
|
-
:host {
|
|
73
|
-
display: contents;
|
|
74
|
-
}
|
|
75
|
-
`,
|
|
76
|
-
html`<slot></slot>`,
|
|
77
|
-
(el) => {
|
|
75
|
+
it('should apply html and css', async () => {
|
|
76
|
+
@element({
|
|
77
|
+
tagName: 'element-4',
|
|
78
|
+
shadowDom: [
|
|
79
|
+
css`
|
|
80
|
+
:host {
|
|
81
|
+
display: contents;
|
|
82
|
+
}
|
|
83
|
+
`,
|
|
84
|
+
html`<slot></slot>`,
|
|
85
|
+
{
|
|
86
|
+
apply(el) {
|
|
78
87
|
const div = document.createElement('div');
|
|
79
88
|
div.innerHTML = 'hello world';
|
|
80
89
|
|
|
81
90
|
el.append(div);
|
|
82
91
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
})
|
|
95
|
+
class MyElement extends HTMLElement {}
|
|
96
|
+
|
|
97
|
+
const el = new MyElement();
|
|
98
|
+
|
|
99
|
+
expect(el.shadowRoot!.adoptedStyleSheets.length).to.equal(1);
|
|
100
|
+
expect(el.shadowRoot!.innerHTML).to.equal(`<slot></slot>`);
|
|
101
|
+
expect(el.innerHTML).to.equal(`<div>hello world</div>`);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should the correct shadow dom mode', async () => {
|
|
105
|
+
@element({
|
|
106
|
+
tagName: 'element-5',
|
|
107
|
+
shadowDom: [],
|
|
108
|
+
shadowDomOpts: {
|
|
109
|
+
mode: 'closed'
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
class MyElement extends HTMLElement {}
|
|
86
113
|
|
|
87
|
-
|
|
114
|
+
const el = new MyElement();
|
|
88
115
|
|
|
89
|
-
|
|
90
|
-
expect(el.shadowRoot!.innerHTML).to.equal(`<slot></slot>`);
|
|
91
|
-
expect(el.innerHTML).to.equal(`<div>hello world</div>`);
|
|
92
|
-
});
|
|
116
|
+
assert.equal(el.shadowRoot, null);
|
|
93
117
|
});
|
package/src/lib/element.ts
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AttrMetadata, metadataStore } from './metadata.js';
|
|
2
2
|
import { ShadowResult } from './result.js';
|
|
3
3
|
|
|
4
|
-
export interface ElementOpts
|
|
4
|
+
export interface ElementOpts {
|
|
5
5
|
tagName?: string;
|
|
6
|
-
|
|
6
|
+
shadowDom?: ShadowResult[];
|
|
7
|
+
shadowDomOpts?: ShadowRootInit;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
10
|
+
interface ElementConstructor {
|
|
11
|
+
new (...args: any[]): HTMLElement;
|
|
12
|
+
}
|
|
12
13
|
|
|
13
|
-
export function element<
|
|
14
|
-
|
|
15
|
-
Instance extends InstanceType<Target>
|
|
16
|
-
>(opts?: ElementOpts<Instance>) {
|
|
17
|
-
return function elementDecorator(Base: Target, ctx: ClassDecoratorContext<Target>) {
|
|
14
|
+
export function element<T extends ElementConstructor>(opts?: ElementOpts) {
|
|
15
|
+
return function elementDecorator(Base: T, ctx: ClassDecoratorContext<T>): T {
|
|
18
16
|
const meta = metadataStore.read(ctx.metadata);
|
|
19
17
|
|
|
20
|
-
ctx.addInitializer(function (
|
|
18
|
+
ctx.addInitializer(function () {
|
|
21
19
|
if (opts?.tagName) {
|
|
22
20
|
if (!customElements.get(opts.tagName)) {
|
|
23
21
|
customElements.define(opts.tagName, this);
|
|
@@ -25,68 +23,126 @@ export function element<
|
|
|
25
23
|
}
|
|
26
24
|
});
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
.map(({ attrName }) => attrName);
|
|
26
|
+
const def = {
|
|
27
|
+
[Base.name]: class extends Base {
|
|
28
|
+
static observedAttributes: string[] = Array.from(meta.attrs.keys());
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
super(...args);
|
|
30
|
+
#removeListeners: Set<Function> = new Set();
|
|
35
31
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
constructor(...args: any[]) {
|
|
33
|
+
super(...args);
|
|
34
|
+
|
|
35
|
+
if (opts?.shadowDom) {
|
|
36
|
+
if (!this.shadowRoot) {
|
|
37
|
+
this.attachShadow(opts.shadowDomOpts ?? { mode: 'open' });
|
|
38
|
+
}
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
res(this as unknown as Instance);
|
|
44
|
-
} else {
|
|
45
|
-
res.run(this);
|
|
40
|
+
for (let res of opts.shadowDom) {
|
|
41
|
+
res.apply(this);
|
|
46
42
|
}
|
|
47
43
|
}
|
|
44
|
+
|
|
45
|
+
for (let cb of meta.onReady) {
|
|
46
|
+
cb.call(this);
|
|
47
|
+
}
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
|
51
|
+
const attr = meta.attrs.get(name);
|
|
52
|
+
const cbs = meta.attrChanges.get(name);
|
|
53
|
+
|
|
54
|
+
if (attr) {
|
|
55
|
+
if (oldValue !== newValue) {
|
|
56
|
+
const ogValue = attr.getPropValue.call(this);
|
|
57
|
+
|
|
58
|
+
if (newValue === '') {
|
|
59
|
+
// treat as boolean
|
|
60
|
+
attr.setPropValue.call(this, true);
|
|
61
|
+
} else if (typeof ogValue === 'number') {
|
|
62
|
+
// treat as number
|
|
63
|
+
attr.setPropValue.call(this, Number(newValue));
|
|
64
|
+
} else {
|
|
65
|
+
// treat as string
|
|
66
|
+
attr.setPropValue.call(this, newValue);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (cbs) {
|
|
71
|
+
for (let cb of cbs) {
|
|
72
|
+
cb.call(this, oldValue, newValue);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (super.attributeChangedCallback) {
|
|
77
|
+
super.attributeChangedCallback(name, oldValue, newValue);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
52
80
|
}
|
|
53
81
|
|
|
54
|
-
|
|
55
|
-
|
|
82
|
+
connectedCallback() {
|
|
83
|
+
if (this.isConnected) {
|
|
84
|
+
for (let { event, cb, selector } of meta.listeners) {
|
|
85
|
+
const root = selector(this);
|
|
56
86
|
|
|
57
|
-
|
|
58
|
-
|
|
87
|
+
if (root) {
|
|
88
|
+
const thisCb = cb.bind(this);
|
|
89
|
+
|
|
90
|
+
root.addEventListener(event, thisCb);
|
|
91
|
+
|
|
92
|
+
this.#removeListeners.add(() => {
|
|
93
|
+
root.removeEventListener(event, thisCb);
|
|
94
|
+
});
|
|
95
|
+
} else {
|
|
96
|
+
throw new Error(`could not add listener to ${root}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
reflectAttributeValues(this, meta.attrs);
|
|
101
|
+
|
|
102
|
+
if (super.connectedCallback) {
|
|
103
|
+
super.connectedCallback();
|
|
104
|
+
}
|
|
59
105
|
}
|
|
60
106
|
}
|
|
61
|
-
}
|
|
62
107
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
108
|
+
disconnectedCallback(): void {
|
|
109
|
+
for (let remove of this.#removeListeners) {
|
|
110
|
+
remove();
|
|
111
|
+
}
|
|
66
112
|
|
|
67
|
-
if (super.
|
|
68
|
-
super.
|
|
113
|
+
if (super.disconnectedCallback) {
|
|
114
|
+
super.disconnectedCallback();
|
|
69
115
|
}
|
|
70
116
|
}
|
|
71
117
|
}
|
|
72
118
|
};
|
|
119
|
+
|
|
120
|
+
return def[Base.name];
|
|
73
121
|
};
|
|
74
122
|
}
|
|
75
123
|
|
|
76
|
-
function reflectAttributeValues(el:
|
|
77
|
-
for (let {
|
|
78
|
-
|
|
124
|
+
function reflectAttributeValues<T extends HTMLElement>(el: T, attrs: AttrMetadata) {
|
|
125
|
+
for (let [attrName, { getPropValue, reflect }] of attrs) {
|
|
126
|
+
if (reflect) {
|
|
127
|
+
const value = getPropValue.call(el);
|
|
128
|
+
|
|
129
|
+
// reflect values back to attributes
|
|
130
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
131
|
+
if (typeof value === 'boolean') {
|
|
132
|
+
if (value === true) {
|
|
133
|
+
// set boolean attribute
|
|
134
|
+
if (!el.hasAttribute(attrName)) {
|
|
135
|
+
el.setAttribute(attrName, '');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
// set key/value attribute
|
|
140
|
+
const strValue = String(value);
|
|
79
141
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (value === true) {
|
|
84
|
-
// set boolean attribute
|
|
85
|
-
el.setAttribute(attrName, '');
|
|
142
|
+
if (el.getAttribute(attrName) !== strValue) {
|
|
143
|
+
el.setAttribute(attrName, strValue);
|
|
144
|
+
}
|
|
86
145
|
}
|
|
87
|
-
} else {
|
|
88
|
-
// set key/value attribute
|
|
89
|
-
el.setAttribute(attrName, String(value));
|
|
90
146
|
}
|
|
91
147
|
}
|
|
92
148
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { assert } from 'chai';
|
|
2
|
+
import { element } from './element.js';
|
|
3
|
+
import { ready } from './lifecycle.js';
|
|
4
|
+
|
|
5
|
+
it('should call all callbacks when template is ready', () => {
|
|
6
|
+
@element({
|
|
7
|
+
tagName: 'template-ready-1'
|
|
8
|
+
})
|
|
9
|
+
class MyElement extends HTMLElement {
|
|
10
|
+
callCount: Record<string, number> = {};
|
|
11
|
+
|
|
12
|
+
@ready()
|
|
13
|
+
onTemplateReady1() {
|
|
14
|
+
this.callCount['onTemplateReady1'] ??= 0;
|
|
15
|
+
this.callCount['onTemplateReady1']++;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@ready()
|
|
19
|
+
onTemplateReady2() {
|
|
20
|
+
this.callCount['onTemplateReady2'] ??= 0;
|
|
21
|
+
this.callCount['onTemplateReady2']++;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const el = new MyElement();
|
|
26
|
+
|
|
27
|
+
assert.deepEqual(el.callCount, {
|
|
28
|
+
onTemplateReady1: 1,
|
|
29
|
+
onTemplateReady2: 1
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { assert } from 'chai';
|
|
2
|
+
|
|
3
|
+
import { element } from './element.js';
|
|
4
|
+
import { listen } from './listen.js';
|
|
5
|
+
|
|
6
|
+
describe('@listen()', () => {
|
|
7
|
+
it('should add listener to an outer HTMLElement', (done) => {
|
|
8
|
+
@element({
|
|
9
|
+
tagName: 'listener-1'
|
|
10
|
+
})
|
|
11
|
+
class MyElement extends HTMLElement {
|
|
12
|
+
@listen('click')
|
|
13
|
+
onClick(e: Event) {
|
|
14
|
+
assert.equal(e.type, 'click');
|
|
15
|
+
|
|
16
|
+
done();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const el = new MyElement();
|
|
21
|
+
|
|
22
|
+
document.body.append(el);
|
|
23
|
+
|
|
24
|
+
el.dispatchEvent(new Event('click'));
|
|
25
|
+
|
|
26
|
+
el.remove();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should add listener to the shadow root if available', (done) => {
|
|
30
|
+
@element({
|
|
31
|
+
tagName: 'listener-2',
|
|
32
|
+
shadowDom: []
|
|
33
|
+
})
|
|
34
|
+
class MyElement extends HTMLElement {
|
|
35
|
+
@listen('click')
|
|
36
|
+
onClick(e: Event) {
|
|
37
|
+
assert.equal(e.type, 'click');
|
|
38
|
+
|
|
39
|
+
done();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const el = new MyElement();
|
|
44
|
+
|
|
45
|
+
document.body.append(el);
|
|
46
|
+
|
|
47
|
+
el.shadowRoot!.dispatchEvent(new Event('click'));
|
|
48
|
+
|
|
49
|
+
el.remove();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should restrict argument to an event or an event subtype', (done) => {
|
|
53
|
+
class CustomEvent extends Event {
|
|
54
|
+
test = 'Hello World';
|
|
55
|
+
|
|
56
|
+
constructor() {
|
|
57
|
+
super('customevent');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@element({
|
|
62
|
+
tagName: 'listener-3'
|
|
63
|
+
})
|
|
64
|
+
class MyElement extends HTMLElement {
|
|
65
|
+
@listen('customevent')
|
|
66
|
+
onClick(e: CustomEvent) {
|
|
67
|
+
assert.equal(e.type, 'customevent');
|
|
68
|
+
|
|
69
|
+
done();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const el = new MyElement();
|
|
74
|
+
|
|
75
|
+
document.body.append(el);
|
|
76
|
+
|
|
77
|
+
el.dispatchEvent(new CustomEvent());
|
|
78
|
+
|
|
79
|
+
el.remove();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should respect a provided selector function', (done) => {
|
|
83
|
+
@element({
|
|
84
|
+
tagName: 'listener-4',
|
|
85
|
+
shadowDom: []
|
|
86
|
+
})
|
|
87
|
+
class MyElement extends HTMLElement {
|
|
88
|
+
@listen('click', (host) => host)
|
|
89
|
+
onClick(e: Event) {
|
|
90
|
+
assert.equal(e.type, 'click');
|
|
91
|
+
|
|
92
|
+
done();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const el = new MyElement();
|
|
97
|
+
|
|
98
|
+
document.body.append(el);
|
|
99
|
+
|
|
100
|
+
el.dispatchEvent(new Event('click'));
|
|
101
|
+
|
|
102
|
+
el.remove();
|
|
103
|
+
});
|
|
104
|
+
});
|
package/src/lib/listen.ts
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ListenerSelector, metadataStore } from './metadata.js';
|
|
2
2
|
|
|
3
|
-
export function listen<This extends HTMLElement>(
|
|
3
|
+
export function listen<This extends HTMLElement>(
|
|
4
|
+
event: string,
|
|
5
|
+
selector?: ListenerSelector<This> | string
|
|
6
|
+
) {
|
|
4
7
|
return function listenDecorator(
|
|
5
|
-
value: (e:
|
|
8
|
+
value: (e: any) => void,
|
|
6
9
|
ctx: ClassMethodDecoratorContext<This>
|
|
7
|
-
) {
|
|
8
|
-
const metadata = metadataStore.read(ctx.metadata);
|
|
10
|
+
): void {
|
|
11
|
+
const metadata = metadataStore.read<This>(ctx.metadata);
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
let selectorInternal: ListenerSelector<This> = (el) => el.shadowRoot ?? el;
|
|
14
|
+
|
|
15
|
+
if (selector) {
|
|
16
|
+
if (typeof selector === 'string') {
|
|
17
|
+
selectorInternal = (el: This) => {
|
|
18
|
+
if (el.shadowRoot) {
|
|
19
|
+
return el.shadowRoot.querySelector(selector);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return el.querySelector(selector);
|
|
23
|
+
};
|
|
24
|
+
} else {
|
|
25
|
+
selectorInternal = selector;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
metadata.listeners.push({
|
|
30
|
+
event,
|
|
11
31
|
cb: value,
|
|
12
|
-
|
|
32
|
+
selector: selectorInternal
|
|
13
33
|
});
|
|
14
34
|
};
|
|
15
35
|
}
|
package/src/lib/metadata.ts
CHANGED
|
@@ -2,19 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
export interface AttrDef {
|
|
4
4
|
propName: string | symbol;
|
|
5
|
-
attrName: string;
|
|
6
5
|
observe: boolean;
|
|
6
|
+
reflect: boolean;
|
|
7
|
+
getPropValue: Function;
|
|
8
|
+
setPropValue: Function;
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
export type
|
|
11
|
+
export type ListenerSelector<T> = (el: T) => Element | ShadowRoot | null;
|
|
10
12
|
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
export interface Listener<T> {
|
|
14
|
+
event: string;
|
|
15
|
+
cb: (e: Event) => void;
|
|
16
|
+
selector: ListenerSelector<T>;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
|
-
export class
|
|
17
|
-
|
|
19
|
+
export class AttrMetadata extends Map<string, AttrDef> {}
|
|
20
|
+
export class AttrChangeMetadata extends Map<string, Set<Function>> {}
|
|
21
|
+
|
|
22
|
+
export class ElementMetadata<T> {
|
|
23
|
+
attrs: AttrMetadata = new AttrMetadata();
|
|
24
|
+
attrChanges: AttrChangeMetadata = new AttrChangeMetadata();
|
|
25
|
+
listeners: Listener<T>[] = [];
|
|
26
|
+
onReady: Set<Function> = new Set();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class MetadataStore extends WeakMap<object, ElementMetadata<unknown>> {
|
|
30
|
+
read<T>(value: object): ElementMetadata<T> {
|
|
18
31
|
if (!this.has(value)) {
|
|
19
32
|
this.set(value, new ElementMetadata());
|
|
20
33
|
}
|
|
@@ -23,4 +36,4 @@ export class MetadataStore extends WeakMap<object, ElementMetadata> {
|
|
|
23
36
|
}
|
|
24
37
|
}
|
|
25
38
|
|
|
26
|
-
export const metadataStore = new MetadataStore();
|
|
39
|
+
export const metadataStore: MetadataStore = new MetadataStore();
|