@dooboostore/dom-render 1.0.99 → 1.0.102

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/README.MD CHANGED
@@ -6,25 +6,25 @@ DOM-RENDER
6
6
  * all internal variables are managed by proxy. (DomRenderProxy)
7
7
 
8
8
  # 🚀 Quick start
9
- ```html
10
- <script src="https://cdn.jsdelivr.net/npm/dom-render@1.0.91/dist/bundle.js"></script>
9
+ ```typescript
10
+ import { DomRender } from '@dooboostore/dom-render';
11
+
12
+ class AppData {
13
+ name: string = 'my name is dom-render';
14
+ }
15
+
16
+ const data = new AppData();
17
+ DomRender.run(data, document.querySelector('#app')!);
11
18
  ```
12
19
  ```html
13
20
  <!DOCTYPE html>
14
21
  <html lang="en">
15
- <head``>
22
+ <head>
16
23
  <meta charset="UTF-8">
17
24
  <title>Title</title>
18
25
  </head>
19
26
  <body id="app">
20
27
  ${this.name}$
21
- <script src="https://cdn.jsdelivr.net/npm/dom-render@1.0.86/dist/bundle.js"></script>
22
- <script>
23
- let data = {
24
- name: 'my name is dom-render'
25
- };
26
- data = DomRender.run(data , document.querySelector('#app'));
27
- </script>
28
28
  </body>
29
29
  </html>
30
30
  ```
@@ -42,14 +42,18 @@ ${this.name}$
42
42
  <body id="app">
43
43
  ${this.name}$ <!-- outout: <i>my name is dom-render</i> -->
44
44
  #{this.name}# <!-- outout text is italic: my name is dom-render -->
45
- <script>
46
- let data = {
47
- name: '<i>my name is dom-render</i>'
48
- };
49
- data = DomRender.run(data , document.querySelector('#app'));
50
- </script>
51
45
  </body>
52
46
  ```
47
+ ```typescript
48
+ import { DomRender } from '@dooboostore/dom-render';
49
+
50
+ class AppData {
51
+ name: string = '<i>my name is dom-render</i>';
52
+ }
53
+
54
+ const data = new AppData();
55
+ DomRender.run(data, document.querySelector('#app')!);
56
+ ```
53
57
  </details>
54
58
 
55
59
  # dom-render attributes
@@ -141,14 +145,18 @@ uuid: rawSetUUID
141
145
  <body id="app">
142
146
  <div dr-if="true">true</div> <!-- render -->
143
147
  <div dr-if="this.gender === 'M'">gender: M</div> <!-- No Render -->
144
- <script>
145
- let data = {
146
- gender: 'F',
147
- };
148
- data = DomRender.run(data , document.querySelector('#app'));
149
- </script>
150
148
  </body>
