ecmact 1.0.2 → 2.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Alexandr Priadchenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,473 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/eternalvision/ecmact/main/public/ecmact.png" alt="ECMact" width="300" />
3
+ </p>
4
+
5
+ # ECMact
6
+
7
+ ECMact is a small component library built on native Web Components. Every component is a custom element with an open Shadow DOM, created through a single factory, `em`. There is no virtual DOM, no template language and no build-time step: components are plain function calls that return real DOM elements.
8
+
9
+ - Zero runtime dependencies
10
+ - Content, attributes and styles are written through DOM APIs, never through `innerHTML` (unless you ask for it)
11
+ - In-place updates: `setState` reuses the DOM node, so focus and input state survive
12
+ - Full TypeScript support, with attributes, events, styles and `ref` typed per HTML tag
13
+ - ES and CommonJS builds, bundled type declarations, `sideEffects: false`
14
+
15
+ ## Table of contents
16
+
17
+ - [Install](#install)
18
+ - [Quick start](#quick-start)
19
+ - [How it works](#how-it-works)
20
+ - [Creating components](#creating-components)
21
+ - [Props reference](#props-reference)
22
+ - [Content and children](#content-and-children)
23
+ - [Attributes](#attributes)
24
+ - [Styles](#styles)
25
+ - [Events](#events)
26
+ - [Refs](#refs)
27
+ - [State and updates](#state-and-updates)
28
+ - [Mounting](#mounting)
29
+ - [Composition](#composition)
30
+ - [TypeScript](#typescript)
31
+ - [Security model](#security-model)
32
+ - [Server-side rendering](#server-side-rendering)
33
+ - [Browser support](#browser-support)
34
+ - [Limitations](#limitations)
35
+ - [Migrating from 1.x](#migrating-from-1x)
36
+ - [Development](#development)
37
+ - [Release](#release)
38
+ - [License](#license)
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ npm install ecmact
44
+ ```
45
+
46
+ Requires a browser (or DOM implementation) with Custom Elements v1 and Shadow DOM. The package ships ES2022 code.
47
+
48
+ ```ts
49
+ import { em, mount } from 'ecmact';
50
+ ```
51
+
52
+ CommonJS is supported as well:
53
+
54
+ ```js
55
+ const { em, mount } = require('ecmact');
56
+ ```
57
+
58
+ ## Quick start
59
+
60
+ ```ts
61
+ import { em, mount } from 'ecmact';
62
+
63
+ let count = 0;
64
+
65
+ const card = em.Card({
66
+ content: 'Click me',
67
+ style: { padding: '20px', backgroundColor: '#cceeff', cursor: 'pointer' },
68
+ onclick: () => {
69
+ count++;
70
+ card.setState({ content: `Clicked ${count} time${count === 1 ? '' : 's'}` });
71
+ },
72
+ });
73
+
74
+ mount('body', card);
75
+ ```
76
+
77
+ ## How it works
78
+
79
+ `em.Card(props)` does three things:
80
+
81
+ 1. On first access of the name `Card`, registers a custom element `em-card` (once per lowercase name).
82
+ 2. Creates an `<em-card>` element.
83
+ 3. Assigns `props`, which renders one inner element (a `div` by default) into the element's Shadow DOM.
84
+
85
+ ```
86
+ <em-card> custom element (the host, returned by em.Card)
87
+ #shadow-root (open)
88
+ <div style="..."> inner element, rendered from props
89
+ text content
90
+ ...children
91
+ </div>
92
+ </em-card>
93
+ ```
94
+
95
+ The returned value is a real `HTMLElement`. You can append it anywhere, query it, add your own listeners to it and read `element.shadowRoot`. The inner element is `element.shadowRoot.firstElementChild`.
96
+
97
+ Rendering is synchronous. Setting props, or calling `setState`, updates the DOM immediately; there is no scheduler and no batching.
98
+
99
+ ## Creating components
100
+
101
+ ```ts
102
+ const card = em.Card({ content: 'Hello' });
103
+ const title = em.Title({ tag: 'h1', content: 'Welcome' });
104
+ const link = em.Link({ tag: 'a', href: '/docs', content: 'Docs' });
105
+ ```
106
+
107
+ `em` is a Proxy: any property name is a component factory. The name only decides the custom element name; what is rendered is decided by `tag`.
108
+
109
+ - `em.Card`, `em.card` and `em.CARD` all produce `<em-card>` elements of the same class.
110
+ - A name must match `/^[A-Za-z][A-Za-z0-9_-]*$/`, otherwise a `TypeError` is thrown.
111
+ - `em.then` and symbol properties return `undefined`, so `await em` and `String(em)` are safe.
112
+ - Factories can be called with no arguments: `em.Card()` renders an empty `div`.
113
+
114
+ Use a distinct name per kind of component. Names are a convention for your own readability and for devtools; they do not carry props or behavior.
115
+
116
+ ## Props reference
117
+
118
+ | Prop | Type | Default | Description |
119
+ | ----------- | ----------------------------- | ------- | --------------------------------------------------------------- |
120
+ | `tag` | `string` | `'div'` | Tag name of the inner element. |
121
+ | `content` | `string` | `''` | Text content. Never parsed as HTML. |
122
+ | `html` | `string` | - | Raw HTML for the inner element. Opt-in, trusted input only. |
123
+ | `children` | `Node[]` | `[]` | Nodes appended after the content. |
124
+ | `style` | object of CSS properties | - | Inline styles, camelCase keys. |
125
+ | `ref` | `(element) => void` | - | Called once, when the inner element is created. |
126
+ | `on<event>` | `(event) => void` | - | Event listener on the inner element, lowercase, e.g. `onclick`. |
127
+ | any other | `string \| number \| boolean` | - | Set as an HTML attribute on the inner element. |
128
+
129
+ All props apply to the inner element, not to the `<em-*>` host.
130
+
131
+ ## Content and children
132
+
133
+ `content` is always inserted as a text node, so markup in it is shown literally:
134
+
135
+ ```ts
136
+ em.Card({ content: '<b>not bold</b>' });
137
+ ```
138
+
139
+ `children` is an array of DOM nodes, appended after the content:
140
+
141
+ ```ts
142
+ em.Card({
143
+ content: 'Parent text',
144
+ children: [em.Card({ content: 'Child 1' }), em.Card({ content: 'Child 2' })],
145
+ });
146
+ ```
147
+
148
+ `html` replaces the inner element's contents with parsed HTML. Children are appended after it. Use it only for trusted markup (see [Security model](#security-model)):
149
+
150
+ ```ts
151
+ em.Card({ html: '<strong>trusted</strong> markup', children: [em.Card({ content: 'after' })] });
152
+ ```
153
+
154
+ When `html` is set, `content` is ignored.
155
+
156
+ Children passed to `children` are moved, not cloned: a node can have only one parent. On every update the inner element's children are replaced with the current `content` or `html` and the current `children` array.
157
+
158
+ ## Attributes
159
+
160
+ Any prop that is not a reserved key (`tag`, `content`, `html`, `style`, `children`, `ref`) and does not start with `on` is set on the inner element with `setAttribute`.
161
+
162
+ | Value | Result |
163
+ | -------------------------------- | ----------------------------------- |
164
+ | `string`, `number` | `setAttribute(key, String(value))` |
165
+ | `true` | empty attribute, e.g. `disabled=""` |
166
+ | `false`, `null`, `undefined` | attribute not set / removed |
167
+ | anything else (object, function) | ignored |
168
+
169
+ ```ts
170
+ em.Field({
171
+ tag: 'input',
172
+ type: 'email',
173
+ placeholder: 'you@example.com',
174
+ required: true,
175
+ maxlength: 64,
176
+ 'data-testid': 'email',
177
+ 'aria-label': 'Email',
178
+ });
179
+ ```
180
+
181
+ Attribute names are HTML attribute names, not DOM property names: use `class`, not `className`, and `for`, not `htmlFor`. Names are written as given; HTML treats them case-insensitively.
182
+
183
+ On `setState`, attributes that are no longer present, or whose value became `false`, `null` or `undefined`, are removed from the element.
184
+
185
+ Values are set through the DOM, so quotes and markup inside a value cannot break out of the attribute.
186
+
187
+ ## Styles
188
+
189
+ `style` takes an object of CSS properties in camelCase. Keys are converted to kebab-case and applied through the CSSOM (`element.style.setProperty`).
190
+
191
+ ```ts
192
+ em.Card({
193
+ style: {
194
+ display: 'flex',
195
+ backgroundColor: '#fafafa',
196
+ borderRadius: '8px',
197
+ zIndex: 2,
198
+ },
199
+ });
200
+ ```
201
+
202
+ - Values must be strings or numbers. Numbers are passed as is, without units: `zIndex: 2` works, `width: 10` does not mean `10px`. Use strings for lengths.
203
+ - Other values (`undefined`, objects) are skipped.
204
+ - On `setState`, properties that are no longer in `style` are removed; the rest are updated.
205
+ - Styles apply to the inner element only. There are no selectors, pseudo-classes or media queries. For those, use a stylesheet that targets the host (`em-card`) with CSS custom properties, or a `ref` that adds a `<style>` to `element.getRootNode()`.
206
+
207
+ Because the content lives in a Shadow DOM, global stylesheets do not reach it.
208
+
209
+ ## Events
210
+
211
+ Props starting with `on` whose value is a function become event listeners on the inner element. The event name is the rest of the key, lowercased.
212
+
213
+ ```ts
214
+ em.Field({
215
+ tag: 'input',
216
+ oninput: (event) => console.log((event.target as HTMLInputElement).value),
217
+ onkeydown: (event) => event.key === 'Enter' && submit(),
218
+ });
219
+ ```
220
+
221
+ - Non-function `on*` values are ignored (an `onclick` string is never evaluated).
222
+ - Listeners are replaced on every update: old ones are removed, current ones are added. There are no duplicate handlers, and removing the prop removes the listener.
223
+ - Listeners are registered without options. For `once`, `passive` or `capture`, or for custom events, add the listener yourself in a `ref`:
224
+
225
+ ```ts
226
+ em.Card({
227
+ ref: (element) => element.addEventListener('wheel', onWheel, { passive: true }),
228
+ });
229
+ ```
230
+
231
+ - Events bubble out of the Shadow DOM according to normal rules (`click`, `input` and other composed events reach listeners on the `<em-*>` host and its ancestors, with the event target retargeted to the host).
232
+
233
+ ## Refs
234
+
235
+ `ref` receives the inner element once, right after it is created. It is called again only if the element is recreated (when `tag` changes).
236
+
237
+ ```ts
238
+ let input!: HTMLInputElement;
239
+
240
+ const field = em.Field({
241
+ tag: 'input',
242
+ ref: (element) => (input = element),
243
+ });
244
+
245
+ input.focus();
246
+ ```
247
+
248
+ Refs are the escape hatch for anything the props do not cover: listener options, live form values, focus, measurements, third-party widgets.
249
+
250
+ ## State and updates
251
+
252
+ Each component exposes its props and an update method.
253
+
254
+ | Member | Description |
255
+ | ----------------------------- | ------------------------------------------------------- |
256
+ | `component.props` | Getter: the current props object. |
257
+ | `component.props = p` | Setter: replaces all props and re-renders. |
258
+ | `component.setState(partial)` | Merges `partial` into the current props and re-renders. |
259
+
260
+ ```ts
261
+ const counter = em.Counter({ content: '0' });
262
+
263
+ counter.setState({ content: '1' });
264
+ counter.setState({ style: { color: 'red' } });
265
+ ```
266
+
267
+ `setState` shallow-merges: nested objects such as `style` are replaced, not merged. To change one style property, pass the whole style object.
268
+
269
+ Updates are in place:
270
+
271
+ - The inner element is reused while `tag` stays the same. Focus, selection, scroll position and the live value of form controls are preserved.
272
+ - Attributes, styles and listeners are diffed against the previous render, and stale ones are removed.
273
+ - If `tag` changes, a new inner element is created (and `ref` is called again).
274
+ - The text content and the `children` are always rebuilt.
275
+
276
+ Components do not track state for you. `setState` is a rendering call, not a store: keep your application state wherever you like and call `setState` when it changes.
277
+
278
+ An `<em-*>` element created outside the library (for example `document.createElement('em-card')`) renders an empty `div` when it is connected.
279
+
280
+ ## Mounting
281
+
282
+ ```ts
283
+ mount(target, ...nodes);
284
+ ```
285
+
286
+ | Argument | Type | Description |
287
+ | -------- | ------------------- | --------------------------------------- |
288
+ | `target` | `string \| Element` | CSS selector or element to append into. |
289
+ | `nodes` | `Node[]` | Components or any other DOM nodes. |
290
+
291
+ Returns the container. Throws `Error: ecmact: mount target "<selector>" not found` if the selector matches nothing, so a typo in a selector fails loudly instead of rendering nothing.
292
+
293
+ ```ts
294
+ mount('#app', header, content, footer);
295
+ mount(document.body, toast);
296
+ ```
297
+
298
+ `mount` is a thin wrapper over `Element.append`; you can always use the DOM directly.
299
+
300
+ ## Composition
301
+
302
+ Components are values. Compose them with ordinary functions:
303
+
304
+ ```ts
305
+ import { em, mount } from 'ecmact';
306
+
307
+ const Item = (label: string, onRemove: () => void) =>
308
+ em.Item({
309
+ tag: 'li',
310
+ style: { display: 'flex', gap: '8px' },
311
+ children: [
312
+ em.Label({ tag: 'span', content: label }),
313
+ em.Remove({ tag: 'button', content: 'x', onclick: onRemove }),
314
+ ],
315
+ });
316
+
317
+ const List = (items: string[]) => {
318
+ const list = em.List({ tag: 'ul' });
319
+ const render = () =>
320
+ list.setState({
321
+ children: items.map((label, index) =>
322
+ Item(label, () => {
323
+ items.splice(index, 1);
324
+ render();
325
+ }),
326
+ ),
327
+ });
328
+ render();
329
+ return list;
330
+ };
331
+
332
+ mount('#app', List(['one', 'two', 'three']));
333
+ ```
334
+
335
+ Since children are rebuilt on every update, create child components inside the render step (as above) when they depend on state, or keep references and update them with `setState` individually.
336
+
337
+ ## TypeScript
338
+
339
+ The package ships its own declarations. Props are typed by the inner tag, which is inferred from the `tag` prop (and defaults to `'div'`).
340
+
341
+ ```ts
342
+ const link = em.Link({ tag: 'a', href: '/docs', target: '_blank' });
343
+
344
+ em.Link({ tag: 'a', src: 'x.png' });
345
+ em.Field({ tag: 'input', type: 'bogus' });
346
+ em.Card({ onclik: () => {} });
347
+ ```
348
+
349
+ The last three calls are compile errors: `src` is not an attribute of `a`, `'bogus'` is not an input type, and `onclik` is not an event.
350
+
351
+ What is typed:
352
+
353
+ - **Return type.** `em.X({ tag: 'a' })` is `Component<'a'>`; `setState` accepts `Partial<ComponentProps<'a'>>`.
354
+ - **Attributes by tag.** Global attributes (`id`, `class`, `title`, `lang`, `dir`, `hidden`, `inert`, `tabindex`, `role`, `slot`, `part`, `accesskey`, `translate`, `spellcheck`, `draggable`, `contenteditable`, `inputmode`) plus `data-*` and `aria-*` apply to every tag. Tag-specific attributes are defined for: `a`, `img`, `input`, `button`, `form`, `label`, `select`, `option`, `optgroup`, `textarea`, `fieldset`, `output`, `progress`, `meter`, `link`, `script`, `iframe`, `video`, `audio`, `source`, `track`, `canvas`, `td`, `th`, `col`, `colgroup`, `ol`, `li`, `details`, `dialog`, `blockquote`, `q`, `time`, `meta`, `slot`, `data`, `del`, `ins`. Other standard tags accept the global attributes only.
355
+ - **Custom tags.** A tag that is not a standard HTML tag (for example `'x-widget'`, or a plain `string`) accepts any attribute.
356
+ - **Events.** `on<event>` props are typed from `HTMLElementEventMap`; the handler receives the right event type. Unknown `on*` names are errors on standard tags and allowed on custom tags.
357
+ - **Styles.** `style` accepts the string-valued properties of `CSSStyleDeclaration`, with `string | number` values.
358
+ - **`ref`.** The element type follows the tag: `HTMLInputElement` for `'input'`, `HTMLElement` for custom tags.
359
+
360
+ Attribute value types follow HTML semantics: booleans for boolean attributes, numbers for numeric ones, string unions for enumerated ones.
361
+
362
+ When the tag is not known at compile time, widen it to `string`; props then become permissive and `setState({ tag })` accepts any tag:
363
+
364
+ ```ts
365
+ const dynamic = em.Dynamic({ tag: tagName as string, content: 'x' });
366
+ dynamic.setState({ tag: 'section' });
367
+ ```
368
+
369
+ Exported types:
370
+
371
+ ```ts
372
+ import type { Component, ComponentProps, AttributeMap, GlobalAttributes, TagName } from 'ecmact';
373
+ ```
374
+
375
+ ## Security model
376
+
377
+ - `content` is inserted as a text node; markup in it is inert.
378
+ - Attribute values and style values are set through `setAttribute` and the CSSOM, never concatenated into HTML, so they cannot inject markup or break out of an attribute or a `<style>` block.
379
+ - `on*` props only accept functions; strings are ignored, so attribute-style handlers such as `onclick="..."` are never created.
380
+ - `html` is the one raw-HTML entry point. It uses `innerHTML` and does not sanitize. Pass only markup you control, or sanitize it first (for example with DOMPurify).
381
+ - `tag` is passed to `document.createElement`; it cannot inject markup. It should still not come from untrusted input, since it decides which element is created (for example `script` or `iframe`).
382
+ - Attributes that carry URLs (`href`, `src`, `action`) are set as given. `javascript:` URLs are not filtered; validate user-supplied URLs.
383
+
384
+ ## Server-side rendering
385
+
386
+ Importing `ecmact` is safe without a DOM (Node, SSR bundlers, tooling): no browser globals are touched at import time. Creating components requires `document` and `customElements`, so call `em.*` and `mount` only in the browser, or in a DOM environment such as happy-dom or jsdom. There is no server-side serialization of components.
387
+
388
+ ## Browser support
389
+
390
+ Any evergreen browser with Custom Elements v1, Shadow DOM and ES2022 class fields (including private `#fields`). No polyfills are included.
391
+
392
+ ## Limitations
393
+
394
+ - **Attributes, not properties.** Props map to attributes. For form controls, `value` and `checked` set the default value/state; after the user interacts with the control, changing the attribute no longer changes what is displayed. Set live values through a `ref` (`input.value = '...'`).
395
+ - **Shadow DOM by default.** Global CSS does not apply inside components, and form controls inside a Shadow DOM do not take part in an outer `<form>`'s submission or validation. Label/`for` and `id` references do not cross the shadow boundary.
396
+ - **One inner element.** A component renders a single element. Compose several components for more complex structures.
397
+ - **Full rebuild of children.** Children and text are rebuilt on every update; there is no keyed child diffing.
398
+ - **No lifecycle hooks, context or store.** The library is a rendering layer.
399
+ - **Handlers without options.** Listener options require a `ref`.
400
+
401
+ ## Migrating from 1.x
402
+
403
+ 2.0.0 fixes the package entry points (1.x published `exports` that pointed to missing files) and closes several injection paths. Breaking changes:
404
+
405
+ | 1.x | 2.x |
406
+ | ---------------------------------------------------------------- | ---------------------------------------------------------- |
407
+ | `content` was inserted as HTML | `content` is text; use `html` for trusted markup |
408
+ | Attribute and style values were concatenated into an HTML string | Set through DOM APIs |
409
+ | `mount` silently did nothing for a missing selector | `mount` throws |
410
+ | `mount` accepted a selector only | accepts a selector or an `Element`, returns the container |
411
+ | `slots` and `customEvents` props (not implemented) | removed |
412
+ | `ref` was declared but never called | `ref` is called when the inner element is created |
413
+ | `em.then` / symbols registered elements or threw | return `undefined`; invalid names throw `TypeError` |
414
+ | Every `setState` rebuilt the whole shadow tree | in-place update, element reused while `tag` is unchanged |
415
+ | Children were rendered through a `<slot>` | children are appended to the inner element |
416
+ | Props were loosely typed | attributes, events, styles and `ref` typed by tag |
417
+ | `dist/es/index.js`, `dist/cjs/index.js` | `dist/index.js`, `dist/index.cjs`, declarations in `dist/` |
418
+
419
+ ## Development
420
+
421
+ ```bash
422
+ npm ci
423
+ npm run lint
424
+ npm run typecheck
425
+ npm test
426
+ npm run build
427
+ ```
428
+
429
+ | Script | Description |
430
+ | ---------------------- | ---------------------------------------------------- |
431
+ | `npm run build` | Vite library build: ES, CJS and `.d.ts` into `dist/` |
432
+ | `npm test` | Vitest with happy-dom and V8 coverage |
433
+ | `npm run typecheck` | `tsc --noEmit` over `src` and `tests` |
434
+ | `npm run lint` | ESLint (flat config, typescript-eslint, prettier) |
435
+ | `npm run lint:fix` | ESLint with autofix |
436
+ | `npm run format` | Prettier write |
437
+ | `npm run format:check` | Prettier check |
438
+
439
+ Tests enforce 100% statement, branch, function and line coverage; the run fails if coverage drops. Type-level behavior (attributes, events, styles, `ref` by tag) is covered by `tests/types.test.ts` and checked by `npm run typecheck`.
440
+
441
+ Source layout:
442
+
443
+ ```
444
+ src/
445
+ index.ts public exports
446
+ em.ts Proxy factory and custom element registration
447
+ component.ts custom element class: props, setState, rendering entry
448
+ types/ ComponentProps, attribute tables, render state
449
+ utils/
450
+ render.ts creates or reuses the inner element and fills it
451
+ syncAttributes.ts attribute diffing
452
+ applyStyle.ts style diffing through the CSSOM
453
+ setEventListeners.ts listener diffing
454
+ mount.ts mount helper
455
+ tests/ Vitest suites (runtime and type-level)
456
+ ```
457
+
458
+ ## Release
459
+
460
+ Releases are published from CI. Bump the version, push, and push a matching tag:
461
+
462
+ ```bash
463
+ npm version <patch|minor|major>
464
+ git push --follow-tags
465
+ ```
466
+
467
+ A tag matching `v*` triggers the release workflow, which runs format check, lint, typecheck and tests, then publishes to npm with provenance via trusted publishing. Configure a trusted publisher for `ecmact` on npmjs.com (repository `eternalvision/ecmact`, workflow `release.yml`) before the first release.
468
+
469
+ The CI workflow runs the full check (format, lint, typecheck, tests with coverage, build, production audit) on every push to `main` and on pull requests.
470
+
471
+ ## License
472
+
473
+ MIT
@@ -0,0 +1,14 @@
1
+ import { ComponentProps } from './types';
2
+ declare const Base: {
3
+ new (): HTMLElement;
4
+ prototype: HTMLElement;
5
+ };
6
+ export declare class Component<K extends string = string> extends Base {
7
+ #private;
8
+ constructor(props?: ComponentProps<K>);
9
+ connectedCallback(): void;
10
+ setState(newProps: Partial<ComponentProps<K>>): void;
11
+ get props(): ComponentProps<K>;
12
+ set props(newProps: ComponentProps<K>);
13
+ }
14
+ export {};
package/dist/em.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { Component } from './component';
2
+ import { ComponentProps } from './types';
3
+ type ComponentFactory = <K extends string = 'div'>(props?: ComponentProps<K>) => Component<K>;
4
+ type ComponentRegistry = Record<string, ComponentFactory>;
5
+ declare const em: ComponentRegistry;
6
+ export { em };
package/dist/index.cjs ADDED
@@ -0,0 +1,2 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=e=>e.replace(/([A-Z])/g,`-$1`).toLowerCase(),t=(t,n={},r)=>{let i=new Map;return Object.entries(n).forEach(([t,n])=>{(typeof n==`string`||typeof n==`number`)&&i.set(e(t),String(n))}),r.forEach(e=>{i.has(e)||t.style.removeProperty(e)}),i.forEach((e,n)=>t.style.setProperty(n,e)),new Set(i.keys())},n=(e,...t)=>{let n=typeof e==`string`?document.querySelector(e):e;if(!n)throw Error(`ecmact: mount target "${e}" not found`);return n.append(...t),n},r=(e,t,n)=>{n.forEach((t,n)=>e.removeEventListener(n,t));let r=new Map;return Object.entries(t).forEach(([e,t])=>{e.startsWith(`on`)&&typeof t==`function`&&r.set(e.slice(2).toLowerCase(),t)}),r.forEach((t,n)=>e.addEventListener(n,t)),r},i=new Set([`tag`,`content`,`html`,`style`,`children`,`ref`]),a=e=>e===!0?``:typeof e==`string`||typeof e==`number`?String(e):null,o=(e,t,n)=>{let r=new Map;return Object.entries(t).forEach(([e,t])=>{if(i.has(e)||e.startsWith(`on`))return;let n=a(t);n!==null&&r.set(e,n)}),n.forEach(t=>{r.has(t)||e.removeAttribute(t)}),r.forEach((t,n)=>e.setAttribute(n,t)),new Set(r.keys())},s=(e,t,n)=>{let r=document.createElement(e);return t.replaceChildren(r),n.attributes=new Set,n.styles=new Set,n.listeners=new Map,n.element=r,r},c=(e,n,i)=>{let{tag:a=`div`,content:c=``,html:l,style:u,children:d=[],ref:f}=n,p=i.element?.localName!==a.toLowerCase(),m=p?s(a,e,i):i.element;return i.attributes=o(m,n,i.attributes),i.styles=t(m,u,i.styles),i.listeners=r(m,n,i.listeners),l===void 0?m.replaceChildren(document.createTextNode(c),...d):(m.innerHTML=l,m.append(...d)),p&&f?.(m),m},l=typeof HTMLElement>`u`?class{}:HTMLElement,u=class extends l{#e;#t;#n={element:null,attributes:new Set,styles:new Set,listeners:new Map};constructor(e={}){super(),this.#t=this.attachShadow({mode:`open`}),this.#e=e}connectedCallback(){this.#n.element||this.#r()}setState(e){this.props={...this.#e,...e}}get props(){return this.#e}set props(e){this.#e=e,this.#r()}#r(){c(this.#t,this.#e,this.#n)}},d=/^[A-Za-z][A-Za-z0-9_-]*$/,f=new Proxy({},{get(e,t){if(typeof t==`symbol`||t===`then`)return;if(!d.test(t))throw TypeError(`ecmact: invalid component name "${t}"`);let n=`em-${t.toLowerCase()}`;return customElements.get(n)||customElements.define(n,class extends u{}),(e={})=>{let t=document.createElement(n);return t.props=e,t}}});exports.em=f,exports.mount=n;
2
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":["toKebabCase","key","applyStyle","next","style","value","previous","element","mount","nodes","container","target","setEventListeners","event","listener","props","reservedKeys","toAttributeValue","syncAttributes","attribute","createElement","tag","root","render","isNew","state","html","content","children","ref","Base","Component","#root","#props","#state","#render","newProps","namePattern","em","prop","tagName"],"sources":["../src/utils/applyStyle.ts","../src/utils/mount.ts","../src/utils/setEventListeners.ts","../src/utils/syncAttributes.ts","../src/utils/render.ts","../src/component.ts","../src/em.ts"],"sourcesContent":["import type { ComponentProps } from '../types';\n\nconst toKebabCase = (key: string) => key.replace(/([A-Z])/g, '-$1').toLowerCase();\n\nexport const applyStyle = (\n element: HTMLElement,\n style: ComponentProps['style'] = {},\n previous: Set<string>,\n) => {\n const next = new Map<string, string>();\n\n Object.entries(style).forEach(([key, value]) => {\n if (typeof value === 'string' || typeof value === 'number') {\n next.set(toKebabCase(key), String(value));\n }\n });\n\n previous.forEach((key) => {\n if (!next.has(key)) element.style.removeProperty(key);\n });\n\n next.forEach((value, key) => element.style.setProperty(key, value));\n\n return new Set(next.keys());\n};\n","export const mount = (target: string | Element, ...nodes: Node[]) => {\n const container = typeof target === 'string' ? document.querySelector(target) : target;\n if (!container) throw new Error(`ecmact: mount target \"${target}\" not found`);\n container.append(...nodes);\n return container;\n};\n","import type { ComponentProps } from '../types';\n\nexport const setEventListeners = (\n element: HTMLElement,\n props: ComponentProps,\n previous: Map<string, EventListener>,\n) => {\n previous.forEach((listener, event) => element.removeEventListener(event, listener));\n\n const next = new Map<string, EventListener>();\n\n Object.entries(props).forEach(([key, value]) => {\n if (key.startsWith('on') && typeof value === 'function') {\n next.set(key.slice(2).toLowerCase(), value as EventListener);\n }\n });\n\n next.forEach((listener, event) => element.addEventListener(event, listener));\n\n return next;\n};\n","import type { ComponentProps } from '../types';\n\nconst reservedKeys = new Set(['tag', 'content', 'html', 'style', 'children', 'ref']);\n\nconst toAttributeValue = (value: unknown) => {\n if (value === true) return '';\n if (typeof value === 'string' || typeof value === 'number') return String(value);\n return null;\n};\n\nexport const syncAttributes = (\n element: HTMLElement,\n props: ComponentProps,\n previous: Set<string>,\n) => {\n const next = new Map<string, string>();\n\n Object.entries(props).forEach(([key, value]) => {\n if (reservedKeys.has(key) || key.startsWith('on')) return;\n const attribute = toAttributeValue(value);\n if (attribute !== null) next.set(key, attribute);\n });\n\n previous.forEach((key) => {\n if (!next.has(key)) element.removeAttribute(key);\n });\n\n next.forEach((value, key) => element.setAttribute(key, value));\n\n return new Set(next.keys());\n};\n","import type { ComponentProps, RenderState } from '../types';\nimport { applyStyle } from './applyStyle';\nimport { setEventListeners } from './setEventListeners';\nimport { syncAttributes } from './syncAttributes';\n\nconst createElement = (tag: string, root: ShadowRoot, state: RenderState) => {\n const element = document.createElement(tag);\n root.replaceChildren(element);\n state.attributes = new Set();\n state.styles = new Set();\n state.listeners = new Map();\n state.element = element;\n return element;\n};\n\nexport const render = (root: ShadowRoot, props: ComponentProps, state: RenderState) => {\n const { tag = 'div', content = '', html, style, children = [], ref } = props;\n\n const isNew = state.element?.localName !== tag.toLowerCase();\n const element = isNew ? createElement(tag, root, state) : (state.element as HTMLElement);\n\n state.attributes = syncAttributes(element, props, state.attributes);\n state.styles = applyStyle(element, style, state.styles);\n state.listeners = setEventListeners(element, props, state.listeners);\n\n if (html === undefined) {\n element.replaceChildren(document.createTextNode(content), ...children);\n } else {\n element.innerHTML = html;\n element.append(...children);\n }\n\n if (isNew) ref?.(element);\n\n return element;\n};\n","import type { ComponentProps, RenderState } from './types';\nimport { render } from './utils';\n\nconst Base = typeof HTMLElement === 'undefined' ? (class {} as typeof HTMLElement) : HTMLElement;\n\nexport class Component<K extends string = string> extends Base {\n #props: ComponentProps<K>;\n readonly #root: ShadowRoot;\n readonly #state: RenderState = {\n element: null,\n attributes: new Set(),\n styles: new Set(),\n listeners: new Map(),\n };\n\n constructor(props: ComponentProps<K> = {} as ComponentProps<K>) {\n super();\n this.#root = this.attachShadow({ mode: 'open' });\n this.#props = props;\n }\n\n connectedCallback() {\n if (!this.#state.element) this.#render();\n }\n\n setState(newProps: Partial<ComponentProps<K>>) {\n this.props = { ...this.#props, ...newProps } as ComponentProps<K>;\n }\n\n get props() {\n return this.#props;\n }\n\n set props(newProps: ComponentProps<K>) {\n this.#props = newProps;\n this.#render();\n }\n\n #render() {\n render(this.#root, this.#props as ComponentProps, this.#state);\n }\n}\n","import { Component } from './component';\nimport type { ComponentProps } from './types';\n\ntype ComponentFactory = <K extends string = 'div'>(props?: ComponentProps<K>) => Component<K>;\n\ntype ComponentRegistry = Record<string, ComponentFactory>;\n\nconst namePattern = /^[A-Za-z][A-Za-z0-9_-]*$/;\n\nconst em: ComponentRegistry = new Proxy({} as ComponentRegistry, {\n get(_, prop) {\n if (typeof prop === 'symbol' || prop === 'then') return undefined;\n\n if (!namePattern.test(prop)) {\n throw new TypeError(`ecmact: invalid component name \"${prop}\"`);\n }\n\n const tagName = `em-${prop.toLowerCase()}`;\n\n if (!customElements.get(tagName)) {\n customElements.define(tagName, class extends Component {});\n }\n\n return <K extends string = 'div'>(props: ComponentProps<K> = {} as ComponentProps<K>) => {\n const element = document.createElement(tagName) as Component<K>;\n element.props = props;\n return element;\n };\n },\n});\n\nexport { em };\n"],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export { em } from './em';
2
+ export { mount } from './utils';
3
+ export type { Component } from './component';
4
+ export type { AttributeMap, ComponentProps, GlobalAttributes, TagName } from './types';
package/dist/index.js ADDED
@@ -0,0 +1,82 @@
1
+ // src/utils/applyStyle.ts
2
+ var e = (e) => e.replace(/([A-Z])/g, "-$1").toLowerCase(), t = (t, n = {}, r) => {
3
+ let i = /* @__PURE__ */ new Map();
4
+ return Object.entries(n).forEach(([t, n]) => {
5
+ (typeof n == "string" || typeof n == "number") && i.set(e(t), String(n));
6
+ }), r.forEach((e) => {
7
+ i.has(e) || t.style.removeProperty(e);
8
+ }), i.forEach((e, n) => t.style.setProperty(n, e)), new Set(i.keys());
9
+ }, n = (e, ...t) => {
10
+ let n = typeof e == "string" ? document.querySelector(e) : e;
11
+ if (!n) throw Error(`ecmact: mount target "${e}" not found`);
12
+ return n.append(...t), n;
13
+ }, r = (e, t, n) => {
14
+ n.forEach((t, n) => e.removeEventListener(n, t));
15
+ let r = /* @__PURE__ */ new Map();
16
+ return Object.entries(t).forEach(([e, t]) => {
17
+ e.startsWith("on") && typeof t == "function" && r.set(e.slice(2).toLowerCase(), t);
18
+ }), r.forEach((t, n) => e.addEventListener(n, t)), r;
19
+ }, i = /* @__PURE__ */ new Set([
20
+ "tag",
21
+ "content",
22
+ "html",
23
+ "style",
24
+ "children",
25
+ "ref"
26
+ ]), a = (e) => e === !0 ? "" : typeof e == "string" || typeof e == "number" ? String(e) : null, o = (e, t, n) => {
27
+ let r = /* @__PURE__ */ new Map();
28
+ return Object.entries(t).forEach(([e, t]) => {
29
+ if (i.has(e) || e.startsWith("on")) return;
30
+ let n = a(t);
31
+ n !== null && r.set(e, n);
32
+ }), n.forEach((t) => {
33
+ r.has(t) || e.removeAttribute(t);
34
+ }), r.forEach((t, n) => e.setAttribute(n, t)), new Set(r.keys());
35
+ }, s = (e, t, n) => {
36
+ let r = document.createElement(e);
37
+ return t.replaceChildren(r), n.attributes = /* @__PURE__ */ new Set(), n.styles = /* @__PURE__ */ new Set(), n.listeners = /* @__PURE__ */ new Map(), n.element = r, r;
38
+ }, c = (e, n, i) => {
39
+ let { tag: a = "div", content: c = "", html: l, style: u, children: d = [], ref: f } = n, p = i.element?.localName !== a.toLowerCase(), m = p ? s(a, e, i) : i.element;
40
+ return i.attributes = o(m, n, i.attributes), i.styles = t(m, u, i.styles), i.listeners = r(m, n, i.listeners), l === void 0 ? m.replaceChildren(document.createTextNode(c), ...d) : (m.innerHTML = l, m.append(...d)), p && f?.(m), m;
41
+ }, l = typeof HTMLElement > "u" ? class {} : HTMLElement, u = class extends l {
42
+ #e;
43
+ #t;
44
+ #n = {
45
+ element: null,
46
+ attributes: /* @__PURE__ */ new Set(),
47
+ styles: /* @__PURE__ */ new Set(),
48
+ listeners: /* @__PURE__ */ new Map()
49
+ };
50
+ constructor(e = {}) {
51
+ super(), this.#t = this.attachShadow({ mode: "open" }), this.#e = e;
52
+ }
53
+ connectedCallback() {
54
+ this.#n.element || this.#r();
55
+ }
56
+ setState(e) {
57
+ this.props = {
58
+ ...this.#e,
59
+ ...e
60
+ };
61
+ }
62
+ get props() {
63
+ return this.#e;
64
+ }
65
+ set props(e) {
66
+ this.#e = e, this.#r();
67
+ }
68
+ #r() {
69
+ c(this.#t, this.#e, this.#n);
70
+ }
71
+ }, d = /^[A-Za-z][A-Za-z0-9_-]*$/, f = new Proxy({}, { get(e, t) {
72
+ if (typeof t == "symbol" || t === "then") return;
73
+ if (!d.test(t)) throw TypeError(`ecmact: invalid component name "${t}"`);
74
+ let n = `em-${t.toLowerCase()}`;
75
+ return customElements.get(n) || customElements.define(n, class extends u {}), (e = {}) => {
76
+ let t = document.createElement(n);
77
+ return t.props = e, t;
78
+ };
79
+ } });
80
+ export { f as em, n as mount };
81
+
82
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["toKebabCase","key","applyStyle","next","style","value","previous","element","mount","nodes","container","target","setEventListeners","event","listener","props","reservedKeys","toAttributeValue","syncAttributes","attribute","createElement","tag","root","render","isNew","state","html","content","children","ref","Base","Component","#root","#props","#state","#render","newProps","namePattern","em","prop","tagName"],"sources":["../src/utils/applyStyle.ts","../src/utils/mount.ts","../src/utils/setEventListeners.ts","../src/utils/syncAttributes.ts","../src/utils/render.ts","../src/component.ts","../src/em.ts"],"sourcesContent":["import type { ComponentProps } from '../types';\n\nconst toKebabCase = (key: string) => key.replace(/([A-Z])/g, '-$1').toLowerCase();\n\nexport const applyStyle = (\n element: HTMLElement,\n style: ComponentProps['style'] = {},\n previous: Set<string>,\n) => {\n const next = new Map<string, string>();\n\n Object.entries(style).forEach(([key, value]) => {\n if (typeof value === 'string' || typeof value === 'number') {\n next.set(toKebabCase(key), String(value));\n }\n });\n\n previous.forEach((key) => {\n if (!next.has(key)) element.style.removeProperty(key);\n });\n\n next.forEach((value, key) => element.style.setProperty(key, value));\n\n return new Set(next.keys());\n};\n","export const mount = (target: string | Element, ...nodes: Node[]) => {\n const container = typeof target === 'string' ? document.querySelector(target) : target;\n if (!container) throw new Error(`ecmact: mount target \"${target}\" not found`);\n container.append(...nodes);\n return container;\n};\n","import type { ComponentProps } from '../types';\n\nexport const setEventListeners = (\n element: HTMLElement,\n props: ComponentProps,\n previous: Map<string, EventListener>,\n) => {\n previous.forEach((listener, event) => element.removeEventListener(event, listener));\n\n const next = new Map<string, EventListener>();\n\n Object.entries(props).forEach(([key, value]) => {\n if (key.startsWith('on') && typeof value === 'function') {\n next.set(key.slice(2).toLowerCase(), value as EventListener);\n }\n });\n\n next.forEach((listener, event) => element.addEventListener(event, listener));\n\n return next;\n};\n","import type { ComponentProps } from '../types';\n\nconst reservedKeys = new Set(['tag', 'content', 'html', 'style', 'children', 'ref']);\n\nconst toAttributeValue = (value: unknown) => {\n if (value === true) return '';\n if (typeof value === 'string' || typeof value === 'number') return String(value);\n return null;\n};\n\nexport const syncAttributes = (\n element: HTMLElement,\n props: ComponentProps,\n previous: Set<string>,\n) => {\n const next = new Map<string, string>();\n\n Object.entries(props).forEach(([key, value]) => {\n if (reservedKeys.has(key) || key.startsWith('on')) return;\n const attribute = toAttributeValue(value);\n if (attribute !== null) next.set(key, attribute);\n });\n\n previous.forEach((key) => {\n if (!next.has(key)) element.removeAttribute(key);\n });\n\n next.forEach((value, key) => element.setAttribute(key, value));\n\n return new Set(next.keys());\n};\n","import type { ComponentProps, RenderState } from '../types';\nimport { applyStyle } from './applyStyle';\nimport { setEventListeners } from './setEventListeners';\nimport { syncAttributes } from './syncAttributes';\n\nconst createElement = (tag: string, root: ShadowRoot, state: RenderState) => {\n const element = document.createElement(tag);\n root.replaceChildren(element);\n state.attributes = new Set();\n state.styles = new Set();\n state.listeners = new Map();\n state.element = element;\n return element;\n};\n\nexport const render = (root: ShadowRoot, props: ComponentProps, state: RenderState) => {\n const { tag = 'div', content = '', html, style, children = [], ref } = props;\n\n const isNew = state.element?.localName !== tag.toLowerCase();\n const element = isNew ? createElement(tag, root, state) : (state.element as HTMLElement);\n\n state.attributes = syncAttributes(element, props, state.attributes);\n state.styles = applyStyle(element, style, state.styles);\n state.listeners = setEventListeners(element, props, state.listeners);\n\n if (html === undefined) {\n element.replaceChildren(document.createTextNode(content), ...children);\n } else {\n element.innerHTML = html;\n element.append(...children);\n }\n\n if (isNew) ref?.(element);\n\n return element;\n};\n","import type { ComponentProps, RenderState } from './types';\nimport { render } from './utils';\n\nconst Base = typeof HTMLElement === 'undefined' ? (class {} as typeof HTMLElement) : HTMLElement;\n\nexport class Component<K extends string = string> extends Base {\n #props: ComponentProps<K>;\n readonly #root: ShadowRoot;\n readonly #state: RenderState = {\n element: null,\n attributes: new Set(),\n styles: new Set(),\n listeners: new Map(),\n };\n\n constructor(props: ComponentProps<K> = {} as ComponentProps<K>) {\n super();\n this.#root = this.attachShadow({ mode: 'open' });\n this.#props = props;\n }\n\n connectedCallback() {\n if (!this.#state.element) this.#render();\n }\n\n setState(newProps: Partial<ComponentProps<K>>) {\n this.props = { ...this.#props, ...newProps } as ComponentProps<K>;\n }\n\n get props() {\n return this.#props;\n }\n\n set props(newProps: ComponentProps<K>) {\n this.#props = newProps;\n this.#render();\n }\n\n #render() {\n render(this.#root, this.#props as ComponentProps, this.#state);\n }\n}\n","import { Component } from './component';\nimport type { ComponentProps } from './types';\n\ntype ComponentFactory = <K extends string = 'div'>(props?: ComponentProps<K>) => Component<K>;\n\ntype ComponentRegistry = Record<string, ComponentFactory>;\n\nconst namePattern = /^[A-Za-z][A-Za-z0-9_-]*$/;\n\nconst em: ComponentRegistry = new Proxy({} as ComponentRegistry, {\n get(_, prop) {\n if (typeof prop === 'symbol' || prop === 'then') return undefined;\n\n if (!namePattern.test(prop)) {\n throw new TypeError(`ecmact: invalid component name \"${prop}\"`);\n }\n\n const tagName = `em-${prop.toLowerCase()}`;\n\n if (!customElements.get(tagName)) {\n customElements.define(tagName, class extends Component {});\n }\n\n return <K extends string = 'div'>(props: ComponentProps<K> = {} as ComponentProps<K>) => {\n const element = document.createElement(tagName) as Component<K>;\n element.props = props;\n return element;\n };\n },\n});\n\nexport { em };\n"],"mappings":"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"}