@joist/element 4.0.0-next.4 → 4.0.0-next.6

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.
Files changed (56) hide show
  1. package/README.md +2 -2
  2. package/package.json +4 -3
  3. package/src/lib/attr.test.ts +107 -98
  4. package/src/lib/attr.ts +2 -2
  5. package/src/lib/element.test.ts +84 -80
  6. package/src/lib/element.ts +5 -13
  7. package/src/lib/lifecycle.test.ts +31 -0
  8. package/src/lib/lifecycle.ts +9 -0
  9. package/src/lib/listen.ts +1 -1
  10. package/src/lib/metadata.ts +1 -0
  11. package/src/lib/query.test.ts +43 -44
  12. package/src/lib/query.ts +1 -1
  13. package/src/lib/result.ts +2 -22
  14. package/src/lib/tags.ts +28 -13
  15. package/src/lib/template.test.ts +65 -0
  16. package/src/lib/template.ts +132 -0
  17. package/src/lib.ts +3 -2
  18. package/target/lib/attr.d.ts +1 -1
  19. package/target/lib/attr.js +1 -1
  20. package/target/lib/attr.js.map +1 -1
  21. package/target/lib/attr.test.js +258 -252
  22. package/target/lib/attr.test.js.map +1 -1
  23. package/target/lib/element.d.ts +2 -5
  24. package/target/lib/element.js +3 -9
  25. package/target/lib/element.js.map +1 -1
  26. package/target/lib/element.test.js +181 -179
  27. package/target/lib/element.test.js.map +1 -1
  28. package/target/lib/lifecycle.d.ts +1 -0
  29. package/target/lib/lifecycle.js +8 -0
  30. package/target/lib/lifecycle.js.map +1 -0
  31. package/target/lib/lifecycle.test.d.ts +1 -0
  32. package/target/lib/lifecycle.test.js +48 -0
  33. package/target/lib/lifecycle.test.js.map +1 -0
  34. package/target/lib/listen.d.ts +1 -1
  35. package/target/lib/listen.js.map +1 -1
  36. package/target/lib/metadata.d.ts +1 -0
  37. package/target/lib/metadata.js +1 -0
  38. package/target/lib/metadata.js.map +1 -1
  39. package/target/lib/query.d.ts +1 -1
  40. package/target/lib/query.test.js +72 -74
  41. package/target/lib/query.test.js.map +1 -1
  42. package/target/lib/result.d.ts +2 -9
  43. package/target/lib/result.js +1 -14
  44. package/target/lib/result.js.map +1 -1
  45. package/target/lib/tags.d.ts +11 -7
  46. package/target/lib/tags.js +20 -11
  47. package/target/lib/tags.js.map +1 -1
  48. package/target/lib/template.d.ts +6 -0
  49. package/target/lib/template.js +91 -0
  50. package/target/lib/template.js.map +1 -0
  51. package/target/lib/template.test.d.ts +1 -0
  52. package/target/lib/template.test.js +47 -0
  53. package/target/lib/template.test.js.map +1 -0
  54. package/target/lib.d.ts +3 -2
  55. package/target/lib.js +3 -2
  56. package/target/lib.js.map +1 -1
package/README.md CHANGED
@@ -11,7 +11,7 @@ npm i @joist/element
11
11
  #### Example:
12
12
 
13
13
  ```TS
14
- import { tagName, css, html, attr, listen, element } from '@joist/element';
14
+ import { css, html, attr, listen, element } from '@joist/element';
15
15
 
16
16
  @element({
17
17
  tagName: 'my-element',
@@ -23,7 +23,7 @@ import { tagName, css, html, attr, listen, element } from '@joist/element';
23
23
  }
24
24
  `,
25
25
  html`
26
- <slot></slot>
26
+ <!--#:value-->
27
27
  `
28
28
  ]
29
29
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joist/element",
3
- "version": "4.0.0-next.4",
3
+ "version": "4.0.0-next.6",
4
4
  "type": "module",
5
5
  "main": "./target/lib.js",
6
6
  "module": "./target/lib.js",
@@ -20,7 +20,7 @@
20
20
  "description": "Intelligently apply styles to WebComponents",
21
21
  "repository": {
22
22
  "type": "git",
23
- "url": "git+https://github.com/deebloo/joist.git"
23
+ "url": "git+https://github.com/joist-framework/joist.git"
24
24
  },
