mono-jsx 0.3.4 → 0.5.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 +269 -127
- package/jsx-runtime.mjs +587 -507
- package/package.json +1 -1
- package/setup.mjs +1 -0
- package/types/html.d.ts +20 -11
- package/types/jsx.d.ts +11 -5
- package/types/mono.d.ts +31 -20
- package/types/render.d.ts +2 -2
package/README.md
CHANGED
|
@@ -6,8 +6,8 @@ mono-jsx is a JSX runtime that renders `<html>` element to `Response` object in
|
|
|
6
6
|
|
|
7
7
|
- 🚀 No build step needed
|
|
8
8
|
- 🦋 Lightweight (8KB gzipped), zero dependencies
|
|
9
|
-
-
|
|
10
|
-
-
|
|
9
|
+
- 🚦 Signals as reactive primitives
|
|
10
|
+
- 🗂️ Complete Web API TypeScript definitions
|
|
11
11
|
- ⏳ Streaming rendering
|
|
12
12
|
- 🥷 [htmx](#using-htmx) integration
|
|
13
13
|
- 🌎 Universal, works in Node.js, Deno, Bun, Cloudflare Workers, etc.
|
|
@@ -72,8 +72,8 @@ export default {
|
|
|
72
72
|
<html>
|
|
73
73
|
<h1>Welcome to mono-jsx!</h1>
|
|
74
74
|
</html>
|
|
75
|
-
)
|
|
76
|
-
}
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
For Deno/Bun users, you can run the `app.tsx` directly:
|
|
@@ -173,7 +173,7 @@ function Container() {
|
|
|
173
173
|
{/* Named slot */}
|
|
174
174
|
<slot name="desc" />
|
|
175
175
|
</div>
|
|
176
|
-
)
|
|
176
|
+
)
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
function App() {
|
|
@@ -184,7 +184,7 @@ function App() {
|
|
|
184
184
|
{/* This goes to the default slot */}
|
|
185
185
|
<h1>Hello world!</h1>
|
|
186
186
|
</Container>
|
|
187
|
-
)
|
|
187
|
+
)
|
|
188
188
|
}
|
|
189
189
|
```
|
|
190
190
|
|
|
@@ -207,7 +207,7 @@ function App() {
|
|
|
207
207
|
<style>{css`h1 { font-size: 3rem; }`}</style>
|
|
208
208
|
<script>{js`console.log("Hello world!")`}</script>
|
|
209
209
|
</head>
|
|
210
|
-
)
|
|
210
|
+
)
|
|
211
211
|
}
|
|
212
212
|
```
|
|
213
213
|
|
|
@@ -224,7 +224,7 @@ function Button() {
|
|
|
224
224
|
<button onClick={(evt) => alert("BOOM!")}>
|
|
225
225
|
Click Me
|
|
226
226
|
</button>
|
|
227
|
-
)
|
|
227
|
+
)
|
|
228
228
|
}
|
|
229
229
|
```
|
|
230
230
|
|
|
@@ -234,9 +234,10 @@ function Button() {
|
|
|
234
234
|
```tsx
|
|
235
235
|
import { doSomething } from "some-library";
|
|
236
236
|
|
|
237
|
-
function Button(this: FC
|
|
238
|
-
|
|
239
|
-
|
|
237
|
+
function Button(this: FC<{ count: 0 }>, props: { role: string }) {
|
|
238
|
+
const message = "BOOM!"; // server-side variable
|
|
239
|
+
this.count = 0; // initialize a signal
|
|
240
|
+
console.log(message); // only prints on server-side
|
|
240
241
|
return (
|
|
241
242
|
<button
|
|
242
243
|
role={props.role}
|
|
@@ -247,137 +248,230 @@ function Button(this: FC, props: { role: string }) {
|
|
|
247
248
|
Deno.exit(0); // ❌ `Deno` is unavailable in the browser
|
|
248
249
|
document.title = "BOOM!"; // ✅ `document` is a browser API
|
|
249
250
|
console.log(evt.target); // ✅ `evt` is the event object
|
|
250
|
-
this.count++; // ✅ update the
|
|
251
|
+
this.count++; // ✅ update the `count` signal
|
|
251
252
|
}}
|
|
252
253
|
>
|
|
253
|
-
|
|
254
|
+
<slot />
|
|
254
255
|
</button>
|
|
255
|
-
)
|
|
256
|
+
)
|
|
256
257
|
}
|
|
257
258
|
```
|
|
258
259
|
|
|
259
|
-
|
|
260
|
+
### Using `<form>` Element
|
|
260
261
|
|
|
261
|
-
|
|
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
|
-
```
|
|
270
|
-
|
|
271
|
-
mono-jsx also accepts functions for the `action` property on `form` elements, which will be called on form submission:
|
|
262
|
+
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
263
|
|
|
273
264
|
```tsx
|
|
274
265
|
function App() {
|
|
275
266
|
return (
|
|
276
|
-
<form action={(data: FormData
|
|
267
|
+
<form action={(data: FormData) => console.log(data.get("name"))}>
|
|
277
268
|
<input type="text" name="name" />
|
|
278
269
|
<button type="submit">Submit</button>
|
|
279
270
|
</form>
|
|
280
|
-
)
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Async Components
|
|
276
|
+
|
|
277
|
+
mono-jsx supports async components that return a `Promise` or an async function. With [streaming rendering](#streaming-rendering), async components are rendered asynchronously, allowing you to fetch data or perform other async operations before rendering the component.
|
|
278
|
+
|
|
279
|
+
```tsx
|
|
280
|
+
async function Loader(props: { url: string }) {
|
|
281
|
+
const data = await fetch(url).then((res) => res.json());
|
|
282
|
+
return <JsonViewer data={data} />;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export default {
|
|
286
|
+
fetch: (req) => (
|
|
287
|
+
<html>
|
|
288
|
+
<Loader url="https://api.example.com/data" placeholder={<p>Loading...</p>} />
|
|
289
|
+
</html>
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
You can also use async generators to yield multiple elements over time. This is useful for streaming rendering of LLM tokens:
|
|
295
|
+
|
|
296
|
+
```jsx
|
|
297
|
+
async function* Chat(props: { prompt: string }) {
|
|
298
|
+
const stream = await openai.chat.completions.create({
|
|
299
|
+
model: "gpt-4",
|
|
300
|
+
messages: [{ role: "user", content: prompt }],
|
|
301
|
+
stream: true,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
for await (const event of stream) {
|
|
305
|
+
const text = event.choices[0]?.delta.content;
|
|
306
|
+
if (text) {
|
|
307
|
+
yield <span>{text}</span>;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export default {
|
|
313
|
+
fetch: (req) => (
|
|
314
|
+
<html>
|
|
315
|
+
<Chat prompt="Tell me a story" placeholder={<span style="color:grey">●</span>} />
|
|
316
|
+
</html>
|
|
317
|
+
)
|
|
281
318
|
}
|
|
282
319
|
```
|
|
283
320
|
|
|
284
|
-
##
|
|
321
|
+
## Using Signals
|
|
285
322
|
|
|
286
|
-
mono-jsx
|
|
323
|
+
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.
|
|
287
324
|
|
|
288
|
-
### Using Component
|
|
325
|
+
### Using Component Signals
|
|
289
326
|
|
|
290
|
-
You can use the `this` keyword in your components to manage
|
|
327
|
+
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:
|
|
291
328
|
|
|
292
329
|
```tsx
|
|
293
330
|
function Counter(
|
|
294
331
|
this: FC<{ count: number }>,
|
|
295
332
|
props: { initialCount?: number },
|
|
296
333
|
) {
|
|
297
|
-
// Initialize
|
|
334
|
+
// Initialize a singal
|
|
298
335
|
this.count = props.initialCount ?? 0;
|
|
299
336
|
|
|
300
337
|
return (
|
|
301
338
|
<div>
|
|
302
|
-
{/* render
|
|
339
|
+
{/* render singal */}
|
|
303
340
|
<span>{this.count}</span>
|
|
304
341
|
|
|
305
|
-
{/* Update
|
|
342
|
+
{/* Update singal to trigger re-render */}
|
|
306
343
|
<button onClick={() => this.count--}>-</button>
|
|
307
344
|
<button onClick={() => this.count++}>+</button>
|
|
308
345
|
</div>
|
|
309
|
-
)
|
|
346
|
+
)
|
|
310
347
|
}
|
|
311
348
|
```
|
|
312
349
|
|
|
313
|
-
### Using App
|
|
350
|
+
### Using App Signals
|
|
314
351
|
|
|
315
|
-
You can define app
|
|
352
|
+
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:
|
|
316
353
|
|
|
317
354
|
```tsx
|
|
318
|
-
|
|
355
|
+
interface AppSignals {
|
|
356
|
+
themeColor: string;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function Header(this: FC<{}, AppSignals>) {
|
|
319
360
|
return (
|
|
320
361
|
<header>
|
|
321
|
-
<h1
|
|
362
|
+
<h1 style={this.computed(() => ({ color: this.app.themeColor }))}>Welcome to mono-jsx!</h1>
|
|
322
363
|
</header>
|
|
323
|
-
)
|
|
364
|
+
)
|
|
324
365
|
}
|
|
325
366
|
|
|
326
|
-
function Footer(this: FC<{},
|
|
367
|
+
function Footer(this: FC<{}, AppSignals>) {
|
|
327
368
|
return (
|
|
328
369
|
<footer>
|
|
329
|
-
<p
|
|
370
|
+
<p style={this.computed(() => ({ color: this.app.themeColor }))}>(c) 2025 mono-jsx.</p>
|
|
330
371
|
</footer>
|
|
331
|
-
)
|
|
372
|
+
)
|
|
332
373
|
}
|
|
333
374
|
|
|
334
|
-
function Main(this: FC<{},
|
|
375
|
+
function Main(this: FC<{}, AppSignals>) {
|
|
335
376
|
return (
|
|
336
377
|
<main>
|
|
337
|
-
<
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
placeholder="Enter a new title"
|
|
342
|
-
/>
|
|
378
|
+
<p>
|
|
379
|
+
<label>Theme Color: </label>
|
|
380
|
+
<input type="color" onInput={({ target }) => this.app.themeColor = target.value}/>
|
|
381
|
+
</p>
|
|
343
382
|
</main>
|
|
344
|
-
)
|
|
383
|
+
)
|
|
345
384
|
}
|
|
346
385
|
|
|
347
386
|
export default {
|
|
348
387
|
fetch: (req) => (
|
|
349
|
-
<html
|
|
388
|
+
<html app={{ themeColor: "#232323" }}>
|
|
350
389
|
<Header />
|
|
351
390
|
<Main />
|
|
352
391
|
<Footer />
|
|
353
392
|
</html>
|
|
354
|
-
)
|
|
355
|
-
}
|
|
393
|
+
)
|
|
394
|
+
}
|
|
356
395
|
```
|
|
357
396
|
|
|
358
|
-
### Using Computed
|
|
397
|
+
### Using Computed Signals
|
|
359
398
|
|
|
360
|
-
You can use `this.computed` to create
|
|
399
|
+
You can use `this.computed` to create a derived signal based on other signals:
|
|
361
400
|
|
|
362
401
|
```tsx
|
|
363
402
|
function App(this: FC<{ input: string }>) {
|
|
364
|
-
this.input = "Welcome to mono-jsx
|
|
403
|
+
this.input = "Welcome to mono-jsx";
|
|
365
404
|
return (
|
|
366
405
|
<div>
|
|
367
406
|
<h1>{this.computed(() => this.input + "!")}</h1>
|
|
368
407
|
|
|
369
408
|
<form action={(fd) => this.input = fd.get("input") as string}>
|
|
370
|
-
<input type="text" name="input" value={this.input} />
|
|
409
|
+
<input type="text" name="input" value={"" + this.input} />
|
|
371
410
|
<button type="submit">Submit</button>
|
|
372
411
|
</form>
|
|
373
412
|
</div>
|
|
374
|
-
)
|
|
413
|
+
)
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### Using Effects
|
|
418
|
+
|
|
419
|
+
You can use `this.effect` to create side effects based on signals. The effect will run whenever the signal changes:
|
|
420
|
+
|
|
421
|
+
```tsx
|
|
422
|
+
function App(this: FC<{ count: number }>) {
|
|
423
|
+
this.count = 0;
|
|
424
|
+
|
|
425
|
+
this.effect(() => {
|
|
426
|
+
console.log("Count changed:", this.count);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
return (
|
|
430
|
+
<div>
|
|
431
|
+
<span>{this.count}</span>
|
|
432
|
+
<button onClick={() => this.count++}>+</button>
|
|
433
|
+
</div>
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
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:
|
|
439
|
+
|
|
440
|
+
```tsx
|
|
441
|
+
function Counter(this: FC<{ count: number }>) {
|
|
442
|
+
this.count = 0;
|
|
443
|
+
|
|
444
|
+
this.effect(() => {
|
|
445
|
+
const interval = setInterval(() => {
|
|
446
|
+
this.count++;
|
|
447
|
+
}, 1000);
|
|
448
|
+
|
|
449
|
+
return () => clearInterval(interval);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
return (
|
|
453
|
+
<div>
|
|
454
|
+
<span>{this.count}</span>
|
|
455
|
+
</div>
|
|
456
|
+
)
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function App(this: FC<{ show: boolean }>) {
|
|
460
|
+
this.show = true
|
|
461
|
+
return (
|
|
462
|
+
<div>
|
|
463
|
+
<toggle show={this.show}>
|
|
464
|
+
<Foo />
|
|
465
|
+
</toggle>
|
|
466
|
+
<button onClick={e => this.show = !this.show }>{this.computed(() => this.show ? 'Hide': 'Show')}</button>
|
|
467
|
+
</div>
|
|
468
|
+
)
|
|
375
469
|
}
|
|
376
470
|
```
|
|
377
471
|
|
|
378
|
-
### Using `<toggle>` Element with
|
|
472
|
+
### Using `<toggle>` Element with Signals
|
|
379
473
|
|
|
380
|
-
The `<toggle>` element conditionally renders content based on the
|
|
474
|
+
The `<toggle>` element conditionally renders content based on the `show` prop:
|
|
381
475
|
|
|
382
476
|
```tsx
|
|
383
477
|
function App(this: FC<{ show: boolean }>) {
|
|
@@ -389,7 +483,7 @@ function App(this: FC<{ show: boolean }>) {
|
|
|
389
483
|
|
|
390
484
|
return (
|
|
391
485
|
<div>
|
|
392
|
-
<toggle
|
|
486
|
+
<toggle show={this.show}>
|
|
393
487
|
<h1>Welcome to mono-jsx!</h1>
|
|
394
488
|
</toggle>
|
|
395
489
|
|
|
@@ -397,16 +491,16 @@ function App(this: FC<{ show: boolean }>) {
|
|
|
397
491
|
{this.computed(() => this.show ? "Hide" : "Show")}
|
|
398
492
|
</button>
|
|
399
493
|
</div>
|
|
400
|
-
)
|
|
494
|
+
)
|
|
401
495
|
}
|
|
402
496
|
```
|
|
403
497
|
|
|
404
|
-
### Using `<switch>` Element with
|
|
498
|
+
### Using `<switch>` Element with Signals
|
|
405
499
|
|
|
406
|
-
The `<switch>` element renders different content based on the value of a
|
|
500
|
+
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:
|
|
407
501
|
|
|
408
502
|
```tsx
|
|
409
|
-
function App(this: FC<{ lang: "en" | "zh" | "
|
|
503
|
+
function App(this: FC<{ lang: "en" | "zh" | "🙂" }>) {
|
|
410
504
|
this.lang = "en";
|
|
411
505
|
|
|
412
506
|
return (
|
|
@@ -419,19 +513,19 @@ function App(this: FC<{ lang: "en" | "zh" | "emoji" }>) {
|
|
|
419
513
|
<p>
|
|
420
514
|
<button onClick={() => this.lang = "en"}>English</button>
|
|
421
515
|
<button onClick={() => this.lang = "zh"}>中文</button>
|
|
422
|
-
<button onClick={() => this.lang = "
|
|
516
|
+
<button onClick={() => this.lang = "🙂"}>🙂</button>
|
|
423
517
|
</p>
|
|
424
518
|
</div>
|
|
425
|
-
)
|
|
519
|
+
)
|
|
426
520
|
}
|
|
427
521
|
```
|
|
428
522
|
|
|
429
|
-
### Limitation of
|
|
523
|
+
### Limitation of Signals
|
|
430
524
|
|
|
431
|
-
1\.
|
|
525
|
+
1\. Arrow function are non-stateful components.
|
|
432
526
|
|
|
433
527
|
```tsx
|
|
434
|
-
// ❌ Won't work -
|
|
528
|
+
// ❌ Won't work - use `this` in a non-stateful component
|
|
435
529
|
const App = () => {
|
|
436
530
|
this.count = 0;
|
|
437
531
|
return (
|
|
@@ -439,7 +533,7 @@ const App = () => {
|
|
|
439
533
|
<span>{this.count}</span>
|
|
440
534
|
<button onClick={() => this.count++}>+</button>
|
|
441
535
|
</div>
|
|
442
|
-
)
|
|
536
|
+
)
|
|
443
537
|
};
|
|
444
538
|
|
|
445
539
|
// ✅ Works correctly
|
|
@@ -450,16 +544,16 @@ function App(this: FC) {
|
|
|
450
544
|
<span>{this.count}</span>
|
|
451
545
|
<button onClick={() => this.count++}>+</button>
|
|
452
546
|
</div>
|
|
453
|
-
)
|
|
547
|
+
)
|
|
454
548
|
}
|
|
455
549
|
```
|
|
456
550
|
|
|
457
|
-
2\.
|
|
551
|
+
2\. Signals cannot be computed outside of the `this.computed` method.
|
|
458
552
|
|
|
459
553
|
```tsx
|
|
460
|
-
// ❌ Won't work -
|
|
554
|
+
// ❌ Won't work - updates of a signal won't refresh the view
|
|
461
555
|
function App(this: FC<{ message: string }>) {
|
|
462
|
-
this.message = "Welcome to mono-jsx
|
|
556
|
+
this.message = "Welcome to mono-jsx";
|
|
463
557
|
return (
|
|
464
558
|
<div>
|
|
465
559
|
<h1 title={this.message + "!"}>{this.message + "!"}</h1>
|
|
@@ -467,12 +561,12 @@ function App(this: FC<{ message: string }>) {
|
|
|
467
561
|
Click Me
|
|
468
562
|
</button>
|
|
469
563
|
</div>
|
|
470
|
-
)
|
|
564
|
+
)
|
|
471
565
|
}
|
|
472
566
|
|
|
473
567
|
// ✅ Works correctly
|
|
474
568
|
function App(this: FC) {
|
|
475
|
-
this.message = "Welcome to mono-jsx
|
|
569
|
+
this.message = "Welcome to mono-jsx";
|
|
476
570
|
return (
|
|
477
571
|
<div>
|
|
478
572
|
<h1 title={this.computed(() => this.message + "!")}>{this.computed(() => this.message + "!")}</h1>
|
|
@@ -480,33 +574,89 @@ function App(this: FC) {
|
|
|
480
574
|
Click Me
|
|
481
575
|
</button>
|
|
482
576
|
</div>
|
|
483
|
-
)
|
|
577
|
+
)
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
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:
|
|
582
|
+
|
|
583
|
+
```tsx
|
|
584
|
+
// ❌ Won't work - throws `Deno is not defined` when the button is clicked
|
|
585
|
+
function App(this: FC<{ message: string }>) {
|
|
586
|
+
this.message = "Welcome to mono-jsx";
|
|
587
|
+
return (
|
|
588
|
+
<div>
|
|
589
|
+
<h1>{this.computed(() => this.message + "! (Deno " + Deno.version.deno + ")")}</h1>
|
|
590
|
+
<button onClick={() => this.message = "Clicked"}>
|
|
591
|
+
Click Me
|
|
592
|
+
</button>
|
|
593
|
+
</div>
|
|
594
|
+
)
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// ✅ Works correctly
|
|
598
|
+
function App(this: FC<{ message: string, denoVersion: string }>) {
|
|
599
|
+
this.denoVersion = Deno.version.deno;
|
|
600
|
+
this.message = "Welcome to mono-jsx";
|
|
601
|
+
return (
|
|
602
|
+
<div>
|
|
603
|
+
<h1>{this.computed(() => this.message + "! (Deno " + this.denoVersion + ")")}</h1>
|
|
604
|
+
<button onClick={() => this.message = "Clicked"}>
|
|
605
|
+
Click Me
|
|
606
|
+
</button>
|
|
607
|
+
</div>
|
|
608
|
+
)
|
|
484
609
|
}
|
|
485
610
|
```
|
|
486
611
|
|
|
487
612
|
## Using `this` in Components
|
|
488
613
|
|
|
489
|
-
mono-jsx binds a
|
|
614
|
+
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.
|
|
490
615
|
|
|
491
|
-
The `this` object
|
|
616
|
+
The `this` object has the following built-in properties:
|
|
492
617
|
|
|
493
|
-
- `app`: The app
|
|
618
|
+
- `app`: The app signals defined on the root `<html>` element.
|
|
494
619
|
- `context`: The context defined on the root `<html>` element.
|
|
495
620
|
- `request`: The request object from the `fetch` handler.
|
|
496
|
-
- `
|
|
621
|
+
- `refs`: A map of refs defined in the component.
|
|
622
|
+
- `computed`: A method to create a computed signal.
|
|
623
|
+
- `effect`: A method to create side effects.
|
|
497
624
|
|
|
498
625
|
```ts
|
|
499
|
-
type FC<
|
|
500
|
-
readonly app:
|
|
626
|
+
type FC<Signals = {}, AppSignals = {}, Context = {}> = {
|
|
627
|
+
readonly app: AppSignals;
|
|
501
628
|
readonly context: Context;
|
|
502
629
|
readonly request: Request;
|
|
503
|
-
readonly
|
|
504
|
-
|
|
630
|
+
readonly refs: Record<string, HTMLElement | null>;
|
|
631
|
+
readonly computed: <T = unknown>(fn: () => T) => T;
|
|
632
|
+
readonly effect: (fn: () => void | (() => void)) => void;
|
|
633
|
+
} & Omit<Signals, "app" | "context" | "request" | "computed" | "effect">;
|
|
505
634
|
```
|
|
506
635
|
|
|
507
|
-
### Using
|
|
636
|
+
### Using Signals
|
|
637
|
+
|
|
638
|
+
See the [Using Signals](#using-signals) section for more details on how to use signals in your components.
|
|
639
|
+
|
|
640
|
+
### Using Refs
|
|
508
641
|
|
|
509
|
-
|
|
642
|
+
You can use `this.refs` to access refs in your components. Define refs in your component using the `ref` attribute:
|
|
643
|
+
|
|
644
|
+
```tsx
|
|
645
|
+
function App(this: FC) {
|
|
646
|
+
this.effect(() => {
|
|
647
|
+
this.refs.input?.addEventListener("input", (evt) => {
|
|
648
|
+
console.log("Input changed:", evt.target.value);
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
return (
|
|
653
|
+
<div>
|
|
654
|
+
<input ref={this.refs.input} type="text" />
|
|
655
|
+
<button onClick={() => this.refs.input?.focus()}>Focus</button>
|
|
656
|
+
</div>
|
|
657
|
+
)
|
|
658
|
+
}
|
|
659
|
+
```
|
|
510
660
|
|
|
511
661
|
### Using Context
|
|
512
662
|
|
|
@@ -520,7 +670,7 @@ function Dash(this: FC<{}, {}, { auth: { uuid: string; name: string } }>) {
|
|
|
520
670
|
<h1>Welcome back, {auth.name}!</h1>
|
|
521
671
|
<p>Your UUID is {auth.uuid}</p>
|
|
522
672
|
</div>
|
|
523
|
-
)
|
|
673
|
+
)
|
|
524
674
|
}
|
|
525
675
|
|
|
526
676
|
export default {
|
|
@@ -531,9 +681,9 @@ export default {
|
|
|
531
681
|
{!auth && <p>Please Login</p>}
|
|
532
682
|
{auth && <Dash />}
|
|
533
683
|
</html>
|
|
534
|
-
)
|
|
535
|
-
}
|
|
536
|
-
}
|
|
684
|
+
)
|
|
685
|
+
}
|
|
686
|
+
}
|
|
537
687
|
```
|
|
538
688
|
|
|
539
689
|
### Accessing Request Info
|
|
@@ -550,7 +700,7 @@ function RequestInfo(this: FC) {
|
|
|
550
700
|
<p>{request.url}</p>
|
|
551
701
|
<p>{request.headers.get("user-agent")}</p>
|
|
552
702
|
</div>
|
|
553
|
-
)
|
|
703
|
+
)
|
|
554
704
|
}
|
|
555
705
|
|
|
556
706
|
export default {
|
|
@@ -558,8 +708,8 @@ export default {
|
|
|
558
708
|
<html request={req}>
|
|
559
709
|
<RequestInfo />
|
|
560
710
|
</html>
|
|
561
|
-
)
|
|
562
|
-
}
|
|
711
|
+
)
|
|
712
|
+
}
|
|
563
713
|
```
|
|
564
714
|
|
|
565
715
|
## Streaming Rendering
|
|
@@ -575,38 +725,29 @@ async function Sleep({ ms }) {
|
|
|
575
725
|
export default {
|
|
576
726
|
fetch: (req) => (
|
|
577
727
|
<html>
|
|
578
|
-
<
|
|
579
|
-
|
|
580
|
-
<Sleep ms={1000} placeholder={<p>Sleeping...</p>}>
|
|
728
|
+
<Sleep ms={1000} placeholder={<p>Loading...</p>}>
|
|
581
729
|
<p>After 1 second</p>
|
|
582
730
|
</Sleep>
|
|
583
731
|
</html>
|
|
584
|
-
)
|
|
585
|
-
}
|
|
732
|
+
)
|
|
733
|
+
}
|
|
586
734
|
```
|
|
587
735
|
|
|
588
736
|
You can set the `rendering` attribute to `"eager"` to force synchronous rendering (the `placeholder` will be ignored):
|
|
589
737
|
|
|
590
738
|
```jsx
|
|
591
|
-
async function Sleep({ ms }) {
|
|
592
|
-
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
593
|
-
return <slot />;
|
|
594
|
-
}
|
|
595
|
-
|
|
596
739
|
export default {
|
|
597
740
|
fetch: (req) => (
|
|
598
741
|
<html>
|
|
599
|
-
<h1>Welcome to mono-jsx!</h1>
|
|
600
|
-
|
|
601
742
|
<Sleep ms={1000} rendering="eager">
|
|
602
743
|
<p>After 1 second</p>
|
|
603
744
|
</Sleep>
|
|
604
745
|
</html>
|
|
605
|
-
)
|
|
606
|
-
}
|
|
746
|
+
)
|
|
747
|
+
}
|
|
607
748
|
```
|
|
608
749
|
|
|
609
|
-
You can add the `catch` attribute to handle errors in async
|
|
750
|
+
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:
|
|
610
751
|
|
|
611
752
|
```jsx
|
|
612
753
|
async function Hello() {
|
|
@@ -617,15 +758,15 @@ async function Hello() {
|
|
|
617
758
|
export default {
|
|
618
759
|
fetch: (req) => (
|
|
619
760
|
<html>
|
|
620
|
-
<Hello catch={err => <p>{err.
|
|
761
|
+
<Hello catch={err => <p>{err.message}</p>} />
|
|
621
762
|
</html>
|
|
622
|
-
)
|
|
623
|
-
}
|
|
763
|
+
)
|
|
764
|
+
}
|
|
624
765
|
```
|
|
625
766
|
|
|
626
767
|
## Customizing html Response
|
|
627
768
|
|
|
628
|
-
|
|
769
|
+
You can add `status` or `headers` attributes to the root `<html>` element to customize the http response:
|
|
629
770
|
|
|
630
771
|
```jsx
|
|
631
772
|
export default {
|
|
@@ -635,12 +776,13 @@ export default {
|
|
|
635
776
|
headers={{
|
|
636
777
|
cacheControl: "public, max-age=0, must-revalidate",
|
|
637
778
|
setCookie: "name=value",
|
|
779
|
+
"x-foo": "bar",
|
|
638
780
|
}}
|
|
639
781
|
>
|
|
640
782
|
<h1>Page Not Found</h1>
|
|
641
783
|
</html>
|
|
642
|
-
)
|
|
643
|
-
}
|
|
784
|
+
)
|
|
785
|
+
}
|
|
644
786
|
```
|
|
645
787
|
|
|
646
788
|
### Using htmx
|
|
@@ -666,9 +808,9 @@ export default {
|
|
|
666
808
|
Click Me
|
|
667
809
|
</button>
|
|
668
810
|
</html>
|
|
669
|
-
)
|
|
670
|
-
}
|
|
671
|
-
}
|
|
811
|
+
)
|
|
812
|
+
}
|
|
813
|
+
}
|
|
672
814
|
```
|
|
673
815
|
|
|
674
816
|
#### Adding htmx Extensions
|
|
@@ -683,8 +825,8 @@ export default {
|
|
|
683
825
|
Click Me
|
|
684
826
|
</button>
|
|
685
827
|
</html>
|
|
686
|
-
)
|
|
687
|
-
}
|
|
828
|
+
)
|
|
829
|
+
}
|
|
688
830
|
```
|
|
689
831
|
|
|
690
832
|
#### Specifying htmx Version
|
|
@@ -699,8 +841,8 @@ export default {
|
|
|
699
841
|
Click Me
|
|
700
842
|
</button>
|
|
701
843
|
</html>
|
|
702
|
-
)
|
|
703
|
-
}
|
|
844
|
+
)
|
|
845
|
+
}
|
|
704
846
|
```
|
|
705
847
|
|
|
706
848
|
#### Installing htmx Manually
|
|
@@ -721,8 +863,8 @@ export default {
|
|
|
721
863
|
</button>
|
|
722
864
|
</body>
|
|
723
865
|
</html>
|
|
724
|
-
)
|
|
725
|
-
}
|
|
866
|
+
)
|
|
867
|
+
}
|
|
726
868
|
```
|
|
727
869
|
|
|
728
870
|
## License
|