@ktjs/core 0.15.5 → 0.16.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/README.md CHANGED
@@ -12,7 +12,7 @@ Core DOM manipulation utilities for KT.js framework with built-in JSX/TSX suppor
12
12
 
13
13
  `@ktjs/core` is the foundation of KT.js, providing the essential `h` function and DOM utilities for building web applications with direct DOM manipulation. It emphasizes performance, type safety, and minimal abstraction over native DOM APIs.
14
14
 
15
- **Current Version:** 0.14.6
15
+ **Current Version:** 0.16.0
16
16
 
17
17
  ## Features
18
18
 
@@ -28,6 +28,12 @@ Core DOM manipulation utilities for KT.js framework with built-in JSX/TSX suppor
28
28
  - `redraw()` method for controlled re-rendering
29
29
  - **k-if directive**: Conditional element creation with `k-if` attribute
30
30
  - Array children support for seamless list rendering
31
+ - **KTFor Component**: Efficient list rendering with key-based optimization (v0.16.0)
32
+ - Key-based DOM reuse similar to Svelte's `{#each}` blocks
33
+ - Minimal DOM operations - only updates what changed
34
+ - Returns Comment anchor node for flexible positioning
35
+ - Type-safe with full TypeScript generics support
36
+ - `KTForStatic` variant for simple lists without key optimization
31
37
  - **KTAsync Component**: Handle async components with ease
32
38
  - Automatic handling of Promise-based components
33
39
  - Seamless integration with JSX/TSX
@@ -200,6 +206,62 @@ const div = (<div>Old content</div>) as KTHTMLElement;
200
206
  div.redraw(undefined, 'New content');
