;
20 |
21 | export interface WebCell extends CustomElement {
22 | props: P & WebCellProps;
23 | internals: ReturnType;
24 | renderer: DOMRenderer;
25 | root: ParentNode;
26 | mounted: boolean;
27 | update: () => Promise;
28 | /**
29 | * Called at DOM tree updated
30 | */
31 | updatedCallback?: () => any;
32 | /**
33 | * Called at first time of DOM tree updated
34 | */
35 | mountedCallback?: () => any;
36 | emit: (event: string, detail?: any, option?: EventInit) => boolean;
37 | }
38 |
39 | interface DelegatedEvent {
40 | type: keyof HTMLElementEventMap;
41 | selector: string;
42 | handler: EventListener;
43 | }
44 | const eventMap = new WeakMap();
45 |
46 | /**
47 | * `class` decorator of Web components
48 | */
49 | export function component(meta: ComponentMeta) {
50 | return (
51 | Class: T,
52 | { addInitializer }: ClassDecoratorContext
53 | ) => {
54 | class RendererComponent
55 | extends (Class as ClassComponent)
56 | implements WebCell
57 | {
58 | declare props: WebCellProps;
59 |
60 | internals = this.tagName.includes('-')
61 | ? this.attachInternals()
62 | : undefined;
63 | renderer = new DOMRenderer();
64 |
65 | get root(): ParentNode {
66 | return this.shadowRoot || this.internals.shadowRoot || this;
67 | }
68 | mounted = false;
69 | declare mountedCallback?: () => any;
70 |
71 | constructor() {
72 | super();
73 |
74 | if (meta.mode && !this.internals?.shadowRoot)
75 | this.attachShadow(meta as ShadowRootInit);
76 | }
77 |
78 | async connectedCallback() {
79 | const { mode } = meta;
80 | const renderChildren = !(mode != null);
81 |
82 | const { root } = this,
83 | events = eventMap.get(this) || [];
84 |
85 | for (const { type, selector, handler } of events) {
86 | if (renderChildren && /^:host/.test(selector))
87 | console.warn(
88 | `[WebCell] DOM Event delegation of "${selector}" won't work if you don't invoke "this.attachShadow()" manually.`
89 | );
90 | root.addEventListener(type, handler);
91 | }
92 |
93 | super['connectedCallback']?.();
94 |
95 | if (this.mounted) return;
96 |
97 | await this.update();
98 |
99 | this.mounted = true;
100 | this.mountedCallback?.();
101 | }
102 |
103 | declare render?: () => VNode;
104 | declare updatedCallback?: () => any;
105 |
106 | protected updateDOM(content: VNode) {
107 | const result = this.renderer.render(
108 | content,
109 | this.root,
110 | meta.renderMode as 'async'
111 | );
112 |
113 | return result instanceof Promise
114 | ? result.then(this.updatedCallback?.bind(this))
115 | : this.updatedCallback?.();
116 | }
117 |
118 | async update() {
119 | const vNode = this.render?.();
120 |
121 | const content = isEmpty(vNode) ? (
122 | meta.mode ? (
123 |
124 | ) : null
125 | ) : (
126 | vNode
127 | );
128 | if (!(content != null)) return;
129 |
130 | if (
131 | !meta.transitible ||
132 | typeof document.startViewTransition !== 'function'
133 | )
134 | return this.updateDOM(content);
135 |
136 | const { updateCallbackDone, finished } =
137 | document.startViewTransition(() => this.updateDOM(content));
138 |
139 | try {
140 | await finished;
141 | } catch {
142 | return updateCallbackDone;
143 | }
144 | }
145 |
146 | disconnectedCallback() {
147 | const { root } = this,
148 | events = eventMap.get(this) || [];
149 |
150 | for (const { type, handler } of events)
151 | root.removeEventListener(type, handler);
152 |
153 | super['disconnectedCallback']?.();
154 | }
155 |
156 | emit(
157 | event: string,
158 | detail?: any,
159 | { cancelable, bubbles, composed }: EventInit = {}
160 | ) {
161 | return this.dispatchEvent(
162 | new CustomEvent(event, {
163 | detail,
164 | cancelable,
165 | bubbles,
166 | composed
167 | })
168 | );
169 | }
170 | }
171 |
172 | addInitializer(function () {
173 | globalThis.customElements?.define(meta.tagName, this, meta);
174 | });
175 |
176 | return RendererComponent as unknown as T;
177 | };
178 | }
179 |
180 | /**
181 | * Method decorator of DOM Event delegation
182 | */
183 | export function on(
184 | type: DelegatedEvent['type'],
185 | selector: string
186 | ) {
187 | return (
188 | method: DelegateEventHandler,
189 | { addInitializer }: ClassMethodDecoratorContext
190 | ) =>
191 | addInitializer(function () {
192 | const events = eventMap.get(this) || [],
193 | handler = delegate(selector, method.bind(this));
194 |
195 | events.push({ type, selector, handler });
196 |
197 | eventMap.set(this, events);
198 | });
199 | }
200 |
--------------------------------------------------------------------------------
/source/WebField.ts:
--------------------------------------------------------------------------------
1 | import { observable } from 'mobx';
2 | import { CustomFormElement, HTMLFieldProps } from 'web-utility';
3 |
4 | import { attribute, reaction } from './decorator';
5 | import { ClassComponent, WebCell } from './WebCell';
6 |
7 | export interface WebField
8 | extends CustomFormElement,
9 | WebCell
{}
10 |
11 | /**
12 | * `class` decorator of Form associated Web components
13 | */
14 | export function formField(
15 | Class: T,
16 | _: ClassDecoratorContext
17 | ) {
18 | class FormFieldComponent
19 | extends (Class as ClassComponent)
20 | implements CustomFormElement
21 | {
22 | /**
23 | * Defined in {@link component}
24 | */
25 | declare internals: ElementInternals;
26 | static formAssociated = true;
27 |
28 | @reaction(({ value }) => value)
29 | setValue(value: string) {
30 | this.internals.setFormValue(value);
31 | }
32 |
33 | formDisabledCallback(disabled: boolean) {
34 | this.disabled = disabled;
35 | }
36 |
37 | @attribute
38 | @observable
39 | accessor name: string;
40 |
41 | @observable
42 | accessor value: string;
43 |
44 | @attribute
45 | @observable
46 | accessor required: boolean;
47 |
48 | @attribute
49 | @observable
50 | accessor disabled: boolean;
51 |
52 | @attribute
53 | @observable
54 | accessor autofocus: boolean;
55 |
56 | set defaultValue(raw: string) {
57 | this.setAttribute('value', raw);
58 |
59 | this.value ??= raw;
60 | }
61 |
62 | get defaultValue() {
63 | return this.getAttribute('value');
64 | }
65 |
66 | get form() {
67 | return this.internals.form;
68 | }
69 | get validity() {
70 | return this.internals.validity;
71 | }
72 | get validationMessage() {
73 | return this.internals.validationMessage;
74 | }
75 | get willValidate() {
76 | return this.internals.willValidate;
77 | }
78 | checkValidity() {
79 | return this.internals.checkValidity();
80 | }
81 | reportValidity() {
82 | return this.internals.reportValidity();
83 | }
84 | }
85 |
86 | return FormFieldComponent as unknown as T;
87 | }
88 |
--------------------------------------------------------------------------------
/source/decorator.ts:
--------------------------------------------------------------------------------
1 | import { DataObject, DOMRenderer, JsxChildren, VNode } from 'dom-renderer';
2 | import {
3 | autorun,
4 | IReactionDisposer,
5 | IReactionPublic,
6 | reaction as watch
7 | } from 'mobx';
8 | import {
9 | CustomElement,
10 | isHTMLElementClass,
11 | parseJSON,
12 | toCamelCase,
13 | toHyphenCase
14 | } from 'web-utility';
15 |
16 | import { getMobxData } from './utility';
17 | import { ClassComponent } from './WebCell';
18 |
19 | export type PropsWithChildren = P & {
20 | children?: JsxChildren;
21 | };
22 | export type FunctionComponent
= (props: P) => VNode;
23 | export type FC
= FunctionComponent
;
24 |
25 | function wrapFunction
(func: FC
) {
26 | const renderer = new DOMRenderer();
27 |
28 | return (props: P) => {
29 | let tree = func(props),
30 | root: Node;
31 |
32 | if (!VNode.isFragment(tree)) {
33 | const disposer = autorun(() => {
34 | tree = func(props);
35 |
36 | if (tree && root) renderer.patch(VNode.fromDOM(root), tree);
37 | });
38 | const { ref } = tree;
39 |
40 | tree.ref = node => {
41 | if (node) root = node;
42 | else disposer();
43 |
44 | ref?.(node);
45 | };
46 | }
47 |
48 | return tree;
49 | };
50 | }
51 |
52 | interface ReactionItem {
53 | expression: ReactionExpression;
54 | effect: (...data: any[]) => any;
55 | }
56 | const reactionMap = new WeakMap();
57 |
58 | function wrapClass(Component: T) {
59 | class ObserverComponent
60 | extends (Component as ClassComponent)
61 | implements CustomElement
62 | {
63 | static observedAttributes = [];
64 |
65 | protected disposers: IReactionDisposer[] = [];
66 |
67 | get props() {
68 | return getMobxData(this);
69 | }
70 |
71 | constructor() {
72 | super();
73 |
74 | Promise.resolve().then(() => this.#boot());
75 | }
76 |
77 | update = () => {
78 | const { update } = Object.getPrototypeOf(this);
79 |
80 | return new Promise(resolve =>
81 | this.disposers.push(
82 | autorun(() => update.call(this).then(resolve))
83 | )
84 | );
85 | };
86 |
87 | #boot() {
88 | const names: string[] =
89 | this.constructor['observedAttributes'] || [],
90 | reactions = reactionMap.get(this) || [];
91 |
92 | this.disposers.push(
93 | ...names.map(name => autorun(() => this.syncPropAttr(name))),
94 | ...reactions.map(({ expression, effect }) =>
95 | watch(
96 | reaction => expression(this, reaction),
97 | effect.bind(this)
98 | )
99 | )
100 | );
101 | }
102 |
103 | disconnectedCallback() {
104 | for (const disposer of this.disposers) disposer();
105 |
106 | this.disposers.length = 0;
107 |
108 | super['disconnectedCallback']?.();
109 | }
110 |
111 | setAttribute(name: string, value: string) {
112 | const old = super.getAttribute(name),
113 | names: string[] = this.constructor['observedAttributes'];
114 |
115 | super.setAttribute(name, value);
116 |
117 | if (names.includes(name))
118 | this.attributeChangedCallback(name, old, value);
119 | }
120 |
121 | attributeChangedCallback(name: string, old: string, value: string) {
122 | this[toCamelCase(name)] = parseJSON(value);
123 |
124 | super['attributeChangedCallback']?.(name, old, value);
125 | }
126 |
127 | syncPropAttr(name: string) {
128 | let value = this[toCamelCase(name)];
129 |
130 | if (!(value != null) || value === false)
131 | return this.removeAttribute(name);
132 |
133 | value = value === true ? name : value;
134 |
135 | if (typeof value === 'object') {
136 | value = value.toJSON?.();
137 |
138 | value =
139 | typeof value === 'object' ? JSON.stringify(value) : value;
140 | }
141 | super.setAttribute(name, value);
142 | }
143 | }
144 |
145 | return ObserverComponent as unknown as T;
146 | }
147 |
148 | export type WebCellComponent = FunctionComponent | ClassComponent;
149 |
150 | /**
151 | * `class` decorator of Web components for MobX
152 | */
153 | export function observer(
154 | func: T,
155 | _: ClassDecoratorContext
156 | ): T;
157 | export function observer(func: T): T;
158 | export function observer(
159 | func: T,
160 | _?: ClassDecoratorContext
161 | ) {
162 | return isHTMLElementClass(func) ? wrapClass(func) : wrapFunction(func);
163 | }
164 |
165 | /**
166 | * `accessor` decorator of MobX `@observable` for HTML attributes
167 | */
168 | export function attribute(
169 | _: ClassAccessorDecoratorTarget,
170 | { name, addInitializer }: ClassAccessorDecoratorContext
171 | ) {
172 | addInitializer(function () {
173 | const names: string[] = this.constructor['observedAttributes'],
174 | attribute = toHyphenCase(name.toString());
175 |
176 | if (!names.includes(attribute)) names.push(attribute);
177 | });
178 | }
179 |
180 | export type ReactionExpression = (
181 | data: I,
182 | reaction: IReactionPublic
183 | ) => O;
184 |
185 | export type ReactionEffect = (
186 | newValue: V,
187 | oldValue: V,
188 | reaction: IReactionPublic
189 | ) => any;
190 |
191 | /**
192 | * Method decorator of MobX `reaction()`
193 | */
194 | export function reaction(
195 | expression: ReactionExpression
196 | ) {
197 | return (
198 | effect: ReactionEffect,
199 | { addInitializer }: ClassMethodDecoratorContext
200 | ) =>
201 | addInitializer(function () {
202 | const reactions = reactionMap.get(this) || [];
203 |
204 | reactions.push({ expression, effect });
205 |
206 | reactionMap.set(this, reactions);
207 | });
208 | }
209 |
--------------------------------------------------------------------------------
/source/index.ts:
--------------------------------------------------------------------------------
1 | export * from './Animation';
2 | export * from './Async';
3 | export * from './decorator';
4 | export * from './utility';
5 | export * from './WebCell';
6 | export * from './WebField';
7 |
--------------------------------------------------------------------------------
/source/polyfill.ts:
--------------------------------------------------------------------------------
1 | import { JSDOM } from 'jsdom';
2 |
3 | const { window } = new JSDOM();
4 |
5 | for (const key of [
6 | 'self',
7 | 'document',
8 | 'customElements',
9 | 'HTMLElement',
10 | 'HTMLUnknownElement',
11 | 'XMLSerializer',
12 | 'CustomEvent'
13 | ])
14 | globalThis[key] = window[key];
15 |
16 | self.requestAnimationFrame = setTimeout;
17 |
--------------------------------------------------------------------------------
/source/utility.ts:
--------------------------------------------------------------------------------
1 | import { DataObject } from 'dom-renderer';
2 | import { ObservableValue } from 'mobx/dist/internal';
3 | import { delegate } from 'web-utility';
4 |
5 | export class Defer {
6 | resolve: (value: T | PromiseLike) => void;
7 | reject: (reason?: any) => void;
8 |
9 | promise = new Promise((resolve, reject) => {
10 | this.resolve = resolve;
11 | this.reject = reject;
12 | });
13 | }
14 |
15 | export function getMobxData(observable: T) {
16 | for (const key of Object.getOwnPropertySymbols(observable)) {
17 | const store = observable[key as keyof T]?.values_ as Map<
18 | string,
19 | ObservableValue
20 | >;
21 | if (store instanceof Map)
22 | return Object.fromEntries(
23 | Array.from(store, ([key, { value_ }]) => [key, value_])
24 | ) as T;
25 | }
26 | }
27 |
28 | export const animated = (
29 | root: T,
30 | targetSelector: string
31 | ) =>
32 | new Promise(resolve => {
33 | const ended = delegate(targetSelector, (event: AnimationEvent) => {
34 | root.removeEventListener('animationend', ended);
35 | root.removeEventListener('animationcancel', ended);
36 | resolve(event);
37 | });
38 |
39 | root.addEventListener('animationend', ended);
40 | root.addEventListener('animationcancel', ended);
41 | });
42 |
--------------------------------------------------------------------------------
/test/Async.spec.tsx:
--------------------------------------------------------------------------------
1 | import 'element-internals-polyfill';
2 |
3 | import { DOMRenderer } from 'dom-renderer';
4 | import { configure } from 'mobx';
5 | import { sleep } from 'web-utility';
6 |
7 | import { lazy } from '../source/Async';
8 | import { FC } from '../source/decorator';
9 | import { WebCellProps } from '../source/WebCell';
10 |
11 | configure({ enforceActions: 'never' });
12 |
13 | describe('Async Box component', () => {
14 | const renderer = new DOMRenderer();
15 |
16 | it('should render an Async Component', async () => {
17 | const Sync: FC> = ({
18 | children,
19 | ...props
20 | }) => {children};
21 |
22 | const Async = lazy(async () => ({ default: Sync }));
23 |
24 | renderer.render(Test);
25 |
26 | expect(document.body.innerHTML).toBe('');
27 |
28 | await sleep();
29 |
30 | expect(document.body.innerHTML).toBe(
31 | 'Test'
32 | );
33 | });
34 | });
35 |
--------------------------------------------------------------------------------
/test/MobX.spec.tsx:
--------------------------------------------------------------------------------
1 | import 'element-internals-polyfill';
2 |
3 | import { DOMRenderer } from 'dom-renderer';
4 | import { configure, observable } from 'mobx';
5 | import { sleep } from 'web-utility';
6 |
7 | import { observer, reaction } from '../source/decorator';
8 | import { component } from '../source/WebCell';
9 |
10 | configure({ enforceActions: 'never' });
11 |
12 | class Test {
13 | @observable
14 | accessor count = 0;
15 | }
16 |
17 | describe('Observer decorator', () => {
18 | const model = new Test(),
19 | renderer = new DOMRenderer();
20 |
21 | it('should re-render Function Component', () => {
22 | const InlineTag = observer(() => {model.count});
23 |
24 | renderer.render();
25 |
26 | expect(document.body.textContent.trim()).toBe('0');
27 |
28 | model.count++;
29 |
30 | expect(document.body.textContent.trim()).toBe('1');
31 | });
32 |
33 | it('should re-render Class Component', () => {
34 | @component({ tagName: 'test-tag' })
35 | @observer
36 | class TestTag extends HTMLElement {
37 | render() {
38 | return {model.count};
39 | }
40 | }
41 | renderer.render();
42 |
43 | expect(document.querySelector('test-tag i').textContent.trim()).toBe(
44 | '1'
45 | );
46 | model.count++;
47 |
48 | expect(document.querySelector('test-tag i').textContent.trim()).toBe(
49 | '2'
50 | );
51 | });
52 |
53 | it('should register a Reaction with MobX', async () => {
54 | const handler = jest.fn();
55 |
56 | @component({ tagName: 'reaction-cell' })
57 | @observer
58 | class ReactionCell extends HTMLElement {
59 | @observable
60 | accessor test = '';
61 |
62 | @reaction(({ test }) => test)
63 | handleReaction(value: string) {
64 | handler(value);
65 | }
66 | }
67 | renderer.render();
68 |
69 | await sleep();
70 |
71 | const tag = document.querySelector('reaction-cell');
72 | tag.test = 'a';
73 |
74 | await sleep();
75 |
76 | expect(handler).toHaveBeenCalledTimes(1);
77 | expect(handler).toHaveBeenCalledWith('a');
78 |
79 | document.body.innerHTML = '';
80 | });
81 | });
82 |
--------------------------------------------------------------------------------
/test/WebCell.spec.tsx:
--------------------------------------------------------------------------------
1 | import 'element-internals-polyfill';
2 |
3 | import { DOMRenderer } from 'dom-renderer';
4 | import { configure, observable } from 'mobx';
5 | import { sleep, stringifyCSS } from 'web-utility';
6 |
7 | import { attribute, observer } from '../source/decorator';
8 | import { component, on, WebCell, WebCellProps } from '../source/WebCell';
9 |
10 | configure({ enforceActions: 'never' });
11 |
12 | describe('Base Class & Decorator', () => {
13 | const renderer = new DOMRenderer();
14 |
15 | it('should define a Custom Element', () => {
16 | @component({
17 | tagName: 'x-first',
18 | mode: 'open'
19 | })
20 | class XFirst extends HTMLElement {}
21 |
22 | renderer.render();
23 |
24 | expect(customElements.get('x-first')).toBe(XFirst);
25 | expect(document.body.lastElementChild.tagName).toBe('X-FIRST');
26 | });
27 |
28 | it('should inject CSS into Shadow Root', async () => {
29 | @component({
30 | tagName: 'x-second',
31 | mode: 'open'
32 | })
33 | class XSecond extends HTMLElement {
34 | private innerStyle = stringifyCSS({
35 | h2: { color: 'red' }
36 | });
37 |
38 | render() {
39 | return (
40 | <>
41 |
42 |
43 | >
44 | );
45 | }
46 | }
47 | renderer.render();
48 |
49 | await sleep();
50 |
51 | const { shadowRoot } = document.body.lastElementChild as XSecond;
52 |
53 | expect(shadowRoot.innerHTML).toBe(``);
56 | });
57 |
58 | it('should put .render() returned DOM into .children of a Custom Element', () => {
59 | @component({ tagName: 'x-third' })
60 | class XThird extends HTMLElement {
61 | render() {
62 | return ;
63 | }
64 | }
65 | renderer.render();
66 |
67 | const { shadowRoot, innerHTML } = document.body.lastElementChild;
68 |
69 | expect(shadowRoot).toBeNull();
70 | expect(innerHTML).toBe('');
71 | });
72 |
73 | it('should update Property & Attribute by watch() & attribute() decorators', async () => {
74 | interface XFourthProps extends WebCellProps {
75 | name?: string;
76 | }
77 |
78 | interface XFourth extends WebCell {}
79 |
80 | @component({ tagName: 'x-fourth' })
81 | @observer
82 | class XFourth extends HTMLElement implements WebCell {
83 | @attribute
84 | @observable
85 | accessor name: string | undefined;
86 |
87 | render() {
88 | return {this.name}
;
89 | }
90 | }
91 | renderer.render();
92 |
93 | const tag = document.body.lastElementChild as XFourth;
94 |
95 | expect(tag.innerHTML).toBe('');
96 |
97 | tag.name = 'test';
98 |
99 | expect(tag.name).toBe('test');
100 |
101 | await sleep();
102 |
103 | expect(tag.getAttribute('name')).toBe('test');
104 | expect(tag.innerHTML).toBe('test
');
105 |
106 | tag.setAttribute('name', 'example');
107 |
108 | expect(tag.name).toBe('example');
109 | });
110 |
111 | it('should delegate DOM Event by on() decorator', () => {
112 | interface XFirthProps extends WebCellProps {
113 | name?: string;
114 | }
115 |
116 | interface XFirth extends WebCell {}
117 |
118 | @component({ tagName: 'x-firth' })
119 | @observer
120 | class XFirth extends HTMLElement implements WebCell {
121 | @observable
122 | accessor name: string | undefined;
123 |
124 | @on('click', 'h2')
125 | handleClick(
126 | { type, detail }: CustomEvent,
127 | { tagName }: HTMLHeadingElement
128 | ) {
129 | this.name = [type, tagName, detail] + '';
130 | }
131 |
132 | render() {
133 | return (
134 |
137 | );
138 | }
139 | }
140 | renderer.render();
141 |
142 | const tag = document.body.lastElementChild as XFirth;
143 |
144 | tag.querySelector('a').dispatchEvent(
145 | new CustomEvent('click', { bubbles: true, detail: 1 })
146 | );
147 | expect(tag.name).toBe('click,H2,1');
148 | });
149 |
150 | it('should extend Original HTML tags', () => {
151 | @component({
152 | tagName: 'x-sixth',
153 | extends: 'blockquote',
154 | mode: 'open'
155 | })
156 | class XSixth extends HTMLQuoteElement {
157 | render() {
158 | return (
159 | <>
160 | 💖
161 |
162 | >
163 | );
164 | }
165 | }
166 | renderer.render(test
);
167 |
168 | const element = document.querySelector('blockquote');
169 |
170 | expect(element).toBeInstanceOf(XSixth);
171 | expect(element).toBeInstanceOf(HTMLQuoteElement);
172 |
173 | expect(element.textContent).toBe('test');
174 | expect(element.shadowRoot.innerHTML).toBe('💖');
175 | });
176 | });
177 |
--------------------------------------------------------------------------------
/test/WebField.spec.tsx:
--------------------------------------------------------------------------------
1 | import 'element-internals-polyfill';
2 |
3 | import { DOMRenderer } from 'dom-renderer';
4 | import { configure } from 'mobx';
5 | import { sleep } from 'web-utility';
6 |
7 | import { observer } from '../source/decorator';
8 | import { component, WebCellProps } from '../source/WebCell';
9 | import { formField, WebField } from '../source/WebField';
10 |
11 | configure({ enforceActions: 'never' });
12 |
13 | describe('Field Class & Decorator', () => {
14 | const renderer = new DOMRenderer();
15 |
16 | interface TestInputProps extends WebCellProps {
17 | a?: number;
18 | }
19 |
20 | interface TestInput extends WebField {}
21 |
22 | @component({ tagName: 'test-input' })
23 | @formField
24 | @observer
25 | class TestInput extends HTMLElement implements WebField {}
26 |
27 | it('should define a Custom Field Element', () => {
28 | renderer.render();
29 |
30 | expect(customElements.get('test-input')).toBe(TestInput);
31 |
32 | expect(document.querySelector('test-input').tagName.toLowerCase()).toBe(
33 | 'test-input'
34 | );
35 | });
36 |
37 | it('should have simple Form properties', async () => {
38 | const input = document.querySelector('test-input');
39 |
40 | input.name = 'test';
41 | await sleep();
42 | expect(input.getAttribute('name')).toBe('test');
43 |
44 | input.required = true;
45 | await sleep();
46 | expect(input.hasAttribute('required')).toBeTruthy();
47 | });
48 |
49 | it('should have advanced Form properties', () => {
50 | const input = new TestInput();
51 |
52 | input.defaultValue = 'example';
53 | expect(input.defaultValue === input.getAttribute('value')).toBeTruthy();
54 |
55 | const form = document.createElement('form');
56 | form.append(input);
57 | expect(input.form === form).toBeTruthy();
58 | });
59 | });
60 |
--------------------------------------------------------------------------------
/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "target": "ES6",
5 | "module": "CommonJS",
6 | "types": ["jest"]
7 | },
8 | "include": ["./*", "../source/**/*"]
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "dist/",
4 | "target": "ES2017",
5 | "checkJs": true,
6 | "declaration": true,
7 | "module": "ES2022",
8 | "moduleResolution": "Node",
9 | "esModuleInterop": true,
10 | "useDefineForClassFields": true,
11 | "jsx": "react-jsx",
12 | "jsxImportSource": "dom-renderer",
13 | "skipLibCheck": true,
14 | "lib": ["ES2022", "DOM"]
15 | },
16 | "include": ["source/**/*", "*.ts"],
17 | "typedocOptions": {
18 | "name": "WebCell",
19 | "excludeExternals": true,
20 | "excludePrivate": true,
21 | "readme": "./ReadMe.md",
22 | "plugin": ["typedoc-plugin-mdn-links"],
23 | "customCss": "./guide/table.css"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------