mono-jsx 0.3.4 → 0.5.0

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