@joist/element 4.0.1-next.0 → 4.0.1

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 (97) hide show
  1. package/README.md +142 -29
  2. package/package.json +7 -9
  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 +175 -47
  6. package/src/lib/attr.ts +60 -40
  7. package/src/lib/element.test.ts +111 -18
  8. package/src/lib/element.ts +141 -39
  9. package/src/lib/lifecycle.test.ts +31 -0
  10. package/src/lib/lifecycle.ts +12 -0
  11. package/src/lib/listen.test.ts +104 -0
  12. package/src/lib/listen.ts +31 -5
  13. package/src/lib/metadata.ts +31 -9
  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 +142 -0
  17. package/src/lib/query.ts +72 -0
  18. package/src/lib/result.ts +2 -26
  19. package/src/lib/tags.ts +26 -65
  20. package/src/lib/template.test.ts +123 -0
  21. package/src/lib/template.ts +130 -0
  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.js +54 -0
  27. package/target/lib/attr-changed.test.js.map +1 -0
  28. package/target/lib/attr.d.ts +6 -1
  29. package/target/lib/attr.js +50 -31
  30. package/target/lib/attr.js.map +1 -1
  31. package/target/lib/attr.test.js +395 -148
  32. package/target/lib/attr.test.js.map +1 -1
  33. package/target/lib/element.d.ts +11 -334
  34. package/target/lib/element.js +105 -32
  35. package/target/lib/element.js.map +1 -1
  36. package/target/lib/element.test.js +162 -66
  37. package/target/lib/element.test.js.map +1 -1
  38. package/target/lib/lifecycle.d.ts +1 -0
  39. package/target/lib/lifecycle.js +8 -0
  40. package/target/lib/lifecycle.js.map +1 -0
  41. package/target/lib/lifecycle.test.js +48 -0
  42. package/target/lib/lifecycle.test.js.map +1 -0
  43. package/target/lib/listen.d.ts +2 -1
  44. package/target/lib/listen.js +22 -4
  45. package/target/lib/listen.js.map +1 -1
  46. package/target/lib/listen.test.js +167 -0
  47. package/target/lib/listen.test.js.map +1 -0
  48. package/target/lib/metadata.d.ts +25 -6
  49. package/target/lib/metadata.js +9 -4
  50. package/target/lib/metadata.js.map +1 -1
  51. package/target/lib/query-all.d.ts +10 -0
  52. package/target/lib/query-all.js +40 -0
  53. package/target/lib/query-all.js.map +1 -0
  54. package/target/lib/query-all.test.d.ts +1 -0
  55. package/target/lib/query-all.test.js +195 -0
  56. package/target/lib/query-all.test.js.map +1 -0
  57. package/target/lib/query.d.ts +10 -0
  58. package/target/lib/query.js +36 -0
  59. package/target/lib/query.js.map +1 -0
  60. package/target/lib/query.test.d.ts +1 -0
  61. package/target/lib/query.test.js +188 -0
  62. package/target/lib/query.test.js.map +1 -0
  63. package/target/lib/result.d.ts +2 -8
  64. package/target/lib/result.js +1 -19
  65. package/target/lib/result.js.map +1 -1
  66. package/target/lib/tags.d.ts +10 -20
  67. package/target/lib/tags.js +21 -30
  68. package/target/lib/tags.js.map +1 -1
  69. package/target/lib/template.d.ts +11 -0
  70. package/target/lib/template.js +89 -0
  71. package/target/lib/template.js.map +1 -0
  72. package/target/lib/template.test.d.ts +1 -0
  73. package/target/lib/template.test.js +91 -0
  74. package/target/lib/template.test.js.map +1 -0
  75. package/target/lib.d.ts +8 -7
  76. package/target/lib.js +8 -7
  77. package/target/lib.js.map +1 -1
  78. package/src/lib/shadow.test.ts +0 -40
  79. package/src/lib/shadow.ts +0 -16
  80. package/src/lib/tag-name.test.ts +0 -13
  81. package/src/lib/tag-name.ts +0 -10
  82. package/src/lib/tags.test.ts +0 -28
  83. package/target/lib/shadow.d.ts +0 -2
  84. package/target/lib/shadow.js +0 -10
  85. package/target/lib/shadow.js.map +0 -1
  86. package/target/lib/shadow.test.js +0 -69
  87. package/target/lib/shadow.test.js.map +0 -1
  88. package/target/lib/tag-name.d.ts +0 -1
  89. package/target/lib/tag-name.js +0 -6
  90. package/target/lib/tag-name.js.map +0 -1
  91. package/target/lib/tag-name.test.js +0 -36
  92. package/target/lib/tag-name.test.js.map +0 -1
  93. package/target/lib/tags.test.js +0 -23
  94. package/target/lib/tags.test.js.map +0 -1
  95. /package/target/lib/{shadow.test.d.ts → attr-changed.test.d.ts} +0 -0
  96. /package/target/lib/{tag-name.test.d.ts → lifecycle.test.d.ts} +0 -0
  97. /package/target/lib/{tags.test.d.ts → listen.test.d.ts} +0 -0