151
149
  ```
150
+ ```typescript
151
+ import { DomRender } from '@dooboostore/dom-render';
152
+
153
+ class AppData {
154
+ gender: string = 'F';
155
+ }
156
+
157
+ const data = new AppData();
158
+ DomRender.run(data, document.querySelector('#app')!);
159
+ ```
152
160
  </details>
153
161
 
154
162
 
@@ -326,6 +334,197 @@ class data {
326
334
  </details>
327
335
 
328
336
 
337
+ ## Component Property binding with `@attribute` decorator
338
+ <details>
339
+ <summary>@attribute<strong>🔻(click)</strong></summary>
340
+
341
+ You can bind component properties directly to HTML attributes using the `@attribute` decorator. When the attribute on the custom element changes, the corresponding property in the component class instance is automatically updated.
342
+
343
+ This requires your component to extend `ComponentBase`.
344
+
345
+ ### Usage
346
+
347
+ The `@attribute` decorator options:
348
+
349
+ * `name: string`: Binds the property to a specifically named attribute. If not provided, the property name is used.
350
+ * `converter: (value: string | null) => any`: A function to convert the string attribute value to another type.
351
+
352
+ ### Example
353
+
354
+ **Component (`MyComponent.ts`)**
355
+ ```typescript
356
+ import { ComponentBase, attribute } from '@dooboostore/dom-render';
357
+
358
+ // Assuming MyComponent is registered with the tag 'my-component'
359
+ export class MyComponent extends ComponentBase<{ 'title': string, 'show-icon': string, 'created-at': string }> {
360
+ @attribute
361
+ title: string = 'Default Title';
362
+
363
+ @attribute({
364
+ name: 'show-icon',
365
+ converter: (val) => val === 'true'
366
+ })
367
+ showIcon: boolean; // The converter handles string -> boolean
368
+
369
+ @attribute({
370
+ name: 'created-at',
371
+ converter: (val) => val ? new Date(val) : null
372
+ })
373
+ createdAt: Date | null;
374
+ }
375
+ ```
376
+
377
+ **Template (`index.html`)**
378
+ ```html
379
+ <my-component
380
+ title="Hello World!"
381
+ show-icon="true"
382
+ created-at="2025-08-30T12:00:00Z">
383
+ </my-component>
384
+
385
+ <script>
386
+ // After some time, change the attribute
387
+ setTimeout(() => {
388
+ const comp = document.querySelector('my-component');
389
+ comp.setAttribute('title', 'New Title!');
390
+ comp.setAttribute('show-icon', 'false');
391
+ comp.setAttribute('created-at', new Date().toISOString());
392
+ }, 2000);
393
+ </script>
394
+ ```
395
+ When attributes change, the `converter` function will be executed before the value is assigned to the property. For `showIcon`, the string `"true"` or `"false"` will be converted to a boolean. For `createdAt`, the date string will be converted to a `Date` object.
396
+
397
+ </details>
398
+
399
+ ## Element Query with `@query` decorator
400
+ <details>
401
+ <summary>@query<strong>🔻(click)</strong></summary>
402
+
403
+ You can get a reference to DOM elements within your component's template using the `@query` decorator. It takes a CSS selector as an argument.
404
+
405
+ - If the decorated property is **not an array type**, it returns the **first** matching element (like `querySelector`).
406
+ - If the decorated property **is an array type** (e.g., `HTMLElement[]`), it returns **all** matching elements as an array (like `querySelectorAll`).
407
+ - When applied to a **method**, the method will be called with the queried element(s) as arguments after the component has been rendered. If the method expects a single element, the first match is passed. If it expects an array, all matches are passed.
408
+
409
+ This is useful for directly accessing and manipulating elements you need to interact with from your component's logic.
410
+
411
+ **Note:** This feature relies on TypeScript's decorator metadata (`emitDecoratorMetadata: true` in `tsconfig.json`).
412
+
413
+ ### Usage Example
414
+
415
+ ```typescript
416
+ import { ComponentBase, query } from '@dooboostore/dom-render';
417
+
418
+ // Assuming MyComponent is rendered with the template below
419
+ export class MyComponent extends ComponentBase<{}> {
420
+ // Gets the first element matching the selector
421
+ @query('h3.title')
422
+ private titleEl: HTMLHeadingElement;
423
+
424
+ // Gets all elements matching the selector as an array
425
+ @query('ul.item-list > li')
426
+ private items: HTMLLIElement[];
427
+
428
+ onInitRender() {
429
+ // The elements are available after the component has been rendered.
430
+ if (this.titleEl) {
431
+ this.titleEl.style.color = 'blue';
432
+ }
433
+
434
+ if (this.items && this.items.length > 0) {
435
+ this.items.forEach((item, index) => {
436
+ item.textContent = `Item ${index + 1} (updated)`;
437
+ });
438
+ }
439
+ }
440
+
441
+ // Method query: This method will be called with the first element matching '.my-button'
442
+ @query('.my-button')
443
+ private setupButton(button: HTMLButtonElement) {
444
+ if (button) {
445
+ button.addEventListener('click', () => alert('Button clicked from method query!'));
446
+ }
447
+ }
448
+
449
+ // Method query: This method will be called with an array of all elements matching 'span.item'
450
+ @query('span.item')
451
+ private processItems(spans: HTMLSpanElement[]) {
452
+ spans.forEach(span => {
453
+ span.style.fontWeight = 'bold';
454
+ });
455
+ }
456
+ }
457
+ ```
458
+
459
+ ### Template
460
+ ```html
461
+ <!-- Template for MyComponent -->
462
+ <div>
463
+ <h3 class="title">Original Title</h3>
464
+ <ul class="item-list">
465
+ <li>Item 1</li>
466
+ <li>Item 2</li>
467
+ <li>Item 3</li>
468
+ </ul>
469
+ <button class="my-button">Click Me</button>
470
+ <p><span class="item">First Span</span> <span class="item">Second Span</span></p>
471
+ </div>
472
+ ```
473
+ The `@query` decorator will assign the corresponding element(s) to the decorated property or pass them to the decorated method. The property will be `null`/`undefined` (for single query) or an empty array (for multiple query) if no elements are found or before the component is rendered. For methods, they will only be called if matching elements are found.
474
+
475
+ </details>
476
+
477
+ </details>
478
+
479
+ ## DOM Event Binding with `@event` decorator
480
+ <details>
481
+ <summary>@event<strong>🔻(click)</strong></summary>
482
+
483
+ You can bind component methods directly to DOM events on elements within your component's template using the `@event` decorator.
484
+
485
+ The decorator takes an options object with two properties:
486
+ - `query: string`: A CSS selector to find the target element(s).
487
+ - `name: string`: The name of the DOM event to listen for (e.g., `'click'`, `'input'`).
488
+
489
+ The framework will automatically add the event listener when the component is initialized and remove it when the component is destroyed, preventing memory leaks.
490
+
491
+ ### Usage Example
492
+
493
+ ```typescript
494
+ import { ComponentBase, event } from '@dooboostore/dom-render';
495
+
496
+ // Assuming MyComponent is rendered with the template below
497
+ export class MyComponent extends ComponentBase<{}> {
498
+
499
+ // This method will be called when any element matching 'button.action' is clicked.
500
+ @event({ query: 'button.action', name: 'click' })
501
+ onActionClick(e: MouseEvent) {
502
+ console.log('Action button clicked!', e.target);
503
+ alert('Action!');
504
+ }
505
+
506
+ // This method will be called for the 'input' event on all text inputs.
507
+ @event({ query: 'input[type="text"]', name: 'input' })
508
+ onTextInput(e: Event) {
509
+ const target = e.target as HTMLInputElement;
510
+ console.log(`Text changed in ${target.id}: ${target.value}`);
511
+ }
512
+ }
513
+ ```
514
+
515
+ ### Template
516
+ ```html
517
+ <!-- Template for MyComponent -->
518
+ <div>
519
+ <input type="text" id="firstname" placeholder="First Name">
520
+ <input type="text" id="lastname" placeholder="Last Name">
521
+ <button class="action">Submit</button>
522
+ </div>
523
+ ```
524
+ In this example, when the "Submit" button is clicked, the `onActionClick` method is executed. When the user types in either of the text inputs, the `onTextInput` method is executed for each keystroke.
525
+
526
+ </details>
527
+
329
528
  ## nearThis
330
529
  ```html
