@joist/element 4.0.0-next.9 → 4.0.0

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 (74) hide show
  1. package/README.md +96 -21
  2. package/package.json +10 -33
  3. package/src/lib/attr-changed.test.ts +34 -0
  4. package/src/lib/attr-changed.ts +15 -0
  5. package/src/lib/attr.test.ts +88 -34
  6. package/src/lib/attr.ts +31 -38
  7. package/src/lib/element.test.ts +69 -49
  8. package/src/lib/element.ts +118 -52
  9. package/src/lib/lifecycle.test.ts +10 -10
  10. package/src/lib/lifecycle.ts +5 -2
  11. package/src/lib/listen.test.ts +43 -27
  12. package/src/lib/listen.ts +13 -9
  13. package/src/lib/metadata.ts +25 -11
  14. package/src/lib/query-all.test.ts +153 -0
  15. package/src/lib/query-all.ts +81 -0
  16. package/src/lib/query.test.ts +113 -24
  17. package/src/lib/query.ts +51 -16
  18. package/src/lib/result.ts +2 -2
  19. package/src/lib/tags.ts +10 -7
  20. package/src/lib/template.test.ts +57 -29
  21. package/src/lib/template.ts +42 -27
  22. package/src/lib.ts +8 -7
  23. package/target/lib/attr-changed.d.ts +2 -0
  24. package/target/lib/attr-changed.js +10 -0
  25. package/target/lib/attr-changed.js.map +1 -0
  26. package/target/lib/attr-changed.test.d.ts +1 -0
  27. package/target/lib/attr-changed.test.js +54 -0
  28. package/target/lib/attr-changed.test.js.map +1 -0
  29. package/target/lib/attr.d.ts +2 -0
  30. package/target/lib/attr.js +28 -30
  31. package/target/lib/attr.js.map +1 -1
  32. package/target/lib/attr.test.js +157 -40
  33. package/target/lib/attr.test.js.map +1 -1
  34. package/target/lib/element.d.ts +9 -337
  35. package/target/lib/element.js +84 -40
  36. package/target/lib/element.js.map +1 -1
  37. package/target/lib/element.test.js +52 -78
  38. package/target/lib/element.test.js.map +1 -1
  39. package/target/lib/lifecycle.d.ts +1 -1
  40. package/target/lib/lifecycle.js +1 -1
  41. package/target/lib/lifecycle.js.map +1 -1
  42. package/target/lib/lifecycle.test.js +10 -10
  43. package/target/lib/lifecycle.test.js.map +1 -1
  44. package/target/lib/listen.d.ts +2 -2
  45. package/target/lib/listen.js +5 -4
  46. package/target/lib/listen.js.map +1 -1
  47. package/target/lib/listen.test.js +35 -27
  48. package/target/lib/listen.test.js.map +1 -1
  49. package/target/lib/metadata.d.ts +21 -11
  50. package/target/lib/metadata.js +8 -3
  51. package/target/lib/metadata.js.map +1 -1
  52. package/target/lib/query-all.d.ts +10 -0
  53. package/target/lib/query-all.js +40 -0
  54. package/target/lib/query-all.js.map +1 -0
  55. package/target/lib/query-all.test.d.ts +1 -0
  56. package/target/lib/query-all.test.js +195 -0
  57. package/target/lib/query-all.test.js.map +1 -0
  58. package/target/lib/query.d.ts +6 -5
  59. package/target/lib/query.js +22 -10
  60. package/target/lib/query.js.map +1 -1
  61. package/target/lib/query.test.js +134 -24
  62. package/target/lib/query.test.js.map +1 -1
  63. package/target/lib/result.d.ts +2 -2
  64. package/target/lib/tags.d.ts +4 -4
  65. package/target/lib/tags.js +6 -3
  66. package/target/lib/tags.js.map +1 -1
  67. package/target/lib/template.d.ts +3 -1
  68. package/target/lib/template.js +26 -20
  69. package/target/lib/template.js.map +1 -1
  70. package/target/lib/template.test.js +47 -26
  71. package/target/lib/template.test.js.map +1 -1
  72. package/target/lib.d.ts +8 -7
  73. package/target/lib.js +8 -7
  74. package/target/lib.js.map +1 -1
