mono-jsx 0.6.12 → 0.7.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 +337 -214
- package/jsx-runtime.mjs +266 -62
- package/package.json +1 -1
- package/types/html.d.ts +16 -9
- package/types/mono.d.ts +140 -12
- package/types/render.d.ts +54 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
mono-jsx is a JSX runtime that renders `<html>` element to `Response` object
|
|
5
|
+
mono-jsx is a JSX runtime that renders the `<html>` element to a `Response` object.
|
|
6
6
|
|
|
7
7
|
- 🚀 No build step needed
|
|
8
8
|
- 🦋 Lightweight (10KB gzipped), zero dependencies
|
|
@@ -11,6 +11,7 @@ mono-jsx is a JSX runtime that renders `<html>` element to `Response` object in
|
|
|
11
11
|
- 💡 Complete Web API TypeScript definitions
|
|
12
12
|
- ⏳ Streaming rendering
|
|
13
13
|
- 🗂️ Built-in router(SPA mode)
|
|
14
|
+
- 🔑 Session storage
|
|
14
15
|
- 🥷 [htmx](#using-htmx) integration
|
|
15
16
|
- 🌎 Universal, works in Node.js, Deno, Bun, Cloudflare Workers, etc.
|
|
16
17
|
|
|
@@ -60,7 +61,7 @@ bunx mono-jsx setup
|
|
|
60
61
|
|
|
61
62
|
### Zero Configuration
|
|
62
63
|
|
|
63
|
-
Alternatively, you can use `@jsxImportSource` pragma directive without
|
|
64
|
+
Alternatively, you can use the `@jsxImportSource` pragma directive without installing mono-jsx (no package.json/tsconfig/node_modules). The runtime (Deno/Bun) automatically installs mono-jsx to your computer:
|
|
64
65
|
|
|
65
66
|
```js
|
|
66
67
|
// Deno, Valtown
|
|
@@ -99,31 +100,15 @@ If you're building a web app with [Cloudflare Workers](https://developers.cloudf
|
|
|
99
100
|
npx wrangler dev app.tsx
|
|
100
101
|
```
|
|
101
102
|
|
|
102
|
-
**Node.js doesn't support JSX syntax or declarative fetch servers**, we recommend using mono-jsx with [srvx](https://srvx.h3.dev
|
|
103
|
-
|
|
104
|
-
```tsx
|
|
105
|
-
// app.tsx
|
|
106
|
-
|
|
107
|
-
import { serve } from "srvx";
|
|
108
|
-
|
|
109
|
-
serve({
|
|
110
|
-
port: 3000,
|
|
111
|
-
fetch: (req) => (
|
|
112
|
-
<html>
|
|
113
|
-
<h1>Welcome to mono-jsx!</h1>
|
|
114
|
-
</html>
|
|
115
|
-
),
|
|
116
|
-
});
|
|
117
|
-
```
|
|
118
|
-
|
|
119
|
-
And you'll need [tsx](https://www.npmjs.com/package/tsx) to start the app without a build step:
|
|
103
|
+
**Node.js doesn't support JSX syntax or declarative fetch servers**, we recommend using mono-jsx with [srvx](https://srvx.h3.dev) and [tsx](https://tsx.is) (as JSX loader) to start your app:
|
|
120
104
|
|
|
121
105
|
```bash
|
|
122
|
-
|
|
106
|
+
# npm i srvx tsx
|
|
107
|
+
npx srvx --import tsx app.tsx
|
|
123
108
|
```
|
|
124
109
|
|
|
125
110
|
> [!NOTE]
|
|
126
|
-
> 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
|
|
111
|
+
> Only the 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 mono-jsx.
|
|
127
112
|
|
|
128
113
|
## Using JSX
|
|
129
114
|
|
|
@@ -172,6 +157,38 @@ mono-jsx supports [pseudo classes](https://developer.mozilla.org/en-US/docs/Web/
|
|
|
172
157
|
</a>;
|
|
173
158
|
```
|
|
174
159
|
|
|
160
|
+
### Using View Transition
|
|
161
|
+
|
|
162
|
+
mono-jsx supports [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) to create smooth transitions between views. To use view transitions, add the `viewTransition` attribute to the following components:
|
|
163
|
+
|
|
164
|
+
- `<toggle viewTransition="transition-name">`
|
|
165
|
+
- `<switch viewTransition="transition-name">`
|
|
166
|
+
- `<component viewTransition="transition-name">`
|
|
167
|
+
- `<router viewTransition="transition-name">`
|
|
168
|
+
|
|
169
|
+
You can set custom transition animations by adding [`::view-transition-group`](https://developer.mozilla.org/en-US/docs/Web/CSS/::view-transition-group), [`::view-transition-old`](https://developer.mozilla.org/en-US/docs/Web/CSS/::view-transition-old), and [`::view-transition-new`](https://developer.mozilla.org/en-US/docs/Web/CSS/::view-transition-new) pseudo-elements with your own CSS animations. For example:
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
function App(this: FC<{ show: boolean }>) {
|
|
173
|
+
return (
|
|
174
|
+
<div
|
|
175
|
+
style={{
|
|
176
|
+
"@keyframes fade-in": { from: { opacity: 0 }, to: { opacity: 1 } },
|
|
177
|
+
"@keyframes fade-out": { from: { opacity: 1 }, to: { opacity: 0 } },
|
|
178
|
+
"::view-transition-group(fade)": { animationDuration: "0.5s" },
|
|
179
|
+
"::view-transition-old(fade)": { animation: "0.5s ease-in both fade-out" },
|
|
180
|
+
"::view-transition-new(fade)": { animation: "0.5s ease-in both fade-in" },
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
<toggle show={this.show} viewTransition="fade">
|
|
184
|
+
<h1>Hello world!</h1>
|
|
185
|
+
</toggle>
|
|
186
|
+
<button onClick={() => this.show = !this.show}>Toggle</button>
|
|
187
|
+
</div>
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
175
192
|
### Using `<slot>` Element
|
|
176
193
|
|
|
177
194
|
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:
|
|
@@ -202,7 +219,7 @@ function App() {
|
|
|
202
219
|
|
|
203
220
|
### Using `html` Tag Function
|
|
204
221
|
|
|
205
|
-
mono-jsx provides an `html` tag function to render raw HTML
|
|
222
|
+
mono-jsx provides an `html` tag function to render raw HTML, which is similar to React's `dangerouslySetInnerHTML`.
|
|
206
223
|
|
|
207
224
|
```tsx
|
|
208
225
|
function App() {
|
|
@@ -210,6 +227,14 @@ function App() {
|
|
|
210
227
|
}
|
|
211
228
|
```
|
|
212
229
|
|
|
230
|
+
Variables in the `html` template literal are escaped. To render raw HTML without escaping, call the `html` function with a string literal.
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
function App() {
|
|
234
|
+
return <div>{html(`${<h1>Hello world!</h1>}`)}</div>;
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
213
238
|
The `html` function is globally available without importing. You can also use `css` and `js` functions for CSS and JavaScript:
|
|
214
239
|
|
|
215
240
|
```tsx
|
|
@@ -283,11 +308,11 @@ function App() {
|
|
|
283
308
|
|
|
284
309
|
## Async Components
|
|
285
310
|
|
|
286
|
-
mono-jsx supports async components that return a `Promise` or an async function. With
|
|
311
|
+
mono-jsx supports async components that return a `Promise` or an async function. With streaming rendering, async components are rendered asynchronously, allowing you to fetch data or perform other async operations before rendering the component.
|
|
287
312
|
|
|
288
313
|
```tsx
|
|
289
314
|
async function Loader(props: { url: string }) {
|
|
290
|
-
const data = await fetch(url).then((res) => res.json());
|
|
315
|
+
const data = await fetch(props.url).then((res) => res.json());
|
|
291
316
|
return <JsonViewer data={data} />;
|
|
292
317
|
}
|
|
293
318
|
|
|
@@ -327,28 +352,147 @@ export default {
|
|
|
327
352
|
}
|
|
328
353
|
```
|
|
329
354
|
|
|
355
|
+
You can use `placeholder` to display a loading state while waiting for async components to render:
|
|
356
|
+
|
|
357
|
+
```tsx
|
|
358
|
+
async function Sleep({ ms }) {
|
|
359
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
360
|
+
return <slot />;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export default {
|
|
364
|
+
fetch: (req) => (
|
|
365
|
+
<html>
|
|
366
|
+
<Sleep ms={1000} placeholder={<p>Loading...</p>}>
|
|
367
|
+
<p>After 1 second</p>
|
|
368
|
+
</Sleep>
|
|
369
|
+
</html>
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
You can set the `rendering` attribute to `"eager"` to force synchronous rendering (the `placeholder` will be ignored):
|
|
375
|
+
|
|
376
|
+
```tsx
|
|
377
|
+
export default {
|
|
378
|
+
fetch: (req) => (
|
|
379
|
+
<html>
|
|
380
|
+
<Sleep ms={1000} rendering="eager">
|
|
381
|
+
<p>After 1 second</p>
|
|
382
|
+
</Sleep>
|
|
383
|
+
</html>
|
|
384
|
+
)
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
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:
|
|
389
|
+
|
|
390
|
+
```tsx
|
|
391
|
+
async function Hello() {
|
|
392
|
+
throw new Error("Something went wrong!");
|
|
393
|
+
return <p>Hello world!</p>;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export default {
|
|
397
|
+
fetch: (req) => (
|
|
398
|
+
<html>
|
|
399
|
+
<Hello catch={err => <p>{err.message}</p>} />
|
|
400
|
+
</html>
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## Lazy Rendering
|
|
406
|
+
|
|
407
|
+
mono-jsx renders HTML on the server side and sends no hydration JavaScript to the client. To render a component dynamically on the client, you can use the `<component>` element to ask the server to render a component:
|
|
408
|
+
|
|
409
|
+
```tsx
|
|
410
|
+
export default {
|
|
411
|
+
fetch: (req) => (
|
|
412
|
+
<html components={{ Foo }}>
|
|
413
|
+
<component name="Foo" props={{ /* props for the component */ }} placeholder={<p>Loading...</p>} />
|
|
414
|
+
</html>
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
You can use the `<toggle>` element to control when to render a component:
|
|
420
|
+
|
|
421
|
+
```tsx
|
|
422
|
+
async function Lazy(this: FC<{ show: boolean }>, props: { url: string }) {
|
|
423
|
+
this.show = false;
|
|
424
|
+
return (
|
|
425
|
+
<div>
|
|
426
|
+
<toggle show={this.show}>
|
|
427
|
+
<component name="Foo" props={{ /* props for the component */ }} placeholder={<p>Loading...</p>} />
|
|
428
|
+
</toggle>
|
|
429
|
+
<button onClick={() => this.show = true }>Load `Foo` Component</button>
|
|
430
|
+
</div>
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export default {
|
|
435
|
+
fetch: (req) => (
|
|
436
|
+
<html components={{ Foo }}>
|
|
437
|
+
<Lazy />
|
|
438
|
+
</html>
|
|
439
|
+
)
|
|
440
|
+
}
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
You can also use signals for `name` or `props` attributes of a component. Changing the signal value will trigger the component to re-render with the new name or props:
|
|
444
|
+
|
|
445
|
+
```tsx
|
|
446
|
+
import { Profile, Projects, Settings } from "./pages.tsx"
|
|
447
|
+
|
|
448
|
+
function Dash(this: FC<{ page: "Profile" | "Projects" | "Settings" }>) {
|
|
449
|
+
this.page = "Profile";
|
|
450
|
+
|
|
451
|
+
return (
|
|
452
|
+
<>
|
|
453
|
+
<div class="tab">
|
|
454
|
+
<button onClick={e => this.page = "Profile"}>Profile</button>
|
|
455
|
+
<button onClick={e => this.page = "Projects"}>Projects</button>
|
|
456
|
+
<button onClick={e => this.page = "Settings"}>Settings</button>
|
|
457
|
+
</div>
|
|
458
|
+
<div class="page">
|
|
459
|
+
<component name={this.page} placeholder={<p>Loading...</p>} />
|
|
460
|
+
</div>
|
|
461
|
+
</>
|
|
462
|
+
)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export default {
|
|
466
|
+
fetch: (req) => (
|
|
467
|
+
<html components={{ Profile, Projects, Settings }}>
|
|
468
|
+
<Dash />
|
|
469
|
+
</html>
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
```
|
|
473
|
+
|
|
330
474
|
## Using Signals
|
|
331
475
|
|
|
332
476
|
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.
|
|
333
477
|
|
|
334
478
|
### Using Component Signals
|
|
335
479
|
|
|
336
|
-
You can use the `this` keyword in your components to manage signals.
|
|
480
|
+
You can use the `this` keyword in your components to manage signals. Signals are bound to the component instance, can be updated directly, and the view will automatically re-render when a signal changes:
|
|
337
481
|
|
|
338
482
|
```tsx
|
|
339
483
|
function Counter(
|
|
340
484
|
this: FC<{ count: number }>,
|
|
341
485
|
props: { initialCount?: number },
|
|
342
486
|
) {
|
|
343
|
-
// Initialize a
|
|
487
|
+
// Initialize a signal
|
|
344
488
|
this.count = props.initialCount ?? 0;
|
|
345
489
|
|
|
346
490
|
return (
|
|
347
491
|
<div>
|
|
348
|
-
{/* render
|
|
492
|
+
{/* render signal */}
|
|
349
493
|
<span>{this.count}</span>
|
|
350
494
|
|
|
351
|
-
{/* Update
|
|
495
|
+
{/* Update signal to trigger re-render */}
|
|
352
496
|
<button onClick={() => this.count--}>-</button>
|
|
353
497
|
<button onClick={() => this.count++}>+</button>
|
|
354
498
|
</div>
|
|
@@ -358,7 +502,7 @@ function Counter(
|
|
|
358
502
|
|
|
359
503
|
### Using App Signals
|
|
360
504
|
|
|
361
|
-
You can define app signals by adding `app` prop to the root `<html>` element. The app signals
|
|
505
|
+
You can define app signals by adding the `app` prop to the root `<html>` element. The app signals are available in all components via `this.app.<SignalName>`. Changes to the app signals will trigger re-renders in all components that use them:
|
|
362
506
|
|
|
363
507
|
```tsx
|
|
364
508
|
interface AppSignals {
|
|
@@ -443,7 +587,7 @@ function App(this: FC<{ count: number }>) {
|
|
|
443
587
|
}
|
|
444
588
|
```
|
|
445
589
|
|
|
446
|
-
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>`
|
|
590
|
+
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>` conditional rendering:
|
|
447
591
|
|
|
448
592
|
```tsx
|
|
449
593
|
function Counter(this: FC<{ count: number }>) {
|
|
@@ -552,12 +696,12 @@ function App(this: FC<{ checked: boolean }>) {
|
|
|
552
696
|
}
|
|
553
697
|
```
|
|
554
698
|
|
|
555
|
-
###
|
|
699
|
+
### Limitations of Signals
|
|
556
700
|
|
|
557
|
-
1\. Arrow
|
|
701
|
+
1\. Arrow functions are non-stateful components.
|
|
558
702
|
|
|
559
703
|
```tsx
|
|
560
|
-
// ❌ Won't work -
|
|
704
|
+
// ❌ Won't work - uses `this` in a non-stateful component
|
|
561
705
|
const App = () => {
|
|
562
706
|
this.count = 0;
|
|
563
707
|
return (
|
|
@@ -610,7 +754,7 @@ function App(this: FC) {
|
|
|
610
754
|
}
|
|
611
755
|
```
|
|
612
756
|
|
|
613
|
-
3\. The callback function of `this.computed` must be a pure function.
|
|
757
|
+
3\. The callback function of `this.computed` must be a pure function. This 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:
|
|
614
758
|
|
|
615
759
|
```tsx
|
|
616
760
|
// ❌ Won't work - throws `Deno is not defined` when the button is clicked
|
|
@@ -647,9 +791,11 @@ mono-jsx binds a scoped signals object to `this` of your component functions. Th
|
|
|
647
791
|
|
|
648
792
|
The `this` object has the following built-in properties:
|
|
649
793
|
|
|
650
|
-
- `app`: The app global signals
|
|
651
|
-
- `context`: The context defined on the root `<html>` element.
|
|
794
|
+
- `app`: The app global signals.
|
|
795
|
+
- `context`: The context object defined on the root `<html>` element.
|
|
652
796
|
- `request`: The request object from the `fetch` handler.
|
|
797
|
+
- `session`: The session storage.
|
|
798
|
+
- `form`: The route form data.
|
|
653
799
|
- `refs`: A map of refs defined in the component.
|
|
654
800
|
- `computed`: A method to create a computed signal.
|
|
655
801
|
- `effect`: A method to create side effects.
|
|
@@ -659,11 +805,13 @@ type FC<Signals = {}, AppSignals = {}, Context = {}, Refs = {}, AppRefs = {}> =
|
|
|
659
805
|
readonly app: AppSignals & { refs: AppRefs; url: WithParams<URL> }
|
|
660
806
|
readonly context: Context;
|
|
661
807
|
readonly request: WithParams<Request>;
|
|
808
|
+
readonly session: Session;
|
|
809
|
+
readonly form?: FormData;
|
|
662
810
|
readonly refs: Refs;
|
|
663
811
|
readonly computed: <T = unknown>(fn: () => T) => T;
|
|
664
812
|
readonly $: FC["computed"];
|
|
665
813
|
readonly effect: (fn: () => void | (() => void)) => void;
|
|
666
|
-
} &
|
|
814
|
+
} & Signals;
|
|
667
815
|
```
|
|
668
816
|
|
|
669
817
|
### Using Signals
|
|
@@ -708,7 +856,7 @@ function Layout(this: Refs<FC, {}, { h1: HTMLH1Element }>) {
|
|
|
708
856
|
}
|
|
709
857
|
```
|
|
710
858
|
|
|
711
|
-
|
|
859
|
+
The `<component>` element also supports the `ref` attribute, which allows you to control the component rendering manually. The `ref` will be a `ComponentElement` that has the `name`, `props`, and `refresh` properties:
|
|
712
860
|
|
|
713
861
|
- `name`: The name of the component to render.
|
|
714
862
|
- `props`: The props to pass to the component.
|
|
@@ -733,6 +881,7 @@ function App(this: Refs<FC, { component: ComponentElement }>) {
|
|
|
733
881
|
</div>
|
|
734
882
|
)
|
|
735
883
|
}
|
|
884
|
+
```
|
|
736
885
|
|
|
737
886
|
### Using Context
|
|
738
887
|
|
|
@@ -788,133 +937,11 @@ export default {
|
|
|
788
937
|
}
|
|
789
938
|
```
|
|
790
939
|
|
|
791
|
-
##
|
|
792
|
-
|
|
793
|
-
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:
|
|
794
|
-
|
|
795
|
-
```tsx
|
|
796
|
-
async function Sleep({ ms }) {
|
|
797
|
-
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
798
|
-
return <slot />;
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
export default {
|
|
802
|
-
fetch: (req) => (
|
|
803
|
-
<html>
|
|
804
|
-
<Sleep ms={1000} placeholder={<p>Loading...</p>}>
|
|
805
|
-
<p>After 1 second</p>
|
|
806
|
-
</Sleep>
|
|
807
|
-
</html>
|
|
808
|
-
)
|
|
809
|
-
}
|
|
810
|
-
```
|
|
811
|
-
|
|
812
|
-
You can set the `rendering` attribute to `"eager"` to force synchronous rendering (the `placeholder` will be ignored):
|
|
813
|
-
|
|
814
|
-
```tsx
|
|
815
|
-
export default {
|
|
816
|
-
fetch: (req) => (
|
|
817
|
-
<html>
|
|
818
|
-
<Sleep ms={1000} rendering="eager">
|
|
819
|
-
<p>After 1 second</p>
|
|
820
|
-
</Sleep>
|
|
821
|
-
</html>
|
|
822
|
-
)
|
|
823
|
-
}
|
|
824
|
-
```
|
|
825
|
-
|
|
826
|
-
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:
|
|
827
|
-
|
|
828
|
-
```tsx
|
|
829
|
-
async function Hello() {
|
|
830
|
-
throw new Error("Something went wrong!");
|
|
831
|
-
return <p>Hello world!</p>;
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
export default {
|
|
835
|
-
fetch: (req) => (
|
|
836
|
-
<html>
|
|
837
|
-
<Hello catch={err => <p>{err.message}</p>} />
|
|
838
|
-
</html>
|
|
839
|
-
)
|
|
840
|
-
}
|
|
841
|
-
```
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
## Lazy Rendering
|
|
845
|
-
|
|
846
|
-
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:
|
|
847
|
-
|
|
848
|
-
```tsx
|
|
849
|
-
export default {
|
|
850
|
-
fetch: (req) => (
|
|
851
|
-
<html components={{ Foo }}>
|
|
852
|
-
<component name="Foo" props={{ /* props for the component */ }} placeholder={<p>Loading...</p>} />
|
|
853
|
-
</html>
|
|
854
|
-
)
|
|
855
|
-
}
|
|
856
|
-
```
|
|
857
|
-
|
|
858
|
-
You can use `<toggle>` element to control when to render the component:
|
|
859
|
-
|
|
860
|
-
```tsx
|
|
861
|
-
async function Lazy(this: FC<{ show: boolean }>, props: { url: string }) {
|
|
862
|
-
this.show = false;
|
|
863
|
-
return (
|
|
864
|
-
<div>
|
|
865
|
-
<toggle show={this.show}>
|
|
866
|
-
<component name="Foo" props={{ /* props for the component */ }} placeholder={<p>Loading...</p>} />
|
|
867
|
-
</toggle>
|
|
868
|
-
<button onClick={() => this.show = true }>Load `Foo` Component</button>
|
|
869
|
-
</div>
|
|
870
|
-
)
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
export default {
|
|
874
|
-
fetch: (req) => (
|
|
875
|
-
<html components={{ Foo }}>
|
|
876
|
-
<Lazy />
|
|
877
|
-
</html>
|
|
878
|
-
)
|
|
879
|
-
}
|
|
880
|
-
```
|
|
881
|
-
|
|
882
|
-
You also can use signal `name` or `props`, change the signal value will trigger the component to re-render with new name or props:
|
|
883
|
-
|
|
884
|
-
```tsx
|
|
885
|
-
import { Profile, Projects, Settings } from "./pages.tsx"
|
|
886
|
-
|
|
887
|
-
function Dash(this: FC<{ page: "Profile" | "Projects" | "Settings" }>) {
|
|
888
|
-
this.page = "Profile";
|
|
889
|
-
|
|
890
|
-
return (
|
|
891
|
-
<>
|
|
892
|
-
<div class="tab">
|
|
893
|
-
<button onClick={e => this.page = "Profile"}>Profile</button>
|
|
894
|
-
<button onClick={e => this.page = "Projects"}>Projects</button>
|
|
895
|
-
<button onClick={e => this.page = "Settings"}>Settings</button>
|
|
896
|
-
</div>
|
|
897
|
-
<div class="page">
|
|
898
|
-
<component name={this.page} placeholder={<p>Loading...</p>} />
|
|
899
|
-
</div>
|
|
900
|
-
</>
|
|
901
|
-
)
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
export default {
|
|
905
|
-
fetch: (req) => (
|
|
906
|
-
<html components={{ Profile, Projects, Settings }}>
|
|
907
|
-
<Dash />
|
|
908
|
-
</html>
|
|
909
|
-
)
|
|
910
|
-
}
|
|
911
|
-
```
|
|
912
|
-
|
|
913
|
-
## Using Router(SPA Mode)
|
|
940
|
+
## Using Router (SPA mode)
|
|
914
941
|
|
|
915
|
-
mono-jsx provides a built-in `<router>` element that allows your app to render components based on the current URL. On client side, it listens all `click` events on `<a>` elements and asynchronously fetches the route component without reloading the entire page.
|
|
942
|
+
mono-jsx provides a built-in `<router>` element that allows your app to render components based on the current URL. On the client side, it listens to all `click` events on `<a>` elements and asynchronously fetches the route component without reloading the entire page.
|
|
916
943
|
|
|
917
|
-
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.
|
|
944
|
+
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 the `routes` prop. The `request` prop is also required to match the current URL against the defined routes.
|
|
918
945
|
|
|
919
946
|
```tsx
|
|
920
947
|
const routes = {
|
|
@@ -938,11 +965,11 @@ export default {
|
|
|
938
965
|
}
|
|
939
966
|
```
|
|
940
967
|
|
|
941
|
-
mono-jsx router requires [URLPattern](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) to match
|
|
968
|
+
The mono-jsx router requires [URLPattern](https://developer.mozilla.org/en-US/docs/Web/API/URLPattern) to match routes:
|
|
942
969
|
|
|
943
970
|
- ✅ Deno
|
|
944
971
|
- ✅ Cloudflare Workers
|
|
945
|
-
- ✅
|
|
972
|
+
- ✅ Node.js (>= 24)
|
|
946
973
|
|
|
947
974
|
For Bun users, mono-jsx provides a `buildRoutes` function that uses Bun's built-in server routing:
|
|
948
975
|
|
|
@@ -973,6 +1000,7 @@ Bun.serve({
|
|
|
973
1000
|
### Using Route `params`
|
|
974
1001
|
|
|
975
1002
|
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.
|
|
1003
|
+
|
|
976
1004
|
You can access the `params` object in your route components to get the values of the parameters defined in the route pattern:
|
|
977
1005
|
|
|
978
1006
|
|
|
@@ -984,9 +1012,68 @@ function Post(this: FC) {
|
|
|
984
1012
|
}
|
|
985
1013
|
```
|
|
986
1014
|
|
|
1015
|
+
### Using Route Form
|
|
1016
|
+
|
|
1017
|
+
When a form is submitted with the `route` attribute in a route component, the form data will be available in the `form` property of the component's `this` context during the form POST request.
|
|
1018
|
+
|
|
1019
|
+
mono-jsx provides two built-in elements to allow you to control the post-submit behavior:
|
|
1020
|
+
|
|
1021
|
+
- `<invalid for="...">{message}</invalid>` to set custom validation state for the form elements.
|
|
1022
|
+
- `<redirect to="..." />` to redirect to a new route/URL.
|
|
1023
|
+
|
|
1024
|
+
```tsx
|
|
1025
|
+
async function Login(this: FC) {
|
|
1026
|
+
if (this.form) {
|
|
1027
|
+
const user = await auth(this.form)
|
|
1028
|
+
if (!user) {
|
|
1029
|
+
return <invalid for="username,password">Invalid Username/Password</invalid>
|
|
1030
|
+
}
|
|
1031
|
+
this.session.set("user", user)
|
|
1032
|
+
return <redirect to="/dash" />
|
|
1033
|
+
}
|
|
1034
|
+
return (
|
|
1035
|
+
<form route style={{ "& input:invalid": { borderColor: "red" } }}>
|
|
1036
|
+
<input type="text" name="username" placeholder="Username" />
|
|
1037
|
+
<input type="password" name="password" placeholder="Password" />
|
|
1038
|
+
<button type="submit">Login</button>
|
|
1039
|
+
</form>
|
|
1040
|
+
)
|
|
1041
|
+
}
|
|
1042
|
+
```
|
|
1043
|
+
|
|
1044
|
+
> [!NOTE]
|
|
1045
|
+
> You can use `:invalid` CSS selector to style the form elements with invalid state.
|
|
1046
|
+
|
|
1047
|
+
You can also return regular HTML elements from the route form post response. The `formslot` element is used to
|
|
1048
|
+
mark the position where the returned HTML elements will be inserted.
|
|
1049
|
+
|
|
1050
|
+
- `<formslot mode="insertbefore" />`: Insert HTML before the `formslot` element.
|
|
1051
|
+
- `<formslot mode="insertafter" />`: Insert HTML after the `formslot` element.
|
|
1052
|
+
- `<formslot mode="replace" />`: Replace the `formslot` children. This is the default mode.
|
|
1053
|
+
|
|
1054
|
+
```tsx
|
|
1055
|
+
function FormSlot(this: FC) {
|
|
1056
|
+
if (this.form) {
|
|
1057
|
+
const message = this.form.get("message") as string | null;
|
|
1058
|
+
if (!message) {
|
|
1059
|
+
return <invalid for="message">Message is required</invalid>
|
|
1060
|
+
}
|
|
1061
|
+
return <p>{message}</p>
|
|
1062
|
+
}
|
|
1063
|
+
return (
|
|
1064
|
+
<form route>
|
|
1065
|
+
{/* <- new message will be inserted here */}
|
|
1066
|
+
<formslot mode="insertbefore" />
|
|
1067
|
+
<input type="text" name="message" placeholder="Type Message..." style={{ ":invalid": { borderColor: "red" } }} />
|
|
1068
|
+
<button type="submit">Send</button>
|
|
1069
|
+
</form>
|
|
1070
|
+
)
|
|
1071
|
+
}
|
|
1072
|
+
```
|
|
1073
|
+
|
|
987
1074
|
### Using `this.app.url` Signal
|
|
988
1075
|
|
|
989
|
-
`this.app.url` is an app-level signal that contains the current route
|
|
1076
|
+
`this.app.url` is an app-level signal that contains the current route URL and parameters. The `this.app.url` signal is automatically updated when the route changes, so you can use it to display the current URL in your components or control the view with `<toggle>` or `<switch>` elements:
|
|
990
1077
|
|
|
991
1078
|
```tsx
|
|
992
1079
|
function App(this: FC) {
|
|
@@ -1003,8 +1090,7 @@ function App(this: FC) {
|
|
|
1003
1090
|
To navigate between pages, you can use `<a>` elements with `href` attributes that match the defined routes. The router will intercept the click events of these links and fetch the corresponding route component without reloading the page:
|
|
1004
1091
|
|
|
1005
1092
|
```tsx
|
|
1006
|
-
|
|
1007
|
-
export default {
|
|
1093
|
+
export default {
|
|
1008
1094
|
fetch: (req) => (
|
|
1009
1095
|
<html request={req} routes={routes}>
|
|
1010
1096
|
<nav>
|
|
@@ -1020,7 +1106,7 @@ function App() {
|
|
|
1020
1106
|
|
|
1021
1107
|
### Nav Links
|
|
1022
1108
|
|
|
1023
|
-
Links under the `<nav>` element will be treated as navigation links by the router. When the `href` of a nav link matches a route,
|
|
1109
|
+
Links under the `<nav>` element will be treated as navigation links by the router. When the `href` of a nav link matches a route, an 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 add styles for the active link using nested CSS selectors in the `style` attribute of the `<nav>` element.
|
|
1024
1110
|
|
|
1025
1111
|
```tsx
|
|
1026
1112
|
export default {
|
|
@@ -1037,76 +1123,113 @@ export default {
|
|
|
1037
1123
|
}
|
|
1038
1124
|
```
|
|
1039
1125
|
|
|
1040
|
-
###
|
|
1126
|
+
### Fallback (404)
|
|
1041
1127
|
|
|
1042
|
-
|
|
1128
|
+
You can add fallback(404) content to the `<router>` element as children, which will be displayed when no route matches the current URL.
|
|
1043
1129
|
|
|
1044
1130
|
```tsx
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
</article>
|
|
1131
|
+
export default {
|
|
1132
|
+
fetch: (req) => (
|
|
1133
|
+
<html request={req} routes={routes}>
|
|
1134
|
+
<router>
|
|
1135
|
+
<p>Page Not Found</p>
|
|
1136
|
+
<p>Back to <a href="/">Home</a></p>
|
|
1137
|
+
</router>
|
|
1138
|
+
</html>
|
|
1054
1139
|
)
|
|
1055
1140
|
}
|
|
1056
1141
|
```
|
|
1057
1142
|
|
|
1058
|
-
|
|
1143
|
+
## Using Session
|
|
1059
1144
|
|
|
1060
|
-
|
|
1145
|
+
mono-jsx provides a built-in session storage that allows you to manage user sessions. To use the session storage, you need to set the `session` prop on the root `<html>` element with the `cookie.secret` option.
|
|
1061
1146
|
|
|
1062
1147
|
```tsx
|
|
1148
|
+
function Index(this: FC) {
|
|
1149
|
+
const user = this.session.get<{ name: string }>("user")
|
|
1150
|
+
if (!user) {
|
|
1151
|
+
return <p>Please <a href="/login">Login</a></p>
|
|
1152
|
+
}
|
|
1153
|
+
return <p>Welcome, {user.name}!</p>
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
async function Login(this: FC) {
|
|
1157
|
+
if (this.form) {
|
|
1158
|
+
const user = await auth(this.form)
|
|
1159
|
+
if (!user) {
|
|
1160
|
+
return <invalid for="username,password">Invalid Username/Password</invalid>
|
|
1161
|
+
}
|
|
1162
|
+
this.session.set("user", user)
|
|
1163
|
+
return <redirect to="/" />
|
|
1164
|
+
}
|
|
1165
|
+
return (
|
|
1166
|
+
<form route>
|
|
1167
|
+
<input type="text" name="username" placeholder="Username" />
|
|
1168
|
+
<input type="password" name="password" placeholder="Password" />
|
|
1169
|
+
<button type="submit">Login</button>
|
|
1170
|
+
</form>
|
|
1171
|
+
)
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const routes = {
|
|
1175
|
+
"/": Index,
|
|
1176
|
+
"/login": Login,
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1063
1179
|
export default {
|
|
1064
1180
|
fetch: (req) => (
|
|
1065
|
-
<html request={req} routes={routes}>
|
|
1066
|
-
<router
|
|
1067
|
-
<p>Page Not Found</p>
|
|
1068
|
-
<p>Back to <a href="/">Home</a></p>
|
|
1069
|
-
</router>
|
|
1181
|
+
<html request={req} routes={routes} session={{ cookie: { secret: "..." } }}>
|
|
1182
|
+
<router />
|
|
1070
1183
|
</html>
|
|
1071
1184
|
)
|
|
1072
1185
|
}
|
|
1073
1186
|
```
|
|
1074
1187
|
|
|
1075
|
-
|
|
1188
|
+
### Session Storage API
|
|
1076
1189
|
|
|
1077
|
-
|
|
1190
|
+
```ts
|
|
1191
|
+
function Component(this: FC) {
|
|
1192
|
+
// set a value in the session
|
|
1193
|
+
this.session.set("user", { name: "John" })
|
|
1194
|
+
// get a value from the session
|
|
1195
|
+
this.session.get<{ name: string }>("user") // { name: "John" }
|
|
1196
|
+
// get all entries from the session
|
|
1197
|
+
this.session.entries() // [["user", { name: "John" }]]
|
|
1198
|
+
// delete a value from the session
|
|
1199
|
+
this.session.delete("user")
|
|
1200
|
+
// destroy the session
|
|
1201
|
+
this.session.destroy()
|
|
1202
|
+
}
|
|
1203
|
+
```
|
|
1078
1204
|
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
-
|
|
1082
|
-
- `<router viewTransition="transition-name">`
|
|
1205
|
+
## Caching
|
|
1206
|
+
|
|
1207
|
+
mono-jsx renders HTML dynamically per request; large apps may tax your CPU resources. To improve rendering performance, mono-jsx introduces two built-in elements that can cache the rendered HTML of the children:
|
|
1083
1208
|
|
|
1084
|
-
|
|
1209
|
+
- `<cache>` with specified `key` and `ttl`
|
|
1210
|
+
- `<static>` for elements that rarely change, such as `<svg>`
|
|
1085
1211
|
|
|
1086
1212
|
```tsx
|
|
1087
|
-
function
|
|
1213
|
+
function BlogPage() {
|
|
1088
1214
|
return (
|
|
1089
|
-
<
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
>
|
|
1098
|
-
<
|
|
1099
|
-
|
|
1100
|
-
</toggle>
|
|
1101
|
-
<button onClick={() => this.show = !this.show}>Toggle</button>
|
|
1102
|
-
</div>
|
|
1215
|
+
<cache key="blog" ttl={86400}>
|
|
1216
|
+
<Blog />
|
|
1217
|
+
</cache>
|
|
1218
|
+
)
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function Icon() {
|
|
1222
|
+
return (
|
|
1223
|
+
<static>
|
|
1224
|
+
<svg>...</svg>
|
|
1225
|
+
</static>
|
|
1103
1226
|
)
|
|
1104
1227
|
}
|
|
1105
1228
|
```
|
|
1106
1229
|
|
|
1107
|
-
## Customizing
|
|
1230
|
+
## Customizing HTML Response
|
|
1108
1231
|
|
|
1109
|
-
You can add `status` or `headers` attributes to the root `<html>` element to customize the
|
|
1232
|
+
You can add `status` or `headers` attributes to the root `<html>` element to customize the HTTP response:
|
|
1110
1233
|
|
|
1111
1234
|
```tsx
|
|
1112
1235
|
export default {
|
|
@@ -1185,9 +1308,9 @@ export default {
|
|
|
1185
1308
|
}
|
|
1186
1309
|
```
|
|
1187
1310
|
|
|
1188
|
-
####
|
|
1311
|
+
#### Setting Up htmx Manually
|
|
1189
1312
|
|
|
1190
|
-
By default, mono-jsx imports htmx from [esm.sh](https://esm.sh/) CDN when you set the `htmx` attribute. You can also
|
|
1313
|
+
By default, mono-jsx imports htmx from the [esm.sh](https://esm.sh/) CDN when you set the `htmx` attribute. You can also set up htmx manually with your own CDN or local copy:
|
|
1191
1314
|
|
|
1192
1315
|
```tsx
|
|
1193
1316
|
export default {
|