@joist/element 4.0.0-next.1 → 4.0.0-next.3

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 (52) hide show
  1. package/README.md +20 -23
  2. package/package.json +1 -1
  3. package/src/lib/attr.test.ts +85 -20
  4. package/src/lib/attr.ts +61 -34
  5. package/src/lib/element.test.ts +70 -8
  6. package/src/lib/element.ts +61 -39
  7. package/src/lib/listen.ts +10 -4
  8. package/src/lib/metadata.ts +10 -3
  9. package/src/lib/result.ts +12 -16
  10. package/src/lib/tags.ts +9 -66
  11. package/src/lib.ts +1 -3
  12. package/target/lib/attr.d.ts +4 -1
  13. package/target/lib/attr.js +49 -28
  14. package/target/lib/attr.js.map +1 -1
  15. package/target/lib/attr.test.js +148 -24
  16. package/target/lib/attr.test.js.map +1 -1
  17. package/target/lib/element.d.ts +6 -1
  18. package/target/lib/element.js +47 -31
  19. package/target/lib/element.js.map +1 -1
  20. package/target/lib/element.test.js +131 -17
  21. package/target/lib/element.test.js.map +1 -1
  22. package/target/lib/listen.d.ts +2 -1
  23. package/target/lib/listen.js +6 -3
  24. package/target/lib/listen.js.map +1 -1
  25. package/target/lib/metadata.d.ts +11 -3
  26. package/target/lib/metadata.js +0 -1
  27. package/target/lib/metadata.js.map +1 -1
  28. package/target/lib/result.d.ts +6 -3
  29. package/target/lib/result.js +7 -4
  30. package/target/lib/result.js.map +1 -1
  31. package/target/lib/tags.d.ts +5 -17
  32. package/target/lib/tags.js +5 -11
  33. package/target/lib/tags.js.map +1 -1
  34. package/target/lib.d.ts +1 -3
  35. package/target/lib.js +1 -3
  36. package/target/lib.js.map +1 -1
  37. package/src/lib/shadow.test.ts +0 -40
  38. package/src/lib/shadow.ts +0 -16
  39. package/src/lib/tag-name.test.ts +0 -13
  40. package/src/lib/tag-name.ts +0 -10
  41. package/target/lib/shadow.d.ts +0 -2
  42. package/target/lib/shadow.js +0 -10
  43. package/target/lib/shadow.js.map +0 -1
  44. package/target/lib/shadow.test.d.ts +0 -1
  45. package/target/lib/shadow.test.js +0 -69
  46. package/target/lib/shadow.test.js.map +0 -1
  47. package/target/lib/tag-name.d.ts +0 -1
  48. package/target/lib/tag-name.js +0 -6
  49. package/target/lib/tag-name.js.map +0 -1
  50. package/target/lib/tag-name.test.d.ts +0 -1
  51. package/target/lib/tag-name.test.js +0 -36
  52. package/target/lib/tag-name.test.js.map +0 -1
package/README.md CHANGED
@@ -11,31 +11,28 @@ npm i @joist/element
11
11
  #### Example:
12
12
 