25
25
  "keywords": [
26
26
  "TypeScript",
@@ -31,7 +31,7 @@
31
31
  "author": "deebloo",
32
32
  "license": "MIT",
33
33
  "bugs": {
34
- "url": "https://github.com/deebloo/joist/issues"
34
+ "url": "https://github.com/joist-framework/joist/issues"
35
35
  },
36
36
  "publishConfig": {
37
37
  "access": "public"
@@ -57,6 +57,7 @@
57
57
  "test": {
58
58
  "command": "wtr --config wtr.config.mjs",
59
59
  "files": [
60
+ "vitest.config.js",
60
61
  "target/**"
61
62
  ],
62
63
  "output": [],
@@ -1,129 +1,138 @@
1
- import { expect, fixture, html } from '@open-wc/testing';
1
+ import { expect } from 'chai';
2
2
 
3
3
  import { attr } from './attr.js';
4
4
  import { element } from './element.js';
5
5
 
6
- describe('@attr()', () => {
7
- it('should read and parse the correct values', async () => {
8
- @element({
9
- tagName: 'attr-test-1'
10
- })
11
- class MyElement extends HTMLElement {
12
- @attr()
13
- accessor value1 = 100; // no attribute
6
+ it('should read and parse the correct values', () => {
7
+ @element({
8
+ tagName: 'attr-test-1'
9
+ })
10
+ class MyElement extends HTMLElement {
11
+ @attr()
12
+ accessor value1 = 100; // no attribute
14
13
 
15
- @attr()
16
- accessor value2 = 0; // number
14
+ @attr()
15
+ accessor value2 = 0; // number
17
16
 
18
- @attr()
19
- accessor value3 = false; // boolean
17
+ @attr()
18
+ accessor value3 = false; // boolean
20
19
 
21
- @attr()
22
- accessor value4 = 'hello'; // string
23
- }
20
+ @attr()
21
+ accessor value4 = 'hello'; // string
22
+ }
24
23
 
25
- const el = await fixture<MyElement>(
26
- html`<attr-test-1 value2="2" value3 value4="world"></attr-test-1>`
27
- );
24
+ const container = document.createElement('div');
25
+ container.innerHTML = /*html*/ `
26
+ <attr-test-1 value2="2" value3 value4="world"></attr-test-1>
27
+ `;
28
28
 
29
- expect(el.value1).to.equal(100);
30
- expect(el.value2).to.equal(2);
31
- expect(el.value3).to.equal(true);
32
- expect(el.value4).to.equal('world');
33
- });
29
+ document.body.append(container);
34
30
 
35
- it('should not write falsy props to attributes', async () => {
36
- @element({
37
- tagName: 'attr-test-2'
38
- })
39
- class MyElement extends HTMLElement {
40
- @attr()
41
- accessor value1 = undefined;
31
+ const el = document.querySelector('attr-test-1') as MyElement;
42
32
 
43
- @attr()
44
- accessor value2 = null;
33
+ expect(el.value1).to.equal(100);
34
+ expect(el.value2).to.equal(2);
35
+ expect(el.value3).to.equal(true);
36
+ expect(el.value4).to.equal('world');
45
37
 
46
- @attr()
47
- accessor value3 = '';
48
- }
38
+ container.remove();
39
+ });
49
40
 
50
- const el = await fixture<MyElement>(html`<attr-test-2></attr-test-2>`);
41
+ it('should not write falsy props to attributes', async () => {
42
+ @element({
43
+ tagName: 'attr-test-2'
44
+ })
45
+ class MyElement extends HTMLElement {
46
+ @attr()
47
+ accessor value1 = undefined;
51
48
 
52
- expect(el.hasAttribute('value1')).to.be.false;
53
- expect(el.hasAttribute('value2')).to.be.false;
54
- expect(el.hasAttribute('value3')).to.be.false;
55
- });
49
+ @attr()
50
+ accessor value2 = null;
56
51
 
57
- it('should update attributes when props are changed', async () => {
58
- @element({
59
- tagName: 'attr-test-3'
60
- })
61
- class MyElement extends HTMLElement {
62
- @attr()
63
- accessor value1 = 'hello'; // no attribute
52
+ @attr()
53
+ accessor value3 = '';
54
+ }
64
55
 
65
- @attr()
66
- accessor value2 = 0; // number
56
+ const el = new MyElement();
67
57
 
68
- @attr()
69
- accessor value3 = true; // boolean
58
+ expect(el.hasAttribute('value1')).to.be.false;
59
+ expect(el.hasAttribute('value2')).to.be.false;
60
+ expect(el.hasAttribute('value3')).to.be.false;
61
+ });
70
62
 
71
- @attr()
72
- accessor value4 = false; // boolean
73
- }
63
+ it('should update attributes when props are changed', async () => {
64
+ @element({
65
+ tagName: 'attr-test-3'
66
+ })
67
+ class MyElement extends HTMLElement {
68
+ @attr()
69
+ accessor value1 = 'hello'; // no attribute
70
+
71
+ @attr()
72
+ accessor value2 = 0; // number
73
+
74
+ @attr()
75
+ accessor value3 = true; // boolean
76
+
77
+ @attr()
78
+ accessor value4 = false; // boolean
79
+ }
80
+
81
+ const el = new MyElement();
82
+
83
+ el.value1 = 'world';
84
+ el.value2 = 100;
85
+ el.value3 = false;
86
+ el.value4 = true;
87
+
88
+ expect(el.getAttribute('value1')).to.equal('world');
89
+ expect(el.getAttribute('value2')).to.equal('100');
90
+ expect(el.hasAttribute('value3')).to.be.false;
91
+ expect(el.hasAttribute('value4')).to.be.true;
92
+ });
74
93
 
75
- const el = await fixture<MyElement>(html`<attr-test-3></attr-test-3>`);
94
+ it('should normalize attribute names', async () => {
95
+ const value3 = Symbol('Value from SYMBOL');
76
96
 
77
- el.value1 = 'world';
78
- el.value2 = 100;
79
- el.value3 = false;
80
- el.value4 = true;
97
+ @element({
98
+ tagName: 'attr-test-4'
99
+ })
100
+ class MyElement extends HTMLElement {
101
+ @attr()
102
+ accessor Value1 = 'hello';
81
103
 
82
- expect(el.getAttribute('value1')).to.equal('world');
83
- expect(el.getAttribute('value2')).to.equal('100');
84
- expect(el.hasAttribute('value3')).to.be.false;
85
- expect(el.hasAttribute('value4')).to.be.true;
86
- });
104
+ @attr()
105
+ accessor ['Value 2'] = 0;
87
106
 
88
- it('should normalize attribute names', async () => {
89
- const value3 = Symbol('Value from SYMBOL');
107
+ @attr()
108
+ accessor [value3] = true;
109
+ }
110
+
111
+ const el = new MyElement();
112
+
113
+ document.body.append(el);
114
+
115
+ expect([...el.attributes].map((attr) => attr.name)).to.deep.equal([
116
+ 'value1',
117
+ 'value-2',
118
+ 'value-from-symbol'
119
+ ]);
120
+
121
+ el.remove();
122
+ });
123
+
124
+ it('should throw an error for symbols with no description', async () => {
125
+ expect(() => {
126
+ const value = Symbol();
90
127
 
91
128
  @element({
92
129
  tagName: 'attr-test-4'
93
130
  })
94
131
  class MyElement extends HTMLElement {
95
132
  @attr()
96
- accessor Value1 = 'hello';
97
-
98
- @attr()
99
- accessor ['Value 2'] = 0;
100
-
101
- @attr()
102
- accessor [value3] = true;
133
+ accessor [value] = true;
103
134
  }
104
135
 
105
- const el = await fixture<MyElement>(html`<attr-test-4></attr-test-4>`);
106
-
107
- expect([...el.attributes].map((attr) => attr.name)).to.deep.equal([
108
- 'value1',
109
- 'value-2',
110
- 'value-from-symbol'
111
- ]);
112
- });
113
-
114
- it('should throw an error for symbols with no description', async () => {
115
- expect(() => {
116
- const value = Symbol();
117
-
118
- @element({
119
- tagName: 'attr-test-4'
120
- })
121
- class MyElement extends HTMLElement {
122
- @attr()
123
- accessor [value] = true;
124
- }
125
-
126
- new MyElement();
127
- }).to.throw('Cannot handle Symbol property without description');
128
- });
136
+ new MyElement();
137
+ }).to.throw('Cannot handle Symbol property without description');
129
138
  });
package/src/lib/attr.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { metadataStore } from './metadata.js';
2
2
 
3
3
  export interface AttrOpts {
4
- observe?: boolean;
4
+ observed?: boolean;
5
5
  }
6
6
 
7
7
  export function attr(opts?: AttrOpts) {
@@ -15,7 +15,7 @@ export function attr(opts?: AttrOpts) {
15
15
  meta.attrs.push({
16
16
  propName: ctx.name,
17
17
  attrName,
18
- observe: opts?.observe ?? true
18
+ observe: opts?.observed ?? true
19
19
  });
20
20
 
21
21
  return {
@@ -1,93 +1,97 @@
1
- import { expect, fixture, html as litHtml } from '@open-wc/testing';
1
+ import { expect } 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
- describe('@element()', () => {
8
- it('should write default value to attribute', async () => {
9
- @element({
10
- tagName: 'element-1'
11
- })
12
- class MyElement extends HTMLElement {
13
- @attr()
14
- accessor value1 = 'hello'; // no attribute
15
-
16
- @attr()
17
- accessor value2 = 0; // number
18
-
19
- @attr()
20
- accessor value3 = true; // boolean
21
- }
22
-
23
- const el = await fixture<MyElement>(litHtml`<element-1></element-1>`);
24
-
25
- expect(el.getAttribute('value1')).to.equal('hello');
26
- expect(el.getAttribute('value2')).to.equal('0');
27
- expect(el.getAttribute('value3')).to.equal('');
28
- });
29
-
30
- it('should register attributes', async () => {
31
- @element({
32
- tagName: 'element-2'
33
- })
34
- class MyElement extends HTMLElement {
35
- @attr()
36
- accessor value1 = 'hello'; // no attribute
37
-
38
- @attr()
39
- accessor value2 = 0; // number
40
-
41
- @attr()
42
- accessor value3 = true; // boolean
43
-
44
- @attr({ observe: false }) // should be filtered out
45
- accessor value4 = 'hello world';
46
- }
47
-
48
- expect(Reflect.get(MyElement, 'observedAttributes')).to.deep.equal([
49
- 'value1',
50
- 'value2',
51
- 'value3'
52
- ]);
53
- });
54
-
55
- it('should attach shadow root when the shadow property exists', async () => {
56
- @element({
57
- tagName: 'element-3',
58
- shadow: []
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) => {
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
14
+
15
+ @attr()
16
+ accessor value2 = 0; // number
17
+
18
+ @attr()
19
+ accessor value3 = true; // boolean
20
+ }
21
+
22
+ const el = new MyElement();
23
+
24
+ document.body.append(el);
25
+
26
+ expect(el.getAttribute('value1')).to.equal('hello');
27
+ expect(el.getAttribute('value2')).to.equal('0');
28
+ expect(el.getAttribute('value3')).to.equal('');
29
+
30
+ el.remove();
31
+ });
32
+
33
+ it('should register attributes', async () => {
34
+ @element({
35
+ tagName: 'element-2'
36
+ })
37
+ class MyElement extends HTMLElement {
38
+ @attr()
39
+ accessor value1 = 'hello'; // no attribute
40
+
41
+ @attr()
42
+ accessor value2 = 0; // number
43
+
44
+ @attr()
45
+ accessor value3 = true; // boolean
46
+
47
+ @attr({ observed: false }) // should be filtered out
48
+ accessor value4 = 'hello world';
49
+ }
50
+
51
+ expect(Reflect.get(MyElement, 'observedAttributes')).to.deep.equal([
52
+ 'value1',
53
+ 'value2',
54
+ 'value3'
55
+ ]);
56
+ });
57
+
58
+ it('should attach shadow root when the shadow property exists', async () => {
59
+ @element({
60
+ tagName: 'element-3',
61
+ shadow: []
62
+ })
63
+ class MyElement extends HTMLElement {}
64
+
65
+ const el = new MyElement();
66
+
67
+ expect(el.shadowRoot).to.be.instanceOf(ShadowRoot);
68
+ });
69
+
70
+ it('should apply html and css', async () => {
71
+ @element({
72
+ tagName: 'element-4',
73
+ shadow: [
74
+ css`
75
+ :host {
76
+ display: contents;
77
+ }
78
+ `,
79
+ html`<slot></slot>`,
80
+ {
81
+ apply(el) {
78
82
  const div = document.createElement('div');
79
83
  div.innerHTML = 'hello world';
80
84
 
81
85
  el.append(div);
82
86
  }
83
- ]
84
- })
85
- class MyElement extends HTMLElement {}
87
+ }
88
+ ]
89
+ })
90
+ class MyElement extends HTMLElement {}
86
91
 
87
- const el = new MyElement();
92
+ const el = new MyElement();
88
93
 
89
- expect(el.shadowRoot!.adoptedStyleSheets.length).to.equal(1);
90
- expect(el.shadowRoot!.innerHTML).to.equal(`<slot></slot>`);
91
- expect(el.innerHTML).to.equal(`<div>hello world</div>`);
92
- });
94
+ expect(el.shadowRoot!.adoptedStyleSheets.length).to.equal(1);
95
+ expect(el.shadowRoot!.innerHTML).to.equal(`<slot></slot>`);
96
+ expect(el.innerHTML).to.equal(`<div>hello world</div>`);
93
97
  });
@@ -1,15 +1,11 @@
1
1
  import { AttrDef, metadataStore } from './metadata.js';
2
2
  import { ShadowResult } from './result.js';
3
3
 
4
- export interface ElementOpts<T> {
4
+ export interface ElementOpts<T extends HTMLElement> {
5
5
  tagName?: string;
6
- shadow?: Array<ShadowResult | ((el: T) => void)>;
6
+ shadow?: Array<ShadowResult<T> | ((el: T) => void)>;
7
7
  }
8
8
 
9
- export const LifeCycle = {
10
- onInit: Symbol('onInit')
11
- };
12
-
13
9
  export function element<
14
10
  Target extends CustomElementConstructor,
15
11
  Instance extends InstanceType<Target>
@@ -42,7 +38,7 @@ export function element<
42
38
  if (typeof res === 'function') {
43
39
  res(this as unknown as Instance);
44
40
  } else {
45
- res.run(this);
41
+ res.apply(this as unknown as Instance);
46
42
  }
47
43
  }
48
44
  }
@@ -51,12 +47,8 @@ export function element<
51
47
  root(this).addEventListener(event, cb.bind(this));
52
48
  }
53
49
 
54
- if (LifeCycle.onInit in this) {
55
- const onInit = Reflect.get(this, LifeCycle.onInit);
56
-
57
- if (typeof onInit === 'function') {
58
- onInit();
59
- }
50
+ for (let cb of meta.onReady) {
51
+ cb.call(this);
60
52
  }
61
53
  }
62
54
 
@@ -0,0 +1,31 @@
1
+ import { assert } from 'chai';
2
+ import { element } from './element.js';
3
+ import { ready } from './lifecycle';
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,9 @@
1
+ import { metadataStore } from './metadata.js';
2
+
3
+ export function ready() {
4
+ return function readyDecorator(val: Function, ctx: ClassMethodDecoratorContext) {
5
+ const metadata = metadataStore.read(ctx.metadata);
6
+
7
+ metadata.onReady.add(val);
8
+ };
9
+ }
package/src/lib/listen.ts CHANGED
@@ -2,7 +2,7 @@ import { ListenerRootSelector, metadataStore } from './metadata.js';
2
2
 
3
3
  export function listen<This extends HTMLElement>(event: string, root?: ListenerRootSelector) {
4
4
  return function listenDecorator(
5
- value: (e: Event) => void,
5
+ value: <T extends Event>(e: T) => void,
6
6
  ctx: ClassMethodDecoratorContext<This>
7
7
  ) {
8
8
  const metadata = metadataStore.read(ctx.metadata);
@@ -11,6 +11,7 @@ export type ListenerRootSelector = (el: HTMLElement) => HTMLElement | ShadowRoot
11
11
  export class ElementMetadata {
12
12
  attrs: AttrDef[] = [];
13
13
  listeners = new Map<string, { cb: (e: Event) => void; root: ListenerRootSelector }>();
14
+ onReady = new Set<Function>();
14
15
  }
15
16
 
16
17
  export class MetadataStore extends WeakMap<object, ElementMetadata> {