custom-elements-ts 0.0.17 → 0.2.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.
- package/.eslintrc.json +47 -0
- package/.github/workflows/ci.yml +49 -0
- package/.prettierrc +7 -0
- package/LICENSE +20 -0
- package/README.md +279 -37
- package/assets/readme-header.png +0 -0
- package/assets/social-preview.jpg +0 -0
- package/demos/counter/counter.element.html +1 -0
- package/demos/counter/counter.element.scss +234 -0
- package/demos/counter/counter.element.ts +68 -0
- package/demos/counter/index.html +205 -0
- package/demos/counter/index.ts +1 -0
- package/demos/site/code-example/code-example.element.scss +168 -0
- package/demos/site/code-example/code-example.element.ts +88 -0
- package/demos/site/event-log/event-log.element.scss +179 -0
- package/demos/site/event-log/event-log.element.ts +123 -0
- package/demos/site/favicon.svg +14 -0
- package/demos/site/index.html +368 -0
- package/demos/site/index.ts +19 -0
- package/demos/site/llms.txt +53 -0
- package/demos/site/message/message.element.scss +75 -0
- package/demos/site/message/message.element.ts +76 -0
- package/demos/site/og-image.png +0 -0
- package/demos/site/scroll-restoration.ts +28 -0
- package/demos/site/source-toggle/source-toggle.ts +88 -0
- package/demos/site/source-viewer/source-viewer.element.scss +292 -0
- package/demos/site/source-viewer/source-viewer.element.ts +138 -0
- package/demos/site/source-viewer/sources.generated.ts +11 -0
- package/demos/site/styles/site.css +1135 -0
- package/demos/site/styles/tokens.css +56 -0
- package/demos/site/toast/toast.element.scss +110 -0
- package/demos/site/toast/toast.element.ts +63 -0
- package/demos/todo-dashboard/index.html +141 -0
- package/demos/todo-dashboard/index.ts +4 -0
- package/demos/todo-dashboard/todo-dashboard.element.scss +1150 -0
- package/demos/todo-dashboard/todo-dashboard.element.ts +333 -0
- package/demos/todo-dashboard/todo-filters.element.ts +54 -0
- package/demos/todo-dashboard/todo-item.element.ts +127 -0
- package/demos/todo-dashboard/todo-stats.element.ts +189 -0
- package/package.json +73 -24
- package/src/custom-element.ts +206 -0
- package/src/index.ts +8 -0
- package/src/listen.ts +70 -0
- package/src/prop.ts +92 -0
- package/src/signal.ts +66 -0
- package/src/state.ts +141 -0
- package/src/template-runtime.ts +783 -0
- package/src/toggle.ts +66 -0
- package/src/tsconfig.json +24 -0
- package/src/util.ts +33 -0
- package/src/watch.ts +14 -0
- package/tests/basic.spec.ts +70 -0
- package/tests/custom-element.spec.ts +77 -0
- package/tests/dispatch.spec.ts +52 -0
- package/tests/init.spec.ts +94 -0
- package/tests/listen.spec.ts +118 -0
- package/tests/map-shallow-state.spec.ts +184 -0
- package/tests/prop.spec.ts +118 -0
- package/tests/signal.spec.ts +117 -0
- package/tests/templating-runtime.spec.ts +575 -0
- package/tests/toggle.spec.ts +92 -0
- package/tests/watch.spec.ts +183 -0
- package/tools/build.js +128 -0
- package/tools/bundle.js +167 -0
- package/tools/generate-sources.js +76 -0
- package/tools/rollup-config.js +70 -0
- package/tools/start.js +194 -0
- package/tsconfig.json +38 -0
- package/vite.config.mts +30 -0
- package/bundles/custom-elements-ts.umd.js +0 -359
- package/bundles/custom-elements-ts.umd.js.map +0 -1
- package/esm2015/custom-elements-ts.js +0 -283
- package/esm2015/custom-elements-ts.js.map +0 -1
- package/esm5/custom-element.d.ts +0 -12
- package/esm5/custom-elements-ts.js +0 -344
- package/esm5/custom-elements-ts.js.map +0 -1
- package/esm5/index.d.ts +0 -5
- package/esm5/listen.d.ts +0 -17
- package/esm5/prop.d.ts +0 -2
- package/esm5/toggle.d.ts +0 -1
- package/esm5/util.d.ts +0 -4
- package/esm5/watch.d.ts +0 -1
package/src/toggle.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { toKebabCase } from './util';
|
|
2
|
+
|
|
3
|
+
export const Toggle = (): any => {
|
|
4
|
+
return (target: any, propName: any) => {
|
|
5
|
+
function get(this: any) {
|
|
6
|
+
const getAttribute = (attrName: string) => {
|
|
7
|
+
if (this.hasAttribute(attrName)) {
|
|
8
|
+
const attrValue = this.getAttribute(attrName);
|
|
9
|
+
if (/^(true|false|^$)$/.test(attrValue)) {
|
|
10
|
+
return attrValue === 'true' || attrValue === '';
|
|
11
|
+
} else {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
};
|
|
17
|
+
return getAttribute(propName);
|
|
18
|
+
}
|
|
19
|
+
function set(this: any, value: any) {
|
|
20
|
+
const oldValue = value;
|
|
21
|
+
if (value !== null && value !== undefined) {
|
|
22
|
+
switch (typeof value) {
|
|
23
|
+
case 'boolean':
|
|
24
|
+
break;
|
|
25
|
+
case 'string':
|
|
26
|
+
if (/^(true|false|^$)$/.test(value)) {
|
|
27
|
+
value = oldValue === 'true' || oldValue === '';
|
|
28
|
+
} else {
|
|
29
|
+
console.warn(
|
|
30
|
+
`TypeError: Cannot set boolean toggle property '${propName}' to '${value}'`
|
|
31
|
+
);
|
|
32
|
+
value = false;
|
|
33
|
+
}
|
|
34
|
+
break;
|
|
35
|
+
default:
|
|
36
|
+
throw new TypeError(`Cannot set boolean toggle property '${propName}' to '${value}'`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (this.__connected) {
|
|
40
|
+
const previous = this.props[propName];
|
|
41
|
+
this.props[propName] = value || false;
|
|
42
|
+
if (oldValue !== '' && oldValue !== null) {
|
|
43
|
+
this.setAttribute(propName, value);
|
|
44
|
+
} else {
|
|
45
|
+
if (value) {
|
|
46
|
+
this.setAttribute(propName, '');
|
|
47
|
+
} else {
|
|
48
|
+
this.removeAttribute(propName);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!Object.is(previous, this.props[propName])) {
|
|
52
|
+
this.__scheduleRender?.();
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
if (!this.hasAttribute(toKebabCase(propName))) {
|
|
56
|
+
this.constructor.propsInit[propName] = value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!target.constructor.propsInit) {
|
|
61
|
+
target.constructor.propsInit = {};
|
|
62
|
+
}
|
|
63
|
+
target.constructor.propsInit[propName] = null;
|
|
64
|
+
Object.defineProperty(target, propName, { get, set });
|
|
65
|
+
};
|
|
66
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"allowSyntheticDefaultImports": true,
|
|
4
|
+
"allowUnreachableCode": false,
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"experimentalDecorators": true,
|
|
7
|
+
"lib": ["es5", "es6", "dom"],
|
|
8
|
+
"module": "es2015",
|
|
9
|
+
"moduleResolution": "node",
|
|
10
|
+
"noUnusedLocals": true,
|
|
11
|
+
"noUnusedParameters": true,
|
|
12
|
+
"outDir": "../dist",
|
|
13
|
+
"removeComments": true,
|
|
14
|
+
"target": "es5",
|
|
15
|
+
"strict": true,
|
|
16
|
+
"noImplicitAny": true,
|
|
17
|
+
"strictNullChecks": true,
|
|
18
|
+
"noFallthroughCasesInSwitch": true,
|
|
19
|
+
"forceConsistentCasingInFileNames": true
|
|
20
|
+
},
|
|
21
|
+
"include": [
|
|
22
|
+
"./*.ts"
|
|
23
|
+
]
|
|
24
|
+
}
|
package/src/util.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const toKebabCase = (str: string) => {
|
|
2
|
+
return str
|
|
3
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
4
|
+
.replace(/[\s_]+/g, '-')
|
|
5
|
+
.toLowerCase();
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const toCamelCase = (str: string) => {
|
|
9
|
+
return str.toLowerCase().replace(/(-\w)/g, (m: string) => m[1].toUpperCase());
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const toDotCase = (str: string) => {
|
|
13
|
+
return str
|
|
14
|
+
.replace(/(?!^)([A-Z])/g, ' $1')
|
|
15
|
+
.replace(/[_\s]+(?=[a-zA-Z])/g, '.')
|
|
16
|
+
.toLowerCase();
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const tryParseInt = (value: unknown) => {
|
|
20
|
+
if (typeof value === 'number' && Number.isInteger(value)) {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
if (typeof value === 'string') {
|
|
24
|
+
const trimmed = value.trim();
|
|
25
|
+
if (trimmed !== '') {
|
|
26
|
+
const parsed = Number(trimmed);
|
|
27
|
+
if (Number.isInteger(parsed) && String(parsed) === trimmed) {
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return value;
|
|
33
|
+
};
|
package/src/watch.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { toKebabCase } from './util';
|
|
2
|
+
|
|
3
|
+
export const Watch = (attrName: string) => {
|
|
4
|
+
return (target: any, propertyName: string) => {
|
|
5
|
+
if (!target.constructor.watchAttributes) {
|
|
6
|
+
target.constructor.watchAttributes = {};
|
|
7
|
+
}
|
|
8
|
+
target.constructor.watchAttributes[toKebabCase(attrName)] = propertyName;
|
|
9
|
+
if (!target.constructor.propsInit) {
|
|
10
|
+
target.constructor.propsInit = {};
|
|
11
|
+
}
|
|
12
|
+
target.constructor.propsInit[attrName] = null;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { CustomElement } from 'custom-elements-ts';
|
|
3
|
+
|
|
4
|
+
@CustomElement({
|
|
5
|
+
tag: 'basic-element',
|
|
6
|
+
template: '<span>my element</span>',
|
|
7
|
+
style: ':host{border:0}',
|
|
8
|
+
})
|
|
9
|
+
class BasicElement extends HTMLElement {}
|
|
10
|
+
|
|
11
|
+
describe('basic test', () => {
|
|
12
|
+
let myElementInstance: any;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
const myElement = document.createElement('basic-element');
|
|
16
|
+
myElementInstance = document.body.appendChild(myElement);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
document.body.innerHTML = '';
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should load html template', () => {
|
|
24
|
+
expect(myElementInstance.shadowRoot.innerHTML).toContain('<span>my element</span>');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should load css', () => {
|
|
28
|
+
expect(myElementInstance.shadowRoot.querySelector('style').innerText).toContain(
|
|
29
|
+
':host{border:0}'
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should have shadowroot', () => {
|
|
34
|
+
expect(myElementInstance.shadowRoot).toBeTruthy();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
39
|
+
@CustomElement({
|
|
40
|
+
tag: 'shadow-false-element',
|
|
41
|
+
template: '<span>my element</span>',
|
|
42
|
+
style: ':host{border:0}',
|
|
43
|
+
shadow: false,
|
|
44
|
+
})
|
|
45
|
+
class ShadowFalseElement extends HTMLElement {}
|
|
46
|
+
|
|
47
|
+
describe('basic test no shadowroot', () => {
|
|
48
|
+
let myElementInstance: any;
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
const myElement = document.createElement('shadow-false-element');
|
|
52
|
+
myElementInstance = document.body.appendChild(myElement);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
document.body.innerHTML = '';
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should load html template', () => {
|
|
60
|
+
expect(myElementInstance.innerHTML).toContain('<span>my element</span>');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should have css', () => {
|
|
64
|
+
expect(myElementInstance.querySelector('style')).toBeTruthy();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('shadow not have a shadowroot', () => {
|
|
68
|
+
expect(myElementInstance.shadowRoot).toBeFalsy();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
2
|
+
import { CustomElement } from 'custom-elements-ts';
|
|
3
|
+
|
|
4
|
+
@CustomElement({
|
|
5
|
+
tag: 'ce-a',
|
|
6
|
+
template: '<div id="t">X</div>',
|
|
7
|
+
style: ':host{display:block}',
|
|
8
|
+
shadow: true,
|
|
9
|
+
})
|
|
10
|
+
class A extends HTMLElement {
|
|
11
|
+
_watchArgs: any = undefined;
|
|
12
|
+
watched(args: any) {
|
|
13
|
+
this._watchArgs = args;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@CustomElement({
|
|
18
|
+
tag: 'ce-b',
|
|
19
|
+
template: '<span id="light">L</span>',
|
|
20
|
+
shadow: false,
|
|
21
|
+
})
|
|
22
|
+
class B extends HTMLElement {}
|
|
23
|
+
|
|
24
|
+
describe('CustomElement decorator', () => {
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
document.body.innerHTML = '';
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('renders template and style into shadowRoot by default', () => {
|
|
30
|
+
const el = document.createElement('ce-a') as A;
|
|
31
|
+
document.body.appendChild(el);
|
|
32
|
+
expect(el.shadowRoot).toBeTruthy();
|
|
33
|
+
expect(el.shadowRoot!.querySelector('#t')!.textContent).toBe('X');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('renders into light DOM when shadow=false', () => {
|
|
37
|
+
const el = document.createElement('ce-b') as B;
|
|
38
|
+
document.body.appendChild(el);
|
|
39
|
+
expect(el.shadowRoot).toBeNull();
|
|
40
|
+
expect(el.querySelector('#light')!.textContent).toBe('L');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('observedAttributes are derived from propsInit and kebab-cased', () => {
|
|
44
|
+
(A as any).propsInit = { myAttr: null, anotherAttr: null };
|
|
45
|
+
const observed = (A as any).observedAttributes as string[];
|
|
46
|
+
expect(observed).toContain('my-attr');
|
|
47
|
+
expect(observed).toContain('another-attr');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('attributeChangedCallback delegates to onAttributeChange and sets camelCase prop', () => {
|
|
51
|
+
(A as any).propsInit = { fooBar: null };
|
|
52
|
+
const el = document.createElement('ce-a') as A;
|
|
53
|
+
const spy = vi.spyOn(el as any, 'onAttributeChange');
|
|
54
|
+
document.body.appendChild(el);
|
|
55
|
+
// Simulate platform callback directly since observedAttributes may be empty at define time
|
|
56
|
+
(el as any).attributeChangedCallback('foo-bar', null as any, 'v1');
|
|
57
|
+
expect(spy).toHaveBeenCalled();
|
|
58
|
+
// property should be set via camelCase mapping
|
|
59
|
+
expect((el as any).fooBar).toBe('v1');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('onAttributeChange triggers watch method only when connected', () => {
|
|
63
|
+
(A as any).watchAttributes = { 'my-attr': 'watched' };
|
|
64
|
+
(A as any).propsInit = { myAttr: null };
|
|
65
|
+
const el = document.createElement('ce-a') as A;
|
|
66
|
+
// not connected: should not call watched
|
|
67
|
+
(el as any).onAttributeChange('my-attr', null as any, 'x');
|
|
68
|
+
expect(el._watchArgs).toBeUndefined();
|
|
69
|
+
|
|
70
|
+
// connected: should call watched
|
|
71
|
+
document.body.appendChild(el);
|
|
72
|
+
el._watchArgs = undefined;
|
|
73
|
+
// Simulate platform callback directly to trigger watcher
|
|
74
|
+
(el as any).onAttributeChange('my-attr', null as any, 'y');
|
|
75
|
+
expect(el._watchArgs).toEqual({ old: null, new: 'y' });
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { CustomElement, Dispatch, DispatchEmitter, Listen } from 'custom-elements-ts';
|
|
3
|
+
@CustomElement({
|
|
4
|
+
tag: 'btn-dispatch',
|
|
5
|
+
template: `<button>Test</button>`,
|
|
6
|
+
})
|
|
7
|
+
class DispatchElement extends HTMLElement {
|
|
8
|
+
@Dispatch() btnClick!: DispatchEmitter;
|
|
9
|
+
@Dispatch('btn.namedClick') btnClickNamed!: DispatchEmitter;
|
|
10
|
+
|
|
11
|
+
constructor() {
|
|
12
|
+
super();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
@Listen('click')
|
|
16
|
+
btnHandler() {
|
|
17
|
+
this.shadowRoot!.querySelector('button')!.innerHTML = 'Hello';
|
|
18
|
+
this.btnClick.emit({ detail: 'Hello' });
|
|
19
|
+
this.btnClickNamed.emit({ detail: 'Hello from named click' });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('dispatch decorators', () => {
|
|
24
|
+
let element: any;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
const btnElement = document.createElement('btn-dispatch');
|
|
28
|
+
element = document.body.appendChild(btnElement);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
document.body.innerHTML = '';
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should trigger a btn.click DispatchEmitter', async () => {
|
|
36
|
+
const eventPromise = new Promise<CustomEvent>((resolve) => {
|
|
37
|
+
element.addEventListener('btn.click', (e: CustomEvent) => resolve(e));
|
|
38
|
+
});
|
|
39
|
+
element.click();
|
|
40
|
+
const e = await eventPromise;
|
|
41
|
+
expect(e.detail).toBe('Hello');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should trigger a btn.namedClick DispatchEmitter', async () => {
|
|
45
|
+
const eventPromise = new Promise<CustomEvent>((resolve) => {
|
|
46
|
+
element.addEventListener('btn.namedClick', (e: CustomEvent) => resolve(e));
|
|
47
|
+
});
|
|
48
|
+
element.click();
|
|
49
|
+
const e = await eventPromise;
|
|
50
|
+
expect(e.detail).toBe('Hello from named click');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { CustomElement, Toggle, Prop, Watch } from 'custom-elements-ts';
|
|
3
|
+
|
|
4
|
+
@CustomElement({})
|
|
5
|
+
class InitElement extends HTMLElement {
|
|
6
|
+
@Toggle() disabled = true;
|
|
7
|
+
@Prop() color = 'blue';
|
|
8
|
+
@Prop() icon: any;
|
|
9
|
+
|
|
10
|
+
setIcon = false;
|
|
11
|
+
@Watch('icon')
|
|
12
|
+
changeIcon(_value: any) {
|
|
13
|
+
this.setIcon = true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
constructor() {
|
|
17
|
+
super();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
connectedCallback() {}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('init state', () => {
|
|
24
|
+
let myElement: any;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
myElement = document.createElement('init-element');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
document.body.innerHTML = '';
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should set attribute based on default prop value on init', () => {
|
|
35
|
+
const element = document.body.appendChild(myElement);
|
|
36
|
+
expect(element.getAttribute('color')).toBe('blue');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should set default prop value on init', () => {
|
|
40
|
+
const element = document.body.appendChild(myElement);
|
|
41
|
+
expect(element.color).toBe('blue');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should set attribute based on default toggle value on init', () => {
|
|
45
|
+
const element = document.body.appendChild(myElement);
|
|
46
|
+
expect(element.hasAttribute('disabled')).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should set default toggle value on init', () => {
|
|
50
|
+
const element = document.body.appendChild(myElement);
|
|
51
|
+
expect(element.disabled).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should reflect attribute to props on init', () => {
|
|
55
|
+
myElement.setAttribute('color', 'red');
|
|
56
|
+
const element = document.body.appendChild(myElement);
|
|
57
|
+
expect(element.color).toBe('red');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should not set attribute on second element instance', () => {
|
|
61
|
+
document.body.innerHTML = `
|
|
62
|
+
<init-element icon="awesome" disabled="false"></init-element>
|
|
63
|
+
<init-element></init-element>
|
|
64
|
+
<init-element></init-element>
|
|
65
|
+
`;
|
|
66
|
+
const initElements: any = document.body.querySelectorAll('init-element');
|
|
67
|
+
expect(initElements[1].icon).toBeFalsy();
|
|
68
|
+
expect(initElements[2].icon).toBeFalsy();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should reflect has attribute to toggle on init', () => {
|
|
72
|
+
myElement.setAttribute('disabled', '');
|
|
73
|
+
const element = document.body.appendChild(myElement);
|
|
74
|
+
expect(element.disabled).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should reflect true attribute to toggle on init', () => {
|
|
78
|
+
myElement.setAttribute('disabled', 'true');
|
|
79
|
+
const element = document.body.appendChild(myElement);
|
|
80
|
+
expect(element.disabled).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should reflect false attribute to toggle on init', () => {
|
|
84
|
+
myElement.setAttribute('disabled', 'false');
|
|
85
|
+
const element = document.body.appendChild(myElement);
|
|
86
|
+
expect(element.disabled).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should execute watch of attribute on init', () => {
|
|
90
|
+
myElement.setAttribute('icon', 'test');
|
|
91
|
+
const element = document.body.appendChild(myElement);
|
|
92
|
+
expect(element.setIcon).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { CustomElement, Dispatch, DispatchEmitter, Listen } from 'custom-elements-ts';
|
|
3
|
+
|
|
4
|
+
@CustomElement({
|
|
5
|
+
tag: 'btn-listen-dispatch',
|
|
6
|
+
template: `<button>Test</button>`,
|
|
7
|
+
})
|
|
8
|
+
class ButtonElement extends HTMLElement {
|
|
9
|
+
@Dispatch() btnClick!: DispatchEmitter;
|
|
10
|
+
@Dispatch('btn.namedClick') btnClickNamed!: DispatchEmitter;
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
super();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
@Listen('click')
|
|
17
|
+
btnHandler() {
|
|
18
|
+
this.shadowRoot!.querySelector('button')!.innerHTML = 'Hello';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Listen('click', 'button')
|
|
22
|
+
btnInnerClick() {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('listen decorator', () => {
|
|
26
|
+
let element: any;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
const btnElement = document.createElement('btn-listen-dispatch');
|
|
30
|
+
element = document.body.appendChild(btnElement);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
document.body.innerHTML = '';
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should call method decorated @Listen', () => {
|
|
38
|
+
const btnHandlerSpy = vi.spyOn(element.btnHandler, 'call');
|
|
39
|
+
element.click();
|
|
40
|
+
expect(btnHandlerSpy).toHaveBeenCalled();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should call method decorated @Listen on children element click', () => {
|
|
44
|
+
const btnHandlerSpy = vi.spyOn(element.btnHandler, 'call');
|
|
45
|
+
element.shadowRoot.querySelector('button').click();
|
|
46
|
+
expect(btnHandlerSpy).toHaveBeenCalled();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should call method decorated @Listen on selector click', () => {
|
|
50
|
+
const btnInnerSpy = vi.spyOn(element.btnInnerClick, 'call');
|
|
51
|
+
element.shadowRoot.querySelector('button').click();
|
|
52
|
+
expect(btnInnerSpy).toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should execute inner method decorated @Listen', () => {
|
|
56
|
+
element.click();
|
|
57
|
+
expect(element.shadowRoot.querySelector('button').innerHTML).toEqual('Hello');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
62
|
+
@CustomElement({
|
|
63
|
+
tag: 'btn-listen-shadow-false',
|
|
64
|
+
template: `<button>Test</button>`,
|
|
65
|
+
shadow: false,
|
|
66
|
+
})
|
|
67
|
+
class ShadowFalseButtonElement extends HTMLElement {
|
|
68
|
+
@Dispatch() btnClick!: DispatchEmitter;
|
|
69
|
+
@Dispatch('btn.namedClick') btnClickNamed!: DispatchEmitter;
|
|
70
|
+
|
|
71
|
+
constructor() {
|
|
72
|
+
super();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@Listen('click')
|
|
76
|
+
btnHandler() {
|
|
77
|
+
this.querySelector('button')!.innerHTML = 'Hello';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
@Listen('click', 'button')
|
|
81
|
+
btnInnerClick() {}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe('listen decorator no shadowroot', () => {
|
|
85
|
+
let element: any;
|
|
86
|
+
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
const btnElement = document.createElement('btn-listen-shadow-false');
|
|
89
|
+
element = document.body.appendChild(btnElement);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
afterEach(() => {
|
|
93
|
+
document.body.innerHTML = '';
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should call method decorated @Listen', () => {
|
|
97
|
+
const btnHandlerSpy = vi.spyOn(element.btnHandler, 'call');
|
|
98
|
+
element.click();
|
|
99
|
+
expect(btnHandlerSpy).toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should call method decorated @Listen on children element click', () => {
|
|
103
|
+
const btnHandlerSpy = vi.spyOn(element.btnHandler, 'call');
|
|
104
|
+
element.querySelector('button').click();
|
|
105
|
+
expect(btnHandlerSpy).toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should call method decorated @Listen on selector click', () => {
|
|
109
|
+
const btnInnerSpy = vi.spyOn(element.btnInnerClick, 'call');
|
|
110
|
+
element.querySelector('button').click();
|
|
111
|
+
expect(btnInnerSpy).toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should execute inner method decorated @Listen', () => {
|
|
115
|
+
element.click();
|
|
116
|
+
expect(element.querySelector('button').innerHTML).toEqual('Hello');
|
|
117
|
+
});
|
|
118
|
+
});
|