13
13
  ```TS
14
- import { tagName, shadow, css, html, attr, listen, element } from '@joist/element';
15
-
16
- @element
14
+ import { tagName, css, html, attr, listen, element } from '@joist/element';
15
+
16
+ @element({
17
+ tagName: 'my-element',
18
+ shadow: [
19
+ css`
20
+ :host {
21
+ display: block;
22
+ color: red;
23
+ }
24
+ `,
25
+ html`
26
+ <slot></slot>
27
+ `
28
+ ]
29
+ })
17
30
  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;
31
+ @attr()
32
+ accessor value = 0;
36
33
 
37
- // listen for events
38
- @listen('click') onClick() {
34
+ @listen('click')
35
+ onClick() {
39
36
  console.log('clicked!')
40
37
  }
41
38
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joist/element",
3
- "version": "4.0.0-next.1",
3
+ "version": "4.0.0-next.3",
4
4
  "type": "module",
5
5
  "main": "./target/lib.js",
6
6
  "module": "./target/lib.js",
@@ -1,20 +1,29 @@
1
1
  import { expect, fixture, html } from '@open-wc/testing';
2
2
 
3
3
  import { attr } from './attr.js';
4
+ import { element } from './element.js';
4
5
 
5
6
  describe('@attr()', () => {
6
7
  it('should read and parse the correct values', async () => {
8
+ @element({
9
+ tagName: 'attr-test-1'
10
+ })
7
11
  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
- }
12
+ @attr()
13
+ accessor value1 = 100; // no attribute
14
+
15
+ @attr()
16
+ accessor value2 = 0; // number
13
17
 
14
- customElements.define('attr-test-2', MyElement);
18
+ @attr()
19
+ accessor value3 = false; // boolean
20
+
21
+ @attr()
22
+ accessor value4 = 'hello'; // string
23
+ }
15
24
 
16
25
  const el = await fixture<MyElement>(
17
- html`<attr-test-2 value2="2" value3 value4="world"></attr-test-2>`
26
+ html`<attr-test-1 value2="2" value3 value4="world"></attr-test-1>`
18
27
  );
19
28
 
20
29
  expect(el.value1).to.equal(100);
@@ -24,15 +33,21 @@ describe('@attr()', () => {
24
33
  });
25
34
 
26
35
  it('should not write falsy props to attributes', async () => {
36
+ @element({
37
+ tagName: 'attr-test-2'
38
+ })
27
39
  class MyElement extends HTMLElement {
28
- @attr accessor value1 = undefined;
29
- @attr accessor value2 = null;
30
- @attr accessor value3 = '';
31
- }
40
+ @attr()
41
+ accessor value1 = undefined;
32
42
 
33
- customElements.define('attr-test-3', MyElement);
43
+ @attr()
44
+ accessor value2 = null;
34
45
 
35
- const el = await fixture<MyElement>(html`<attr-test-3></attr-test-3>`);
46
+ @attr()
47
+ accessor value3 = '';
48
+ }
49
+
50
+ const el = await fixture<MyElement>(html`<attr-test-2></attr-test-2>`);
36
51
 
37
52
  expect(el.hasAttribute('value1')).to.be.false;
38
53
  expect(el.hasAttribute('value2')).to.be.false;
@@ -40,16 +55,24 @@ describe('@attr()', () => {
40
55
  });
41
56
 
42
57
  it('should update attributes when props are changed', async () => {
58
+ @element({
59
+ tagName: 'attr-test-3'
60
+ })
43
61
  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
- }
62
+ @attr()
63
+ accessor value1 = 'hello'; // no attribute
49
64
 
50
- customElements.define('attr-test-4', MyElement);
65
+ @attr()
66
+ accessor value2 = 0; // number
51
67
 
52
- const el = await fixture<MyElement>(html`<attr-test-4></attr-test-4>`);
68
+ @attr()
69
+ accessor value3 = true; // boolean
70
+
71
+ @attr()
72
+ accessor value4 = false; // boolean
73
+ }
74
+
75
+ const el = await fixture<MyElement>(html`<attr-test-3></attr-test-3>`);
53
76
 
54
77
  el.value1 = 'world';
55
78
  el.value2 = 100;
@@ -61,4 +84,46 @@ describe('@attr()', () => {
61
84
  expect(el.hasAttribute('value3')).to.be.false;
62
85
  expect(el.hasAttribute('value4')).to.be.true;
63
86
  });
87
+
88
+ it('should normalize attribute names', async () => {
89
+ const value3 = Symbol('Value from SYMBOL');
90
+
91
+ @element({
92
+ tagName: 'attr-test-4'
93
+ })
94
+ class MyElement extends HTMLElement {
95
+ @attr()
96
+ accessor Value1 = 'hello';
97
+
98
+ @attr()
99
+ accessor ['Value 2'] = 0;
100
+
101
+ @attr()
102
+ accessor [value3] = true;
103
+ }
104
+
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
+ });
64
129
  });
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
+ observe?: 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?.observe ?? 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,86 @@
1
- import { expect, fixture, html } from '@open-wc/testing';
1
+ import { expect, fixture, html as litHtml } from '@open-wc/testing';
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
7
  describe('@element()', () => {
8
8
  it('should write default value to attribute', async () => {
9
- @element
9
+ @element({
10
+ tagName: 'element-1'
11
+ })
10
12
  class MyElement extends HTMLElement {
11
- @tagName static tag = 'element-1';
13
+ @attr()
14
+ accessor value1 = 'hello'; // no attribute
12
15
 
13
- @attr accessor value1 = 'hello'; // no attribute
14
- @attr accessor value2 = 0; // number
15
- @attr accessor value3 = true; // boolean
16
+ @attr()
17
+ accessor value2 = 0; // number
18
+
19
+ @attr()
20
+ accessor value3 = true; // boolean
16
21
  }
17
22
 
18
- const el = await fixture<MyElement>(html`<element-1></element-1>`);
23
+ const el = await fixture<MyElement>(litHtml`<element-1></element-1>`);
19
24
 
20
25
  expect(el.getAttribute('value1')).to.equal('hello');
21
26
  expect(el.getAttribute('value2')).to.equal('0');
22
27
  expect(el.getAttribute('value3')).to.equal('');
23
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
+ ]
78
+ })
79
+ class MyElement extends HTMLElement {}
80
+
81
+ const el = new MyElement();
82
+
83
+ expect(el.shadowRoot!.adoptedStyleSheets.length).to.equal(1);
84
+ expect(el.shadowRoot!.innerHTML).to.equal(`<slot></slot>`);
85
+ });
24
86
  });
@@ -1,55 +1,77 @@
1
- import { metadataStore } from './metadata.js';
1
+ import { AttrDef, metadataStore } from './metadata.js';
2
+ import { ShadowResult } from './result.js';
2
3
 
3
- export function element<Target extends CustomElementConstructor>(
4
- Base: Target,
5
- ctx: ClassDecoratorContext<Target>
6
- ) {
7
- const meta = metadataStore.read(ctx.metadata);
4
+ export interface ElementOpts<T> {
5
+ tagName?: string;
6
+ shadow?: Array<ShadowResult | ((el: T) => void)>;
7
+ }
8
8
 
9
- ctx.addInitializer(function (this: Target) {
10
- if (meta.tagName) {
11
- const val = meta.tagName(this);
9
+ export function element<
10
+ Target extends CustomElementConstructor,
11
+ Instance extends InstanceType<Target>
12
+ >(opts?: ElementOpts<Instance>) {
13
+ return function elementDecorator(Base: Target, ctx: ClassDecoratorContext<Target>) {
14
+ const meta = metadataStore.read(ctx.metadata);
12
15
 
13
- if (!customElements.get(val)) {
14
- customElements.define(val, this);
16
+ ctx.addInitializer(function (this: Target) {
17
+ if (opts?.tagName) {
18
+ if (!customElements.get(opts.tagName)) {
19
+ customElements.define(opts.tagName, this);
20
+ }
15
21
  }
16
- }
17
- });
22
+ });
18
23
 
19
- return class JoistElement extends Base {
20
- // make all attrs observable
21
- static observedAttributes = [...meta.attrs];
22
- constructor(...args: any[]) {
23
- super(...args);
24
+ return class JoistElement extends Base {
25
+ static observedAttributes = meta.attrs
26
+ .filter(({ observe }) => observe) // filter out attributes that are not to be observed
27
+ .map(({ attrName }) => attrName);
24
28
 
25
- const root = this.shadowRoot || this;
29
+ constructor(...args: any[]) {
30
+ super(...args);
26
31
 
27
- for (let [event, listener] of meta.listeners) {
28
- root.addEventListener(event, listener.bind(this));
29
- }
30
- }
32
+ if (opts?.shadow) {
33
+ this.attachShadow({ mode: 'open' });
31
34
 
32
- connectedCallback() {
33
- for (let attr of meta.attrs) {
34
- const value = Reflect.get(this, attr);
35
-
36
- // reflect values back to attributes
37
- if (value !== null && value !== undefined && value !== '') {
38
- if (typeof value === 'boolean') {
39
- if (value === true) {
40
- // set boolean attribute
41
- this.setAttribute(attr, '');
35
+ for (let res of opts.shadow) {
36
+ if (typeof res === 'function') {
37
+ res(this as unknown as Instance);
38
+ } else {
39
+ res.run(this);
42
40
  }
43
- } else {
44
- // set key/value attribute
45
- this.setAttribute(attr, String(value));
46
41
  }
47
42
  }
43
+
44
+ for (let [event, { cb, root }] of meta.listeners) {
45
+ root(this).addEventListener(event, cb.bind(this));
46
+ }
48
47
  }
49
48
 
50
- if (super.connectedCallback) {
51
- super.connectedCallback();
49
+ connectedCallback() {
50
+ reflectAttributeValues(this, meta.attrs);
51
+
52
+ if (super.connectedCallback) {
53
+ super.connectedCallback();
54
+ }
52
55
  }
53
- }
56
+ };
54
57
  };
55
58
  }
59
+
60
+ function reflectAttributeValues(el: HTMLElement, attrs: AttrDef[]) {
61
+ for (let { propName, attrName } of attrs) {
62
+ const value = Reflect.get(el, propName);
63
+
64
+ // reflect values back to attributes
65
+ if (value !== null && value !== undefined && value !== '') {
66
+ if (typeof value === 'boolean') {
67
+ if (value === true) {
68
+ // set boolean attribute
69
+ el.setAttribute(attrName, '');
70
+ }
71
+ } else {
72
+ // set key/value attribute
73
+ el.setAttribute(attrName, String(value));
74
+ }
75
+ }
76
+ }
77
+ }
package/src/lib/listen.ts CHANGED
@@ -1,9 +1,15 @@
1
- import { metadataStore } from './metadata.js';
1
+ import { ListenerRootSelector, metadataStore } from './metadata.js';
2
2
 
3
- export function listen<This extends HTMLElement>(event: string) {
4
- return (value: (e: Event) => void, ctx: ClassMethodDecoratorContext<This>) => {
3
+ export function listen<This extends HTMLElement>(event: string, root?: ListenerRootSelector) {
4
+ return function listenDecorator(
5
+ value: (e: Event) => void,
6
+ ctx: ClassMethodDecoratorContext<This>
7
+ ) {
5
8
  const metadata = metadataStore.read(ctx.metadata);
6
9
 
7
- metadata.listeners.set(event, value);
10
+ metadata.listeners.set(event, {
11
+ cb: value,
12
+ root: root ?? ((el: HTMLElement) => el.shadowRoot ?? el)
13
+ });
8
14
  };
9
15
  }
@@ -1,9 +1,16 @@
1
1
  (Symbol as any).metadata ??= Symbol('Symbol.metadata');
2
2
 
3
+ export interface AttrDef {
4
+ propName: string | symbol;
5
+ attrName: string;
6
+ observe: boolean;
7
+ }
8
+
9
+ export type ListenerRootSelector = (el: HTMLElement) => HTMLElement | ShadowRoot;
10
+
3
11
  export class ElementMetadata {
4
- attrs: string[] = [];
5
- tagName?: (val: any) => string;
6
- listeners = new Map<string, (e: Event) => void>();
12
+ attrs: AttrDef[] = [];
13
+ listeners = new Map<string, { cb: (e: Event) => void; root: ListenerRootSelector }>();
7
14
  }
8
15
 
9
16
  export class MetadataStore extends WeakMap<object, ElementMetadata> {
package/src/lib/result.ts CHANGED
@@ -1,27 +1,23 @@
1
- export abstract class ShadowResult {
2
- strings: TemplateStringsArray;
3
- values: any[];
4
-
5
- #shadow: ShadowRoot | undefined = undefined;
6
-
7
- get shadow() {
8
- if (!this.#shadow) {
9
- throw new Error('ShadowResult has not been applied');
10
- }
1
+ export interface ShadowResult {
2
+ run(el: HTMLElement): void;
3
+ }
11
4
 
12
- return this.#shadow;
13
- }
5
+ export abstract class JoistShadowResult implements ShadowResult {
6
+ strings;
7
+ values;
14
8
 
15
9
  constructor(raw: TemplateStringsArray, ...values: any[]) {
16
10
  this.strings = raw;
17
11
  this.values = values;
18
12
  }
19
13
 
20
- execute(root: ShadowRoot) {
21
- this.#shadow = root;
14
+ run(el: HTMLElement) {
15
+ if (!el.shadowRoot) {
16
+ throw new Error('ShadowResult has not been applied');
17
+ }
22
18
 
23
- this.apply(root);
19
+ this.setup(el.shadowRoot);
24
20
  }
25
21
 
26
- abstract apply(root: ShadowRoot): void;
22
+ abstract setup(root: ShadowRoot): void;
27
23
  }