mono-jsx 0.10.0-beta.2 → 0.10.0-beta.20

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
@@ -964,8 +964,6 @@ The `<component>` element also supports the `ref` prop, which allows you to cont
964
964
  - `refresh`: A method to re-render the component with the current name and props.
965
965
 
966
966
  ```tsx
967
- import type { ComponentElement } from "mono-jsx";
968
-
969
967
  function App(this: WithRefs<FC, { component: ComponentElement }>) {
970
968
  this.effect(() => {
971
969
  // updating the component name and props will trigger a re-render of the component
@@ -1110,16 +1108,208 @@ You can access the `params` object in your route components to get the values of
1110
1108
  ```tsx
1111
1109
  // router pattern: "/post/:id"
1112
1110
  function Post(this: FC) {
1113
- this.request.url // "http://localhost:3000/post/123"
1114
- this.request.params?.id // "123"
1111
+ console.log(this.request.url) // "http://localhost:3000/post/123"
1112
+ console.log(this.request.params?.id) // "123"
1113
+ }
1114
+ ```
1115
+
1116
+ You can use `this.app.url` signal to get route URL and parameters:
1117
+
1118
+ ```tsx
1119
+ function Post(this: FC) {
1120
+ return (
1121
+ <div>
1122
+ <p>Current URL: {this.$(() => this.app.url.href)}</p>
1123
+ <p>Post id: {this.$(() => this.app.url.params?.id)}</p>
1124
+ </div>
1125
+ )
1126
+ }
1127
+ ```
1128
+
1129
+ ### Using `this.app.url` Signal
1130
+
1131
+ The `this.app.url` in a component is an app-level signal that contains the current route URL and parameters. It 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 `<show>`, `<hidden>` or `<switch>` elements:
1132
+
1133
+ ```tsx
1134
+ function App(this: FC) {
1135
+ return (
1136
+ <div>
1137
+ <h1>Current Pathname: {this.$(() => this.app.url.pathname)}</h1>
1138
+ </div>
1139
+ )
1140
+ }
1141
+ ```
1142
+
1143
+ ### Navigation between Pages
1144
+
1145
+ To navigate between pages, you can use `<a>` elements with `href` props 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:
1146
+
1147
+ ```tsx
1148
+ export default {
1149
+ fetch: (req) => (
1150
+ <html request={req} routes={routes}>
1151
+ <nav>
1152
+ <a href="/">Home</a>
1153
+ <a href="/about">About</a>
1154
+ <a href="/blog">Blog</a>
1155
+ </nav>
1156
+ <router />
1157
+ </html>
1158
+ )
1159
+ }
1160
+ ```
1161
+
1162
+ You can also use the `navigate` method of the `<router>` element to navigate to a new route programmatically.
1163
+
1164
+ ```tsx
1165
+ function App(this: FC<{}, { router: RouterElement }>) {
1166
+ return (
1167
+ <>
1168
+ <header>
1169
+ <button
1170
+ onClick={() => this.refs.router.navigate("/about", { replace: false, refresh: false })}
1171
+ >About</button>
1172
+ </header>
1173
+ <router ref={this.refs.router} />
1174
+ </>
1175
+ )
1176
+ }
1177
+ ```
1178
+
1179
+ ### Nav Links
1180
+
1181
+ 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` prop on the `<nav>` element. You can add styles for the active link using nested CSS selectors in the `style` prop of the `<nav>` element.
1182
+
1183
+ ```tsx
1184
+ export default {
1185
+ fetch: (req) => (
1186
+ <html request={req} routes={routes}>
1187
+ <nav style={{ "& a.active": { fontWeight: "bold" } }} data-active-class="active">
1188
+ <a href="/">Home</a>
1189
+ <a href="/about">About</a>
1190
+ <a href="/blog">Blog</a>
1191
+ </nav>
1192
+ <router />
1193
+ </html>
1194
+ )
1115
1195
  }
1116
1196
  ```
1117
1197
 
1118
- ### Using Route Form
1198
+ ### Adding Page Metadata
1119
1199
 
1120
- mono-jsx allows you to define a `FormHandler` function for route components to handle form data from form submissions on the current route page on the client side. To submit the form data to the `FormHandler` function, you need to set the `route` prop on the `<form>` element.
1200
+ You can add metadata to the route component by setting the `metadata` property on the route component.
1121
1201
 
