@joist/element 3.9.1 → 4.0.0-next.10

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 (81) hide show
  1. package/README.md +67 -29
  2. package/package.json +5 -4
  3. package/src/lib/attr.test.ts +122 -48
  4. package/src/lib/attr.ts +61 -34
  5. package/src/lib/element.test.ts +89 -16
  6. package/src/lib/element.ts +74 -38
  7. package/src/lib/lifecycle.test.ts +31 -0
  8. package/src/lib/lifecycle.ts +9 -0
  9. package/src/lib/listen.test.ts +88 -0
  10. package/src/lib/listen.ts +27 -4
  11. package/src/lib/metadata.ts +17 -3
  12. package/src/lib/query.test.ts +53 -0
  13. package/src/lib/query.ts +37 -0
  14. package/src/lib/result.ts +2 -26
  15. package/src/lib/tags.ts +22 -64
  16. package/src/lib/template.test.ts +95 -0
  17. package/src/lib/template.ts +115 -0
  18. package/src/lib.ts +3 -3
  19. package/target/lib/attr.d.ts +4 -1
  20. package/target/lib/attr.js +49 -28
  21. package/target/lib/attr.js.map +1 -1
  22. package/target/lib/attr.test.js +277 -147
  23. package/target/lib/attr.test.js.map +1 -1
  24. package/target/lib/element.d.ts +6 -1
  25. package/target/lib/element.js +58 -29
  26. package/target/lib/element.js.map +1 -1
  27. package/target/lib/element.test.js +186 -64
  28. package/target/lib/element.test.js.map +1 -1
  29. package/target/lib/lifecycle.d.ts +1 -0
  30. package/target/lib/lifecycle.js +8 -0
  31. package/target/lib/lifecycle.js.map +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 +2 -1
  35. package/target/lib/listen.js +21 -3
  36. package/target/lib/listen.js.map +1 -1
  37. package/target/lib/listen.test.js +159 -0
  38. package/target/lib/listen.test.js.map +1 -0
  39. package/target/lib/metadata.d.ts +14 -3
  40. package/target/lib/metadata.js +2 -2
  41. package/target/lib/metadata.js.map +1 -1
  42. package/target/lib/query.d.ts +9 -0
  43. package/target/lib/query.js +24 -0
  44. package/target/lib/query.js.map +1 -0
  45. package/target/lib/query.test.js +78 -0
  46. package/target/lib/query.test.js.map +1 -0
  47. package/target/lib/result.d.ts +2 -8
  48. package/target/lib/result.js +1 -19
  49. package/target/lib/result.js.map +1 -1
  50. package/target/lib/tags.d.ts +10 -20
  51. package/target/lib/tags.js +17 -29
  52. package/target/lib/tags.js.map +1 -1
  53. package/target/lib/template.d.ts +9 -0
  54. package/target/lib/template.js +83 -0
  55. package/target/lib/template.js.map +1 -0
  56. package/target/lib/template.test.d.ts +1 -0
  57. package/target/lib/template.test.js +70 -0
  58. package/target/lib/template.test.js.map +1 -0
  59. package/target/lib.d.ts +3 -3
  60. package/target/lib.js +3 -3
  61. package/target/lib.js.map +1 -1
  62. package/src/lib/shadow.test.ts +0 -40
  63. package/src/lib/shadow.ts +0 -16
  64. package/src/lib/tag-name.test.ts +0 -13
  65. package/src/lib/tag-name.ts +0 -10
  66. package/src/lib/tags.test.ts +0 -28
  67. package/target/lib/shadow.d.ts +0 -2
  68. package/target/lib/shadow.js +0 -10
  69. package/target/lib/shadow.js.map +0 -1
  70. package/target/lib/shadow.test.js +0 -69
  71. package/target/lib/shadow.test.js.map +0 -1
  72. package/target/lib/tag-name.d.ts +0 -1
  73. package/target/lib/tag-name.js +0 -6
  74. package/target/lib/tag-name.js.map +0 -1
  75. package/target/lib/tag-name.test.js +0 -36
  76. package/target/lib/tag-name.test.js.map +0 -1
  77. package/target/lib/tags.test.js +0 -23
  78. package/target/lib/tags.test.js.map +0 -1
  79. /package/target/lib/{shadow.test.d.ts → lifecycle.test.d.ts} +0 -0
  80. /package/target/lib/{tag-name.test.d.ts → listen.test.d.ts} +0 -0
  81. /package/target/lib/{tags.test.d.ts → query.test.d.ts} +0 -0
