mono-jsx 0.6.11 → 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 +356 -192
- package/jsx-runtime.mjs +327 -111
- package/package.json +1 -1
- package/types/html.d.ts +17 -9
- package/types/mono.d.ts +145 -13
- 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 }>) {
|
|
@@ -543,12 +687,21 @@ function App(this: FC<{ value: string }>) {
|
|
|
543
687
|
}
|
|
544
688
|
```
|
|
545
689
|
|
|
546
|
-
|
|
690
|
+
You can also use the `$checked` attribute to bind a signal to the checked state of a checkbox or radio input element.
|
|
691
|
+
|
|
692
|
+
```tsx
|
|
693
|
+
function App(this: FC<{ checked: boolean }>) {
|
|
694
|
+
this.checked = false;
|
|
695
|
+
return <input type="checkbox" $checked={this.checked} />;
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### Limitations of Signals
|
|
547
700
|
|
|
548
|
-
1\. Arrow
|
|
701
|
+
1\. Arrow functions are non-stateful components.
|
|
549
702
|
|
|
550
703
|
```tsx
|
|
551
|
-
// ❌ Won't work -
|
|
704
|
+
// ❌ Won't work - uses `this` in a non-stateful component
|
|
552
705
|
const App = () => {
|
|
553
706
|
this.count = 0;
|
|
554
707
|
return (
|
|
@@ -601,7 +754,7 @@ function App(this: FC) {
|
|
|
601
754
|
}
|
|
602
755
|
```
|
|
603
756
|
|
|
604
|
-
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:
|
|
605
758
|
|
|
606
759
|
```tsx
|
|
607
760
|
// ❌ Won't work - throws `Deno is not defined` when the button is clicked
|
|
@@ -638,9 +791,11 @@ mono-jsx binds a scoped signals object to `this` of your component functions. Th
|
|
|
638
791
|
|
|
639
792
|
The `this` object has the following built-in properties:
|
|
640
793
|
|
|
641
|
-
- `app`: The app global signals
|
|
642
|
-
- `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.
|
|
643
796
|
- `request`: The request object from the `fetch` handler.
|
|
797
|
+
- `session`: The session storage.
|
|
798
|
+
- `form`: The route form data.
|
|
644
799
|
- `refs`: A map of refs defined in the component.
|
|
645
800
|
- `computed`: A method to create a computed signal.
|
|
646
801
|
- `effect`: A method to create side effects.
|
|
@@ -650,11 +805,13 @@ type FC<Signals = {}, AppSignals = {}, Context = {}, Refs = {}, AppRefs = {}> =
|
|
|
650
805
|
readonly app: AppSignals & { refs: AppRefs; url: WithParams<URL> }
|
|
651
806
|
readonly context: Context;
|
|
652
807
|
readonly request: WithParams<Request>;
|
|
808
|
+
readonly session: Session;
|
|
809
|
+
readonly form?: FormData;
|
|
653
810
|
readonly refs: Refs;
|
|
654
811
|
readonly computed: <T = unknown>(fn: () => T) => T;
|
|
655
812
|
readonly $: FC["computed"];
|
|
656
813
|
readonly effect: (fn: () => void | (() => void)) => void;
|
|
657
|
-
} &
|
|
814
|
+
} & Signals;
|
|
658
815
|
```
|
|
659
816
|
|
|
660
817
|
### Using Signals
|
|
@@ -699,7 +856,7 @@ function Layout(this: Refs<FC, {}, { h1: HTMLH1Element }>) {
|
|
|
699
856
|
}
|
|
700
857
|
```
|
|
701
858
|
|
|
702
|
-
|
|
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:
|
|
703
860
|
|
|
704
861
|
- `name`: The name of the component to render.
|
|
705
862
|
- `props`: The props to pass to the component.
|
|
@@ -724,6 +881,7 @@ function App(this: Refs<FC, { component: ComponentElement }>) {
|
|
|
724
881
|
</div>
|
|
725
882
|
)
|
|
726
883
|
}
|
|
884
|
+
```
|
|
727
885
|
|
|
728
886
|
### Using Context
|
|
729
887
|
|
|
@@ -779,133 +937,11 @@ export default {
|
|
|
779
937
|
}
|
|
780
938
|
```
|
|
781
939
|
|
|
782
|
-
##
|
|
783
|
-
|
|
784
|
-
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:
|
|
785
|
-
|
|
786
|
-
```tsx
|
|
787
|
-
async function Sleep({ ms }) {
|
|
788
|
-
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
789
|
-
return <slot />;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
export default {
|
|
793
|
-
fetch: (req) => (
|
|
794
|
-
<html>
|
|
795
|
-
<Sleep ms={1000} placeholder={<p>Loading...</p>}>
|
|
796
|
-
<p>After 1 second</p>
|
|
797
|
-
</Sleep>
|
|
798
|
-
</html>
|
|
799
|
-
)
|
|
800
|
-
}
|
|
801
|
-
```
|
|
802
|
-
|
|
803
|
-
You can set the `rendering` attribute to `"eager"` to force synchronous rendering (the `placeholder` will be ignored):
|
|
804
|
-
|
|
805
|
-
```tsx
|
|
806
|
-
export default {
|
|
807
|
-
fetch: (req) => (
|
|
808
|
-
<html>
|
|
809
|
-
<Sleep ms={1000} rendering="eager">
|
|
810
|
-
<p>After 1 second</p>
|
|
811
|
-
</Sleep>
|
|
812
|
-
</html>
|
|
813
|
-
)
|
|
814
|
-
}
|
|
815
|
-
```
|
|
816
|
-
|
|
817
|
-
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:
|
|
818
|
-
|
|
819
|
-
```tsx
|
|
820
|
-
async function Hello() {
|
|
821
|
-
throw new Error("Something went wrong!");
|
|
822
|
-
return <p>Hello world!</p>;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
export default {
|
|
826
|
-
fetch: (req) => (
|
|
827
|
-
<html>
|
|
828
|
-
<Hello catch={err => <p>{err.message}</p>} />
|
|
829
|
-
</html>
|
|
830
|
-
)
|
|
831
|
-
}
|
|
832
|
-
```
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
## Lazy Rendering
|
|
836
|
-
|
|
837
|
-
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:
|
|
940
|
+
## Using Router (SPA mode)
|
|
838
941
|
|
|
839
|
-
|
|
840
|
-
export default {
|
|
841
|
-
fetch: (req) => (
|
|
842
|
-
<html components={{ Foo }}>
|
|
843
|
-
<component name="Foo" props={{ /* props for the component */ }} placeholder={<p>Loading...</p>} />
|
|
844
|
-
</html>
|
|
845
|
-
)
|
|
846
|
-
}
|
|
847
|
-
```
|
|
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.
|
|
848
943
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
```tsx
|
|
852
|
-
async function Lazy(this: FC<{ show: boolean }>, props: { url: string }) {
|
|
853
|
-
this.show = false;
|
|
854
|
-
return (
|
|
855
|
-
<div>
|
|
856
|
-
<toggle show={this.show}>
|
|
857
|
-
<component name="Foo" props={{ /* props for the component */ }} placeholder={<p>Loading...</p>} />
|
|
858
|
-
</toggle>
|
|
859
|
-
<button onClick={() => this.show = true }>Load `Foo` Component</button>
|
|
860
|
-
</div>
|
|
861
|
-
)
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
export default {
|
|
865
|
-
fetch: (req) => (
|
|
866
|
-
<html components={{ Foo }}>
|
|
867
|
-
<Lazy />
|
|
868
|
-
</html>
|
|
869
|
-
)
|
|
870
|
-
}
|
|
871
|
-
```
|
|
872
|
-
|
|
873
|
-
You also can use signal `name` or `props`, change the signal value will trigger the component to re-render with new name or props:
|
|
874
|
-
|
|
875
|
-
```tsx
|
|
876
|
-
import { Profile, Projects, Settings } from "./pages.tsx"
|
|
877
|
-
|
|
878
|
-
function Dash(this: FC<{ page: "Profile" | "Projects" | "Settings" }>) {
|
|
879
|
-
this.page = "Profile";
|
|
880
|
-
|
|
881
|
-
return (
|
|
882
|
-
<>
|
|
883
|
-
<div class="tab">
|
|
884
|
-
<button onClick={e => this.page = "Profile"}>Profile</button>
|
|
885
|
-
<button onClick={e => this.page = "Projects"}>Projects</button>
|
|
886
|
-
<button onClick={e => this.page = "Settings"}>Settings</button>
|
|
887
|
-
</div>
|
|
888
|
-
<div class="page">
|
|
889
|
-
<component name={this.page} placeholder={<p>Loading...</p>} />
|
|
890
|
-
</div>
|
|
891
|
-
</>
|
|
892
|
-
)
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
export default {
|
|
896
|
-
fetch: (req) => (
|
|
897
|
-
<html components={{ Profile, Projects, Settings }}>
|
|
898
|
-
<Dash />
|
|
899
|
-
</html>
|
|
900
|
-
)
|
|
901
|
-
}
|
|
902
|
-
```
|
|
903
|
-
|
|
904
|
-
## Using Router(SPA Mode)
|
|
905
|
-
|
|
906
|
-
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.
|
|
907
|
-
|
|
908
|
-
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.
|
|
909
945
|
|
|
910
946
|
```tsx
|
|
911
947
|
const routes = {
|
|
@@ -929,11 +965,11 @@ export default {
|
|
|
929
965
|
}
|
|
930
966
|
```
|
|
931
967
|
|
|
932
|
-
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:
|
|
933
969
|
|
|
934
970
|
- ✅ Deno
|
|
935
971
|
- ✅ Cloudflare Workers
|
|
936
|
-
- ✅
|
|
972
|
+
- ✅ Node.js (>= 24)
|
|
937
973
|
|
|
938
974
|
For Bun users, mono-jsx provides a `buildRoutes` function that uses Bun's built-in server routing:
|
|
939
975
|
|
|
@@ -964,6 +1000,7 @@ Bun.serve({
|
|
|
964
1000
|
### Using Route `params`
|
|
965
1001
|
|
|
966
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
|
+
|
|
967
1004
|
You can access the `params` object in your route components to get the values of the parameters defined in the route pattern:
|
|
968
1005
|
|
|
969
1006
|
|
|
@@ -975,9 +1012,68 @@ function Post(this: FC) {
|
|
|
975
1012
|
}
|
|
976
1013
|
```
|
|
977
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
|
+
|
|
978
1074
|
### Using `this.app.url` Signal
|
|
979
1075
|
|
|
980
|
-
`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:
|
|
981
1077
|
|
|
982
1078
|
```tsx
|
|
983
1079
|
function App(this: FC) {
|
|
@@ -994,8 +1090,7 @@ function App(this: FC) {
|
|
|
994
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:
|
|
995
1091
|
|
|
996
1092
|
```tsx
|
|
997
|
-
|
|
998
|
-
export default {
|
|
1093
|
+
export default {
|
|
999
1094
|
fetch: (req) => (
|
|
1000
1095
|
<html request={req} routes={routes}>
|
|
1001
1096
|
<nav>
|
|
@@ -1011,7 +1106,7 @@ function App() {
|
|
|
1011
1106
|
|
|
1012
1107
|
### Nav Links
|
|
1013
1108
|
|
|
1014
|
-
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.
|
|
1015
1110
|
|
|
1016
1111
|
```tsx
|
|
1017
1112
|
export default {
|
|
@@ -1028,44 +1123,113 @@ export default {
|
|
|
1028
1123
|
}
|
|
1029
1124
|
```
|
|
1030
1125
|
|
|
1031
|
-
###
|
|
1126
|
+
### Fallback (404)
|
|
1032
1127
|
|
|
1033
|
-
|
|
1128
|
+
You can add fallback(404) content to the `<router>` element as children, which will be displayed when no route matches the current URL.
|
|
1034
1129
|
|
|
1035
1130
|
```tsx
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
</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>
|
|
1045
1139
|
)
|
|
1046
1140
|
}
|
|
1047
1141
|
```
|
|
1048
1142
|
|
|
1049
|
-
|
|
1143
|
+
## Using Session
|
|
1050
1144
|
|
|
1051
|
-
|
|
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.
|
|
1052
1146
|
|
|
1053
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
|
+
|
|
1054
1179
|
export default {
|
|
1055
1180
|
fetch: (req) => (
|
|
1056
|
-
<html request={req} routes={routes}>
|
|
1057
|
-
<router
|
|
1058
|
-
<p>Page Not Found</p>
|
|
1059
|
-
<p>Back to <a href="/">Home</a></p>
|
|
1060
|
-
</router>
|
|
1181
|
+
<html request={req} routes={routes} session={{ cookie: { secret: "..." } }}>
|
|
1182
|
+
<router />
|
|
1061
1183
|
</html>
|
|
1062
1184
|
)
|
|
1063
1185
|
}
|
|
1064
1186
|
```
|
|
1065
1187
|
|
|
1066
|
-
|
|
1188
|
+
### Session Storage API
|
|
1189
|
+
|
|
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
|
+
```
|
|
1204
|
+
|
|
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:
|
|
1208
|
+
|
|
1209
|
+
- `<cache>` with specified `key` and `ttl`
|
|
1210
|
+
- `<static>` for elements that rarely change, such as `<svg>`
|
|
1211
|
+
|
|
1212
|
+
```tsx
|
|
1213
|
+
function BlogPage() {
|
|
1214
|
+
return (
|
|
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>
|
|
1226
|
+
)
|
|
1227
|
+
}
|
|
1228
|
+
```
|
|
1229
|
+
|
|
1230
|
+
## Customizing HTML Response
|
|
1067
1231
|
|
|
1068
|
-
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:
|
|
1069
1233
|
|
|
1070
1234
|
```tsx
|
|
1071
1235
|
export default {
|
|
@@ -1144,9 +1308,9 @@ export default {
|
|
|
1144
1308
|
}
|
|
1145
1309
|
```
|
|
1146
1310
|
|
|
1147
|
-
####
|
|
1311
|
+
#### Setting Up htmx Manually
|
|
1148
1312
|
|
|
1149
|
-
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:
|
|
1150
1314
|
|
|
1151
1315
|
```tsx
|
|
1152
1316
|
export default {
|