1122
- mono-jsx provides two built-in elements to allow you to control the post-submit behavior:
1202
+ ```tsx
1203
+ function Home(this: FC) {
1204
+ return <p>Home</p>
1205
+ }
1206
+
1207
+ Home.metadata = {
1208
+ title: "Home",
1209
+ description: "Home page",
1210
+ }
1211
+
1212
+ const routes = {
1213
+ "/": Home,
1214
+ }
1215
+ ```
1216
+
1217
+ Or use `getMetadata` property on the route component to dynamically generate the metadata.
1218
+
1219
+ ```tsx
1220
+ async function Post(this: FC) {
1221
+ const post = await getPost(this.request.params.slug)
1222
+ return <div>
1223
+ <h1>{post.title}</h1>
1224
+ <h2>{post.description}</h2>
1225
+ <div>{post.content}</div>
1226
+ </div>
1227
+ }
1228
+
1229
+ Post.getMetadata = async function(this: FC) {
1230
+ const post = await getPost(this.request.params.slug)
1231
+ return {
1232
+ title: post.title,
1233
+ description: post.description,
1234
+ }
1235
+ }
1236
+
1237
+ const routes = {
1238
+ "/post/:slug": Post,
1239
+ }
1240
+ ```
1241
+
1242
+ You can define global metadata with the `metadata` prop on the root `<html>` element. It applies to every page in your app, and page-specific metadata takes precedence over these global values.
1243
+
1244
+ To render metadata, add `<metadata />` inside the `head` tag:
1245
+
1246
+ ```tsx
1247
+ export default {
1248
+ fetch: (req) => (
1249
+ <html
1250
+ request={req}
1251
+ routes={routes}
1252
+ metadata={{ title: "My App" }}
1253
+ >
1254
+ <head>
1255
+ <metadata /> { /* <- `<title>My App<title>` will be rendered here */}
1256
+ </head>
1257
+ <body>
1258
+ <rouer>
1259
+ <p>Page not found</p>
1260
+ </rouer>
1261
+ </body>
1262
+ </html>
1263
+ )
1264
+ }
1265
+ ```
1266
+
1267
+ ### Fallback (404)
1268
+
1269
+ You can add fallback(404) content to the `<router>` element as children, which will be displayed when no route matches the current URL.
1270
+
1271
+ ```tsx
1272
+ export default {
1273
+ fetch: (req) => (
1274
+ <html request={req} routes={routes}>
1275
+ <router>
1276
+ <p>Page Not Found</p>
1277
+ <p>Back to <a href="/">Home</a></p>
1278
+ </router>
1279
+ </html>
1280
+ )
1281
+ }
1282
+ ```
1283
+
1284
+ ### Route Client Caching
1285
+
1286
+ By default, the router client caches the html content from the server. To disable the caching, you can add the `dynamic` option to the route component.
1287
+
1288
+ ```tsx
1289
+ // Home is a static route that can be cached on the client side
1290
+ function Home(this: FC) {
1291
+ return <p>Home</p>
1292
+ }
1293
+
1294
+ // Dash is a dynamic route that will not be cached on the client side
1295
+ // it will be fetched from the server on every navigation
1296
+ function Dash(this: FC) {
1297
+ const user = this.session.get<{ name: string }>("user")
1298
+ return <p>Welcome back, {user?.name}!</p>
1299
+ }
1300
+ Dash.dynamic = true;
1301
+
1302
+ const routes = {
1303
+ "/": Home,
1304
+ "/dash": Dash,
1305
+ }
1306
+ ```
1307
+
1308
+ ## Using Route Form
1309
+
1310
+ mono-jsx allows you to define a `FormHandler` function for route components to handle form data from client side form submissions. To submit the form data to the `FormHandler` function, you need to add the `route` prop to the `<form>` element.
1311
+
1312
+ The `FormHandler` function is also a component, so you can use all the features of the component system in it. mono-jsx provides two built-in elements to allow you to control the post-submit behavior:
1123
1313
 
1124
1314
  - `<invalid for="...">{message}</invalid>` to set custom validation state for the form elements.
1125
1315
  - `<redirect to="..." />` to redirect to a new route/URL.