package/README.md CHANGED
@@ -1,42 +1,155 @@
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
+ - [Styles](#styles)
11
+ - [Listeners](#listeners)
12
+ - [Queries](#queries)
13
+
14
+ ## Installation
6
15
 
7
16
  ```BASH
8
- npm i @joist/element
17
+ npm i @joist/element@next
18
+ ```
19
+
20
+ ## Custom Element
21
+
22
+ To define a custom element decorate your custom element class and add a tagName
23
+
24
+ ```ts
25
+ @element({
26
+ tagName: 'my-element'
27
+ })
28
+ export class MyElement extends HTMLElement {}
9
29
  ```
10
30
 
11
- #### Example:
31
+ ## Attributes
12
32
 
13
- ```TS
14
- import { tagName, shadow, css, html, attr, listen, element } from '@joist/element';
33
+ Attributes can be managed using the `@attr` decorator. This decorator will read attribute values and and write properties back to attributes;
15
34
 
16
- @element
35
+ ```ts
36
+ @element({
37
+ tagName: 'my-element'
38
+ })
17
39
  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!')
40
+ @attr()
41
+ accessor greeting = 'Hello World';
42
+ }
43
+ ```
44
+
45
+ ## HTML and CSS
46
+
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.
50
+
51
+ ```ts
52
+ @element({
53
+ tagName: 'my-element',
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: [
106
+ html`
107
+ <label for="my-input">
108
+ <slot></slot>
109
+ </label>
110
+
111
+ <input id="my-input" />
112
+ `
113
+ ]
114
+ })
115
+ export class MyElement extends HTMLElement {
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
131
+
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;
145
+
146
+ #inputs = queryAll('input');
147
+
148
+ @effect()
149
+ onChange() {
150
+ this.#input(() => {
151
+ return { value: this.value }
152
+ })
40
153
  }
41
154
  }
42
155
  ```
package/package.json CHANGED
@@ -1,16 +1,13 @@
1
1
  {
2
2
  "name": "@joist/element",
3
- "version": "4.0.1-next.0",
3
+ "version": "4.0.1",
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/*.js"
13
- }
8
+ ".": "./target/lib.js",
9
+ "./*": "./target/lib/*",
10
+ "./package.json": "./package.json"
14
11
  },
15
12
  "files": [
16
13
  "src",
@@ -20,7 +17,7 @@
20
17
  "description": "Intelligently apply styles to WebComponents",
21
18
  "repository": {
22
19
  "type": "git",
23
- "url": "git+https://github.com/deebloo/joist.git"
20
+ "url": "git+https://github.com/joist-framework/joist.git"
24
21
  },
25
22
  "keywords": [
26
23
  "TypeScript",
@@ -31,7 +28,7 @@
31
28
  "author": "deebloo",
32
29
  "license": "MIT",
33
30
  "bugs": {
34
- "url": "https://github.com/deebloo/joist/issues"
31
+ "url": "https://github.com/joist-framework/joist/issues"
35
32
  },
36
33
  "publishConfig": {
37
34
  "access": "public"
@@ -57,6 +54,7 @@
57
54
  "test": {
58
55
  "command": "wtr --config wtr.config.mjs",
59
56
  "files": [
57
+ "vitest.config.js",
60
58
  "target/**"
61
59
  ],
62
60
  "output": [],
@@ -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,64 +1,192 @@
1
- import { expect, fixture, html } from '@open-wc/testing';
1
+ import { expect } from "chai";
2
2
 
3
- import { attr } from './attr.js';
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
+ `;
28
+
29
+ document.body.append(container);
30
+
31
+ const el = document.querySelector("attr-test-1") as MyElement;
32
+
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");
37
+
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
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;
32
87
 
