mono-jsx 0.4.0 → 0.6.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 +444 -142
- package/index.mjs +13 -1
- package/jsx-runtime.mjs +830 -556
- package/package.json +1 -1
- package/setup.mjs +1 -0
- package/types/html.d.ts +20 -11
- package/types/index.d.ts +6 -1
- package/types/jsx.d.ts +11 -7
- package/types/mono.d.ts +49 -25
- package/types/render.d.ts +14 -2
package/README.md
CHANGED
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
mono-jsx is a JSX runtime that renders `<html>` element to `Response` object in JavaScript runtimes like Node.js, Deno, Bun, Cloudflare Workers, etc.
|
|
6
6
|
|
|
7
7
|
- 🚀 No build step needed
|
|
8
|
-
- 🦋 Lightweight (
|
|
9
|
-
-
|
|
10
|
-
-
|
|
8
|
+
- 🦋 Lightweight (10KB gzipped), zero dependencies
|
|
9
|
+
- 🚦 Signals as reactive primitives
|
|
10
|
+
- ⚡️ Use web comonents, no virtual DOM
|
|
11
|
+
- 💡 Complete Web API TypeScript definitions
|
|
11
12
|
- ⏳ Streaming rendering
|
|
13
|
+
- 🗂️ Built-in router
|
|
12
14
|
- 🥷 [htmx](#using-htmx) integration
|
|
13
15
|
- 🌎 Universal, works in Node.js, Deno, Bun, Cloudflare Workers, etc.
|
|
14
16
|
|
|
@@ -72,8 +74,8 @@ export default {
|
|
|
72
74
|
<html>
|
|
73
75
|
<h1>Welcome to mono-jsx!</h1>
|
|
74
76
|
</html>
|
|
75
|
-
)
|
|
76
|
-
}
|
|
77
|
+
)
|
|
78
|
+
}
|
|
77
79
|
```
|
|
78
80
|
|
|
79
81
|
For Deno/Bun users, you can run the `app.tsx` directly:
|
|
@@ -83,13 +85,13 @@ deno serve app.tsx
|
|
|
83
85
|
bun run app.tsx
|
|
84
86
|
```
|
|
85
87
|
|
|
86
|
-
If you're building a web app with [Cloudflare Workers](https://developers.cloudflare.com/workers/wrangler/commands/#dev), use `wrangler dev` to start
|
|
88
|
+
If you're building a web app with [Cloudflare Workers](https://developers.cloudflare.com/workers/wrangler/commands/#dev), use `wrangler dev` to start your app in development mode:
|
|
87
89
|
|
|
88
90
|
```bash
|
|
89
91
|
npx wrangler dev app.tsx
|
|
90
92
|
```
|
|
91
93
|
|
|
92
|
-
**Node.js doesn't support JSX syntax or declarative fetch servers**,
|
|
94
|
+
**Node.js doesn't support JSX syntax or declarative fetch servers**, we recommend using mono-jsx with [srvx](https://srvx.h3.dev/):
|
|
93
95
|
|
|
94
96
|
```tsx
|
|
95
97
|
// app.tsx
|
|
@@ -106,14 +108,14 @@ serve({
|
|
|
106
108
|
});
|
|
107
109
|
```
|
|
108
110
|
|
|
109
|
-
|
|
111
|
+
And you'll need [tsx](https://www.npmjs.com/package/tsx) to start the app without a build step:
|
|
110
112
|
|
|
111
113
|
```bash
|
|
112
114
|
npx tsx app.tsx
|
|
113
115
|
```
|
|
114
116
|
|
|
115
117
|
> [!NOTE]
|
|
116
|
-
> Only root `<html>` element will be rendered as a `Response` object. You cannot return a `<div>` or any other element directly from the `fetch` handler. This is a limitation of the mono-jsx
|
|
118
|
+
> Only root `<html>` element will be rendered as a `Response` object. You cannot return a `<div>` or any other element directly from the `fetch` handler. This is a limitation of the mono-jsx.
|
|
117
119
|
|
|
118
120
|
## Using JSX
|
|
119
121
|
|
|
@@ -131,7 +133,7 @@ mono-jsx adopts standard HTML property names, avoiding React's custom naming con
|
|
|
131
133
|
|
|
132
134
|
mono-jsx allows you to compose the `class` property using arrays of strings, objects, or expressions:
|
|
133
135
|
|
|
134
|
-
```
|
|
136
|
+
```tsx
|
|
135
137
|
<div
|
|
136
138
|
class={[
|
|
137
139
|
"container box",
|
|
@@ -145,7 +147,7 @@ mono-jsx allows you to compose the `class` property using arrays of strings, obj
|
|
|
145
147
|
|
|
146
148
|
mono-jsx supports [pseudo classes](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-classes), [pseudo elements](https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements), [media queries](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries), and [CSS nesting](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting/Using_CSS_nesting) in the `style` property:
|
|
147
149
|
|
|
148
|
-
```
|
|
150
|
+
```tsx
|
|
149
151
|
<a
|
|
150
152
|
style={{
|
|
151
153
|
color: "black",
|
|
@@ -164,7 +166,7 @@ mono-jsx supports [pseudo classes](https://developer.mozilla.org/en-US/docs/Web/
|
|
|
164
166
|
|
|
165
167
|
mono-jsx uses [`<slot>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot) elements to render slotted content (equivalent to React's `children` property). You can also add the `name` attribute to define named slots:
|
|
166
168
|
|
|
167
|
-
```
|
|
169
|
+
```tsx
|
|
168
170
|
function Container() {
|
|
169
171
|
return (
|
|
170
172
|
<div class="container">
|
|
@@ -173,7 +175,7 @@ function Container() {
|
|
|
173
175
|
{/* Named slot */}
|
|
174
176
|
<slot name="desc" />
|
|
175
177
|
</div>
|
|
176
|
-
)
|
|
178
|
+
)
|
|
177
179
|
}
|
|
178
180
|
|
|
179
181
|
function App() {
|
|
@@ -184,7 +186,7 @@ function App() {
|
|
|
184
186
|
{/* This goes to the default slot */}
|
|
185
187
|
<h1>Hello world!</h1>
|
|
186
188
|
</Container>
|
|
187
|
-
)
|
|
189
|
+
)
|
|
188
190
|
}
|
|
189
191
|
```
|
|
190
192
|
|
|
@@ -192,7 +194,7 @@ function App() {
|
|
|
192
194
|
|
|
193
195
|
mono-jsx provides an `html` tag function to render raw HTML in JSX instead of React's `dangerouslySetInnerHTML`:
|
|
194
196
|
|
|
195
|
-
```
|
|
197
|
+
```tsx
|
|
196
198
|
function App() {
|
|
197
199
|
return <div>{html`<h1>Hello world!</h1>`}</div>;
|
|
198
200
|
}
|
|
@@ -200,14 +202,14 @@ function App() {
|
|
|
200
202
|
|
|
201
203
|
The `html` tag function is globally available without importing. You can also use `css` and `js` tag functions for CSS and JavaScript:
|
|
202
204
|
|
|
203
|
-
```
|
|
205
|
+
```tsx
|
|
204
206
|
function App() {
|
|
205
207
|
return (
|
|
206
208
|
<head>
|
|
207
209
|
<style>{css`h1 { font-size: 3rem; }`}</style>
|
|
208
210
|
<script>{js`console.log("Hello world!")`}</script>
|
|
209
211
|
</head>
|
|
210
|
-
)
|
|
212
|
+
)
|
|
211
213
|
}
|
|
212
214
|
```
|
|
213
215
|
|
|
@@ -218,13 +220,13 @@ function App() {
|
|
|
218
220
|
|
|
219
221
|
mono-jsx lets you write event handlers directly in JSX, similar to React:
|
|
220
222
|
|
|
221
|
-
```
|
|
223
|
+
```tsx
|
|
222
224
|
function Button() {
|
|
223
225
|
return (
|
|
224
226
|
<button onClick={(evt) => alert("BOOM!")}>
|
|
225
227
|
Click Me
|
|
226
228
|
</button>
|
|
227
|
-
)
|
|
229
|
+
)
|
|
228
230
|
}
|
|
229
231
|
```
|
|
230
232
|
|
|
@@ -234,9 +236,10 @@ function Button() {
|
|
|
234
236
|
```tsx
|
|
235
237
|
import { doSomething } from "some-library";
|
|
236
238
|
|
|
237
|
-
function Button(this: FC
|
|
238
|
-
|
|
239
|
-
|
|
239
|
+
function Button(this: FC<{ count: 0 }>, props: { role: string }) {
|
|
240
|
+
const message = "BOOM!"; // server-side variable
|
|
241
|
+
this.count = 0; // initialize a signal
|
|
242
|
+
console.log(message); // only prints on server-side
|
|
240
243
|
return (
|
|
241
244
|
<button
|
|
242
245
|
role={props.role}
|
|
@@ -247,37 +250,27 @@ function Button(this: FC, props: { role: string }) {
|
|
|
247
250
|
Deno.exit(0); // ❌ `Deno` is unavailable in the browser
|
|
248
251
|
document.title = "BOOM!"; // ✅ `document` is a browser API
|
|
249
252
|
console.log(evt.target); // ✅ `evt` is the event object
|
|
250
|
-
this.count++; // ✅ update the
|
|
253
|
+
this.count++; // ✅ update the `count` signal
|
|
251
254
|
}}
|
|
252
255
|
>
|
|
253
|
-
|
|
256
|
+
<slot />
|
|
254
257
|
</button>
|
|
255
|
-
)
|
|
258
|
+
)
|
|
256
259
|
}
|
|
257
260
|
```
|
|
258
261
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
```jsx
|
|
262
|
-
function App() {
|
|
263
|
-
return (
|
|
264
|
-
<div onMount={(evt) => console.log(evt.target, "Mounted!")}>
|
|
265
|
-
<h1>Welcome to mono-jsx!</h1>
|
|
266
|
-
</div>
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
```
|
|
262
|
+
### Using `<form>` Element
|
|
270
263
|
|
|
271
|
-
mono-jsx
|
|
264
|
+
mono-jsx supports `<form>` elements with the `action` attribute. The `action` attribute can be a string URL or a function that accepts a `FormData` object. The function will be called on form submission, and the `FormData` object will contain the form data.
|
|
272
265
|
|
|
273
266
|
```tsx
|
|
274
267
|
function App() {
|
|
275
268
|
return (
|
|
276
|
-
<form action={(data: FormData
|
|
269
|
+
<form action={(data: FormData) => console.log(data.get("name"))}>
|
|
277
270
|
<input type="text" name="name" />
|
|
278
271
|
<button type="submit">Submit</button>
|
|
279
272
|
</form>
|
|
280
|
-
)
|
|
273
|
+
)
|
|
281
274
|
}
|
|
282
275
|
```
|
|
283
276
|
|
|
@@ -291,18 +284,18 @@ async function Loader(props: { url: string }) {
|
|
|
291
284
|
return <JsonViewer data={data} />;
|
|
292
285
|
}
|
|
293
286
|
|
|
294
|
-
default {
|
|
287
|
+
export default {
|
|
295
288
|
fetch: (req) => (
|
|
296
289
|
<html>
|
|
297
290
|
<Loader url="https://api.example.com/data" placeholder={<p>Loading...</p>} />
|
|
298
291
|
</html>
|
|
299
|
-
)
|
|
300
|
-
}
|
|
292
|
+
)
|
|
293
|
+
}
|
|
301
294
|
```
|
|
302
295
|
|
|
303
296
|
You can also use async generators to yield multiple elements over time. This is useful for streaming rendering of LLM tokens:
|
|
304
297
|
|
|
305
|
-
```
|
|
298
|
+
```tsx
|
|
306
299
|
async function* Chat(props: { prompt: string }) {
|
|
307
300
|
const stream = await openai.chat.completions.create({
|
|
308
301
|
model: "gpt-4",
|
|
@@ -318,113 +311,169 @@ async function* Chat(props: { prompt: string }) {
|
|
|
318
311
|
}
|
|
319
312
|
}
|
|
320
313
|
|
|
321
|
-
|
|
322
|
-
default {
|
|
314
|
+
export default {
|
|
323
315
|
fetch: (req) => (
|
|
324
316
|
<html>
|
|
325
317
|
<Chat prompt="Tell me a story" placeholder={<span style="color:grey">●</span>} />
|
|
326
318
|
</html>
|
|
327
|
-
)
|
|
328
|
-
}
|
|
319
|
+
)
|
|
320
|
+
}
|
|
329
321
|
```
|
|
330
322
|
|
|
331
|
-
##
|
|
323
|
+
## Using Signals
|
|
332
324
|
|
|
333
|
-
mono-jsx
|
|
325
|
+
mono-jsx uses signals for updating the view when a signal changes. Signals are similar to React's state, but they are more lightweight and efficient. You can use signals to manage state in your components.
|
|
334
326
|
|
|
335
|
-
### Using Component
|
|
327
|
+
### Using Component Signals
|
|
336
328
|
|
|
337
|
-
You can use the `this` keyword in your components to manage
|
|
329
|
+
You can use the `this` keyword in your components to manage signals. The signals is bound to the component instance and can be updated directly, and will automatically re-render the view when a signal changes:
|
|
338
330
|
|
|
339
331
|
```tsx
|
|
340
332
|
function Counter(
|
|
341
333
|
this: FC<{ count: number }>,
|
|
342
334
|
props: { initialCount?: number },
|
|
343
335
|
) {
|
|
344
|
-
// Initialize
|
|
336
|
+
// Initialize a singal
|
|
345
337
|
this.count = props.initialCount ?? 0;
|
|
346
338
|
|
|
347
339
|
return (
|
|
348
340
|
<div>
|
|
349
|
-
{/* render
|
|
341
|
+
{/* render singal */}
|
|
350
342
|
<span>{this.count}</span>
|
|
351
343
|
|
|
352
|
-
{/* Update
|
|
344
|
+
{/* Update singal to trigger re-render */}
|
|
353
345
|
<button onClick={() => this.count--}>-</button>
|
|
354
346
|
<button onClick={() => this.count++}>+</button>
|
|
355
347
|
</div>
|
|
356
|
-
)
|
|
348
|
+
)
|
|
357
349
|
}
|
|
358
350
|
```
|
|
359
351
|
|
|
360
|
-
### Using App
|
|
352
|
+
### Using App Signals
|
|
361
353
|
|
|
362
|
-
You can define app
|
|
354
|
+
You can define app signals by adding `app` prop to the root `<html>` element. The app signals is available in all components via `this.app.<SignalName>`. Changes to the app signals will trigger re-renders in all components that use it:
|
|
363
355
|
|
|
364
356
|
```tsx
|
|
365
|
-
|
|
357
|
+
interface AppSignals {
|
|
358
|
+
themeColor: string;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function Header(this: FC<{}, AppSignals>) {
|
|
366
362
|
return (
|
|
367
363
|
<header>
|
|
368
|
-
<h1
|
|
364
|
+
<h1 style={this.computed(() => ({ color: this.app.themeColor }))}>Welcome to mono-jsx!</h1>
|
|
369
365
|
</header>
|
|
370
|
-
)
|
|
366
|
+
)
|
|
371
367
|
}
|
|
372
368
|
|
|
373
|
-
function Footer(this: FC<{},
|
|
369
|
+
function Footer(this: FC<{}, AppSignals>) {
|
|
374
370
|
return (
|
|
375
371
|
<footer>
|
|
376
|
-
<p
|
|
372
|
+
<p style={this.computed(() => ({ color: this.app.themeColor }))}>(c) 2025 mono-jsx.</p>
|
|
377
373
|
</footer>
|
|
378
|
-
)
|
|
374
|
+
)
|
|
379
375
|
}
|
|
380
376
|
|
|
381
|
-
function Main(this: FC<{},
|
|
377
|
+
function Main(this: FC<{}, AppSignals>) {
|
|
382
378
|
return (
|
|
383
379
|
<main>
|
|
384
|
-
<
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
placeholder="Enter a new title"
|
|
389
|
-
/>
|
|
380
|
+
<p>
|
|
381
|
+
<label>Theme Color: </label>
|
|
382
|
+
<input type="color" onInput={({ target }) => this.app.themeColor = target.value}/>
|
|
383
|
+
</p>
|
|
390
384
|
</main>
|
|
391
|
-
)
|
|
385
|
+
)
|
|
392
386
|
}
|
|
393
387
|
|
|
394
388
|
export default {
|
|
395
389
|
fetch: (req) => (
|
|
396
|
-
<html
|
|
390
|
+
<html app={{ themeColor: "#232323" }}>
|
|
397
391
|
<Header />
|
|
398
392
|
<Main />
|
|
399
393
|
<Footer />
|
|
400
394
|
</html>
|
|
401
|
-
)
|
|
402
|
-
}
|
|
395
|
+
)
|
|
396
|
+
}
|
|
403
397
|
```
|
|
404
398
|
|
|
405
|
-
### Using Computed
|
|
399
|
+
### Using Computed Signals
|
|
406
400
|
|
|
407
|
-
You can use `this.computed` to create
|
|
401
|
+
You can use `this.computed` to create a derived signal based on other signals:
|
|
408
402
|
|
|
409
403
|
```tsx
|
|
410
404
|
function App(this: FC<{ input: string }>) {
|
|
411
|
-
this.input = "Welcome to mono-jsx
|
|
405
|
+
this.input = "Welcome to mono-jsx";
|
|
412
406
|
return (
|
|
413
407
|
<div>
|
|
414
408
|
<h1>{this.computed(() => this.input + "!")}</h1>
|
|
415
409
|
|
|
416
410
|
<form action={(fd) => this.input = fd.get("input") as string}>
|
|
417
|
-
<input type="text" name="input" value={this.input} />
|
|
411
|
+
<input type="text" name="input" value={"" + this.input} />
|
|
418
412
|
<button type="submit">Submit</button>
|
|
419
413
|
</form>
|
|
420
414
|
</div>
|
|
421
|
-
)
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Using Effects
|
|
420
|
+
|
|
421
|
+
You can use `this.effect` to create side effects based on signals. The effect will run whenever the signal changes:
|
|
422
|
+
|
|
423
|
+
```tsx
|
|
424
|
+
function App(this: FC<{ count: number }>) {
|
|
425
|
+
this.count = 0;
|
|
426
|
+
|
|
427
|
+
this.effect(() => {
|
|
428
|
+
console.log("Count changed:", this.count);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
return (
|
|
432
|
+
<div>
|
|
433
|
+
<span>{this.count}</span>
|
|
434
|
+
<button onClick={() => this.count++}>+</button>
|
|
435
|
+
</div>
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
The callback function of `this.effect` can return a cleanup function that gets run once the component element has been removed via `<toggle>` or `<switch>` condition rendering:
|
|
441
|
+
|
|
442
|
+
```tsx
|
|
443
|
+
function Counter(this: FC<{ count: number }>) {
|
|
444
|
+
this.count = 0;
|
|
445
|
+
|
|
446
|
+
this.effect(() => {
|
|
447
|
+
const interval = setInterval(() => {
|
|
448
|
+
this.count++;
|
|
449
|
+
}, 1000);
|
|
450
|
+
|
|
451
|
+
return () => clearInterval(interval);
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
return (
|
|
455
|
+
<div>
|
|
456
|
+
<span>{this.count}</span>
|
|
457
|
+
</div>
|
|
458
|
+
)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function App(this: FC<{ show: boolean }>) {
|
|
462
|
+
this.show = true
|
|
463
|
+
return (
|
|
464
|
+
<div>
|
|
465
|
+
<toggle show={this.show}>
|
|
466
|
+
<Foo />
|
|
467
|
+
</toggle>
|
|
468
|
+
<button onClick={e => this.show = !this.show }>{this.computed(() => this.show ? 'Hide': 'Show')}</button>
|
|
469
|
+
</div>
|
|
470
|
+
)
|
|
422
471
|
}
|
|
423
472
|
```
|
|
424
473
|
|
|
425
|
-
### Using `<toggle>` Element with
|
|
474
|
+
### Using `<toggle>` Element with Signals
|
|
426
475
|
|
|
427
|
-
The `<toggle>` element conditionally renders content based on the
|
|
476
|
+
The `<toggle>` element conditionally renders content based on the `show` prop:
|
|
428
477
|
|
|
429
478
|
```tsx
|
|
430
479
|
function App(this: FC<{ show: boolean }>) {
|
|
@@ -436,7 +485,7 @@ function App(this: FC<{ show: boolean }>) {
|
|
|
436
485
|
|
|
437
486
|
return (
|
|
438
487
|
<div>
|
|
439
|
-
<toggle
|
|
488
|
+
<toggle show={this.show}>
|
|
440
489
|
<h1>Welcome to mono-jsx!</h1>
|
|
441
490
|
</toggle>
|
|
442
491
|
|
|
@@ -444,16 +493,16 @@ function App(this: FC<{ show: boolean }>) {
|
|
|
444
493
|
{this.computed(() => this.show ? "Hide" : "Show")}
|
|
445
494
|
</button>
|
|
446
495
|
</div>
|
|
447
|
-
)
|
|
496
|
+
)
|
|
448
497
|
}
|
|
449
498
|
```
|
|
450
499
|
|
|
451
|
-
### Using `<switch>` Element with
|
|
500
|
+
### Using `<switch>` Element with Signals
|
|
452
501
|
|
|
453
|
-
The `<switch>` element renders different content based on the value of a
|
|
502
|
+
The `<switch>` element renders different content based on the value of a signal. Elements with matching `slot` attributes are displayed when their value matches, otherwise default slots are shown:
|
|
454
503
|
|
|
455
504
|
```tsx
|
|
456
|
-
function App(this: FC<{ lang: "en" | "zh" | "
|
|
505
|
+
function App(this: FC<{ lang: "en" | "zh" | "🙂" }>) {
|
|
457
506
|
this.lang = "en";
|
|
458
507
|
|
|
459
508
|
return (
|
|
@@ -466,19 +515,19 @@ function App(this: FC<{ lang: "en" | "zh" | "emoji" }>) {
|
|
|
466
515
|
<p>
|
|
467
516
|
<button onClick={() => this.lang = "en"}>English</button>
|
|
468
517
|
<button onClick={() => this.lang = "zh"}>中文</button>
|
|
469
|
-
<button onClick={() => this.lang = "
|
|
518
|
+
<button onClick={() => this.lang = "🙂"}>🙂</button>
|
|
470
519
|
</p>
|
|
471
520
|
</div>
|
|
472
|
-
)
|
|
521
|
+
)
|
|
473
522
|
}
|
|
474
523
|
```
|
|
475
524
|
|
|
476
|
-
### Limitation of
|
|
525
|
+
### Limitation of Signals
|
|
477
526
|
|
|
478
|
-
1\.
|
|
527
|
+
1\. Arrow function are non-stateful components.
|
|
479
528
|
|
|
480
529
|
```tsx
|
|
481
|
-
// ❌ Won't work -
|
|
530
|
+
// ❌ Won't work - use `this` in a non-stateful component
|
|
482
531
|
const App = () => {
|
|
483
532
|
this.count = 0;
|
|
484
533
|
return (
|
|
@@ -486,7 +535,7 @@ const App = () => {
|
|
|
486
535
|
<span>{this.count}</span>
|
|
487
536
|
<button onClick={() => this.count++}>+</button>
|
|
488
537
|
</div>
|
|
489
|
-
)
|
|
538
|
+
)
|
|
490
539
|
};
|
|
491
540
|
|
|
492
541
|
// ✅ Works correctly
|
|
@@ -497,16 +546,16 @@ function App(this: FC) {
|
|
|
497
546
|
<span>{this.count}</span>
|
|
498
547
|
<button onClick={() => this.count++}>+</button>
|
|
499
548
|
</div>
|
|
500
|
-
)
|
|
549
|
+
)
|
|
501
550
|
}
|
|
502
551
|
```
|
|
503
552
|
|
|
504
|
-
2\.
|
|
553
|
+
2\. Signals cannot be computed outside of the `this.computed` method.
|
|
505
554
|
|
|
506
555
|
```tsx
|
|
507
|
-
// ❌ Won't work -
|
|
556
|
+
// ❌ Won't work - updates of a signal won't refresh the view
|
|
508
557
|
function App(this: FC<{ message: string }>) {
|
|
509
|
-
this.message = "Welcome to mono-jsx
|
|
558
|
+
this.message = "Welcome to mono-jsx";
|
|
510
559
|
return (
|
|
511
560
|
<div>
|
|
512
561
|
<h1 title={this.message + "!"}>{this.message + "!"}</h1>
|
|
@@ -514,12 +563,12 @@ function App(this: FC<{ message: string }>) {
|
|
|
514
563
|
Click Me
|
|
515
564
|
</button>
|
|
516
565
|
</div>
|
|
517
|
-
)
|
|
566
|
+
)
|
|
518
567
|
}
|
|
519
568
|
|
|
520
569
|
// ✅ Works correctly
|
|
521
570
|
function App(this: FC) {
|
|
522
|
-
this.message = "Welcome to mono-jsx
|
|
571
|
+
this.message = "Welcome to mono-jsx";
|
|
523
572
|
return (
|
|
524
573
|
<div>
|
|
525
574
|
<h1 title={this.computed(() => this.message + "!")}>{this.computed(() => this.message + "!")}</h1>
|
|
@@ -527,33 +576,89 @@ function App(this: FC) {
|
|
|
527
576
|
Click Me
|
|
528
577
|
</button>
|
|
529
578
|
</div>
|
|
530
|
-
)
|
|
579
|
+
)
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
3\. The callback function of `this.computed` must be a pure function. That means it should not create side effects or access any non-stateful variables. For example, you cannot use `Deno` or `document` in the callback function:
|
|
584
|
+
|
|
585
|
+
```tsx
|
|
586
|
+
// ❌ Won't work - throws `Deno is not defined` when the button is clicked
|
|
587
|
+
function App(this: FC<{ message: string }>) {
|
|
588
|
+
this.message = "Welcome to mono-jsx";
|
|
589
|
+
return (
|
|
590
|
+
<div>
|
|
591
|
+
<h1>{this.computed(() => this.message + "! (Deno " + Deno.version.deno + ")")}</h1>
|
|
592
|
+
<button onClick={() => this.message = "Clicked"}>
|
|
593
|
+
Click Me
|
|
594
|
+
</button>
|
|
595
|
+
</div>
|
|
596
|
+
)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ✅ Works correctly
|
|
600
|
+
function App(this: FC<{ message: string, denoVersion: string }>) {
|
|
601
|
+
this.denoVersion = Deno.version.deno;
|
|
602
|
+
this.message = "Welcome to mono-jsx";
|
|
603
|
+
return (
|
|
604
|
+
<div>
|
|
605
|
+
<h1>{this.computed(() => this.message + "! (Deno " + this.denoVersion + ")")}</h1>
|
|
606
|
+
<button onClick={() => this.message = "Clicked"}>
|
|
607
|
+
Click Me
|
|
608
|
+
</button>
|
|
609
|
+
</div>
|
|
610
|
+
)
|
|
531
611
|
}
|
|
532
612
|
```
|
|
533
613
|
|
|
534
614
|
## Using `this` in Components
|
|
535
615
|
|
|
536
|
-
mono-jsx binds a
|
|
616
|
+
mono-jsx binds a scoped signals object to `this` of your component functions. This allows you to access signals, context, and request information directly in your components.
|
|
537
617
|
|
|
538
|
-
The `this` object
|
|
618
|
+
The `this` object has the following built-in properties:
|
|
539
619
|
|
|
540
|
-
- `app`: The app
|
|
620
|
+
- `app`: The app signals defined on the root `<html>` element.
|
|
541
621
|
- `context`: The context defined on the root `<html>` element.
|
|
542
622
|
- `request`: The request object from the `fetch` handler.
|
|
543
|
-
- `
|
|
623
|
+
- `refs`: A map of refs defined in the component.
|
|
624
|
+
- `computed`: A method to create a computed signal.
|
|
625
|
+
- `effect`: A method to create side effects.
|
|
544
626
|
|
|
545
627
|
```ts
|
|
546
|
-
type FC<
|
|
547
|
-
readonly app:
|
|
628
|
+
type FC<Signals = {}, AppSignals = {}, Context = {}> = {
|
|
629
|
+
readonly app: AppSignals;
|
|
548
630
|
readonly context: Context;
|
|
549
|
-
readonly request: Request;
|
|
550
|
-
readonly
|
|
551
|
-
|
|
631
|
+
readonly request: Request & { params?: Record<string, string> };
|
|
632
|
+
readonly refs: Record<string, HTMLElement | null>;
|
|
633
|
+
readonly computed: <T = unknown>(fn: () => T) => T;
|
|
634
|
+
readonly effect: (fn: () => void | (() => void)) => void;
|
|
635
|
+
} & Omit<Signals, "app" | "context" | "request" | "computed" | "effect">;
|
|
552
636
|
```
|
|
553
637
|
|
|
554
|
-
### Using
|
|
638
|
+
### Using Signals
|
|
639
|
+
|
|
640
|
+
See the [Using Signals](#using-signals) section for more details on how to use signals in your components.
|
|
555
641
|
|
|
556
|
-
|
|
642
|
+
### Using Refs
|
|
643
|
+
|
|
644
|
+
You can use `this.refs` to access refs in your components. Define refs in your component using the `ref` attribute:
|
|
645
|
+
|
|
646
|
+
```tsx
|
|
647
|
+
function App(this: FC) {
|
|
648
|
+
this.effect(() => {
|
|
649
|
+
this.refs.input?.addEventListener("input", (evt) => {
|
|
650
|
+
console.log("Input changed:", evt.target.value);
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
return (
|
|
655
|
+
<div>
|
|
656
|
+
<input ref={this.refs.input} type="text" />
|
|
657
|
+
<button onClick={() => this.refs.input?.focus()}>Focus</button>
|
|
658
|
+
</div>
|
|
659
|
+
)
|
|
660
|
+
}
|
|
661
|
+
```
|
|
557
662
|
|
|
558
663
|
### Using Context
|
|
559
664
|
|
|
@@ -567,7 +672,7 @@ function Dash(this: FC<{}, {}, { auth: { uuid: string; name: string } }>) {
|
|
|
567
672
|
<h1>Welcome back, {auth.name}!</h1>
|
|
568
673
|
<p>Your UUID is {auth.uuid}</p>
|
|
569
674
|
</div>
|
|
570
|
-
)
|
|
675
|
+
)
|
|
571
676
|
}
|
|
572
677
|
|
|
573
678
|
export default {
|
|
@@ -578,9 +683,9 @@ export default {
|
|
|
578
683
|
{!auth && <p>Please Login</p>}
|
|
579
684
|
{auth && <Dash />}
|
|
580
685
|
</html>
|
|
581
|
-
)
|
|
582
|
-
}
|
|
583
|
-
}
|
|
686
|
+
)
|
|
687
|
+
}
|
|
688
|
+
}
|
|
584
689
|
```
|
|
585
690
|
|
|
586
691
|
### Accessing Request Info
|
|
@@ -597,7 +702,7 @@ function RequestInfo(this: FC) {
|
|
|
597
702
|
<p>{request.url}</p>
|
|
598
703
|
<p>{request.headers.get("user-agent")}</p>
|
|
599
704
|
</div>
|
|
600
|
-
)
|
|
705
|
+
)
|
|
601
706
|
}
|
|
602
707
|
|
|
603
708
|
export default {
|
|
@@ -605,15 +710,15 @@ export default {
|
|
|
605
710
|
<html request={req}>
|
|
606
711
|
<RequestInfo />
|
|
607
712
|
</html>
|
|
608
|
-
)
|
|
609
|
-
}
|
|
713
|
+
)
|
|
714
|
+
}
|
|
610
715
|
```
|
|
611
716
|
|
|
612
717
|
## Streaming Rendering
|
|
613
718
|
|
|
614
719
|
mono-jsx renders your `<html>` as a readable stream, allowing async components to render asynchronously. You can use `placeholder` to display a loading state while waiting for async components to render:
|
|
615
720
|
|
|
616
|
-
```
|
|
721
|
+
```tsx
|
|
617
722
|
async function Sleep({ ms }) {
|
|
618
723
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
619
724
|
return <slot />;
|
|
@@ -626,13 +731,13 @@ export default {
|
|
|
626
731
|
<p>After 1 second</p>
|
|
627
732
|
</Sleep>
|
|
628
733
|
</html>
|
|
629
|
-
)
|
|
630
|
-
}
|
|
734
|
+
)
|
|
735
|
+
}
|
|
631
736
|
```
|
|
632
737
|
|
|
633
738
|
You can set the `rendering` attribute to `"eager"` to force synchronous rendering (the `placeholder` will be ignored):
|
|
634
739
|
|
|
635
|
-
```
|
|
740
|
+
```tsx
|
|
636
741
|
export default {
|
|
637
742
|
fetch: (req) => (
|
|
638
743
|
<html>
|
|
@@ -640,13 +745,13 @@ export default {
|
|
|
640
745
|
<p>After 1 second</p>
|
|
641
746
|
</Sleep>
|
|
642
747
|
</html>
|
|
643
|
-
)
|
|
644
|
-
}
|
|
748
|
+
)
|
|
749
|
+
}
|
|
645
750
|
```
|
|
646
751
|
|
|
647
752
|
You can add the `catch` attribute to handle errors in the async component. The `catch` attribute should be a function that returns a JSX element:
|
|
648
753
|
|
|
649
|
-
```
|
|
754
|
+
```tsx
|
|
650
755
|
async function Hello() {
|
|
651
756
|
throw new Error("Something went wrong!");
|
|
652
757
|
return <p>Hello world!</p>;
|
|
@@ -657,15 +762,212 @@ export default {
|
|
|
657
762
|
<html>
|
|
658
763
|
<Hello catch={err => <p>{err.message}</p>} />
|
|
659
764
|
</html>
|
|
660
|
-
)
|
|
661
|
-
}
|
|
765
|
+
)
|
|
766
|
+
}
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
## Lazy Rendering
|
|
771
|
+
|
|
772
|
+
Since mono-jsx renders html on server side, and no hydration JS sent to client side. To render a component dynamically on client side, you can use the `<component>` element to ask the server to render a component and send the html back to client:
|
|
773
|
+
|
|
774
|
+
```tsx
|
|
775
|
+
export default {
|
|
776
|
+
fetch: (req) => (
|
|
777
|
+
<html components={{ Foo }}>
|
|
778
|
+
<component name="Foo" props={{ /* props for the component */ }} placeholder={<p>Loading...</p>} />
|
|
779
|
+
</html>
|
|
780
|
+
)
|
|
781
|
+
}
|
|
782
|
+
```
|
|
783
|
+
|
|
784
|
+
You can use `<toggle>` element to control when to render the component:
|
|
785
|
+
|
|
786
|
+
```tsx
|
|
787
|
+
async function Lazy(this: FC<{ show: boolean }>, props: { url: string }) {
|
|
788
|
+
this.show = false;
|
|
789
|
+
return (
|
|
790
|
+
<div>
|
|
791
|
+
<toggle value={this.show}>
|
|
792
|
+
<component name="Foo" props={{ /* props for the component */ }} placeholder={<p>Loading...</p>} />
|
|
793
|
+
</toggle>
|
|
794
|
+
<button onClick={() => this.show = true }>Load `Foo` Component</button>
|
|
795
|
+
</div>
|
|
796
|
+
)
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
export default {
|
|
800
|
+
fetch: (req) => (
|
|
801
|
+
<html components={{ Foo }}>
|
|
802
|
+
<Lazy />
|
|
803
|
+
</html>
|
|
804
|
+
)
|
|
805
|
+
}
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
You also can use signal `name` or `props`, change the signal value will trigger the component to re-render with new name or props:
|
|
809
|
+
|
|
810
|
+
```tsx
|
|
811
|
+
import { Profile, Projects, Settings } from "./pages.tsx"
|
|
812
|
+
|
|
813
|
+
async function Dash(this: FC<{ page: "Profile" | "Projects" | "Settings" }>) {
|
|
814
|
+
this.page = "Projects";
|
|
815
|
+
|
|
816
|
+
return (
|
|
817
|
+
<>
|
|
818
|
+
<div class="tab">
|
|
819
|
+
<button onClick={e => this.page = "Profile"}>Profile</botton>
|
|
820
|
+
<button onClick={e => this.page = "Projects"}>Projects</botton>
|
|
821
|
+
<button onClick={e => this.page = "Settings"}>Settings</botton>
|
|
822
|
+
</div>
|
|
823
|
+
<div class="page">
|
|
824
|
+
<component name={this.page} placeholder={<p>Loading...</p>} />
|
|
825
|
+
</div>
|
|
826
|
+
</>
|
|
827
|
+
)
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
export default {
|
|
831
|
+
fetch: (req) => (
|
|
832
|
+
<html components={{ Profile, Projects, Settings }}>
|
|
833
|
+
<Dash />
|
|
834
|
+
</html>
|
|
835
|
+
)
|
|
836
|
+
}
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
## Using Router(SPA)
|
|
840
|
+
|
|
841
|
+
mono-jsx provides a built-in `<router>` element that allows your app to render components based on the current URL. On client side, it hijacks all `click` events on `<a>` elements and asynchronously fetches the route component without reloading the entire page.
|
|
842
|
+
|
|
843
|
+
To use the router, you need to define your routes as a mapping of URL patterns to components and pass it to the `<html>` element as `routes` prop. The `request` prop is also required to match the current URL against the defined routes.
|
|
844
|
+
|
|
845
|
+
```tsx
|
|
846
|
+
const routes = {
|
|
847
|
+
"/": Home,
|
|
848
|
+
"/about": About,
|
|
849
|
+
"/blog": Blog,
|
|
850
|
+
"/post/:id": Post,
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
export default {
|
|
854
|
+
fetch: (req) => (
|
|
855
|
+
<html request={req} routes={routes}>
|
|
856
|
+
<header>
|
|
857
|
+
<nav>
|
|
858
|
+
<a href="/">Home</a>
|
|
859
|
+
<a href="/about">About</a>
|
|
860
|
+
<a href="/blog">Blog</a>
|
|
861
|
+
</nav>
|
|
862
|
+
</header>
|
|
863
|
+
<router />
|
|
864
|
+
</html>
|
|
865
|
+
)
|
|
866
|
+
}
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
mono-jsx router requires [URLPattern](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) to match a route:
|
|
870
|
+
|
|
871
|
+
- ✅ Deno
|
|
872
|
+
- ✅ Cloudflare Workers
|
|
873
|
+
- ✅ Nodejs (>= 24)
|
|
874
|
+
|
|
875
|
+
For Bun users, mono-jsx provides a `monoRoutes` function that uses Bun's builtin routing:
|
|
876
|
+
|
|
877
|
+
```tsx
|
|
878
|
+
// bun app.tsx
|
|
879
|
+
|
|
880
|
+
import { monoRoutes } from "mono-jsx"
|
|
881
|
+
|
|
882
|
+
const routes = {
|
|
883
|
+
"/": Home,
|
|
884
|
+
"/about": About,
|
|
885
|
+
"/blog": Blog,
|
|
886
|
+
"/post/:id": Post,
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
export default {
|
|
890
|
+
routes: monoRoutes(routes, (request) => (
|
|
891
|
+
<html request={request}>
|
|
892
|
+
<router />
|
|
893
|
+
</html>
|
|
894
|
+
))
|
|
895
|
+
}
|
|
896
|
+
```
|
|
897
|
+
|
|
898
|
+
### Using Route `params`
|
|
899
|
+
|
|
900
|
+
When you define a route with a parameter (e.g., `/post/:id`), mono-jsx will automatically extract the parameter from the URL and make it available in the route component. The `params` object is available in the `request` property of the component's `this` context.
|
|
901
|
+
You can access the `params` object in your route components to get the values of the parameters defined in the route pattern:
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
```tsx
|
|
905
|
+
// router pattern: "/post/:id"
|
|
906
|
+
function Post(this: FC) {
|
|
907
|
+
this.request.url // "http://localhost:3000/post/123"
|
|
908
|
+
this.request.params?.id // "123"
|
|
909
|
+
}
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
### Using DB/Storage in Route Components
|
|
913
|
+
|
|
914
|
+
Route components are always rendered on server-side, you can use any database or storage API to fetch data in your route components. For example, if you're using a SQL database, you can use the `sql` tag function to query the database:
|
|
915
|
+
|
|
916
|
+
```tsx
|
|
917
|
+
async function Post(this: FC) {
|
|
918
|
+
const post = await sql`SELECT * FROM posts WHERE id = ${ this.request.params!.id }`
|
|
919
|
+
return (
|
|
920
|
+
<article>
|
|
921
|
+
<h2>{post.title}<h2>
|
|
922
|
+
<div>html`${post.content}`</div>
|
|
923
|
+
</article>
|
|
924
|
+
)
|
|
925
|
+
}
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
### Nav Links
|
|
929
|
+
|
|
930
|
+
Links under a `<nav>` element will be treated as navigation links by the router. When the `href` of a link matches a route, A active class will be added to the link element. By default, the active class is `active`, but you can customize it by setting the `data-active-class` attribute on the `<nav>` element. You can also add a custom style for the active link using nested CSS selectors in the `style` attribute of the `<nav>` element:
|
|
931
|
+
|
|
932
|
+
```tsx
|
|
933
|
+
export default {
|
|
934
|
+
fetch: (req) => (
|
|
935
|
+
<html request={req} routes={routes}>
|
|
936
|
+
<header>
|
|
937
|
+
<nav style={{ "& a.active": { fontWeight: "bold" } }} data-active-class="active">
|
|
938
|
+
<a href="/">Home</a>
|
|
939
|
+
<a href="/about">About</a>
|
|
940
|
+
<a href="/blog">Blog</a>
|
|
941
|
+
</nav>
|
|
942
|
+
</header>
|
|
943
|
+
<router />
|
|
944
|
+
</html>
|
|
945
|
+
)
|
|
946
|
+
}
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
### Setting Fallback(404) Content
|
|
950
|
+
|
|
951
|
+
You can add fallback(404) content to the `<router>` element as children, which will be displayed when no route matches the current URL:
|
|
952
|
+
|
|
953
|
+
```tsx
|
|
954
|
+
export default {
|
|
955
|
+
fetch: (req) => (
|
|
956
|
+
<html request={req} routes={routes}>
|
|
957
|
+
<router>
|
|
958
|
+
<p>Page Not Found</p>
|
|
959
|
+
<p>Back to <a href="/">Home</a></p>
|
|
960
|
+
</router>
|
|
961
|
+
</html>
|
|
962
|
+
)
|
|
963
|
+
}
|
|
662
964
|
```
|
|
663
965
|
|
|
664
966
|
## Customizing html Response
|
|
665
967
|
|
|
666
968
|
You can add `status` or `headers` attributes to the root `<html>` element to customize the http response:
|
|
667
969
|
|
|
668
|
-
```
|
|
970
|
+
```tsx
|
|
669
971
|
export default {
|
|
670
972
|
fetch: (req) => (
|
|
671
973
|
<html
|
|
@@ -678,15 +980,15 @@ export default {
|
|
|
678
980
|
>
|
|
679
981
|
<h1>Page Not Found</h1>
|
|
680
982
|
</html>
|
|
681
|
-
)
|
|
682
|
-
}
|
|
983
|
+
)
|
|
984
|
+
}
|
|
683
985
|
```
|
|
684
986
|
|
|
685
987
|
### Using htmx
|
|
686
988
|
|
|
687
989
|
mono-jsx integrates with [htmx](https://htmx.org/) and [typed-htmx](https://github.com/Desdaemon/typed-htmx). To use htmx, add the `htmx` attribute to the root `<html>` element:
|
|
688
990
|
|
|
689
|
-
```
|
|
991
|
+
```tsx
|
|
690
992
|
export default {
|
|
691
993
|
fetch: (req) => {
|
|
692
994
|
const url = new URL(req.url);
|
|
@@ -705,16 +1007,16 @@ export default {
|
|
|
705
1007
|
Click Me
|
|
706
1008
|
</button>
|
|
707
1009
|
</html>
|
|
708
|
-
)
|
|
709
|
-
}
|
|
710
|
-
}
|
|
1010
|
+
)
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
711
1013
|
```
|
|
712
1014
|
|
|
713
1015
|
#### Adding htmx Extensions
|
|
714
1016
|
|
|
715
1017
|
You can add htmx [extensions](https://htmx.org/docs/#extensions) by adding the `htmx-ext-*` attribute to the root `<html>` element:
|
|
716
1018
|
|
|
717
|
-
```
|
|
1019
|
+
```tsx
|
|
718
1020
|
export default {
|
|
719
1021
|
fetch: (req) => (
|
|
720
1022
|
<html htmx htmx-ext-response-targets htmx-ext-ws>
|
|
@@ -722,15 +1024,15 @@ export default {
|
|
|
722
1024
|
Click Me
|
|
723
1025
|
</button>
|
|
724
1026
|
</html>
|
|
725
|
-
)
|
|
726
|
-
}
|
|
1027
|
+
)
|
|
1028
|
+
}
|
|
727
1029
|
```
|
|
728
1030
|
|
|
729
1031
|
#### Specifying htmx Version
|
|
730
1032
|
|
|
731
1033
|
You can specify the htmx version by setting the `htmx` attribute to a specific version:
|
|
732
1034
|
|
|
733
|
-
```
|
|
1035
|
+
```tsx
|
|
734
1036
|
export default {
|
|
735
1037
|
fetch: (req) => (
|
|
736
1038
|
<html htmx="2.0.4" htmx-ext-response-targets="2.0.2" htmx-ext-ws="2.0.2">
|
|
@@ -738,15 +1040,15 @@ export default {
|
|
|
738
1040
|
Click Me
|
|
739
1041
|
</button>
|
|
740
1042
|
</html>
|
|
741
|
-
)
|
|
742
|
-
}
|
|
1043
|
+
)
|
|
1044
|
+
}
|
|
743
1045
|
```
|
|
744
1046
|
|
|
745
1047
|
#### Installing htmx Manually
|
|
746
1048
|
|
|
747
1049
|
By default, mono-jsx installs htmx from [esm.sh](https://esm.sh/) CDN when you set the `htmx` attribute. You can also install htmx manually with your own CDN or local copy:
|
|
748
1050
|
|
|
749
|
-
```
|
|
1051
|
+
```tsx
|
|
750
1052
|
export default {
|
|
751
1053
|
fetch: (req) => (
|
|
752
1054
|
<html>
|
|
@@ -760,8 +1062,8 @@ export default {
|
|
|
760
1062
|
</button>
|
|
761
1063
|
</body>
|
|
762
1064
|
</html>
|
|
763
|
-
)
|
|
764
|
-
}
|
|
1065
|
+
)
|
|
1066
|
+
}
|
|
765
1067
|
```
|
|
766
1068
|
|
|
767
1069
|
## License
|