package/README.md CHANGED
@@ -1,42 +1,80 @@
1
1
  # Element
2
2
 
3
- Create a shadow root and apply styles and html as defined
3
+ Utilities for building web compnennts. Especially targeted at
4
4
 
5
- #### Installation:
5
+ ## Table of Contents
6
+
7
+ - [Installation](#installation)
8
+ - [Custom Element](#custom-element)
9
+ - [Attributes](#attributes)
10
+ - [Template](#template)
11
+ - [Styles](#styles)
12
+ - [Listeners](#listeners)
13
+ - [Queries](#queries)
14
+
15
+ ## Installation
6
16
 
7
17
  ```BASH
8
- npm i @joist/element
18
+ npm i @joist/element@next
19
+ ```
20
+
21
+ ## Custom Element
22
+
23
+ To define a custom element decorate your custom element class and add a tagName
24
+
25
+ ```ts
26
+ @element({
27
+ tagName: 'my-element'
28
+ })
29
+ export class MyElement extends HTMLElement {}
30
+ ```
31
+
32
+ ## Attributes
33
+
34
+ Attributes can be managed using the `@attr` decorator. This decorator will read attribute values and and write properties back to attributes;
35
+
36
+ ```ts
37
+ @element({
38
+ tagName: 'my-element'
39
+ })
40
+ export class MyElement extends HTMLElement {
41
+ @attr()
42
+ accessor greeting = 'Hello World';
43
+ }
9
44
  ```
10
45
 
11
- #### Example:
46
+ ## Template
12
47
 
13
- ```TS
14
- import { tagName, shadow, css, html, attr, listen, element } from '@joist/element';
48
+ Joist ships with a very simple template library. It is designed to be very small and is only responsible for updating text in different DOM nodes.
15
49
 
16
- @element
50
+ ```ts
51
+ @element({
52
+ tagName: 'my-element',
53
+ shadow: [
54
+ html`
55
+ <h1 #:bind="greeting" #:hidden="!greeting"></h1>
56
+
57
+ <ul>
58
+ <li #:bind="items.0"></li>
59
+ <li #:bind="items.1"></li>
60
+ <li #:bind="items.2"></li>
61
+ <li #:bind="items.3"></li>
62
+ <li #:bind="items.4"></li>
63
+ </ul>
64
+ `
65
+ ]
66
+ })
17
67
  export class MyElement extends HTMLElement {
18
- // define a custom element
19
- @tagName static tagName = 'my-element';
20
-
21
- // apply styles to shadow dom
22
- @shadow styles = css`
23
- :host {
24
- display: block;
25
- color: red;
26
- }
27
- `;
28
-
29
- // apply html to shadow dom
30
- @shadow template = html`
31
- <slot></slot>
32
- `;
33
-
34
- // define attributes
35
- @attr accessor value = 0;
36
-
37
- // listen for events
38
- @listen('click') onClick() {
39
- console.log('clicked!')
68
+ @attr()
69
+ accessor greeting = 'Hello World';
70
+
71
+ items = ['first', 'second', 'third', 'fourth', 'fifth'];
72
+
73
+ #render = template();
74
+
75
+ @ready()
76
+ onReady() {
77
+ this.#render();
40
78
  }
41
79
  }
42
80
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joist/element",
3
- "version": "3.9.1",
3
+ "version": "4.0.0-next.10",
4
4
  "type": "module",
5
5
  "main": "./target/lib.js",
6
6
  "module": "./target/lib.js",
@@ -9,7 +9,7 @@
9
9
  "import": "./target/lib.js"
10
10
  },
11
11
  "./*": {
12
- "import": "./target/lib/*.js"
12
+ "import": "./target/lib"
13
13
  }
14
14
  },
15
15
  "files": [
@@ -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,64 +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
+ import { element } from './element.js';
4
5
 
5
- describe('@attr()', () => {
6
- it('should read and parse the correct values', async () => {
7
- class MyElement extends HTMLElement {
8
- @attr accessor value1 = 100; // no attribute
9
- @attr accessor value2 = 0; // number
10
- @attr accessor value3 = false; // boolean
11
- @attr accessor value4 = 'hello'; // string
12
- }
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
13
13
 
14
- customElements.define('attr-test-2', MyElement);
14
+ @attr()
15
+ accessor value2 = 0; // number
15
16
 
16
- const el = await fixture<MyElement>(
17
- html`<attr-test-2 value2="2" value3 value4="world"></attr-test-2>`
18
- );
17
+ @attr()
18
+ accessor value3 = false; // boolean
19
19
 
20
- expect(el.value1).to.equal(100);
21
- expect(el.value2).to.equal(2);
22
- expect(el.value3).to.equal(true);
23
- expect(el.value4).to.equal('world');
24
- });
20
+ @attr()
21
+ accessor value4 = 'hello'; // string
22
+ }
25
23
 
26
- it('should not write falsy props to attributes', async () => {
27
- class MyElement extends HTMLElement {
28
- @attr accessor value1 = undefined;
29
- @attr accessor value2 = null;
30
- @attr accessor value3 = '';
31
- }
24
+ const container = document.createElement('div');
25
+ container.innerHTML = /*html*/ `
26
+ <attr-test-1 value2="2" value3 value4="world"></attr-test-1>
27
+ `;
32
28
 
33
- customElements.define('attr-test-3', MyElement);
29
+ document.body.append(container);
34
30
 
35
- const el = await fixture<MyElement>(html`<attr-test-3></attr-test-3>`);
31
+ const el = document.querySelector('attr-test-1') as MyElement;
36
32
 
37
- expect(el.hasAttribute('value1')).to.be.false;
38
- expect(el.hasAttribute('value2')).to.be.false;
39
- expect(el.hasAttribute('value3')).to.be.false;
40
- });
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');
41
37
 
42
- it('should update attributes when props are changed', async () => {
43
- class MyElement extends HTMLElement {
44
- @attr accessor value1 = 'hello'; // no attribute
45
- @attr accessor value2 = 0; // number
46
- @attr accessor value3 = true; // boolean
47
- @attr accessor value4 = false; // boolean
48
- }
38
+ container.remove();
39
+ });
40
+
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;
48
+
49
+ @attr()
50
+ accessor value2 = null;
51
+
52
+ @attr()
53
+ accessor value3 = '';
54
+ }
55
+
56
+ const el = new MyElement();
57
+
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
+ });
62
+
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
49
76
 
50
- customElements.define('attr-test-4', MyElement);
77
+ @attr()
78
+ accessor value4 = false; // boolean
79
+ }
51
80
 
52
- const el = await fixture<MyElement>(html`<attr-test-4></attr-test-4>`);
81
+ const el = new MyElement();
53
82
 
54
- el.value1 = 'world';
55
- el.value2 = 100;
56
- el.value3 = false;
57
- el.value4 = true;
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
+ });
93
+
94
+ it('should normalize attribute names', async () => {
95
+ const value3 = Symbol('Value from SYMBOL');
96
+
97
+ @element({
98
+ tagName: 'attr-test-4'
99
+ })
100
+ class MyElement extends HTMLElement {
101
+ @attr()
102
+ accessor Value1 = 'hello';
103
+
104
+ @attr()
105
+ accessor ['Value 2'] = 0;
106
+
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();
127
+
128
+ @element({
129
+ tagName: 'attr-test-4'
130
+ })
131
+ class MyElement extends HTMLElement {
132
+ @attr()
133
+ accessor [value] = true;
134
+ }
58
135
 
59
- expect(el.getAttribute('value1')).to.equal('world');
60
- expect(el.getAttribute('value2')).to.equal('100');
61
- expect(el.hasAttribute('value3')).to.be.false;
62
- expect(el.hasAttribute('value4')).to.be.true;
63
- });
136
+ new MyElement();
137
+ }).to.throw('Cannot handle Symbol property without description');
64
138
  });