33
- customElements.define('attr-test-3', MyElement);
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 value2 = "Value 2";
96
+ const value3 = Symbol("Value from SYMBOL");
97
+
98
+ @element({
99
+ tagName: "attr-test-4",
100
+ })
101
+ class MyElement extends HTMLElement {
102
+ @attr()
103
+ accessor Value1 = "hello";
104
+
105
+ @attr()
106
+ accessor [value2] = 0;
107
+
108
+ @attr()
109
+ accessor [value3] = true;
110
+ }
111
+
112
+ const el = new MyElement();
34
113
 
35
- const el = await fixture<MyElement>(html`<attr-test-3></attr-test-3>`);
114
+ document.body.append(el);
36
115
 
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
- });
116
+ expect([...el.attributes].map((attr) => attr.name)).to.deep.equal([
117
+ "value1",
118
+ "value-2",
119
+ "value-from-symbol",
120
+ ]);
41
121
 
42
- it('should update attributes when props are changed', async () => {
122
+ el.remove();
123
+ });
124
+
125
+ it("should throw an error for symbols with no description", async () => {
126
+ expect(() => {
127
+ const value = Symbol();
128
+
129
+ @element({
130
+ tagName: "attr-test-4",
131
+ })
43
132
  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
133
+ @attr()
134
+ accessor [value] = true;
48
135
  }
49
136
 
50
- customElements.define('attr-test-4', MyElement);
137
+ new MyElement();
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");
51
186
 
52
- const el = await fixture<MyElement>(html`<attr-test-4></attr-test-4>`);
187
+ document.body.append(el);
53
188
 
54
- el.value1 = 'world';
55
- el.value2 = 100;
56
- el.value3 = false;
57
- el.value4 = true;
189
+ expect(el.value).to.equal("TEST");
58
190
 
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
- });
191
+ el.remove();
64
192
  });
package/src/lib/attr.ts CHANGED
@@ -1,46 +1,66 @@
1
- import { metadataStore } from './metadata.js';
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
- }
20
-
21
- set.call(this, value);
22
- },
23
- get() {
24
- const ogValue = get.call(this);
25
- const attr = this.getAttribute(name);
26
-
27
- if (attr !== null) {
28
- // treat as boolean
29
- if (attr === '') {
30
- return true;
31
- }
1
+ import { metadataStore } from "./metadata.js";
2
+
3
+ export interface AttrOpts {
4
+ name?: string;
5
+ observed?: boolean;
6
+ reflect?: boolean;
7
+ }
8
+
9
+ export function attr(opts?: AttrOpts) {
10
+ return function attrDecorator<This extends HTMLElement>(
11
+ { get, set }: ClassAccessorDecoratorTarget<This, unknown>,
12
+ ctx: ClassAccessorDecoratorContext<This>,
13
+ ): ClassAccessorDecoratorResult<This, any> {
14
+ const attrName = opts?.name ?? parseAttrName(ctx.name);
15
+ const meta = metadataStore.read<This>(ctx.metadata);
16
+ const reflect = opts?.reflect ?? true;
17
+
18
+ meta.attrs.set(attrName, {
19
+ propName: ctx.name,
20
+ observe: opts?.observed ?? true,
21
+ reflect,
22
+ getPropValue: get,
23
+ setPropValue: set,
24
+ });
25
+
26
+ return {
27
+ set(value: unknown) {
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);
32
39
 
33
- // treat as number
34
- if (typeof ogValue === 'number') {
35
- return Number(attr);
40
+ if (this.getAttribute(attrName) !== strValue) {
41
+ this.setAttribute(attrName, strValue);
42
+ }
43
+ }
36
44
  }
37
45
 
38
- // treat as string
39
- return attr;
40
- }
46
+ set.call(this, value);
47
+ },
48
+ };
49
+ };
50
+ }
51
+
52
+ function parseAttrName(val: string | symbol): string {
53
+ let value: string;
41
54
 
42
- // no readable value return original
43
- return ogValue;
55
+ if (typeof val === "symbol") {
56
+ if (val.description) {
57
+ value = val.description;
58
+ } else {
59
+ throw new Error("Cannot handle Symbol property without description");
44
60
  }
45
- };
61
+ } else {
62
+ value = val;
63
+ }
64
+
65
+ return value.toLowerCase().replaceAll(" ", "-");
46
66
  }