331
530
  <div dr-this="@this@.dictionary"> (${@nearThis@.name}$) ${$nearThis.name}$ </div>
@@ -465,16 +664,22 @@ dr-value-link: <input type="text" dr-value-link="@this@.office.addr.street"> <br
465
664
  age: <input name="age">
466
665
  <button type="submit">submit</button>
467
666
  </form>
468
- <script>
469
- let data = {
470
- form: {},
471
- submit() {
472
- console.log(this.form);
473
- }
474
- };
475
- data = DomRender.run(data , document.querySelector('#app'));
476
- </script>
477
667
  </body>
668
+ ```
669
+ ```typescript
670
+ import { DomRender } from '@dooboostore/dom-render';
671
+
672
+ class FormData {
673
+ form: any = {};
674
+
675
+ submit() {
676
+ console.log(this.form);
677
+ }
678
+ }
679
+
680
+ const data = new FormData();
681
+ DomRender.run(data, document.querySelector('#app')!);
682
+ ```
478
683
  <!-- 💥 submit call -->
479
684
  <!-- console: {name: 'name data', age: 'age data'} -->
480
685
  ```
@@ -490,24 +695,33 @@ dr-value-link: <input type="text" dr-value-link="@this@.office.addr.street"> <br
490
695
  age: <input name="age">
491
696
  <button type="submit">submit</button>