package/README.md CHANGED
@@ -7,7 +7,6 @@ Utilities for building web compnennts. Especially targeted at
7
7
  - [Installation](#installation)
8
8
  - [Custom Element](#custom-element)
9
9
  - [Attributes](#attributes)
10
- - [Template](#template)
11
10
  - [Styles](#styles)
12
11
  - [Listeners](#listeners)
13
12
  - [Queries](#queries)
@@ -15,7 +14,7 @@ Utilities for building web compnennts. Especially targeted at
15
14
  ## Installation
16
15
 
17
16
  ```BASH
18
- npm i @joist/element
17
+ npm i @joist/element@next
19
18
  ```
20
19
 
21
20
  ## Custom Element
@@ -43,38 +42,114 @@ export class MyElement extends HTMLElement {
43
42
  }
44
43
  ```
45
44
 
46
- ## Template
45
+ ## HTML and CSS
47
46
 
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.
47
+ HTML templates can be applied by passing the result of the `html` tag to the shaodw list.
48
+ CSS can be applied by passing the result of the `css` tag to the shadow list.
49
+ Any new tagged template literal that returns a `ShadowResult` can be used.
49
50
 
50
51
  ```ts
51
52
  @element({
52
53
  tagName: 'my-element',
53
- shadow: [
54
+ shadowDom: [
55
+ css`
56
+ h1 {
57
+ color: red;
58
+ }
59
+ `,
60
+ html`<h1>Hello World</h1>`
61
+ ]
62
+ })
63
+ export class MyElement extends HTMLElement {}
64
+ ```
65
+
66
+ ## Listeners
67
+
68
+ The `@listen` decorator allows you to easy setup event listeners. By default the listener will be attached to the shadow root if it exists or the host element if it doesn't. This can be customized by pass a selector function to the decorator
69
+
70
+ ```ts
71
+ @element({
72
+ tagName: 'my-element',
73
+ shadowDom: []
74
+ })
75
+ export class MyElement extends HTMLElement {
76
+ @listen('eventname')
77
+ onEventName1() {
78
+ // all listener to the shadow root
79
+ }
80
+
81
+ @listen('eventname', (host) => host)
82
+ onEventName2() {
83
+ // all listener to the host element
84
+ }
85
+
86
+ @listen('eventname', (host) => host.querySelector('button'))
87
+ onEventName3() {
88
+ // add listener to a button found in the light dom
89
+ }
90
+
91
+ @listen('eventname', '#test')
92
+ onEventName4() {
93
+ // add listener to element with the id of "test" that is found in the shadow dom
94
+ }
95
+ }
96
+ ```
97
+
98
+ ## Query
99
+
100
+ The `query` function will query for a particular element and allow you to easily patch that element with new properties.
101
+
102
+ ```ts
103
+ @element({
104
+ tagName: 'my-element',
105
+ shadowDom: [
54
106
  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>
107
+ <label for="my-input">
108
+ <slot></slot>
109
+ </label>
110
+
111
+ <input id="my-input" />
64
112
  `
65
113
  ]
66
114
  })
67
115
  export class MyElement extends HTMLElement {
68
- @attr()
69
- accessor greeting = 'Hello World';
116
+ @observe()
117
+ value: string;
118
+
119
+ #input = query('input');
120
+
121
+ @effect()
122
+ onChange() {
123
+ const input = this.#input({ value: this.value});
124
+ }
125
+ }
126
+ ```
127
+
128
+ ## QueryAll
129
+
130
+ The `queryAll` function will get all elements that match the given query. A patching function can be passed to update any or all items in the list
70
131
 
71
- items = ['first', 'second', 'third', 'fourth', 'fifth'];
132
+ ```ts
133
+ @element({
134
+ tagName: 'my-element',
135
+ shadowDom: [
136
+ html`
137
+ <input id="first" />
138
+ <input id="second" />
139
+ `
140
+ ]
141
+ })
142
+ export class MyElement extends HTMLElement {
143
+ @observe()
144
+ value: string;
72
145
 
73
- #render = template();
146
+ #inputs = queryAll('input');
74
147
 
75
- @ready()
76
- onReady() {
77
- this.#render();
148
+ @effect()
149
+ onChange() {
150
+ this.#input(() => {
151
+ return { value: this.value }
152
+ })
78
153
  }
79
154
  }
80
155
  ```
package/package.json CHANGED
@@ -1,33 +1,22 @@
1
1
  {
2
2
  "name": "@joist/element",
3
- "version": "4.0.0-next.9",
3
+ "version": "4.0.0",
4
4
  "type": "module",
5
5
  "main": "./target/lib.js",
6
6
  "module": "./target/lib.js",
7
7
  "exports": {
8
- ".": {
9
- "import": "./target/lib.js"
10
- },
11
- "./*": {
12
- "import": "./target/lib"
13
- }
8
+ ".": "./target/lib.js",
9
+ "./*": "./target/lib/*",
10
+ "./package.json": "./package.json"
14
11
  },
15
- "files": [
16
- "src",
17
- "target"
18
- ],
12
+ "files": ["src", "target"],
19
13
  "sideEffects": false,
20
14
  "description": "Intelligently apply styles to WebComponents",
21
15
  "repository": {
22
16
  "type": "git",
23
17
  "url": "git+https://github.com/joist-framework/joist.git"
24
18
  },
25
- "keywords": [
26
- "TypeScript",
27
- "WebComponents",
28
- "CSS",
29
- "ShadowDOM"
30
- ],
19
+ "keywords": ["TypeScript", "WebComponents", "CSS", "ShadowDOM"],
31
20
  "author": "deebloo",
32
21
  "license": "MIT",
33
22
  "bugs": {
@@ -44,26 +33,14 @@
44
33
  "build": {
45
34
  "command": "tsc --build --pretty",
46
35
  "clean": "if-file-deleted",
47
- "files": [
48
- "src/**",
49
- "tsconfig.json",
50
- "../../tsconfig.json"
51
- ],
52
- "output": [
53
- "target/**",
54
- "tsconfig.tsbuildinfo"
55
- ]
36
+ "files": ["src/**", "tsconfig.json", "../../tsconfig.json"],
37
+ "output": ["target/**", "tsconfig.tsbuildinfo"]
56
38
  },
57
39
  "test": {
58
40
  "command": "wtr --config wtr.config.mjs",
59
- "files": [
60
- "vitest.config.js",
61
- "target/**"
62
- ],
41
+ "files": ["vitest.config.js", "target/**"],
63
42
  "output": [],
64
- "dependencies": [
65
- "build"
66
- ]
43
+ "dependencies": ["build"]
67
44
  }
68
45
  }
69
46
  }
@@ -0,0 +1,34 @@
1
+ import { assert } from "chai";
2
+
3
+ import { attrChanged } from "./attr-changed.js";
4
+ import { attr } from "./attr.js";
5
+ import { element } from "./element.js";
6
+
7
+ it("should call specific attrbute callback", () => {
8
+ let args: string[] = [];
9
+
10
+ @element({
11
+ tagName: "attr-changed-1",
12
+ })
13
+ class MyElement extends HTMLElement {
14
+ @attr()
15
+ accessor test = "hello";
16
+
17
+ @attrChanged("test")
18
+ onTestChanged(oldValue: string, newValue: string) {
19
+ args = [oldValue, newValue];
20
+ }
21
+ }
22
+
23
+ const el = new MyElement();
24
+
25
+ document.body.append(el);
26
+
27
+ assert.deepEqual(args, [null, "hello"]);
28
+
29
+ el.setAttribute("test", "world");
30
+
31
+ assert.deepEqual(args, ["hello", "world"]);
32
+
33
+ el.remove();
34
+ });
@@ -0,0 +1,15 @@
1
+ import { type AttrChangedCallback, metadataStore } from "./metadata.js";
2
+
3
+ export function attrChanged(name: string) {
4
+ return function attrChangedDecorator<This extends HTMLElement>(
5
+ cb: AttrChangedCallback,
6
+ ctx: ClassMethodDecoratorContext<This>,
7
+ ): void {
8
+ const meta = metadataStore.read(ctx.metadata);
9
+ const val = meta.attrChanges.get(name) ?? new Set();
10
+
11
+ val.add(cb);
12
+
13
+ meta.attrChanges.set(name, val);
14
+ };
15
+ }
@@ -1,11 +1,11 @@
1
- import { expect } from 'chai';
1
+ import { expect } from "chai";
2
2
 
3
- import { attr } from './attr.js';
4
- import { element } from './element.js';
3
+ import { attr } from "./attr.js";
4
+ import { element } from "./element.js";
5
5
 
6
- it('should read and parse the correct values', () => {
6
+ it("should read and parse the correct values", () => {
7
7
  @element({
8
- tagName: 'attr-test-1'
8
+ tagName: "attr-test-1",
9
9
  })
10
10
  class MyElement extends HTMLElement {
11
11
  @attr()
@@ -18,29 +18,29 @@ it('should read and parse the correct values', () => {
18
18
  accessor value3 = false; // boolean
19
19
 
20
20
  @attr()
21
- accessor value4 = 'hello'; // string
21
+ accessor value4 = "hello"; // string
22
22
  }
23
23
 
24
- const container = document.createElement('div');
24
+ const container = document.createElement("div");
25
25
  container.innerHTML = /*html*/ `
26
26
  <attr-test-1 value2="2" value3 value4="world"></attr-test-1>
27
27
  `;
28
28
 
29
29
  document.body.append(container);
30
30
 
31
- const el = document.querySelector('attr-test-1') as MyElement;
31
+ const el = document.querySelector("attr-test-1") as MyElement;
32
32
 
33
33
  expect(el.value1).to.equal(100);
34
34
  expect(el.value2).to.equal(2);
35
35
  expect(el.value3).to.equal(true);
36
- expect(el.value4).to.equal('world');
36
+ expect(el.value4).to.equal("world");
37
37
 
38
38
  container.remove();
39
39
  });
40
40
 
41
- it('should not write falsy props to attributes', async () => {
41
+ it("should not write falsy props to attributes", async () => {
42
42
  @element({
43
- tagName: 'attr-test-2'
43
+ tagName: "attr-test-2",
44
44
  })
45
45
  class MyElement extends HTMLElement {
46
46
  @attr()
@@ -50,23 +50,23 @@ it('should not write falsy props to attributes', async () => {
50
50
  accessor value2 = null;
51
51
 
52
52
  @attr()
53
- accessor value3 = '';
53
+ accessor value3 = "";
54
54
  }
55
55
 
56
56
  const el = new MyElement();
57
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;
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
61
  });
62
62
 
63
- it('should update attributes when props are changed', async () => {
63
+ it("should update attributes when props are changed", async () => {
64
64
  @element({
65
- tagName: 'attr-test-3'
65
+ tagName: "attr-test-3",
66
66
  })
67
67
  class MyElement extends HTMLElement {
68
68
  @attr()
69
- accessor value1 = 'hello'; // no attribute
69
+ accessor value1 = "hello"; // no attribute
70
70
 
71
71
  @attr()
72
72
  accessor value2 = 0; // number
@@ -80,29 +80,30 @@ it('should update attributes when props are changed', async () => {
80
80
 
81
81
  const el = new MyElement();
82
82
 
83
- el.value1 = 'world';
83
+ el.value1 = "world";
84
84
  el.value2 = 100;
85
85
  el.value3 = false;
86
86
  el.value4 = true;
87
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;
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
92
  });
93
93
 
94
- it('should normalize attribute names', async () => {
95
- const value3 = Symbol('Value from SYMBOL');
94
+ it("should normalize attribute names", async () => {
95
+ const value2 = "Value 2";
96
+ const value3 = Symbol("Value from SYMBOL");
96
97
 
97
98
  @element({
98
- tagName: 'attr-test-4'
99
+ tagName: "attr-test-4",
99
100
  })
100
101
  class MyElement extends HTMLElement {
101
102
  @attr()
102
- accessor Value1 = 'hello';
103
+ accessor Value1 = "hello";
103
104
 
104
105
  @attr()
105
- accessor ['Value 2'] = 0;
106
+ accessor [value2] = 0;
106
107
 
107
108
  @attr()
108
109
  accessor [value3] = true;
@@ -113,20 +114,20 @@ it('should normalize attribute names', async () => {
113
114
  document.body.append(el);
114
115
 
115
116
  expect([...el.attributes].map((attr) => attr.name)).to.deep.equal([
116
- 'value1',
117
- 'value-2',
118
- 'value-from-symbol'
117
+ "value1",
118
+ "value-2",
119
+ "value-from-symbol",
119
120
  ]);
120
121
 
121
122
  el.remove();
122
123
  });
123
124
 
124
- it('should throw an error for symbols with no description', async () => {
125
+ it("should throw an error for symbols with no description", async () => {
125
126
  expect(() => {
126
127
  const value = Symbol();
127
128
 
128
129
  @element({
129
- tagName: 'attr-test-4'
130
+ tagName: "attr-test-4",
130
131
  })
131
132
  class MyElement extends HTMLElement {
132
133
  @attr()
@@ -134,5 +135,58 @@ it('should throw an error for symbols with no description', async () => {
134
135
  }
135
136
 
136
137
  new MyElement();
137
- }).to.throw('Cannot handle Symbol property without description');
138
+ }).to.throw("Cannot handle Symbol property without description");
139
+ });
140
+
141
+ it("should not reflect property to attribute", async () => {
142
+ @element({
143
+ tagName: "attr-test-5",
144
+ })
145
+ class MyElement extends HTMLElement {
146
+ @attr({ reflect: false })
147
+ accessor value = "foo";
148
+ }
149
+
150
+ const el = new MyElement();
151
+ el.value = "bar";
152
+
153
+ expect(el.value).to.equal("bar");
154
+
155
+ expect(el.hasAttribute("value")).to.be.false;
156
+ });
157
+
158
+ it("non reflective attributes should still read new attribute values", async () => {
159
+ @element({
160
+ tagName: "attr-test-6",
161
+ })
162
+ class MyElement extends HTMLElement {
163
+ @attr({ reflect: false })
164
+ accessor value = "foo";
165
+ }
166
+
167
+ const el = new MyElement();
168
+ el.setAttribute("value", "bar");
169
+
170
+ expect(el.value).to.equal("bar");
171
+ });
172
+
173
+ it("should allow a manually defined attribute name", async () => {
174
+ @element({
175
+ tagName: "attr-test-7",
176
+ })
177
+ class MyElement extends HTMLElement {
178
+ @attr({
179
+ name: "aria-label",
180
+ })
181
+ accessor value = "";
182
+ }
183
+
184
+ const el = new MyElement();
185
+ el.setAttribute("aria-label", "TEST");
186
+
187
+ document.body.append(el);
188
+
189
+ expect(el.value).to.equal("TEST");
190
+
191
+ el.remove();
138
192
  });
package/src/lib/attr.ts CHANGED
@@ -1,57 +1,50 @@
1
- import { metadataStore } from './metadata.js';
1
+ import { metadataStore } from "./metadata.js";
2
2
 
3
3
  export interface AttrOpts {
4
+ name?: string;
4
5
  observed?: boolean;
6
+ reflect?: boolean;
5
7
  }
6
8
 
7
9
  export function attr(opts?: AttrOpts) {
8
10
  return function attrDecorator<This extends HTMLElement>(
9
11
  { get, set }: ClassAccessorDecoratorTarget<This, unknown>,
10
- ctx: ClassAccessorDecoratorContext<This>
12
+ ctx: ClassAccessorDecoratorContext<This>,
11
13
  ): ClassAccessorDecoratorResult<This, any> {
12
- const attrName = parseAttrName(ctx.name);
13
- const meta = metadataStore.read(ctx.metadata);
14
+ const attrName = opts?.name ?? parseAttrName(ctx.name);
15
+ const meta = metadataStore.read<This>(ctx.metadata);
16
+ const reflect = opts?.reflect ?? true;
14
17
 
15
- meta.attrs.push({
18
+ meta.attrs.set(attrName, {
16
19
  propName: ctx.name,
17
- attrName,
18
- observe: opts?.observed ?? true
20
+ observe: opts?.observed ?? true,
21
+ reflect,
22
+ getPropValue: get,
23
+ setPropValue: set,
19
24
  });
20
25
 
21
26
  return {
22
27
  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));
29
- }
28
+ if (reflect) {
29
+ if (value === true) {
30
+ if (!this.hasAttribute(attrName)) {
31
+ this.setAttribute(attrName, "");
32
+ }
33
+ } else if (value === false) {
34
+ if (this.hasAttribute(attrName)) {
35
+ this.removeAttribute(attrName);
36
+ }
37
+ } else {
38
+ const strValue = String(value);
30
39
 
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;
40
+ if (this.getAttribute(attrName) !== strValue) {
41
+ this.setAttribute(attrName, strValue);
42
+ }
41
43
  }
42
-
43
- // treat as number
44
- if (typeof ogValue === 'number') {
45
- return Number(attr);
46
- }
47
-
48
- // treat as string
49
- return attr;
50
44
  }
51
45
 
52
- // no readable value return original
53
- return ogValue;
54
- }
46
+ set.call(this, value);
47
+ },
55
48
  };
56
49
  };
57
50
  }
@@ -59,15 +52,15 @@ export function attr(opts?: AttrOpts) {
59
52
  function parseAttrName(val: string | symbol): string {
60
53
  let value: string;
61
54
 
62
- if (typeof val === 'symbol') {
55
+ if (typeof val === "symbol") {
63
56
  if (val.description) {
64
57
  value = val.description;
65
58
  } else {
66
- throw new Error('Cannot handle Symbol property without description');
59
+ throw new Error("Cannot handle Symbol property without description");
67
60
  }
68
61
  } else {
69
62
  value = val;
70
63
  }
71
64
 
72
- return value.toLowerCase().replaceAll(' ', '-');
65
+ return value.toLowerCase().replaceAll(" ", "-");
73
66
  }