@joist/element 3.0.6 → 3.0.7

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joist/element",
3
- "version": "3.0.6",
3
+ "version": "3.0.7",
4
4
  "type": "module",
5
5
  "main": "./target/lib.js",
6
6
  "module": "./target/lib.js",
@@ -13,6 +13,7 @@
13
13
  }
14
14
  },
15
15
  "files": [
16
+ "src",
16
17
  "target"
17
18
  ],
18
19
  "sideEffects": false,
@@ -0,0 +1,80 @@
1
+ import { expect, fixture, html } from '@open-wc/testing';
2
+
3
+ import { attr } from './attr.js';
4
+
5
+ describe('observable: attr()', () => {
6
+ it('should write default value to attribute', async () => {
7
+ class MyElement extends HTMLElement {
8
+ @attr accessor value1 = 'hello'; // no attribute
9
+ @attr accessor value2 = 0; // number
10
+ @attr accessor value3 = true; // boolean
11
+ }
12
+
13
+ customElements.define('attr-test-1', MyElement);
14
+
15
+ const el = await fixture(html`<attr-test-1></attr-test-1>`);
16
+
17
+ expect(el.getAttribute('value1')).to.equal('hello');
18
+ expect(el.getAttribute('value2')).to.equal('0');
19
+ expect(el.getAttribute('value3')).to.equal('');
20
+ });
21
+
22
+ it('should read and parse the correct values', async () => {
23
+ class MyElement extends HTMLElement {
24
+ @attr accessor value1 = 100; // no attribute
25
+ @attr accessor value2 = 0; // number
26
+ @attr accessor value3 = false; // boolean
27
+ @attr accessor value4 = 'hello'; // string
28
+ }
29
+
30
+ customElements.define('attr-test-2', MyElement);
31
+
32
+ const el = await fixture<MyElement>(
33
+ html`<attr-test-2 value2="2" value3 value4="world"></attr-test-2>`
34
+ );
35
+
36
+ expect(el.value1).to.equal(100);
37
+ expect(el.value2).to.equal(2);
38
+ expect(el.value3).to.equal(true);
39
+ expect(el.value4).to.equal('world');
40
+ });
41
+
42
+ it('should not write falsy props to attributes', async () => {
43
+ class MyElement extends HTMLElement {
44
+ @attr accessor value1 = undefined;
45
+ @attr accessor value2 = null;
46
+ @attr accessor value3 = '';
47
+ }
48
+
49
+ customElements.define('attr-test-3', MyElement);
50
+
51
+ const el = await fixture<MyElement>(html`<attr-test-3></attr-test-3>`);
52
+
53
+ expect(el.hasAttribute('value1')).to.be.false;
54
+ expect(el.hasAttribute('value2')).to.be.false;
55
+ expect(el.hasAttribute('value3')).to.be.false;
56
+ });
57
+
58
+ it('should update attributes when props are changed', async () => {
59
+ class MyElement extends HTMLElement {
60
+ @attr accessor value1 = 'hello'; // no attribute
61
+ @attr accessor value2 = 0; // number
62
+ @attr accessor value3 = true; // boolean
63
+ @attr accessor value4 = false; // boolean
64
+ }
65
+
66
+ customElements.define('attr-test-4', MyElement);
67
+
68
+ const el = await fixture<MyElement>(html`<attr-test-4></attr-test-4>`);
69
+
70
+ el.value1 = 'world';
71
+ el.value2 = 100;
72
+ el.value3 = false;
73
+ el.value4 = true;
74
+
75
+ expect(el.getAttribute('value1')).to.equal('world');
76
+ expect(el.getAttribute('value2')).to.equal('100');
77
+ expect(el.hasAttribute('value3')).to.be.false;
78
+ expect(el.hasAttribute('value4')).to.be.true;
79
+ });
80
+ });
@@ -0,0 +1,91 @@
1
+ export function attr<This extends HTMLElement>(
2
+ { get, set }: ClassAccessorDecoratorTarget<This, unknown>,
3
+ ctx: ClassAccessorDecoratorContext<This>
4
+ ): ClassAccessorDecoratorResult<This, any> {
5
+ return {
6
+ init(value: unknown) {
7
+ if (typeof ctx.name === 'string') {
8
+ if (this.hasAttribute(ctx.name)) {
9
+ const attr = this.getAttribute(ctx.name);
10
+
11
+ // treat as boolean
12
+ if (attr === '') {
13
+ return true;
14
+ }
15
+
16
+ // treat as number
17
+ if (typeof value === 'number') {
18
+ return Number(attr);
19
+ }
20
+
21
+ // treat as string
22
+ return attr;
23
+ }
24
+
25
+ /**
26
+ * should set attributes AFTER init to allow setup to complete
27
+ * this causes attribute changed callback to fire
28
+ * If the user attempts to read or write to this property in that cb it will fail
29
+ * this also normalizes when the attributeChangedCallback is called in different rendering scenarios
30
+ */
31
+ Promise.resolve().then(() => {
32
+ const cached = get.call(this);
33
+
34
+ if (cached !== null && cached !== undefined && cached !== '') {
35
+ if (typeof cached === 'boolean') {
36
+ if (cached === true) {
37
+ // set boolean attribute
38
+ this.setAttribute(ctx.name.toString(), '');
39
+ }
40
+ } else {
41
+ // set key/value attribute
42
+ this.setAttribute(ctx.name.toString(), String(cached));
43
+ }
44
+ }
45
+ });
46
+ }
47
+
48
+ return value;
49
+ },
50
+ set(value: unknown) {
51
+ if (typeof ctx.name === 'string') {
52
+ if (typeof value === 'boolean') {
53
+ if (value) {
54
+ this.setAttribute(ctx.name, '');
55
+ } else {
56
+ this.removeAttribute(ctx.name);
57
+ }
58
+ } else {
59
+ this.setAttribute(ctx.name, String(value));
60
+ }
61
+ }
62
+
63
+ return set.call(this, value);
64
+ },
65
+ get() {
66
+ const ogValue = get.call(this);
67
+
68
+ if (typeof ctx.name === 'string') {
69
+ const attr = this.getAttribute(ctx.name);
70
+
71
+ if (attr !== null) {
72
+ // treat as boolean
73
+ if (attr === '') {
74
+ return true;
75
+ }
76
+
77
+ // treat as number
78
+ if (typeof ogValue === 'number') {
79
+ return Number(attr);
80
+ }
81
+
82
+ // treat as string
83
+ return attr;
84
+ }
85
+ }
86
+
87
+ // no readable value return original
88
+ return ogValue;
89
+ },
90
+ };
91
+ }
@@ -0,0 +1,14 @@
1
+ export function listen<This extends HTMLElement>(
2
+ event: string,
3
+ root: (el: This) => ShadowRoot | This = (el) => el.shadowRoot || el
4
+ ) {
5
+ return (value: (e: Event) => void, ctx: ClassMethodDecoratorContext<This>) => {
6
+ ctx.addInitializer(function () {
7
+ // method initializers are run before fields and accessors.
8
+ // we want to wait till after all have run so we can check if there is a shadowRoot or not.
9
+ Promise.resolve().then(() => {
10
+ root(this).addEventListener(event, value.bind(this));
11
+ });
12
+ });
13
+ };
14
+ }
@@ -0,0 +1,27 @@
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
+ }
11
+
12
+ return this.#shadow;
13
+ }
14
+
15
+ constructor(raw: TemplateStringsArray, ...values: any[]) {
16
+ this.strings = raw;
17
+ this.values = values;
18
+ }
19
+
20
+ execute(root: ShadowRoot) {
21
+ this.#shadow = root;
22
+
23
+ this.apply(root);
24
+ }
25
+
26
+ abstract apply(root: ShadowRoot): void;
27
+ }
@@ -0,0 +1,40 @@
1
+ import { expect } from '@open-wc/testing';
2
+
3
+ import { css, html } from './tags.js';
4
+ import { shadow } from './shadow.js';
5
+
6
+ describe('template', () => {
7
+ it('should apply a stylesheet', () => {
8
+ class MyElement extends HTMLElement {
9
+ @shadow styles = css`
10
+ :host {
11
+ display: flex;
12
+ }
13
+ `;
14
+ }
15
+
16
+ customElements.define('template-1', MyElement);
17
+
18
+ const el = new MyElement();
19
+
20
+ expect(el.shadowRoot!.adoptedStyleSheets.length).to.eq(1);
21
+ });
22
+
23
+ it('should apply html', () => {
24
+ class MyElement extends HTMLElement {
25
+ @shadow styles = css`
26
+ :host {
27
+ display: flex;
28
+ }
29
+ `;
30
+
31
+ @shadow template = html`<slot></slot>`;
32
+ }
33
+
34
+ customElements.define('template-2', MyElement);
35
+
36
+ const el = new MyElement();
37
+
38
+ expect(el.shadowRoot?.innerHTML).to.eq('<slot></slot>');
39
+ });
40
+ });
@@ -0,0 +1,18 @@
1
+ import { ShadowResult } from './result.js';
2
+
3
+ export function shadow<This extends HTMLElement, T extends ShadowResult>(
4
+ _: undefined,
5
+ ctx: ClassFieldDecoratorContext<This, T>
6
+ ) {
7
+ ctx.addInitializer(function () {
8
+ if (!this.shadowRoot) {
9
+ this.attachShadow({ mode: 'open' });
10
+ }
11
+ });
12
+
13
+ return function (this: This, result: T) {
14
+ result.execute(this.shadowRoot!);
15
+
16
+ return result;
17
+ };
18
+ }
@@ -0,0 +1,11 @@
1
+ import { tagName } from './tag-name.js';
2
+
3
+ describe('tag-name', () => {
4
+ it('should define a custom element', async () => {
5
+ class MyElement extends HTMLElement {
6
+ @tagName static tagName = 'tn-test-1';
7
+ }
8
+
9
+ return customElements.whenDefined(MyElement.tagName);
10
+ });
11
+ });
@@ -0,0 +1,11 @@
1
+ export function tagName(_val: unknown, _ctx: ClassFieldDecoratorContext) {
2
+ return function (this: CustomElementConstructor, val: string) {
3
+ Promise.resolve().then(() => {
4
+ if (!customElements.get(val)) {
5
+ customElements.define(val, this);
6
+ }
7
+ });
8
+
9
+ return val;
10
+ };
11
+ }
@@ -0,0 +1,28 @@
1
+ import { expect } from '@open-wc/testing';
2
+ import { css, html, htmlTemplateCache, styleSheetCache } from './tags.js';
3
+
4
+ describe('tags', () => {
5
+ it('should ensure return the same CSSResult', () => {
6
+ class Test {
7
+ styles = css`Hello World`;
8
+ }
9
+
10
+ const a = new Test();
11
+ const b = new Test();
12
+
13
+ expect(a.styles.strings).to.equal(b.styles.strings);
14
+ expect(styleSheetCache.get(a.styles.strings)).to.equal(styleSheetCache.get(b.styles.strings));
15
+ });
16
+
17
+ it('should cache the HTMLTemplateElement', () => {
18
+ class Test {
19
+ dom = html`Hello World`;
20
+ }
21
+
22
+ const a = new Test();
23
+ const b = new Test();
24
+
25
+ expect(a.dom.strings).to.equal(b.dom.strings);
26
+ expect(htmlTemplateCache.get(a.dom.strings)).to.equal(htmlTemplateCache.get(b.dom.strings));
27
+ });
28
+ });
@@ -0,0 +1,94 @@
1
+ /**
2
+ * NOTE: TemplateStringsArray can be used to cache via a WeakMap.
3
+ *
4
+ * function html(strs: TemplateStringsArray) {
5
+ * return strs
6
+ * }
7
+ *
8
+ * class Foo {
9
+ * hello = html`world`;
10
+ * }
11
+ *
12
+ * // these will be the same instance of TemplateStringsArray
13
+ * new Foo().hello === new Foo().hello
14
+ */
15
+
16
+ import { ShadowResult } from './result.js';
17
+
18
+ type Tags = keyof HTMLElementTagNameMap;
19
+ type SVGTags = keyof SVGElementTagNameMap;
20
+ type MathTags = keyof MathMLElementTagNameMap;
21
+
22
+ export const htmlTemplateCache = new WeakMap<TemplateStringsArray, HTMLTemplateElement>();
23
+
24
+ export class HTMLResult extends ShadowResult {
25
+ query<K extends Tags>(selectors: K): HTMLElementTagNameMap[K] | null;
26
+ query<K extends SVGTags>(selectors: K): SVGElementTagNameMap[K] | null;
27
+ query<K extends MathTags>(selectors: K): MathMLElementTagNameMap[K] | null;
28
+ query<E extends Element = Element>(selectors: string): E | null;
29
+ query<K extends Tags>(query: K) {
30
+ return this.shadow.querySelector<K>(query);
31
+ }
32
+
33
+ queryAll<K extends Tags>(selectors: K): NodeListOf<HTMLElementTagNameMap[K]>;
34
+ queryAll<K extends SVGTags>(selectors: K): NodeListOf<SVGElementTagNameMap[K]>;
35
+ queryAll<K extends MathTags>(selectors: K): NodeListOf<MathMLElementTagNameMap[K]>;
36
+ queryAll<E extends Element = Element>(selectors: string): NodeListOf<E>;
37
+ queryAll<K extends Tags>(query: K) {
38
+ return this.shadow.querySelectorAll<K>(query);
39
+ }
40
+
41
+ /**
42
+ * THe HTMLTemplateElement itself will be cached but a new instance of the result returned
43
+ */
44
+ apply(root: ShadowRoot): void {
45
+ let template: HTMLTemplateElement;
46
+
47
+ if (htmlTemplateCache.has(this.strings)) {
48
+ template = htmlTemplateCache.get(this.strings) as HTMLTemplateElement;
49
+ } else {
50
+ template = document.createElement('template');
51
+
52
+ template.innerHTML = concat(this.strings);
53
+ htmlTemplateCache.set(this.strings, template);
54
+ }
55
+
56
+ root.append(template.content.cloneNode(true));
57
+ }
58
+ }
59
+
60
+ export function html(strings: TemplateStringsArray, ...values: any[]): HTMLResult {
61
+ return new HTMLResult(strings, ...values);
62
+ }
63
+
64
+ export const styleSheetCache = new WeakMap<TemplateStringsArray, CSSStyleSheet>();
65
+
66
+ export class CSSResult extends ShadowResult {
67
+ apply(root: ShadowRoot): void {
68
+ let sheet: CSSStyleSheet;
69
+
70
+ if (styleSheetCache.has(this.strings)) {
71
+ sheet = styleSheetCache.get(this.strings) as CSSStyleSheet;
72
+ } else {
73
+ sheet = new CSSStyleSheet();
74
+
75
+ sheet.replaceSync(concat(this.strings));
76
+ }
77
+
78
+ root.adoptedStyleSheets = [...root.adoptedStyleSheets, sheet];
79
+ }
80
+ }
81
+
82
+ export function css(strings: TemplateStringsArray): CSSResult {
83
+ return new CSSResult(strings);
84
+ }
85
+
86
+ function concat(strings: TemplateStringsArray) {
87
+ let res = '';
88
+
89
+ for (let i = 0; i < strings.length; i++) {
90
+ res += strings[i];
91
+ }
92
+
93
+ return res;
94
+ }
package/src/lib.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { ShadowResult as TemplateResult } from './lib/result.js';
2
+ export { css, html, HTMLResult, CSSResult } from './lib/tags.js';
3
+ export { shadow } from './lib/shadow.js';
4
+ export { attr } from './lib/attr.js';
5
+ export { listen } from './lib/listen.js';
6
+ export { tagName } from './lib/tag-name.js';
@@ -1,7 +1,9 @@
1
1
  export function listen(event, root = (el) => el.shadowRoot || el) {
2
2
  return (value, ctx) => {
3
3
  ctx.addInitializer(function () {
4
- root(this).addEventListener(event, value.bind(this));
4
+ Promise.resolve().then(() => {
5
+ root(this).addEventListener(event, value.bind(this));
6
+ });
5
7
  });
6
8
  };
7
9
  }