package/src/lib/attr.ts CHANGED
@@ -1,46 +1,73 @@
1
1
  import { metadataStore } from './metadata.js';
2
2
 
3
- export function attr<This extends HTMLElement>(
4
- { get, set }: ClassAccessorDecoratorTarget<This, unknown>,
5
- ctx: ClassAccessorDecoratorContext<This>
6
- ): ClassAccessorDecoratorResult<This, any> {
7
- const name = String(ctx.name);
8
- const meta = metadataStore.read(ctx.metadata);
9
- meta.attrs.push(name);
10
-
11
- return {
12
- set(value: unknown) {
13
- if (value === true) {
14
- this.setAttribute(name, '');
15
- } else if (value === false) {
16
- this.removeAttribute(name);
17
- } else {
18
- this.setAttribute(name, String(value));
19
- }
3
+ export interface AttrOpts {
4
+ observed?: boolean;
5
+ }
6
+
7
+ export function attr(opts?: AttrOpts) {
8
+ return function attrDecorator<This extends HTMLElement>(
9
+ { get, set }: ClassAccessorDecoratorTarget<This, unknown>,
10
+ ctx: ClassAccessorDecoratorContext<This>
11
+ ): ClassAccessorDecoratorResult<This, any> {
12
+ const attrName = parseAttrName(ctx.name);
13
+ const meta = metadataStore.read(ctx.metadata);
20
14
 
21
- set.call(this, value);
22
- },
23
- get() {
24
- const ogValue = get.call(this);
25
- const attr = this.getAttribute(name);
15
+ meta.attrs.push({
16
+ propName: ctx.name,
17
+ attrName,
18
+ observe: opts?.observed ?? true
19
+ });
26
20
 
27
- if (attr !== null) {
28
- // treat as boolean
29
- if (attr === '') {
30
- return true;
21
+ return {
22
+ set(value: unknown) {
23
+ if (value === true) {
24
+ this.setAttribute(attrName, '');
25
+ } else if (value === false) {
26
+ this.removeAttribute(attrName);
27
+ } else {
28
+ this.setAttribute(attrName, String(value));
31
29
  }
32
30
 
33
- // treat as number
34
- if (typeof ogValue === 'number') {
35
- return Number(attr);
31
+ set.call(this, value);
32
+ },
33
+ get() {
34
+ const ogValue = get.call(this);
35
+ const attr = this.getAttribute(attrName);
36
+
37
+ if (attr !== null) {
38
+ // treat as boolean
39
+ if (attr === '') {
40
+ return true;
41
+ }
42
+
43
+ // treat as number
44
+ if (typeof ogValue === 'number') {
45
+ return Number(attr);
46
+ }
47
+
48
+ // treat as string
49
+ return attr;
36
50
  }
37
51
 
38
- // treat as string
39
- return attr;
52
+ // no readable value return original
53
+ return ogValue;
40
54
  }
55
+ };
56
+ };
57
+ }
58
+
59
+ function parseAttrName(val: string | symbol): string {
60
+ let value: string;
41
61
 
42
- // no readable value return original
43
- return ogValue;
62
+ if (typeof val === 'symbol') {
63
+ if (val.description) {
64
+ value = val.description;
65
+ } else {
66
+ throw new Error('Cannot handle Symbol property without description');
44
67
  }
45
- };
68
+ } else {
69
+ value = val;
70
+ }
71
+
72
+ return value.toLowerCase().replaceAll(' ', '-');
46
73
  }
@@ -1,24 +1,97 @@
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
- import { tagName } from './tag-name.js';
5
+ import { css, html } from './tags.js';
6
6
 
7
- describe('@element()', () => {
8
- it('should write default value to attribute', async () => {
9
- @element
10
- class MyElement extends HTMLElement {
11
- @tagName static tag = 'element-1';
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
12
14
 
13
- @attr accessor value1 = 'hello'; // no attribute
14
- @attr accessor value2 = 0; // number
15
- @attr accessor value3 = true; // boolean
16
- }
15
+ @attr()
16
+ accessor value2 = 0; // number
17
17
 
18
- const el = await fixture<MyElement>(html`<element-1></element-1>`);
18
+ @attr()
19
+ accessor value3 = true; // boolean
20
+ }
19
21
 
20
- expect(el.getAttribute('value1')).to.equal('hello');
21
- expect(el.getAttribute('value2')).to.equal('0');
22
- expect(el.getAttribute('value3')).to.equal('');
23
- });
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) {
82
+ const div = document.createElement('div');
83
+ div.innerHTML = 'hello world';
84
+
85
+ el.append(div);
86
+ }
87
+ }
88
+ ]
89
+ })
90
+ class MyElement extends HTMLElement {}
91
+
92
+ const el = new MyElement();
93
+
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>`);
24
97
  });