@@ -1147,19 +1337,19 @@ Login.FormHandler = function(this: FC, data: FormData) {
1147
1337
 
1148
1338
  const routes = {
1149
1339
  "/login": Login,
1150
- // ... other routes ...
1151
1340
  }
1152
1341
  ```
1153
1342
 
1154
- > [!NOTE]
1343
+ > [!TIP]
1155
1344
  > You can use `:invalid` CSS selector to style the form elements with invalid state.
1156
1345
 
1157
- You can also return regular HTML elements from the route form post response. The `formslot` element is used to
1158
- mark the position where the returned HTML elements will be inserted.
1346
+ ### Using `<formslot>` element
1347
+
1348
+ You can return regular HTML elements from the form handler function. By default, the returned HTML is appended to the form element. Use the `<formslot>` element to control where the returned content is inserted. If any `<formslot>` element exists, the `mode` attribute on the `<form route>` element is ignored. `<formslot>` supports the following modes:
1159
1349
 
1160
- - `<formslot mode="replaceChildren" />`: Replace the `formslot` element's children with the HTML. This is the default mode.
1161
- - `<formslot mode="insertafter" />`: Insert HTML after the `formslot` element.
1162
- - `<formslot mode="insertbefore" />`: Insert HTML before the `formslot` element.
1350
+ - **"replaceChildren"** (default): Replace children of the `<formslot>` element with the returned HTML.
1351
+ - **"insertafter"**: Insert HTML after the `<formslot>` element.
1352
+ - **"insertbefore"**: Insert HTML before the `<formslot>` element.
1163
1353
 
1164
1354
  ```tsx
1165
1355
  function MyRoute(this: FC) {
@@ -1182,115 +1372,103 @@ MyRoute.FormHandler = function(this: FC, data: FormData) {
1182
1372
  }
1183
1373
  ```
1184
1374
 
1185
- You can also use the `name` prop to specify the name of the formslot element. And you can use the `formslot` prop to specify the name of the slot to insert the HTML into.
1375
+ You can add the `name` prop to specify the name of the formslot element. And use `formslot` prop in the form handler function to specify the name of the slot to insert the HTML into.
1186
1376
 
1187
1377
  ```tsx
1188
1378
  function MyRoute(this: FC) {
1189
1379
  return (
1190
1380
  <div>
1191
- <formslot name="message" />
1192
1381
  <form route>
1193
1382
  <button type="submit">Send</button>
1383
+ <formslot name="info" /> { /* <- "This is info message" will be inserted here */ }
1384
+ <formslot name="error" /> { /* <- "This is error message" will be inserted here */ }
1194
1385
  </form>
1195
1386
  </div>
1196
1387
  )
1197
1388
  }
1198
1389
 
1199
1390
  MyRoute.FormHandler = function(this: FC, data: FormData) {
1200
- return <p formslot="message">Hello, world!</p>
1391
+ return (
1392
+ <>
1393
+ <p formslot="info">This is info message</p>
1394
+ <p formslot="error">This is error message</p>
1395
+ </>
1396
+ )
1201
1397
  }
1202
1398
  ```
1203
1399
 
1204
- `formslot` element accepts the `onUpdate` prop to set a callback function that will be called when the formslot element is updated.
1400
+ The `formslot` prop also accepts the following special values:
1401
+
1402
+ - `:form`: Replace the form element with the returned HTML.
1403
+ - `:router`: Replace the children of current `<router>` element with the returned HTML.
1404
+ - `:root`: Replace the children of the page with the returned HTML.
1205
1405
 
1206
1406
  ```tsx
1207
1407
  function MyRoute(this: FC) {
1208
1408
  return (
1209
- <form>
1210
- <input type="text" name="message" placeholder="Type Message..." />
1409
+ { /* the form will be replaced with the returned HTML after the form is submitted */ }
1410
+ <form route>
1211
1411
  <button type="submit">Send</button>
1212
- <formslot onUpdate={(evt) => console.log("message updated:", evt.target.textContent)} />
1213
1412
  </form>
1214
1413
  )
1215
1414
  }
1216
1415
 
1217
1416
  MyRoute.FormHandler = function(this: FC, data: FormData) {
1218
- return <p>{data.get("message")}</p>
1417
+ return <p formslot=":form">Form submitted</p>
1219
1418
  }
1220
1419
  ```
1221
1420
 
1222
- The `hidden` prop can be used to hide the formslot payload from the form handler.
1223
-
1224
- ```tsx
1225
- <formslot onUpdate={(evt) => console.log("message updated:", evt.target.textContent)} hidden />
1226
- ```
1227
-
1228
- ### Using `this.app.url` Signal
1229
-
1230
- `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 `<show>`, `<hidden>` or `<switch>` elements:
1421
+ The `<formslot>` element accepts a `onUpdate` prop as a callback function that will be invoked when the formslot element is updated.
1231
1422
 
1232
1423
  ```tsx
1233
- function App(this: FC) {
1424
+ function MyRoute(this: FC) {
1234
1425
  return (
1235
- <div>
1236
- <h1>Current Pathname: {this.$(() => this.app.url.pathname)}</h1>
1237
- </div>
1426
+ <form>
1427
+ <input type="text" name="message" placeholder="Type Message..." />
1428
+ <button type="submit">Send</button>
1429
+ <formslot hidden onUpdate={(evt) => console.log("message updated:", evt.target.textContent)} />
1430
+ </form>
1238
1431
  )
1239
1432
  }
1433
+
1434
+ MyRoute.FormHandler = function(this: FC, data: FormData) {
1435
+ return <p>{data.get("message")}</p>
1436
+ }
1240
1437
  ```
1241
1438
 
1242
- ### Navigation between Pages
1439
+ > [!TIP]
1440
+ > You can use the `hidden` prop with the `onUpdate` prop to hide the formslot element. It is useful when you only want to know what content is returned from the form handler and don't want to display it on the page.
1243
1441
 
1244
- To navigate between pages, you can use `<a>` elements with `href` props 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:
1442
+ ### Submitting State
1245
1443
 
1246
- ```tsx
1247
- export default {
1248
- fetch: (req) => (
1249
- <html request={req} routes={routes}>
1250
- <nav>
1251
- <a href="/">Home</a>
1252
- <a href="/about">About</a>
1253
- <a href="/blog">Blog</a>
1254
- </nav>
1255
- <router />
1256
- </html>
1257
- )
1258
- }
1259
- ```
1444
+ When a `<form route>` is submitted, mono-jsx automatically manages a short "submitting" state on the client:
1260
1445
 
1261
- ### Nav Links
1446
+ - Adds a CSS class to the form while the request is in flight (default: `submitting`).
1447
+ - Disables all form controls to prevent double submit, then restores their original disabled state.
1448
+ - Clears the current content of local `<formslot>` elements before inserting the next response payload.
1262
1449
 
1263
- 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` prop on the `<nav>` element. You can add styles for the active link using nested CSS selectors in the `style` prop of the `<nav>` element.
1450
+ You can use the `data-submitting-class` attribute to customize the submitting state class name:
1264
1451
 
1265
1452
  ```tsx
1266
- export default {
1267
- fetch: (req) => (
1268
- <html request={req} routes={routes}>
1269
- <nav style={{ "& a.active": { fontWeight: "bold" } }} data-active-class="active">
1270
- <a href="/">Home</a>
1271
- <a href="/about">About</a>
1272
- <a href="/blog">Blog</a>
1273
- </nav>
1274
- <router />
1275
- </html>
1453
+ function Contact(this: FC) {
1454
+ return (
1455
+ <form route data-submitting-class="is-loading">
1456
+ <input type="email" name="email" required />
1457
+ <button type="submit">Subscribe</button>
1458
+ <formslot />
1459
+ </form>
1276
1460
  )
1277
1461
  }
1278
- ```
1279
1462
 
1280
- ### Fallback (404)
1281
-
1282
- You can add fallback(404) content to the `<router>` element as children, which will be displayed when no route matches the current URL.
1463
+ Contact.FormHandler = function(this: FC, data: FormData) {
1464
+ return <p>Thanks, {data.get("email")}!</p>
1465
+ }
1466
+ ```
1283
1467
 
1284
- ```tsx
1285
- export default {
1286
- fetch: (req) => (
1287
- <html request={req} routes={routes}>
1288
- <router>
1289
- <p>Page Not Found</p>
1290
- <p>Back to <a href="/">Home</a></p>
1291
- </router>
1292
- </html>
1293
- )
1468
+ ```css
1469
+ form.is-loading {
1470
+ opacity: 0.6;
1471
+ pointer-events: none;
1294
1472
  }
1295
1473
  ```
1296
1474
 
@@ -1343,17 +1521,39 @@ export default {
1343
1521
  ### Session Storage API
1344
1522
 
1345
1523
  ```ts
1346
- function Component(this: FC) {
1347
- // set a value in the session
1348
- this.session.set("user", { name: "John" })
1349
- // get a value from the session
1350
- this.session.get<{ name: string }>("user") // { name: "John" }
1351
- // get all entries from the session
1352
- this.session.entries() // [["user", { name: "John" }]]
1353
- // delete a value from the session
1354
- this.session.delete("user")
1355
- // destroy the session
1356
- this.session.destroy()
1524
+ export interface Session {
1525
+ /**
1526
+ * The session ID.
1527
+ */
1528
+ readonly sessionId: string;
1529
+ /**
1530
+ * If true, update the session cookie to the client.
1531
+ */
1532
+ readonly isDirty: boolean;
1533
+ /**
1534
+ * If true, the session is expired.
1535
+ */
1536
+ readonly isExpired: boolean;
1537
+ /**
1538
+ * Gets a value from the session.
1539
+ */
1540
+ get<T = unknown>(key: string): T | undefined;
1541
+ /**
1542
+ * Gets all the entries from the session.
1543
+ */
1544
+ all(): Record<string, unknown>;
1545
+ /**
1546
+ * Sets a value in the session.
1547
+ */
1548
+ set(key: string, value: string | number | boolean | any[] | Record<string, unknown>): void;
1549
+ /**
1550
+ * Deletes a value from the session.
1551
+ */
1552
+ delete(key: string): void;
1553
+ /**
1554
+ * Destroys the session.
1555
+ */
1556
+ destroy(): void;
1357
1557
  }
1358
1558
  ```
1359
1559
 
@@ -1368,14 +1568,14 @@ mono-jsx provides a built-in RPC API that allows you to call functions on the se
1368
1568
  import { createRPC } from "mono-jsx"
1369
1569
 
1370
1570
  const rpc = createRPC({
1371
- whoami: () => ({ name: "John" })
1571
+ whoami: () => ({ name: "John Wick" })
1372
1572
  })
1373
1573
 
1374
- function App(this: FC<{ user?: { name: string } }>) {
1574
+ function App(this: FC<{ user?: { id: number, name: string } }>) {
1375
1575
  return (
1376
1576
  <div>
1377
1577
  <show when={this.user}>
1378
- <p>Welcome, {this.user!.name}!</p>
1578
+ <p>Welcome, {this.user!.name}</p>
1379
1579
  </show>
1380
1580
  <button onClick={async () => this.user = await rpc.whoami()}>Who am I?</button>
1381
1581
  </div>
@@ -1402,38 +1602,27 @@ You can access the request info in RPC functions by using the `this` scope:
1402
1602
  - `this.context`: The [context](#using-context) object.
1403
1603
  - `this.session`: The [session](#session-storage-api) storage.
1404
1604
 
1405
- ```ts
1406
- type Admin = {
1407
- isAdmin: (id: number) => boolean;
1408
- }
1409
-
1605
+ ```tsx
1410
1606
  const rpc = createRPC({
1411
- whoami: function (this: WithContext<RPC, { admin: Admin }>) {
1412
- const { admin } = this.context;
1413
- const user = this.session.get<{ name: string }>("user")
1607
+ whoami: function (this: WithContext<RPC, { group: string }>) {
1608
+ const user = this.session.get<{ id: number, name: string }>("user")
1414
1609
  return {
1610
+ ...user,
1611
+ group: this.context.group,
1415
1612
  ip: this.request.headers.get("x-real-ip"),
1416
- isAdmin: user ? admin.isAdmin(user.id) : false,
1417
- user: user,
1418
1613
  }
1419
1614
  },
1420
1615
  })
1421
1616
 
1422
- function App(this: FC) {
1423
- return (
1424
- <button onClick={async () => console.log(await rpc.whoami())}>Who am I?</button>
1425
- )
1426
- }
1427
-
1428
1617
  export default {
1429
1618
  fetch: (req) => (
1430
1619
  <html
1431
1620
  request={req}
1432
1621
  expose={{ rpc }}
1433
1622
  session={{ cookie: { secret: "..." } }}
1434
- context={{ admin: { isAdmin: (id: number) => id === 1 } }}
1623
+ context={{ group: "admin" }}
1435
1624
  >
1436
- <App />
1625
+ <button onClick={async () => console.log(await rpc.whoami())}>Who am I?</button>
1437
1626
  </html>
1438
1627
  )
1439
1628
  }