@@ -1 +1 @@
1
- {"version":3,"file":"listen.js","sourceRoot":"","sources":["../../src/lib/listen.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,MAAM,CACpB,KAAa,EACb,OAAwC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,IAAI,EAAE;IAEnE,OAAO,CAAC,KAAyB,EAAE,GAAsC,EAAE,EAAE;QAC3E,GAAG,CAAC,cAAc,CAAC;YACjB,IAAI,CAAC,IAAI,CAAC,CAAC,gBAAgB,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACvD,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"listen.js","sourceRoot":"","sources":["../../src/lib/listen.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,MAAM,CACpB,KAAa,EACb,OAAwC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,UAAU,IAAI,EAAE;IAEnE,OAAO,CAAC,KAAyB,EAAE,GAAsC,EAAE,EAAE;QAC3E,GAAG,CAAC,cAAc,CAAC;YAGjB,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;gBAC1B,IAAI,CAAC,IAAI,CAAC,CAAC,gBAAgB,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;YACvD,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC,CAAC;AACJ,CAAC"}
@@ -1 +1 @@
1
- export declare function tagName(_val: unknown, ctx: ClassFieldDecoratorContext): (this: CustomElementConstructor, val: string) => string;
1
+ export declare function tagName(_val: unknown, _ctx: ClassFieldDecoratorContext): (this: CustomElementConstructor, val: string) => string;
@@ -1,11 +1,10 @@
1
- export function tagName(_val, ctx) {
1
+ export function tagName(_val, _ctx) {
2
2
  return function (val) {
3
- if (!ctx.static) {
4
- throw new Error('tagName can only be used on static fields');
5
- }
6
- if (!customElements.get(val)) {
7
- customElements.define(val, this);
8
- }
3
+ Promise.resolve().then(() => {
4
+ if (!customElements.get(val)) {
5
+ customElements.define(val, this);
6
+ }
7
+ });
9
8
  return val;
10
9
  };
11
10
  }
@@ -1 +1 @@
1
- {"version":3,"file":"tag-name.js","sourceRoot":"","sources":["../../src/lib/tag-name.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,OAAO,CAAC,IAAa,EAAE,GAA+B;IACpE,OAAO,UAA0C,GAAW;QAC1D,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE;YACf,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;SAC9D;QAED,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;YAC5B,cAAc,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;SAClC;QAED,OAAO,GAAG,CAAC;IACb,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"tag-name.js","sourceRoot":"","sources":["../../src/lib/tag-name.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,OAAO,CAAC,IAAa,EAAE,IAAgC;IACrE,OAAO,UAA0C,GAAW;QAC1D,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE;YAC1B,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;gBAC5B,cAAc,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;aAClC;QACH,CAAC,CAAC,CAAC;QAEH,OAAO,GAAG,CAAC;IACb,CAAC,CAAC;AACJ,CAAC"}