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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ![`<html>` as a `Response`](./.github/og-image.png)
4
4
 
5
- mono-jsx is a JSX runtime that renders `<html>` element to `Response` object in JavaScript runtimes like Node.js, Deno, Bun, Cloudflare Workers, etc.
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 installation mono-jsx(no package.json/tsconfig/node_modules), the runtime(deno/bun) automatically installs mono-jsx to your computer:
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
- npx tsx app.tsx
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 the mono-jsx.
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 in JSX instead of React's `dangerouslySetInnerHTML` property.
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 [streaming rendering](#streaming-rendering), async components are rendered asynchronously, allowing you to fetch data or perform other async operations before rendering the component.
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. The signals is bound to the component instance and can be updated directly, and will automatically re-render the view when a signal changes:
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 singal
487
+ // Initialize a signal
344
488
  this.count = props.initialCount ?? 0;
345
489
 
346
490
  return (
347
491
  <div>
348
- {/* render singal */}
492
+ {/* render signal */}
349
493
  <span>{this.count}</span>
350
494
 
351
- {/* Update singal to trigger re-render */}
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 is available in all components via `this.app.<SignalName>`. Changes to the app signals will trigger re-renders in all components that use it:
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>` condition rendering:
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
- ### Limitation of Signals
699
+ ### Limitations of Signals
556
700
 
557
- 1\. Arrow function are non-stateful components.
701
+ 1\. Arrow functions are non-stateful components.
558
702
 
559
703
  ```tsx
560
- // ❌ Won't work - use `this` in a non-stateful component
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. 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:
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
- } & Omit<Signals, "app" | "context" | "request" | "computed" | "effect">;
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
- Element `<componet>` also support `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:
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
- ## Streaming Rendering
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 a route:
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
- - ✅ Nodejs (>= 24)
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 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 view with `<toggle>` or `<switch>` elements:
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
- function App() {
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, a active class will be added to the link element. By default, the active class is `active`, but you can customize it by setting the `data-active-class` attribute on the `<nav>` element. You can add style for the active link using nested CSS selectors in the `style` attribute of the `<nav>` element.
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
- ### Using DB/Storage in Route Components
1126
+ ### Fallback (404)
1041
1127
 
1042
- Route components are always rendered on server-side, you can use any database or storage API to fetch data in your route components.
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
- async function Post(this: FC) {
1046
- const post = await sql`SELECT * FROM posts WHERE id = ${ this.request.params!.id }`
1047
- return (
1048
- <article>
1049
- <h2>{post.title}<h2>
1050
- <section>
1051
- {html(post.content)}
1052
- </section>
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
- ### Fallback(404)
1143
+ ## Using Session
1059
1144
 
1060
- You can add fallback(404) content to the `<router>` element as children, which will be displayed when no route matches the current URL.
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
- ## Using View Transition
1188
+ ### Session Storage API
1076
1189
 
1077
- 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 transition, add `viewTransition` attribute to below components:
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
- - `<toggle viewTransition="transition-name">`
1080
- - `<switch viewTransition="transition-name">`
1081
- - `<component viewTransition="transition-name">`
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
- You can set custom transition animation by adding [`::view-transition-group`](https://developer.mozilla.org/en-US/docs/Web/CSS/::view-transition-group) and [`::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 animation. For example,
1209
+ - `<cache>` with specified `key` and `ttl`
1210
+ - `<static>` for elements that rarely change, such as `<svg>`
1085
1211
 
1086
1212
  ```tsx
1087
- function App(this: FC<{ show: boolean }>) {
1213
+ function BlogPage() {
1088
1214
  return (
1089
- <div
1090
- style={{
1091
- "@keyframes fade-in": { from: { opacity: 0 }, to: { opacity: 1 } },
1092
- "@keyframes fade-out": { from: { opacity: 1 }, to: { opacity: 0 } },
1093
- "::view-transition-group(fade)": { animationDuration: "0.5s" },
1094
- "::view-transition-old(fade)": { animation: "0.5s ease-in both fade-out" },
1095
- "::view-transition-new(fade)": { animation: "0.5s ease-in both fade-in" },
1096
- }}
1097
- >
1098
- <toggle show={this.show} viewTransition="fade">
1099
- <h1>Hello world!</h1>
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 html Response
1230
+ ## Customizing HTML Response
1108
1231
 
1109
- You can add `status` or `headers` attributes to the root `<html>` element to customize the http response:
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
- #### Setup htmx Manually
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 setup htmx manually with your own CDN or local copy:
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 {