kt.js 0.20.3 → 0.22.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.
Files changed (3) hide show
  1. package/README.md +117 -330
  2. package/dist/jsx-runtime.mjs +134 -89
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -2,11 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/kt.js.svg)](https://www.npmjs.com/package/kt.js) [![license](https://img.shields.io/github/license/baendlorel/kt.js.svg)](https://github.com/baendlorel/kt.js/blob/main/LICENSE)
4
4
 
5
- [CHANGLOG✨](CHANGELOG.md)
6
-
7
- ## What's New (v0.19.x)
8
-
9
- - Release `0.19.0` (2026-01-31): build & packaging fixes, TypeScript and lint cleanups, MUI-focused fixes (JSX handling, `jsxImportSource` set to `@ktjs/core`, radio/checkbox/TextField fixes), and repo cleanup (removed alias detection, moved shared utilities into `shared`). See the full details in the CHANGELOG.
5
+ [CHANGLOG✨](../../CHANGELOG.md)
10
6
 
11
7
  > Note: This framework is still under development. APIs, type declarations, and other parts **may change frequently**. If you use it, please watch for updates in the near future. Feel free to mail me if you have any questions!
12
8
 
@@ -21,6 +17,7 @@ KT.js is now a **monorepo** containing multiple packages:
21
17
  - **[kt.js](./packages/kt.js)**: Main entry package that re-exports all functionality
22
18
  - **[@ktjs/core](./packages/core)**: Core DOM manipulation utilities and the `h` function. SX/TSX support with full TypeScript integration (included in kt.js package)
23
19
  - **[@ktjs/router](./packages/router)**: Client-side routing with navigation guards (not included in kt.js package)
20
+ - **[@ktjs/mui](./packages/mui)**: Material UI components built on top of KT.js (not included in kt.js package)
24
21
 
25
22
  You can install the full package or individual packages as needed:
26
23
 
@@ -28,10 +25,8 @@ You can install the full package or individual packages as needed:
28
25
  pnpm add kt.js
29
26
 
30
27
  # Or install individual packages
31
- pnpm add @ktjs/core # Core DOM utilities (independent)
32
28
  pnpm add @ktjs/router # Client-side router (requires @ktjs/core)
33
29
  pnpm add @ktjs/mui # Material UI components (requires @ktjs/core)
34
- pnpm add @ktjs/shortcuts # Shortcuts (requires @ktjs/core)
35
30
  ```
36
31
 
37
32
  ## Philosophy
@@ -40,78 +35,9 @@ As a web framework, repeatedly creating a large number of variables and objects
40
35
 
41
36
  KT.js follows one rule: **full control of DOM and avoid unnecessary repainting**.
42
37
 
43
- ## Key Features
44
-
45
- - **Monorepo Architecture**: Modular packages that can be installed independently or together
46
- - **Tiny Bundle Size**: Minimal runtime overhead with aggressive tree-shaking
47
- - **`h` function**: Create DOM elements with a simple, flexible API
48
- - Shortcut functions for all HTML elements (`div`, `span`, `button`, etc.)
49
- - Event handlers with `on:<eventName>` syntax or function attributes
50
- - Full TypeScript support with intelligent type inference
51
- - **JSX/TSX Support**: Full JSX syntax support with TypeScript integration
52
- - Zero virtual DOM - JSX compiles directly to `h()` function calls
53
- - Full HTML element type inference (`<button>` returns `HTMLButtonElement`)
54
- - Support for `on:click` event handler syntax
55
- - `redraw()` method for controlled component updates (v0.11+)
56
- - `k-if` directive for conditional rendering (v0.14.6+)
57
- - Array children support for seamless `.map()` integration (v0.14.1+)
58
- - **List Rendering**: Efficient list rendering with `KTFor` component (v0.16.0+)
59
- - Comment anchor with `__kt_for_list__` array property
60
- - Key-based DOM reuse for minimal updates
61
- - Auto-appends list items when anchor added to parent
62
- - **Async Components**: Built-in support for Promise-based components
63
- - `KTAsync` component for handling async operations
64
- - Automatic placeholder management during loading
65
- - Seamless integration with JSX/TSX syntax
66
- - **Client-Side Router** (separate package):
67
- - Hash-based routing only (simplified from v0.14.7+)
68
- - Async navigation guards with Promise support
69
- - Dynamic route parameters and query string parsing
70
- - RouterView component for declarative routing
71
- - Pure routing logic - no rendering, no dependencies
72
- - **Shortcuts & Utilities**:
73
- - `withDefaults`: Wrap element creation functions with default properties
74
- - Convenient shorthand functions for common operations
75
- - Form helpers and layout utilities
76
- - **Full ES5 Compatibility**: Works in IE9+ and all modern browsers
77
- - Transpiled to ES5 with no modern syntax
78
- - Optional minimal Promise polyfill for older environments
79
- - **Shared Runtime**: Efficient code sharing across packages with zero overhead
80
-
81
- ## Getting started
82
-
83
- Install via package managers:
84
-
85
- ```bash
86
- npm install kt.js
87
- # or
88
- pnpm add kt.js
89
- ```
90
-
91
- ```ts
92
- import { h, div } from 'kt.js';
93
-
94
- const container = div('container', [div('header'), div('body', 'something'), div('footer')]);
95
- const app = h('section', { id: 'app' }, container);
96
- ```
97
-
98
- This will create the following DOM structure:
99
-
100
- ```html
101
- <section id="app">
102
- <div class="container">
103
- <div class="header"></div>
104
- <div class="body">something</div>
105
- <div class="footer"></div>
106
- </div>
107
- </section>
108
- ```
109
-
110
- ### Using JSX/TSX
111
-
112
- KT.js now has full JSX support! With the `@ktjs/jsx` package (included in the main `kt.js` package), you can write components using familiar JSX syntax:
38
+ ## Configuration
113
39
 
114
- **TypeScript Configuration** (`tsconfig.json`):
40
+ for TSX/JSX support, add this to your `tsconfig.json`:
115
41
 
116
42
  ```json
117
43
  {
@@ -122,332 +48,193 @@ KT.js now has full JSX support! With the `@ktjs/jsx` package (included in the ma
122
48
  }
123
49
  ```
124
50
 
125
- **Basic JSX Example**:
126
-
127
- ```tsx
128
- import { jsx } from 'kt.js';
129
-
130
- function Counter() {
131
- const count = 0;
51
+ ## Core Features (@ktjs/core)
132
52
 
133
- return (
134
- <div class="counter">
135
- <h1>Counter: {count}</h1>
136
- <button on:click={() => console.log('Clicked!')}>Increment</button>
137
- </div>
138
- );
139
- }
140
-
141
- // JSX compiles to direct h() function calls - no virtual DOM!
142
- const counterElement = <Counter />;
143
- ```
53
+ ### JSX/TSX Support
144
54
 
145
- **Event Handling with @ Syntax**:
55
+ KT.js provides first-class JSX/TSX support with zero virtual DOM overhead. JSX compiles directly to `h()` function calls.
146
56
 
147
57
  ```tsx
148
- function App() {
149
- const handleClick = () => alert('Button clicked!');
58
+ // Function components
59
+ const Button = ({ onClick, children }) => (
60
+ <button on:click={onClick} class="btn">
61
+ {children}
62
+ </button>
63
+ );
150
64
 
151
- return (
152
- <div>
153
- <button on:click={handleClick}>Click me</button>
65
+ // Conditional rendering with k-if
66
+ const UserCard = ({ user, showDetails }) => (
67
+ <div class="card">
68
+ <h3>{user.name}</h3>
69
+ <div k-if={showDetails}>
70
+ <p>Email: {user.email}</p>
71
+ <p>Role: {user.role}</p>
154
72
  </div>
155
- );
156
- }
157
- ```
158
-
159
- **Type Safety**:
160
-
161
- ```tsx
162
- // TypeScript knows this is an HTMLButtonElement
163
- const button: HTMLButtonElement = <button>Click</button>;
73
+ </div>
74
+ );
164
75
 
165
- // TypeScript knows this is an HTMLInputElement
166
- const input: HTMLInputElement = <input type="text" value="hello" />;
76
+ // List rendering with KTFor
77
+ const UserList = ({ users }) => <KTFor list={users} key={(user) => user.id} map={(user) => <UserCard user={user} />} />;
167
78
 
168
- // TypeScript provides autocomplete for HTML attributes
169
- const div: HTMLDivElement = <div className="container" id="main" />;
79
+ // Async components
80
+ const AsyncContent = async () => {
81
+ const data = await fetchData();
82
+ return <div>{data}</div>;
83
+ };
170
84
  ```
171
85
 
172
- **Important Notes**:
86
+ ### Directives
173
87
 
174
- - KT.js JSX has **no Fragment support** - we don't have a Fragment concept
175
- - JSX compiles directly to `h()` function calls - **zero virtual DOM overhead**
176
- - Use `on:click` syntax for event handlers to avoid conflicts with existing attributes
177
- - All JSX elements have proper HTML element type inference in TypeScript
178
- - Use `k-if` attribute for conditional rendering (v0.14.6+)
179
- - Children can be arrays for easy `.map()` integration (v0.14.1+)
180
-
181
- **Conditional Rendering with k-if** (v0.14.6+):
88
+ #### `k-if`: Conditional rendering.
182
89
 
183
90
  ```tsx
184
- import { jsx } from 'kt.js';
185
-
186
- function UserProfile({ user, isLoggedIn }: { user: any; isLoggedIn: boolean }) {
187
- return (
188
- <div>
189
- <h1>Profile</h1>
190
- {/* Element only created if condition is true */}
191
- <div k-if={isLoggedIn}>
192
- <p>Welcome, {user.name}!</p>
193
- <button>Logout</button>
194
- </div>
195
- {/* Element only created if condition is true */}
196
- <div k-if={!isLoggedIn}>
197
- <p>Please log in</p>
198
- <button>Login</button>
199
- </div>
200
- </div>
201
- );
202
- }
91
+ <div k-if={condition}>This will render only if condition is true.</div>
203
92
  ```
204
93
 
205
- **Array Children Support** (v0.14.1+):
206
-
207
- ```tsx
208
- import { jsx } from 'kt.js';
209
-
210
- function TodoList({ todos }: { todos: string[] }) {
211
- return (
212
- <div>
213
- <h2>Todo List</h2>
214
- <ul>
215
- {/* Map arrays directly as children */}
216
- {todos.map((todo) => (
217
- <li>{todo}</li>
218
- ))}
219
- </ul>
220
- </div>
221
- );
222
- }
223
-
224
- // Mix mapped elements with other elements
225
- function MixedList({ items }: { items: string[] }) {
226
- return (
227
- <ul>
228
- <li>Header Item</li>
229
- {items.map((item) => (
230
- <li>{item}</li>
231
- ))}
232
- <li>Footer Item</li>
233
- </ul>
234
- );
235
- }
236
- ```
94
+ If a `ref` instance is bound, the element will automatically redraw on changes.
237
95
 
238
- ### Async Components with KTAsync
96
+ #### `k-model`: Two-way data binding for form elements.
239
97
 
240
- KT.js provides built-in support for async components through the `KTAsync` component:
98
+ Must bind to a `ref` instance.
241
99
 
242
100
  ```tsx
243
- import { KTAsync, ref } from 'kt.js';
244
-
245
- // Define an async component that returns a Promise<HTMLElement>
246
- const AsyncUserCard = function () {
247
- return fetch('/api/user')
248
- .then((res) => res.json())
249
- .then((user) => (
250
- <div class="user-card">
251
- <h2>{user.name}</h2>
252
- <p>{user.email}</p>
253
- </div>
254
- ));
255
- };
101
+ import { ref } from 'kt.js';
102
+ const name = ref('');
256
103
 
257
- // Use KTAsync to handle the async component
258
- function App() {
104
+ function NameInput() {
259
105
  return (
260
- <div class="app">
261
- <h1>User Profile</h1>
262
- <KTAsync component={AsyncUserCard} />
106
+ <div>
107
+ <input type="text" k-model={name} />
263
108
  </div>
264
109
  );
265
110
  }
266
111
 
267
- // The component starts with a placeholder comment node
268
- // When the Promise resolves, it automatically replaces with the actual element
112
+ name.value = 'New Name'; // Updates input value
269
113
  ```
270
114
 
271
- **How KTAsync works:**
272
-
273
- 1. Creates a placeholder comment node (`ktjs-suspense-placeholder`) immediately
274
- 2. Calls your component function (which should return a `Promise<HTMLElement>` or `HTMLElement`)
275
- 3. When the Promise resolves, automatically replaces the placeholder with the resolved element
276
- 4. If your component returns a non-Promise value, it's used directly without async handling
115
+ ### CreateRedrawable
277
116
 
278
- **Example with dynamic updates:**
117
+ Create redrawable components with `createRedrawable()`.
279
118
 
280
119
  ```tsx
281
- const DynamicContent = function () {
282
- const count = ref(0);
283
- const container = (
284
- <div>
285
- <p>Count: {count}</p>
286
- <button on:click={() => count.value++}>Increment</button>
287
- </div>
288
- );
289
-
290
- // Simulate async data loading
291
- return new Promise<HTMLElement>((resolve) => {
292
- setTimeout(() => resolve(container), 500);
293
- });
294
- };
295
-
296
- // Usage
297
- const app = (
298
- <div>
299
- <h1>Loading async content...</h1>
300
- <KTAsync component={DynamicContent} />
301
- </div>
302
- );
120
+ import { createRedrawable } from '@ktjs/core';
121
+ let text = 'aa';
122
+ const el = createRedrawable(() => <div>{text}</div>);
123
+ // el is now `<div>aa</div>`
124
+ text = 'bb';
125
+ el.redraw();
126
+ // el is now `<div>bb</div>`
303
127
  ```
304
128
 
305
- If you give a function in attributes, it will be treated as an event listener, and the key will be considered as the event name. `@<eventName>` will also be considered as the handler to avoid conflicts with existing attributes:
129
+ ### Reactive References
306
130
 
307
- ```ts
308
- const button = btn(
309
- {
310
- dblclick: '22',
311
- 'on:dblclick': function trueHandler() {
312
- /* ... */
313
- },
314
- },
315
- 'Click me',
316
- );
131
+ Create reactive values with `ref()` and listen to changes.
317
132
 
318
- // This is equivalent to:
319
- const button = btn(undefined, 'Click me');
320
- button.setAttribute('dblclick', '22');
321
- button.addEventListener('click', () => alert('Clicked!'));
322
- button.addEventListener('dblclick', function trueHandler() {
323
- /* ... */
324
- });
325
- ```
133
+ ```typescript
134
+ import { ref } from '@ktjs/core';
326
135
 
327
- ### Working with CSS-in-JS Libraries
136
+ const count = ref(0);
137
+ count.addOnChange((newVal, oldVal) => {
138
+ console.log(`Count changed from ${oldVal} to ${newVal}`);
139
+ });
328
140
 
329
- KT.js works seamlessly with CSS-in-JS libraries like `@emotion/css`:
141
+ // Update triggers change listeners
142
+ count.value = 1;
143
+ ```
330
144
 
331
- ```ts
332
- import { css } from '@emotion/css';
333
- import { h, div } from 'kt.js';
145
+ #### Surface Reactive Objects
334
146
 
335
- const className = css`
336
- color: red;
337
- font-size: 20px;
338
- `;
147
+ Create depth-1 reactive objects with `surfaceRef()`.
148
+ Basically a helper of wrapping each property with `ref()`.
339
149
 
340
- // Pass class name as attribute
341
- h('div', { class: className }, 'Styled text');
150
+ ```tsx
151
+ const obj = surfaceRef({ a: 1, b: 2 });
152
+ obj.a.value = 3; // Triggers change listeners for obj.a
342
153
 
343
- // Or as the first string argument
344
- div(className, 'Styled text');
154
+ const data = obj.kcollect(); // Deref to the original object { a: 3, b: 2 }
345
155
  ```
346
156
 
347
- ### Using Shortcuts with Default Values
348
-
349
- The `withDefaults` function allows you to create element factories with predefined properties:
157
+ ### Redraw Mechanism
350
158
 
351
- ```ts
352
- import { withDefaults, div, button } from 'kt.js';
159
+ `<KTFor... />` elements in-place with `redraw()` for minimal DOM updates.
353
160
 
354
- // Create a styled div factory
355
- const card = withDefaults(div, { class: 'card' });
356
- const blueCard = withDefaults(card, { style: 'background: blue' });
357
-
358
- // Use them
359
- const myCard = card('card-body', 'Content'); // <div class="card"><div class="card-body">Content</div></div>
360
- const myBlueCard = blueCard('title', 'Blue!'); // <div class="card" style="background: blue"><div class="title">Blue!</div></div>
161
+ ```tsx
162
+ const element = <KTFor list={someArray} map={(v) => 2 * v} />;
163
+ element.redraw({ list: [1, 2, 3] });
361
164
  ```
362
165
 
363
- ## Router
166
+ ## Router (@ktjs/router)
167
+
168
+ Client-side hash-based routing with async navigation guards.
364
169
 
365
- The router is available as a separate package `@ktjs/router`:
170
+ ### Basic Routing
366
171
 
367
- ```ts
172
+ ```typescript
368
173
  import { createRouter } from '@ktjs/router';
369
- import { div, h1 } from 'kt.js';
370
174
 
371
175
  const router = createRouter({
372
176
  routes: [
373
177
  {
374
178
  path: '/',
375
179
  name: 'home',
376
- beforeEnter: (to) => {
377
- // Render your page here
378
- document.getElementById('app')!.innerHTML = '';
379
- document.getElementById('app')!.appendChild(div({}, [h1({}, 'Home Page')]));
380
- },
180
+ component: () => <HomePage />
381
181
  },
382
182
  {
383
183
  path: '/user/:id',
384
184
  name: 'user',
385
- beforeEnter: (to) => {
386
- // Route-specific guard and rendering
387
- console.log('Entering user page');
388
- document.getElementById('app')!.innerHTML = '';
389
- document.getElementById('app')!.appendChild(div({}, [h1({}, `User ${to.params.id}`)]));
390
- return true;
391
- },
392
- },
185
+ component: (to) => <UserPage id={to.params.id} />
186
+ }
393
187
  ],
394
188
  beforeEach: async (to, from) => {
395
- // Global navigation guard - return false to block navigation
396
- console.log('Navigating to:', to.path);
397
- return true;
398
- },
399
- afterEach: (to) => {
400
- // Called after successful navigation
401
- document.title = to.name || to.path;
402
- },
403
- onError: (error) => {
404
- console.error('Router error:', error);
405
- },
189
+ // Auth check
190
+ if (to.path === '/admin' && !isAdmin()) {
191
+ return '/login';
192
+ }
193
+ }
406
194
  });
195
+ ```
407
196
 
408
- // Navigate programmatically
409
- router.push('/user/123');
410
- router.push('/user/456?page=2');
197
+ ### Navigation
411
198
 
412
- // Navigate by route name
413
- router.push({ name: 'user', params: { id: '789' } });
199
+ ```typescript
200
+ // Programmatic navigation
201
+ router.push('/user/123');
202
+ router.push({ name: 'user', params: { id: '456' } });
414
203
 
415
- // Get current route
416
- console.log(router.current?.path, router.current?.params, router.current?.query);
204
+ // Access current route
205
+ console.log(router.current?.path, router.current?.params);
417
206
  ```
418
207
 
419
- ### Router Features
420
-
421
- - **Hash-based Routing Only** (v0.14.7+): Uses URL hash for client-side navigation (`#/path`)
422
- - **Dynamic Parameters**: Support for dynamic route segments (`/user/:id`)
423
- - **Query Strings**: Automatic parsing of query parameters (`?key=value`)
424
- - **Named Routes**: Navigate using route names instead of paths
425
- - **Async Navigation Guards**:
426
- - `beforeEach`: Global guard before navigation (async)
427
- - `beforeEnter`: Per-route guard (can also be used for rendering, async)
428
- - `afterEach`: Global hook after navigation
429
- - All guards support Promise-based async operations
430
- - Guards can return `false` to cancel, string/object to redirect
431
- - `GuardLevel` for fine-grained control over guard execution
432
- - **Error Handling**: `onError` and `onNotFound` callbacks
433
- - **Optimized Performance**: Pre-flattened routes and efficient matching algorithm
434
- - **Zero Dependencies**: Fully self-contained router implementation (does not require `@ktjs/core` for runtime, only for TypeScript types)
435
- - **Pure Routing**: No rendering logic - you control the DOM
436
- - **Automatic Initialization**: Router auto-initializes on creation (v0.14.7+)
208
+ ## Installation
437
209
 
438
- ## Browser Compatibility
210
+ ```bash
211
+ # Full package (includes core)
212
+ pnpm add kt.js
439
213
 
440
- KT.js is transpiled to ES5 and works in all modern browsers as well as legacy browsers including IE9+.
214
+ # Individual packages
215
+ pnpm add @ktjs/core @ktjs/router
216
+ ```
441
217
 
442
- ### Promise Polyfill
218
+ ## TypeScript Configuration
443
219
 
444
- For environments without native `Promise` support (like IE).
220
+ For JSX/TSX support, configure your `tsconfig.json`:
445
221
 
446
- ```js
447
- import 'some promise polyfill'; // Will fallback to sync version if Promise is not available
448
- import { h, div, createRouter } from 'kt.js';
222
+ ```json
223
+ {
224
+ "compilerOptions": {
225
+ "jsx": "react-jsx",
226
+ "jsxImportSource": "@ktjs/core"
227
+ }
228
+ }
449
229
  ```
450
230
 
231
+ ## Philosophy
232
+
233
+ - **Direct DOM Manipulation**: No virtual DOM, minimal abstraction
234
+ - **Zero Re-renders**: Update only what changes, avoid full component re-renders
235
+ - **Type Safety**: Full TypeScript support with accurate type inference
236
+ - **Lightweight**: Small bundle size, no unnecessary dependencies
237
+
451
238
  ## License
452
239
 
453
240
  MIT
@@ -6,9 +6,6 @@ const $isThenable = (o) => typeof o === 'object' && o !== null && typeof o.then
6
6
  const $throw = (message) => {
7
7
  throw new Error('@ktjs/shared: ' + message);
8
8
  };
9
-
10
- // DOM manipulation utilities
11
- // # dom natives
12
9
  /**
13
10
  * & Remove `bind` because it is shockingly slower than wrapper
14
11
  * & `window.document` is safe because it is not configurable and its setter is undefined
@@ -48,7 +45,73 @@ const { get: $buttonDisabledGetter, set: $buttonDisabledSetter } = Object.getOwn
48
45
 
49
46
  // Shared utilities and cached native methods for kt.js framework
50
47
  // Re-export all utilities
51
- Object.defineProperty(window, '@ktjs/shared', { value: '0.20.0' });
48
+ Object.defineProperty(window, '@ktjs/shared', { value: '0.20.2' });
49
+
50
+ class KTRef {
51
+ /**
52
+ * Indicates that this is a KTRef instance
53
+ */
54
+ isKT = true;
55
+ /**
56
+ * @internal
57
+ */
58
+ _value;
59
+ /**
60
+ * @internal
61
+ */
62
+ _onChanges;
63
+ constructor(_value, _onChanges) {
64
+ this._value = _value;
65
+ this._onChanges = _onChanges;
66
+ }
67
+ /**
68
+ * If new value and old value are both nodes, the old one will be replaced in the DOM
69
+ */
70
+ get value() {
71
+ return this._value;
72
+ }
73
+ set value(newValue) {
74
+ if (newValue === this._value) {
75
+ return;
76
+ }
77
+ // replace the old node with the new one in the DOM if both are nodes
78
+ if (this._value instanceof Node && newValue instanceof Node) {
79
+ if (newValue.contains(this._value)) {
80
+ this._value.remove();
81
+ }
82
+ this._value.replaceWith(newValue);
83
+ }
84
+ const oldValue = this._value;
85
+ this._value = newValue;
86
+ for (let i = 0; i < this._onChanges.length; i++) {
87
+ this._onChanges[i](newValue, oldValue);
88
+ }
89
+ }
90
+ addOnChange(callback) {
91
+ this._onChanges.push(callback);
92
+ }
93
+ removeOnChange(callback) {
94
+ for (let i = this._onChanges.length - 1; i >= 0; i--) {
95
+ if (this._onChanges[i] === callback) {
96
+ this._onChanges.splice(i, 1);
97
+ return true;
98
+ }
99
+ }
100
+ return false;
101
+ }
102
+ }
103
+ const isKTRef = (obj) => {
104
+ return typeof obj === 'object' && obj !== null && obj.isKT === true;
105
+ };
106
+ /**
107
+ * Reference to the created HTML element.
108
+ * - **Only** respond to `ref.value` changes, not reactive to internal changes of the element.
109
+ * - can alse be used to store normal values, but it is not reactive.
110
+ * @param value mostly an HTMLElement
111
+ */
112
+ function ref(value, onChange) {
113
+ return new KTRef(value, []);
114
+ }
52
115
 
53
116
  const booleanHandler = (element, key, value) => {
54
117
  if (key in element) {
@@ -114,6 +177,7 @@ function attrIsObject(element, attr) {
114
177
  key === 'style' ||
115
178
  key === 'children' ||
116
179
  key === 'k-if' ||
180
+ key.startsWith('k-model') ||
117
181
  key === 'ref') {
118
182
  continue;
119
183
  }
@@ -191,7 +255,29 @@ function applyContent(element, content) {
191
255
  }
192
256
  }
193
257
 
194
- document.createElement('div');
258
+ function register(element, valueRef, propName, eventName) {
259
+ element[propName] = valueRef.value; // initialize
260
+ valueRef.addOnChange((newValue) => (element[propName] = newValue));
261
+ element.addEventListener(eventName, () => (valueRef.value = element[propName]));
262
+ }
263
+ function applyModel(element, valueRef) {
264
+ if (element instanceof HTMLInputElement) {
265
+ if (element.type === 'radio' || element.type === 'checkbox') {
266
+ register(element, valueRef, 'checked', 'change');
267
+ }
268
+ else {
269
+ register(element, valueRef, 'value', 'input');
270
+ }
271
+ }
272
+ else if (element instanceof HTMLSelectElement) {
273
+ register(element, valueRef, 'value', 'change');
274
+ }
275
+ else if (element instanceof HTMLTextAreaElement) {
276
+ register(element, valueRef, 'value', 'input');
277
+ }
278
+ console.warn('[kt.js warn] not supported element for k-model:', element.tagName);
279
+ }
280
+
195
281
  const htmlCreator = (tag) => document.createElement(tag);
196
282
  const svgCreator = (tag) => document.createElementNS('http://www.w3.org/2000/svg', tag);
197
283
  const mathMLCreator = (tag) => document.createElementNS('http://www.w3.org/1998/Math/MathML', tag);
@@ -209,7 +295,7 @@ const MATHML_ATTR_FLAG = '__kt_mathml__';
209
295
  * ## About
210
296
  * @package @ktjs/core
211
297
  * @author Kasukabe Tsumugi <futami16237@gmail.com>
212
- * @version 0.20.3 (Last Update: 2026.02.01 18:38:02.198)
298
+ * @version 0.22.0 (Last Update: 2026.02.02 17:11:04.703)
213
299
  * @license MIT
214
300
  * @link https://github.com/baendlorel/kt.js
215
301
  * @link https://baendlorel.github.io/ Welcome to my site!
@@ -238,101 +324,60 @@ const h = (tag, attr, content) => {
238
324
  // * Handle content
239
325
  applyAttr(element, attr);
240
326
  applyContent(element, content);
241
- // if (tag === 'svg') {
242
- // tempWrapper.innerHTML = element.outerHTML;
243
- // return tempWrapper.firstChild as HTML<T>;
244
- // }
245
- // if (tag === 'math') {
246
- // tempWrapper.innerHTML = element.outerHTML;
247
- // return tempWrapper.firstChild as HTML<T>;
248
- // }
249
- return element;
250
- };
251
-
252
- class KTRef {
253
- /**
254
- * Indicates that this is a KTRef instance
255
- */
256
- isKT = true;
257
- /**
258
- * @internal
259
- */
260
- _value;
261
- /**
262
- * @internal
263
- */
264
- _onChanges;
265
- constructor(_value, _onChanges) {
266
- this._value = _value;
267
- this._onChanges = _onChanges;
268
- }
269
- /**
270
- * If new value and old value are both nodes, the old one will be replaced in the DOM
271
- */
272
- get value() {
273
- return this._value;
274
- }
275
- set value(newValue) {
276
- if (newValue === this._value) {
277
- return;
327
+ if (typeof attr === 'object' && attr !== null && 'k-model' in attr) {
328
+ const kmodel = attr['k-model'];
329
+ if (isKTRef(kmodel)) {
330
+ applyModel(element, kmodel);
278
331
  }
279
- // replace the old node with the new one in the DOM if both are nodes
280
- if (this._value instanceof Node && newValue instanceof Node) {
281
- if (newValue.contains(this._value)) {
282
- this._value.remove();
283
- }
284
- this._value.replaceWith(newValue);
285
- }
286
- const oldValue = this._value;
287
- this._value = newValue;
288
- for (let i = 0; i < this._onChanges.length; i++) {
289
- this._onChanges[i](newValue, oldValue);
290
- }
291
- }
292
- addOnChange(callback) {
293
- this._onChanges.push(callback);
294
- }
295
- removeOnChange(callback) {
296
- for (let i = this._onChanges.length - 1; i >= 0; i--) {
297
- if (this._onChanges[i] === callback) {
298
- this._onChanges.splice(i, 1);
299
- return true;
300
- }
332
+ else {
333
+ $throw('k-model value must be a KTRef.');
301
334
  }
302
- return false;
303
335
  }
304
- }
305
- /**
306
- * Reference to the created HTML element.
307
- * - can alse be used to store normal values, but it is not reactive.
308
- * @param value mostly an HTMLElement
309
- */
310
- function ref(value, onChange) {
311
- return new KTRef(value, []);
312
- }
336
+ return element;
337
+ };
313
338
 
314
339
  const dummyRef = { value: null };
340
+ const create = (tag, props) => {
341
+ if (typeof tag === 'function') {
342
+ return tag(props);
343
+ }
344
+ else {
345
+ return h(tag, props, props.children);
346
+ }
347
+ };
348
+ const placeholder = () => document.createComment('k-if');
315
349
  /**
316
350
  * @param tag html tag or function component
317
351
  * @param props properties/attributes
318
352
  */
319
353
  function jsx(tag, props) {
320
- const ref = props.ref?.isKT ? props.ref : dummyRef;
354
+ const maybeDummyRef = isKTRef(props.ref) ? props.ref : dummyRef;
321
355
  let el;
322
- if ('k-if' in props && !props['k-if']) {
323
- // & make comment placeholder in case that ref might be redrawn later
324
- el = document.createComment('k-if');
325
- ref.value = el;
326
- return el;
327
- }
328
- // Handle function components
329
- if (typeof tag === 'function') {
330
- el = tag(props);
331
- }
332
- else {
333
- el = h(tag, props, props.children);
356
+ if ('k-if' in props) {
357
+ const kif = props['k-if'];
358
+ let condition = kif; // assume boolean by default
359
+ // Handle reactive k-if
360
+ if (isKTRef(kif)) {
361
+ kif.addOnChange((newValue, oldValue) => {
362
+ if (newValue === oldValue) {
363
+ return;
364
+ }
365
+ const oldEl = el;
366
+ el = newValue ? create(tag, props) : placeholder();
367
+ oldEl.replaceWith(el);
368
+ maybeDummyRef.value = el;
369
+ });
370
+ condition = kif.value;
371
+ }
372
+ if (!condition) {
373
+ // & make comment placeholder in case that ref might be redrawn later
374
+ el = placeholder();
375
+ maybeDummyRef.value = el;
376
+ return el;
377
+ }
334
378
  }
335
- ref.value = el;
379
+ el = create(tag, props);
380
+ maybeDummyRef.value = el;
336
381
  return el;
337
382
  }
338
383
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kt.js",
3
- "version": "0.20.3",
3
+ "version": "0.22.0",
4
4
  "author": {
5
5
  "name": "Kasukabe Tsumugi",
6
6
  "email": "futami16237@gmail.com"
@@ -41,7 +41,7 @@
41
41
  ],
42
42
  "license": "MIT",
43
43
  "dependencies": {
44
- "@ktjs/core": "0.20.3"
44
+ "@ktjs/core": "0.22.0"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "rollup -c rollup.config.mjs",