201
207
  ```
202
208
 
209
+ ### List Rendering with KTFor (v0.16.0)
210
+
211
+ The `KTFor` component provides efficient list rendering with key-based DOM reuse:
212
+
213
+ ```tsx
214
+ import { KTFor, KTForStatic, KTForAnchor } from '@ktjs/core';
215
+
216
+ interface Todo {
217
+ id: number;
218
+ text: string;
219
+ done: boolean;
220
+ }
221
+
222
+ let todos: Todo[] = [
223
+ { id: 1, text: 'Buy milk', done: false },
224
+ { id: 2, text: 'Write code', done: true },
225
+ ];
226
+
227
+ // Create optimized list with key-based reuse
228
+ const todoList = (
229
+ <KTFor
230
+ list={todos}
231
+ key={(item) => item.id} // Stable key for efficient updates
232
+ mapper={(item, index) => (
233
+ <div class={`todo ${item.done ? 'done' : ''}`}>
234
+ <input type="checkbox" checked={item.done} />
235
+ <span>{item.text}</span>
236
+ <button on:click={() => deleteTodo(item.id)}>Delete</button>
237
+ </div>
238
+ )}
239
+ />
240
+ ) as KTForAnchor;
241
+
242
+ // Add to DOM (anchor + all items are rendered)
243
+ document.body.appendChild(todoList);
244
+
245
+ // Update the list - only changed items are updated
246
+ todos = [...todos, { id: 3, text: 'New task', done: false }];
247
+ todoList.redraw({ list: todos });
248
+
249
+ // For simple static lists, use KTForStatic (no key optimization)
250
+ const simpleList = (
251
+ <KTForStatic list={['Apple', 'Banana', 'Orange']} mapper={(item) => <div>{item}</div>} />
252
+ ) as KTForAnchor;
253
+ ```
254
+
255
+ **How it works:**
256
+
257
+ - Returns a Comment node (`<!-- kt-for -->`) as anchor point
258
+ - All list items are rendered as siblings after the anchor
259
+ - Uses key-based diff algorithm to reuse DOM nodes
260
+ - Only adds/removes/moves nodes that changed
261
+ - Similar to Svelte's `{#each}` blocks
262
+
263
+ ````
264
+
203
265
  ### Array Children Support (v0.14.1+)
204
266
 
205
267
  Children can now be arrays for easier list rendering:
@@ -231,7 +293,7 @@ const TodoList = ({ todos }: { todos: string[] }) => (
231
293
  </ul>
232
294
  </div>
233
295
  );
234
- ```
296
+ ````
235
297
 
236
298
  ### Async Components
237
299
 
package/dist/index.d.ts CHANGED
@@ -98,7 +98,7 @@ interface KTBaseAttribute {
98
98
  title?: string;
99
99
  placeholder?: string;
100
100
  contenteditable?: boolean;
101
- value?: string;
101
+ value?: any;
102
102
  valueAsDate?: Date;
103
103
  valueAsNumber?: number;
104
104
  label?: string;
@@ -120,12 +120,30 @@ type KTPrefixedEventHandlers = {
120
120
  };
121
121
 
122
122
  type KTSpecialEventHandlers = {
123
+ /**
124
+ * Directly extract `value` from the input element
125
+ */
123
126
  'on:ktchange'?: (value: string) => void;
127
+ /**
128
+ * Directly extract `value` and trim it
129
+ */
124
130
  'ontrim:ktchange'?: (value: string) => void;
131
+ /**
132
+ * Directly extract `value` and parse it to number
133
+ */
125
134
  'on:ktchangenumber'?: (value: number) => void;
126
135
 
136
+ /**
137
+ * Directly extract `value` from the input element
138
+ */
127
139
  'on:ktinput'?: (value: string) => void;
140
+ /**
141
+ * Directly extract `value` and trim it
142
+ */
128
143
  'ontrim:ktinput'?: (value: string) => void;
144
+ /**
145
+ * Directly extract `value` and parse it to number
146
+ */
129
147
  'on:ktinputnumber'?: (value: number) => void;
130
148
  };
131
149
 
@@ -150,7 +168,7 @@ type HTML<T extends (HTMLTag | SVGTag) & otherstring> = T extends SVGTag ? SVGEl
150
168
  * ## About
151
169
  * @package @ktjs/core
152
170
  * @author Kasukabe Tsumugi <futami16237@gmail.com>
153
- * @version 0.15.5 (Last Update: 2026.01.25 18:25:48.373)
171
+ * @version 0.16.0 (Last Update: 2026.01.27 17:30:39.673)
154
172
  * @license MIT
155
173
  * @link https://github.com/baendlorel/kt.js
156
174
  * @link https://baendlorel.github.io/ Welcome to my site!
@@ -224,5 +242,63 @@ declare function KTAsync<T extends KTComponent>(props: {
224
242
  children?: KTRawContent;
225
243
  } & ExtractComponentProps<T>): KTHTMLElement;
226
244
 
227
- export { Fragment, KTAsync, h as createElement, createRedrawable, createRedrawableNoref, h, jsx, jsxDEV, jsxs, ref };
228
- export type { EventHandler, HTMLTag, KTAttribute, KTHTMLElement, KTRawAttr, KTRawContent, KTRawContents, KTRef };
245
+ interface KTForProps<T> {
246
+ list: T[];
247
+ /**
248
+ * Key function to uniquely identify each item
249
+ * Using stable keys enables efficient DOM reuse and updates
250
+ */
251
+ key: (item: T, index: number) => string | number;
252
+ /**
253
+ * Mapper function to render each item
254
+ */
255
+ mapper: (item: T, index: number) => HTMLElement;
256
+ }
257
+ /**
258
+ * Extended Comment node with redraw capability for KTFor
259
+ */
260
+ interface KTForAnchor extends Comment {
261
+ redraw: (newProps?: {
262
+ list?: any[];
263
+ mapper?: (item: any, index: number) => HTMLElement;
264
+ }) => void;
265
+ }
266
+ /**
267
+ * KTFor component - Efficient list rendering with key-based DOM reuse
268
+ * Similar to Svelte's {#each} blocks, provides optimized updates
269
+ *
270
+ * Returns a Comment node as anchor point. All list items are rendered after this anchor.
271
+ *
272
+ * @example
273
+ * ```tsx
274
+ * const listAnchor = <KTFor
275
+ * list={items}
276
+ * key={(item) => item.id}
277
+ * mapper={(item) => <div>{item.name}</div>}
278
+ * /> as KTForAnchor;
279
+ *
280
+ * document.body.appendChild(listAnchor); // Anchor + all items are rendered
281
+ *
282
+ * // Update the list
283
+ * listAnchor.redraw({ list: newItems });
284
+ * ```
285
+ */
286
+ declare function KTFor<T>(props: KTForProps<T>): KTForAnchor;
287
+ /**
288
+ * Simple list rendering without key optimization
289
+ * Rebuilds the entire list on each update - use for small static lists
290
+ *
291
+ * Returns a Comment anchor node. All items are rendered after this anchor.
292
+ *
293
+ * @example
294
+ * ```tsx
295
+ * const listAnchor = <KTForStatic
296
+ * list={items}
297
+ * mapper={(item) => <div>{item}</div>}
298
+ * /> as KTForAnchor;
299
+ * ```
300
+ */
301
+ declare function KTForStatic<T>(props: Omit<KTForProps<T>, 'key'>): KTForAnchor;
302
+
303
+ export { Fragment, KTAsync, KTFor, KTForStatic, h as createElement, createRedrawable, createRedrawableNoref, h, jsx, jsxDEV, jsxs, ref };
304
+ export type { EventHandler, HTMLTag, KTAttribute, KTForAnchor, KTForProps, KTHTMLElement, KTRawAttr, KTRawContent, KTRawContents, KTRef };
@@ -206,7 +206,7 @@ var __ktjs_core__ = (function (exports) {
206
206
  * ## About
207
207
  * @package @ktjs/core
208
208
  * @author Kasukabe Tsumugi <futami16237@gmail.com>
209
- * @version 0.15.5 (Last Update: 2026.01.25 18:25:48.373)
209
+ * @version 0.16.0 (Last Update: 2026.01.27 17:30:39.673)
210
210
  * @license MIT
211
211
  * @link https://github.com/baendlorel/kt.js
212
212
  * @link https://baendlorel.github.io/ Welcome to my site!
@@ -378,8 +378,206 @@ var __ktjs_core__ = (function (exports) {
378
378
  return comp;
379
379
  }
380
380
 
381
+ /**
382
+ * KTFor component - Efficient list rendering with key-based DOM reuse
383
+ * Similar to Svelte's {#each} blocks, provides optimized updates
384
+ *
385
+ * Returns a Comment node as anchor point. All list items are rendered after this anchor.
386
+ *
387
+ * @example
388
+ * ```tsx
389
+ * const listAnchor = <KTFor
390
+ * list={items}
391
+ * key={(item) => item.id}
392
+ * mapper={(item) => <div>{item.name}</div>}
393
+ * /> as KTForAnchor;
394
+ *
395
+ * document.body.appendChild(listAnchor); // Anchor + all items are rendered
396
+ *
397
+ * // Update the list
398
+ * listAnchor.redraw({ list: newItems });
399
+ * ```
400
+ */
401
+ function KTFor(props) {
402
+ const { list: initList, key: getKey, mapper } = props;
403
+ // Cache: key -> { node: HTMLElement, item: T }
404
+ const nodeCache = new Map();
405
+ // Current key order
406
+ let currentKeys = [];
407
+ // Create anchor comment node - marks the position of the list
408
+ const anchor = document.createComment('kt-for');
409
+ /**
410
+ * Get all nodes currently in the DOM that belong to this list
411
+ * They are all siblings after the anchor node
412
+ */
413
+ const getListNodes = () => {
414
+ const nodes = [];
415
+ if (!anchor.parentNode)
416
+ return nodes;
417
+ let current = anchor.nextSibling;
418
+ new Set(currentKeys);
419
+ // Collect nodes that belong to this list
420
+ while (current) {
421
+ const isListNode = Array.from(nodeCache.values()).some((cached) => cached.node === current);
422
+ if (!isListNode)
423
+ break;
424
+ nodes.push(current);
425
+ current = current.nextSibling;
426
+ }
427
+ return nodes;
428
+ };
429
+ /**
430
+ * Initialize list - append all initial items after anchor
431
+ */
432
+ const initialize = () => {
433
+ for (let i = 0; i < initList.length; i++) {
434
+ const item = initList[i];
435
+ const key = getKey(item, i);
436
+ const node = mapper(item, i);
437
+ nodeCache.set(key, { node, item });
438
+ currentKeys.push(key);
439
+ // Append to parent if anchor is already in DOM
440
+ if (anchor.parentNode) {
441
+ const lastNode = getListNodes()[getListNodes().length - 1];
442
+ if (lastNode) {
443
+ lastNode.after(node);
444
+ }
445
+ else {
446
+ anchor.after(node);
447
+ }
448
+ }
449
+ }
450
+ };
451
+ /**
452
+ * Smart update method - only modifies DOM nodes that changed
453
+ * Uses key-based diff algorithm to minimize DOM operations
454
+ */
455
+ const smartUpdate = (newList, newMapper) => {
456
+ const mapperFn = newMapper ?? mapper;
457
+ const newKeys = newList.map((item, index) => getKey(item, index));
458
+ if (!anchor.parentNode) {
459
+ // If anchor is not in DOM yet, just update cache
460
+ nodeCache.clear();
461
+ currentKeys = [];
462
+ for (let i = 0; i < newList.length; i++) {
463
+ const item = newList[i];
464
+ const key = newKeys[i];
465
+ const node = mapperFn(item, i);
466
+ nodeCache.set(key, { node, item });
467
+ currentKeys.push(key);
468
+ }
469
+ return;
470
+ }
471
+ // Find keys to remove (exist in current but not in new)
472
+ const keysToRemove = currentKeys.filter((k) => !newKeys.includes(k));
473
+ for (let i = 0; i < keysToRemove.length; i++) {
474
+ const key = keysToRemove[i];
475
+ const cached = nodeCache.get(key);
476
+ if (cached && cached.node.parentNode) {
477
+ cached.node.parentNode.removeChild(cached.node);
478
+ }
479
+ nodeCache.delete(key);
480
+ }
481
+ // Update/add nodes in correct order
482
+ let previousNode = anchor;
483
+ for (let i = 0; i < newList.length; i++) {
484
+ const item = newList[i];
485
+ const key = newKeys[i];
486
+ let cached = nodeCache.get(key);
487
+ // New item: create and cache
488
+ if (!cached) {
489
+ const node = mapperFn(item, i);
490
+ cached = { node, item };
491
+ nodeCache.set(key, cached);
492
+ }
493
+ else {
494
+ // Update cached item reference
495
+ cached.item = item;
496
+ }
497
+ // Ensure node is at correct position (right after previousNode)
498
+ if (previousNode.nextSibling !== cached.node) {
499
+ if (previousNode.nextSibling) {
500
+ anchor.parentNode.insertBefore(cached.node, previousNode.nextSibling);
501
+ }
502
+ else {
503
+ anchor.parentNode.appendChild(cached.node);
504
+ }
505
+ }
506
+ previousNode = cached.node;
507
+ }
508
+ currentKeys = newKeys;
509
+ };
510
+ // Mount redraw method on anchor
511
+ anchor.redraw = (newProps) => {
512
+ if (newProps?.list) {
513
+ smartUpdate(newProps.list, newProps.mapper);
514
+ }
515
+ };
516
+ // Initialize the list
517
+ initialize();
518
+ return anchor;
519
+ }
520
+ /**
521
+ * Simple list rendering without key optimization
522
+ * Rebuilds the entire list on each update - use for small static lists
523
+ *
524
+ * Returns a Comment anchor node. All items are rendered after this anchor.
525
+ *
526
+ * @example
527
+ * ```tsx
528
+ * const listAnchor = <KTForStatic
529
+ * list={items}
530
+ * mapper={(item) => <div>{item}</div>}
531
+ * /> as KTForAnchor;
532
+ * ```
533
+ */
534
+ function KTForStatic(props) {
535
+ const { list: initList, mapper } = props;
536
+ // Create anchor comment node
537
+ const anchor = document.createComment('kt-for-static');
538
+ let nodes = [];
539
+ // Simple rebuild on redraw
540
+ const rebuild = (newList) => {
541
+ // Remove all old nodes
542
+ for (let i = 0; i < nodes.length; i++) {
543
+ const node = nodes[i];
544
+ if (node.parentNode) {
545
+ node.parentNode.removeChild(node);
546
+ }
547
+ }
548
+ nodes = [];
549
+ // Create new nodes
550
+ let previousNode = anchor;
551
+ for (let i = 0; i < newList.length; i++) {
552
+ const item = newList[i];
553
+ const node = mapper(item, i);
554
+ nodes.push(node);
555
+ if (anchor.parentNode) {
556
+ if (previousNode.nextSibling) {
557
+ anchor.parentNode.insertBefore(node, previousNode.nextSibling);
558
+ }
559
+ else {
560
+ anchor.parentNode.appendChild(node);
561
+ }
562
+ previousNode = node;
563
+ }
564
+ }
565
+ };
566
+ // Initial render
567
+ rebuild(initList);
568
+ // Mount redraw
569
+ anchor.redraw = (newProps) => {
570
+ if (newProps?.list) {
571
+ rebuild(newProps.list);
572
+ }
573
+ };
574
+ return anchor;
575
+ }
576
+
381
577
  exports.Fragment = Fragment;
382
578
  exports.KTAsync = KTAsync;
579
+ exports.KTFor = KTFor;
580
+ exports.KTForStatic = KTForStatic;
383
581
  exports.createElement = h;
384
582
  exports.createRedrawable = createRedrawable;
385
583
  exports.createRedrawableNoref = createRedrawableNoref;
@@ -231,7 +231,7 @@ var __ktjs_core__ = (function (exports) {
231
231
  * ## About
232
232
  * @package @ktjs/core
233
233
  * @author Kasukabe Tsumugi <futami16237@gmail.com>
234
- * @version 0.15.5 (Last Update: 2026.01.25 18:25:48.373)
234
+ * @version 0.16.0 (Last Update: 2026.01.27 17:30:39.673)
235
235
  * @license MIT
236
236
  * @link https://github.com/baendlorel/kt.js
237
237
  * @link https://baendlorel.github.io/ Welcome to my site!
@@ -443,8 +443,206 @@ var __ktjs_core__ = (function (exports) {
443
443
  return comp;
444
444
  }
445
445
 
446
+ /**
447
+ * KTFor component - Efficient list rendering with key-based DOM reuse
448
+ * Similar to Svelte's {#each} blocks, provides optimized updates
449
+ *
450
+ * Returns a Comment node as anchor point. All list items are rendered after this anchor.
451
+ *
452
+ * @example
453
+ * ```tsx
454
+ * const listAnchor = <KTFor
455
+ * list={items}
456
+ * key={(item) => item.id}
457
+ * mapper={(item) => <div>{item.name}</div>}
458
+ * /> as KTForAnchor;
459
+ *
460
+ * document.body.appendChild(listAnchor); // Anchor + all items are rendered
461
+ *
462
+ * // Update the list
463
+ * listAnchor.redraw({ list: newItems });
464
+ * ```
465
+ */
466
+ function KTFor(props) {
467
+ var initList = props.list, getKey = props.key, mapper = props.mapper;
468
+ // Cache: key -> { node: HTMLElement, item: T }
469
+ var nodeCache = new Map();
470
+ // Current key order
471
+ var currentKeys = [];
472
+ // Create anchor comment node - marks the position of the list
473
+ var anchor = document.createComment('kt-for');
474
+ /**
475
+ * Get all nodes currently in the DOM that belong to this list
476
+ * They are all siblings after the anchor node
477
+ */
478
+ var getListNodes = function () {
479
+ var nodes = [];
480
+ if (!anchor.parentNode)
481
+ return nodes;
482
+ var current = anchor.nextSibling;
483
+ new Set(currentKeys);
484
+ // Collect nodes that belong to this list
485
+ while (current) {
486
+ var isListNode = Array.from(nodeCache.values()).some(function (cached) { return cached.node === current; });
487
+ if (!isListNode)
488
+ break;
489
+ nodes.push(current);
490
+ current = current.nextSibling;
491
+ }
492
+ return nodes;
493
+ };
494
+ /**
495
+ * Initialize list - append all initial items after anchor
496
+ */
497
+ var initialize = function () {
498
+ for (var i = 0; i < initList.length; i++) {
499
+ var item = initList[i];
500
+ var key = getKey(item, i);
501
+ var node = mapper(item, i);
502
+ nodeCache.set(key, { node: node, item: item });
503
+ currentKeys.push(key);
504
+ // Append to parent if anchor is already in DOM
505
+ if (anchor.parentNode) {
506
+ var lastNode = getListNodes()[getListNodes().length - 1];
507
+ if (lastNode) {
508
+ lastNode.after(node);
509
+ }
510
+ else {
511
+ anchor.after(node);
512
+ }
513
+ }
514
+ }
515
+ };
516
+ /**
517
+ * Smart update method - only modifies DOM nodes that changed
518
+ * Uses key-based diff algorithm to minimize DOM operations
519
+ */
520
+ var smartUpdate = function (newList, newMapper) {
521
+ var mapperFn = newMapper !== null && newMapper !== void 0 ? newMapper : mapper;
522
+ var newKeys = newList.map(function (item, index) { return getKey(item, index); });
523
+ if (!anchor.parentNode) {
524
+ // If anchor is not in DOM yet, just update cache
525
+ nodeCache.clear();
526
+ currentKeys = [];
527
+ for (var i = 0; i < newList.length; i++) {
528
+ var item = newList[i];
529
+ var key = newKeys[i];
530
+ var node = mapperFn(item, i);
531
+ nodeCache.set(key, { node: node, item: item });
532
+ currentKeys.push(key);
533
+ }
534
+ return;
535
+ }
536
+ // Find keys to remove (exist in current but not in new)
537
+ var keysToRemove = currentKeys.filter(function (k) { return !newKeys.includes(k); });
538
+ for (var i = 0; i < keysToRemove.length; i++) {
539
+ var key = keysToRemove[i];
540
+ var cached = nodeCache.get(key);
541
+ if (cached && cached.node.parentNode) {
542
+ cached.node.parentNode.removeChild(cached.node);
543
+ }
544
+ nodeCache.delete(key);
545
+ }
546
+ // Update/add nodes in correct order
547
+ var previousNode = anchor;
548
+ for (var i = 0; i < newList.length; i++) {
549
+ var item = newList[i];
550
+ var key = newKeys[i];
551
+ var cached = nodeCache.get(key);
552
+ // New item: create and cache
553
+ if (!cached) {
554
+ var node = mapperFn(item, i);
555
+ cached = { node: node, item: item };
556
+ nodeCache.set(key, cached);
557
+ }
558
+ else {
559
+ // Update cached item reference
560
+ cached.item = item;
561
+ }
562
+ // Ensure node is at correct position (right after previousNode)
563
+ if (previousNode.nextSibling !== cached.node) {
564
+ if (previousNode.nextSibling) {
565
+ anchor.parentNode.insertBefore(cached.node, previousNode.nextSibling);
566
+ }
567
+ else {
568
+ anchor.parentNode.appendChild(cached.node);
569
+ }
570
+ }
571
+ previousNode = cached.node;
572
+ }
573
+ currentKeys = newKeys;
574
+ };
575
+ // Mount redraw method on anchor
576
+ anchor.redraw = function (newProps) {
577
+ if (newProps === null || newProps === void 0 ? void 0 : newProps.list) {
578
+ smartUpdate(newProps.list, newProps.mapper);
579
+ }
580
+ };
581
+ // Initialize the list
582
+ initialize();
583
+ return anchor;
584
+ }
585
+ /**
586
+ * Simple list rendering without key optimization
587
+ * Rebuilds the entire list on each update - use for small static lists
588
+ *
589
+ * Returns a Comment anchor node. All items are rendered after this anchor.
590
+ *
591
+ * @example
592
+ * ```tsx
593
+ * const listAnchor = <KTForStatic
594
+ * list={items}
595
+ * mapper={(item) => <div>{item}</div>}
596
+ * /> as KTForAnchor;
597
+ * ```
598
+ */
599
+ function KTForStatic(props) {
600
+ var initList = props.list, mapper = props.mapper;
601
+ // Create anchor comment node
602
+ var anchor = document.createComment('kt-for-static');
603
+ var nodes = [];
604
+ // Simple rebuild on redraw
605
+ var rebuild = function (newList) {
606
+ // Remove all old nodes
607
+ for (var i = 0; i < nodes.length; i++) {
608
+ var node = nodes[i];
609
+ if (node.parentNode) {
610
+ node.parentNode.removeChild(node);
611
+ }
612
+ }
613
+ nodes = [];
614
+ // Create new nodes
615
+ var previousNode = anchor;
616
+ for (var i = 0; i < newList.length; i++) {
617
+ var item = newList[i];
618
+ var node = mapper(item, i);
619
+ nodes.push(node);
620
+ if (anchor.parentNode) {
621
+ if (previousNode.nextSibling) {
622
+ anchor.parentNode.insertBefore(node, previousNode.nextSibling);
623
+ }
624
+ else {
625
+ anchor.parentNode.appendChild(node);
626
+ }
627
+ previousNode = node;
628
+ }
629
+ }
630
+ };
631
+ // Initial render
632
+ rebuild(initList);
633
+ // Mount redraw
634
+ anchor.redraw = function (newProps) {
635
+ if (newProps === null || newProps === void 0 ? void 0 : newProps.list) {
636
+ rebuild(newProps.list);
637
+ }
638
+ };
639
+ return anchor;
640
+ }
641
+
446
642
  exports.Fragment = Fragment;
447
643
  exports.KTAsync = KTAsync;
644
+ exports.KTFor = KTFor;
645
+ exports.KTForStatic = KTForStatic;
448
646
  exports.createElement = h;
449
647
  exports.createRedrawable = createRedrawable;
450
648
  exports.createRedrawableNoref = createRedrawableNoref;
package/dist/index.mjs CHANGED
@@ -203,7 +203,7 @@ let creator = defaultCreator;
203
203
  * ## About
204
204
  * @package @ktjs/core
205
205
  * @author Kasukabe Tsumugi <futami16237@gmail.com>
206
- * @version 0.15.5 (Last Update: 2026.01.25 18:25:48.373)
206
+ * @version 0.16.0 (Last Update: 2026.01.27 17:30:39.673)
207
207
  * @license MIT
208
208
  * @link https://github.com/baendlorel/kt.js
209
209
  * @link https://baendlorel.github.io/ Welcome to my site!
@@ -375,4 +375,200 @@ function KTAsync(props) {
375
375
  return comp;
376
376
  }
377
377
 
378
- export { Fragment, KTAsync, h as createElement, createRedrawable, createRedrawableNoref, h, jsx, jsxDEV, jsxs, ref };
378
+ /**
379
+ * KTFor component - Efficient list rendering with key-based DOM reuse
380
+ * Similar to Svelte's {#each} blocks, provides optimized updates
381
+ *
382
+ * Returns a Comment node as anchor point. All list items are rendered after this anchor.
383
+ *
384
+ * @example
385
+ * ```tsx
386
+ * const listAnchor = <KTFor
387
+ * list={items}
388
+ * key={(item) => item.id}
389
+ * mapper={(item) => <div>{item.name}</div>}
390
+ * /> as KTForAnchor;
391
+ *
392
+ * document.body.appendChild(listAnchor); // Anchor + all items are rendered
393
+ *
394
+ * // Update the list
395
+ * listAnchor.redraw({ list: newItems });
396
+ * ```
397
+ */
398
+ function KTFor(props) {
399
+ const { list: initList, key: getKey, mapper } = props;
400
+ // Cache: key -> { node: HTMLElement, item: T }
401
+ const nodeCache = new Map();
402
+ // Current key order
403
+ let currentKeys = [];
404
+ // Create anchor comment node - marks the position of the list
405
+ const anchor = document.createComment('kt-for');
406
+ /**
407
+ * Get all nodes currently in the DOM that belong to this list
408
+ * They are all siblings after the anchor node
409
+ */
410
+ const getListNodes = () => {
411
+ const nodes = [];
412
+ if (!anchor.parentNode)
413
+ return nodes;
414
+ let current = anchor.nextSibling;
415
+ new Set(currentKeys);
416
+ // Collect nodes that belong to this list
417
+ while (current) {
418
+ const isListNode = Array.from(nodeCache.values()).some((cached) => cached.node === current);
419
+ if (!isListNode)
420
+ break;
421
+ nodes.push(current);
422
+ current = current.nextSibling;
423
+ }
424
+ return nodes;
425
+ };
426
+ /**
427
+ * Initialize list - append all initial items after anchor
428
+ */
429
+ const initialize = () => {
430
+ for (let i = 0; i < initList.length; i++) {
431
+ const item = initList[i];
432
+ const key = getKey(item, i);
433
+ const node = mapper(item, i);
434
+ nodeCache.set(key, { node, item });
435
+ currentKeys.push(key);
436
+ // Append to parent if anchor is already in DOM
437
+ if (anchor.parentNode) {
438
+ const lastNode = getListNodes()[getListNodes().length - 1];
439
+ if (lastNode) {
440
+ lastNode.after(node);
441
+ }
442
+ else {
443
+ anchor.after(node);
444
+ }
445
+ }
446
+ }
447
+ };
448
+ /**
449
+ * Smart update method - only modifies DOM nodes that changed
450
+ * Uses key-based diff algorithm to minimize DOM operations
451
+ */
452
+ const smartUpdate = (newList, newMapper) => {
453
+ const mapperFn = newMapper ?? mapper;
454
+ const newKeys = newList.map((item, index) => getKey(item, index));
455
+ if (!anchor.parentNode) {
456
+ // If anchor is not in DOM yet, just update cache
457
+ nodeCache.clear();
458
+ currentKeys = [];
459
+ for (let i = 0; i < newList.length; i++) {
460
+ const item = newList[i];
461
+ const key = newKeys[i];
462
+ const node = mapperFn(item, i);
463
+ nodeCache.set(key, { node, item });
464
+ currentKeys.push(key);
465
+ }
466
+ return;
467
+ }
468
+ // Find keys to remove (exist in current but not in new)
469
+ const keysToRemove = currentKeys.filter((k) => !newKeys.includes(k));
470
+ for (let i = 0; i < keysToRemove.length; i++) {
471
+ const key = keysToRemove[i];
472
+ const cached = nodeCache.get(key);
473
+ if (cached && cached.node.parentNode) {
474
+ cached.node.parentNode.removeChild(cached.node);
475
+ }
476
+ nodeCache.delete(key);
477
+ }
478
+ // Update/add nodes in correct order
479
+ let previousNode = anchor;
480
+ for (let i = 0; i < newList.length; i++) {
481
+ const item = newList[i];
482
+ const key = newKeys[i];
483
+ let cached = nodeCache.get(key);
484
+ // New item: create and cache
485
+ if (!cached) {
486
+ const node = mapperFn(item, i);
487
+ cached = { node, item };
488
+ nodeCache.set(key, cached);
489
+ }
490
+ else {
491
+ // Update cached item reference
492
+ cached.item = item;
493
+ }
494
+ // Ensure node is at correct position (right after previousNode)
495
+ if (previousNode.nextSibling !== cached.node) {
496
+ if (previousNode.nextSibling) {
497
+ anchor.parentNode.insertBefore(cached.node, previousNode.nextSibling);
498
+ }
499
+ else {
500
+ anchor.parentNode.appendChild(cached.node);
501
+ }
502
+ }
503
+ previousNode = cached.node;
504
+ }
505
+ currentKeys = newKeys;
506
+ };
507
+ // Mount redraw method on anchor
508
+ anchor.redraw = (newProps) => {
509
+ if (newProps?.list) {
510
+ smartUpdate(newProps.list, newProps.mapper);
511
+ }
512
+ };
513
+ // Initialize the list
514
+ initialize();
515
+ return anchor;
516
+ }
517
+ /**
518
+ * Simple list rendering without key optimization
519
+ * Rebuilds the entire list on each update - use for small static lists
520
+ *
521
+ * Returns a Comment anchor node. All items are rendered after this anchor.
522
+ *
523
+ * @example
524
+ * ```tsx
525
+ * const listAnchor = <KTForStatic
526
+ * list={items}
527
+ * mapper={(item) => <div>{item}</div>}
528
+ * /> as KTForAnchor;
529
+ * ```
530
+ */
531
+ function KTForStatic(props) {
532
+ const { list: initList, mapper } = props;
533
+ // Create anchor comment node
534
+ const anchor = document.createComment('kt-for-static');
535
+ let nodes = [];
536
+ // Simple rebuild on redraw
537
+ const rebuild = (newList) => {
538
+ // Remove all old nodes
539
+ for (let i = 0; i < nodes.length; i++) {
540
+ const node = nodes[i];
541
+ if (node.parentNode) {
542
+ node.parentNode.removeChild(node);
543
+ }
544
+ }
545
+ nodes = [];
546
+ // Create new nodes
547
+ let previousNode = anchor;
548
+ for (let i = 0; i < newList.length; i++) {
549
+ const item = newList[i];
550
+ const node = mapper(item, i);
551
+ nodes.push(node);
552
+ if (anchor.parentNode) {
553
+ if (previousNode.nextSibling) {
554
+ anchor.parentNode.insertBefore(node, previousNode.nextSibling);
555
+ }
556
+ else {
557
+ anchor.parentNode.appendChild(node);
558
+ }
559
+ previousNode = node;
560
+ }
561
+ }
562
+ };
563
+ // Initial render
564
+ rebuild(initList);
565
+ // Mount redraw
566
+ anchor.redraw = (newProps) => {
567
+ if (newProps?.list) {
568
+ rebuild(newProps.list);
569
+ }
570
+ };
571
+ return anchor;
572
+ }
573
+
574
+ export { Fragment, KTAsync, KTFor, KTForStatic, h as createElement, createRedrawable, createRedrawableNoref, h, jsx, jsxDEV, jsxs, ref };
@@ -92,7 +92,7 @@ interface KTBaseAttribute {
92
92
  title?: string;
93
93
  placeholder?: string;
94
94
  contenteditable?: boolean;
95
- value?: string;
95
+ value?: any;
96
96
  valueAsDate?: Date;
97
97
  valueAsNumber?: number;
98
98
  label?: string;
@@ -114,12 +114,30 @@ type KTPrefixedEventHandlers = {
114
114
  };
115
115
 
116
116
  type KTSpecialEventHandlers = {
117
+ /**
118
+ * Directly extract `value` from the input element
119
+ */
117
120
  'on:ktchange'?: (value: string) => void;
121
+ /**
122
+ * Directly extract `value` and trim it
123
+ */
118
124
  'ontrim:ktchange'?: (value: string) => void;
125
+ /**
126
+ * Directly extract `value` and parse it to number
127
+ */
119
128
  'on:ktchangenumber'?: (value: number) => void;
120
129
 
130
+ /**
131
+ * Directly extract `value` from the input element
132
+ */
121
133
  'on:ktinput'?: (value: string) => void;
134
+ /**
135
+ * Directly extract `value` and trim it
136
+ */
122
137
  'ontrim:ktinput'?: (value: string) => void;
138
+ /**
139
+ * Directly extract `value` and parse it to number
140
+ */
123
141
  'on:ktinputnumber'?: (value: number) => void;
124
142
  };
125
143
 
@@ -136,7 +154,7 @@ type HTML<T extends (HTMLTag | SVGTag) & otherstring> = T extends SVGTag ? SVGEl
136
154
  * ## About
137
155
  * @package @ktjs/core
138
156
  * @author Kasukabe Tsumugi <futami16237@gmail.com>
139
- * @version 0.15.5 (Last Update: 2026.01.25 18:25:48.373)
157
+ * @version 0.16.0 (Last Update: 2026.01.27 17:30:39.673)
140
158
  * @license MIT
141
159
  * @link https://github.com/baendlorel/kt.js
142
160
  * @link https://baendlorel.github.io/ Welcome to my site!
@@ -203,7 +203,7 @@ let creator = defaultCreator;
203
203
  * ## About
204
204
  * @package @ktjs/core
205
205
  * @author Kasukabe Tsumugi <futami16237@gmail.com>
206
- * @version 0.15.5 (Last Update: 2026.01.25 18:25:48.373)
206
+ * @version 0.16.0 (Last Update: 2026.01.27 17:30:39.673)
207
207
  * @license MIT
208
208
  * @link https://github.com/baendlorel/kt.js
209
209
  * @link https://baendlorel.github.io/ Welcome to my site!
@@ -86,7 +86,7 @@ interface KTBaseAttribute {
86
86
  title?: string;
87
87
  placeholder?: string;
88
88
  contenteditable?: boolean;
89
- value?: string;
89
+ value?: any;
90
90
  valueAsDate?: Date;
91
91
  valueAsNumber?: number;
92
92
  label?: string;
@@ -108,12 +108,30 @@ type KTPrefixedEventHandlers = {
108
108
  };
109
109
 
110
110
  type KTSpecialEventHandlers = {
111
+ /**
112
+ * Directly extract `value` from the input element
113
+ */
111
114
  'on:ktchange'?: (value: string) => void;
115
+ /**
116
+ * Directly extract `value` and trim it
117
+ */
112
118
  'ontrim:ktchange'?: (value: string) => void;
119
+ /**
120
+ * Directly extract `value` and parse it to number
121
+ */
113
122
  'on:ktchangenumber'?: (value: number) => void;
114
123
 
124
+ /**
125
+ * Directly extract `value` from the input element
126
+ */
115
127
  'on:ktinput'?: (value: string) => void;
128
+ /**
129
+ * Directly extract `value` and trim it
130
+ */
116
131
  'ontrim:ktinput'?: (value: string) => void;
132
+ /**
133
+ * Directly extract `value` and parse it to number
134
+ */
117
135
  'on:ktinputnumber'?: (value: number) => void;
118
136
  };
119
137
 
@@ -130,7 +148,7 @@ type HTML<T extends (HTMLTag | SVGTag) & otherstring> = T extends SVGTag ? SVGEl
130
148
  * ## About
131
149
  * @package @ktjs/core
132
150
  * @author Kasukabe Tsumugi <futami16237@gmail.com>
133
- * @version 0.15.5 (Last Update: 2026.01.25 18:25:48.373)
151
+ * @version 0.16.0 (Last Update: 2026.01.27 17:30:39.673)
134
152
  * @license MIT
135
153
  * @link https://github.com/baendlorel/kt.js
136
154
  * @link https://baendlorel.github.io/ Welcome to my site!
@@ -203,7 +203,7 @@ let creator = defaultCreator;
203
203
  * ## About
204
204
  * @package @ktjs/core
205
205
  * @author Kasukabe Tsumugi <futami16237@gmail.com>
206
- * @version 0.15.5 (Last Update: 2026.01.25 18:25:48.373)
206
+ * @version 0.16.0 (Last Update: 2026.01.27 17:30:39.673)
207
207
  * @license MIT
208
208
  * @link https://github.com/baendlorel/kt.js
209
209
  * @link https://baendlorel.github.io/ Welcome to my site!
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ktjs/core",
3
- "version": "0.15.5",
3
+ "version": "0.16.0",
4
4
  "description": "Core functionality for kt.js - DOM manipulation utilities with JSX/TSX support",
5
5
  "type": "module",
6
6
  "module": "./dist/index.mjs",