492
697
  </form>
493
- <script>
494
- const form = new FormValidator();
495
- form.name = new NotEmptyValidator();
496
- form.age = new NotEmptyValidator();
497
- let data = {
498
- form,
499
- submit() {
500
- if (this.form.valid()){
501
- console.log('valid');
502
- } else {
503
- console.log('inValid');
504
- }
505
- }
506
- };
507
- data = DomRender.run(data , document.querySelector('#app'));
508
- </script>
509
698
  </body>
510
699
  ```
700
+ ```typescript
701
+ import { DomRender } from '@dooboostore/dom-render';
702
+ import { FormValidator, NotEmptyValidator } from '@dooboostore/dom-render/validator';
703
+
704
+ class FormDataWithValidator {
705
+ form: FormValidator = new FormValidator();
706
+
707
+ constructor() {
708
+ this.form.name = new NotEmptyValidator();
709
+ this.form.age = new NotEmptyValidator();
710
+ }
711
+
712
+ submit() {
713
+ if (this.form.valid()){
714
+ console.log('valid');
715
+ } else {
716
+ console.log('inValid');
717
+ }
718
+ }
719
+ }
720
+
721
+ const data = new FormDataWithValidator();
722
+ DomRender.run(data, document.querySelector('#app')!);
723
+ ```
724
+ ```
511
725
  validator
512
726
  - Validator (abstract)
513
727
  - ValidatorArray (abstract)
@@ -1,3 +1,4 @@
1
+ import 'reflect-metadata';
1
2
  import { OnChangeAttrRender, OtherData } from '../lifecycle/OnChangeAttrRender';
2
3
  import { OnCreateRenderData, OnCreateRenderDataParams } from '../lifecycle/OnCreateRenderData';
3
4
  import { RawSet } from '../rawsets/RawSet';
@@ -8,14 +9,29 @@ import { OnDestroyRender, OnDestroyRenderParams } from '../lifecycle/OnDestroyRe
8
9
  import { OnDrThisUnBind } from '../lifecycle/dr-this/OnDrThisUnBind';
9
10
  import { OnDrThisBind } from '../lifecycle/dr-this/OnDrThisBind';
10
11
  import * as Render from '../rawsets/Render';
12
+ export declare const ATTRIBUTE_METADATA_KEY: unique symbol;
13
+ export declare const QUERY_METADATA_KEY: unique symbol;
14
+ export declare const EVENT_METADATA_KEY: unique symbol;
15
+ export declare function attribute(options?: {
16
+ name?: string;
17
+ converter?: (value: string | null) => any;
18
+ }): PropertyDecorator;
19
+ export declare function query(selector: string): (target: object, propertyKey: string | symbol, descriptor?: PropertyDescriptor) => void;
20
+ export declare function event(options: {
21
+ query: string;
22
+ name: string;
23
+ }): MethodDecorator;
11
24
  export declare abstract class ComponentBase<T> implements OnChangeAttrRender, OnCreateRenderData, OnCreatedThisChild, OnDrThisBind, OnDrThisUnBind, OnInitRender, OnDestroyRender {
12
25
  private config?;
13
26
  private _rawSet?;
14
27
  private _children;
15
28
  private _render;
16
29
  private _attribute?;
30
+ private _boundEventListeners;
17
31
  get render(): Render.Render | undefined;
18
32
  get rawSet(): RawSet | undefined;
33
+ private _queryElements;
34
+ private _cleanupEventListeners;
19
35
  getAttribute<K extends keyof T>(name: K, attribute?: T): T[K] | null;
20
36
  hasAttribute(name: keyof T): boolean;
21
37
  equalsAttributeName(n: string, name: keyof T): boolean;
@@ -29,6 +45,7 @@ export declare abstract class ComponentBase<T> implements OnChangeAttrRender, On
29
45
  constructor(config?: {
30
46
  onlyParentType: ConstructorType<any>[] | ConstructorType<any>;
31
47
  });
48
+ private setRawSet;
32
49
  setAttribute(attribute: T): void;
33
50
  onCreatedThisChild(child: any, data: OnCreateRenderDataParams): void;
34
51
  onCreateRenderData(data: OnCreateRenderDataParams): void;