mancha 0.17.0 → 0.17.4

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.
Files changed (86) hide show
  1. package/.github/workflows/ci.yml +11 -10
  2. package/.prettierrc +2 -2
  3. package/.vscode/extensions.json +1 -1
  4. package/.vscode/launch.json +33 -43
  5. package/README.md +144 -84
  6. package/dist/browser.d.ts +2 -0
  7. package/dist/browser.js +1 -1
  8. package/dist/browser.js.map +1 -1
  9. package/dist/cli.js.map +1 -1
  10. package/dist/css_gen_basic.js.map +1 -1
  11. package/dist/css_gen_utils.d.ts +786 -0
  12. package/dist/css_gen_utils.js +350 -399
  13. package/dist/css_gen_utils.js.map +1 -1
  14. package/dist/dome.d.ts +1 -0
  15. package/dist/dome.js +15 -0
  16. package/dist/dome.js.map +1 -1
  17. package/dist/expressions/ast.d.ts +16 -16
  18. package/dist/expressions/ast.test.js +89 -64
  19. package/dist/expressions/ast.test.js.map +1 -1
  20. package/dist/expressions/ast_factory.d.ts +1 -1
  21. package/dist/expressions/ast_factory.js +17 -17
  22. package/dist/expressions/ast_factory.js.map +1 -1
  23. package/dist/expressions/ast_factory.test.js +42 -36
  24. package/dist/expressions/ast_factory.test.js.map +1 -1
  25. package/dist/expressions/constants.js +56 -56
  26. package/dist/expressions/constants.js.map +1 -1
  27. package/dist/expressions/constants.test.js +57 -57
  28. package/dist/expressions/constants.test.js.map +1 -1
  29. package/dist/expressions/eval.d.ts +17 -17
  30. package/dist/expressions/eval.js +58 -60
  31. package/dist/expressions/eval.js.map +1 -1
  32. package/dist/expressions/eval.test.js +11 -8
  33. package/dist/expressions/eval.test.js.map +1 -1
  34. package/dist/expressions/expressions.test.d.ts +6 -6
  35. package/dist/expressions/expressions.test.js +6 -6
  36. package/dist/expressions/index.d.ts +6 -6
  37. package/dist/expressions/index.js +6 -6
  38. package/dist/expressions/parser.d.ts +3 -3
  39. package/dist/expressions/parser.js +45 -45
  40. package/dist/expressions/parser.js.map +1 -1
  41. package/dist/expressions/parser.test.js +43 -2
  42. package/dist/expressions/parser.test.js.map +1 -1
  43. package/dist/expressions/tokenizer.js +22 -25
  44. package/dist/expressions/tokenizer.js.map +1 -1
  45. package/dist/expressions/tokenizer.test.js +40 -15
  46. package/dist/expressions/tokenizer.test.js.map +1 -1
  47. package/dist/index.d.ts +2 -0
  48. package/dist/index.js +1 -0
  49. package/dist/index.js.map +1 -1
  50. package/dist/interfaces.d.ts +1 -1
  51. package/dist/iterator.js.map +1 -1
  52. package/dist/mancha.js +1 -1
  53. package/dist/mancha.js.map +1 -1
  54. package/dist/plugins.js +96 -25
  55. package/dist/plugins.js.map +1 -1
  56. package/dist/query.js.map +1 -1
  57. package/dist/renderer.js +4 -2
  58. package/dist/renderer.js.map +1 -1
  59. package/dist/safe_browser.js +1 -1
  60. package/dist/safe_browser.js.map +1 -1
  61. package/dist/store.d.ts +10 -4
  62. package/dist/store.js +26 -10
  63. package/dist/store.js.map +1 -1
  64. package/dist/test_utils.d.ts +2 -0
  65. package/dist/test_utils.js +14 -1
  66. package/dist/test_utils.js.map +1 -1
  67. package/dist/trusted_attributes.js +2 -0
  68. package/dist/trusted_attributes.js.map +1 -1
  69. package/dist/type_checker.js +57 -22
  70. package/dist/type_checker.js.map +1 -1
  71. package/dist/worker.d.ts +2 -0
  72. package/dist/worker.js +1 -0
  73. package/dist/worker.js.map +1 -1
  74. package/docs/css.md +309 -0
  75. package/docs/quickstart.md +414 -241
  76. package/global.d.ts +2 -2
  77. package/gulpfile.ts +44 -0
  78. package/package.json +86 -84
  79. package/scripts/generate-css-docs.ts +263 -0
  80. package/tsconfig.json +42 -19
  81. package/tsec_exemptions.json +8 -3
  82. package/webpack.config.esmodule.ts +26 -0
  83. package/webpack.config.ts +21 -0
  84. package/gulpfile.js +0 -44
  85. package/webpack.config.esmodule.js +0 -23
  86. package/webpack.config.js +0 -18
@@ -12,30 +12,31 @@ To get started, simply add this to your HTML head attribute:
12
12
  ## Basic Form
13
13
 
14
14
  After importing `Mancha`, you can take advantage of reactivity and tailwind-compatible CSS styling.
15
+ For a full list of supported CSS utilities and basic styles, see the [CSS Documentation](./css.md).
15
16
  For example, a basic form might look like this
16
17
 
17
18
  ```html
18
19
  <body :data="{ name: null }">
19
- <form
20
- class="flex flex-col max-w-md p-4 bg-white rounded-lg"
21
- :on:submit="console.log('submitted')"
22
- >
23
- <label class="w-full mb-4">
24
- <span class="block text-sm font-medium text-gray-700">Name</span>
25
- <input
26
- required
27
- type="text"
28
- :bind="name"
29
- class="w-full mt-1 p-2 border border-gray-300 rounded-md focus:border-indigo-500"
30
- />
31
- </label>
32
- <button
33
- type="submit"
34
- class="w-full py-2 px-4 bg-indigo-600 text-white rounded-md hover:bg-indigo-700"
35
- >
36
- Submit
37
- </button>
38
- </form>
20
+ <form
21
+ class="flex flex-col max-w-md p-4 bg-white rounded-lg"
22
+ :on:submit="console.log('submitted')"
23
+ >
24
+ <label class="w-full mb-4">
25
+ <span class="block text-sm font-medium text-gray-700">Name</span>
26
+ <input
27
+ required
28
+ type="text"
29
+ :bind="name"
30
+ class="w-full mt-1 p-2 border border-gray-300 rounded-md focus:border-indigo-500"
31
+ />
32
+ </label>
33
+ <button
34
+ type="submit"
35
+ class="w-full py-2 px-4 bg-indigo-600 text-white rounded-md hover:bg-indigo-700"
36
+ >
37
+ Submit
38
+ </button>
39
+ </form>
39
40
  </body>
40
41
  ```
41
42
 
@@ -46,42 +47,42 @@ modifier to the event attribute, for example `:on:click.prevent`. To provide mor
46
47
 
47
48
  ```html
48
49
  <body :data="{ name: null, message: null }">
49
- <!-- Form with handler referencing a user-defined function -->
50
- <form class="flex flex-col max-w-md p-4 bg-white rounded-lg" :on:submit="handleForm($event)">
51
- <label class="w-full mb-4">
52
- <span class="block text-sm font-medium text-gray-700">Name</span>
53
- <input
54
- required
55
- type="text"
56
- :bind="name"
57
- class="w-full mt-1 p-2 border border-gray-300 rounded-md focus:border-indigo-500"
58
- />
59
- </label>
60
- <button
61
- type="submit"
62
- class="w-full py-2 px-4 bg-indigo-600 text-white rounded-md hover:bg-indigo-700"
63
- >
64
- Submit
65
- </button>
66
-
67
- <!-- To be shown only once `message` is truthy -->
68
- <p class="w-full mt-4 text-gray-700 text-center" :show="message">{{ message }}</p>
69
- </form>
50
+ <!-- Form with handler referencing a user-defined function -->
51
+ <form class="flex flex-col max-w-md p-4 bg-white rounded-lg" :on:submit="handleForm($event)">
52
+ <label class="w-full mb-4">
53
+ <span class="block text-sm font-medium text-gray-700">Name</span>
54
+ <input
55
+ required
56
+ type="text"
57
+ :bind="name"
58
+ class="w-full mt-1 p-2 border border-gray-300 rounded-md focus:border-indigo-500"
59
+ />
60
+ </label>
61
+ <button
62
+ type="submit"
63
+ class="w-full py-2 px-4 bg-indigo-600 text-white rounded-md hover:bg-indigo-700"
64
+ >
65
+ Submit
66
+ </button>
67
+
68
+ <!-- To be shown only once `message` is truthy -->
69
+ <p class="w-full mt-4 text-gray-700 text-center" :show="message">{{ message }}</p>
70
+ </form>
70
71
  </body>
71
72
 
72
73
  <script>
73
- // Mancha is a global variable and $ is a shorthand for the renderer context.
74
- const { $ } = Mancha;
75
-
76
- // We can use the $ shorthand to access the form data and define variables.
77
- $.handleForm = function (event) {
78
- console.log(event);
79
- this.message = `Hello, ${this.name}!`;
80
- };
81
-
82
- // The script tag already contains the `init` attribute. So we don't need
83
- // to call `$.mount()` explicitly.
84
- // $.mount(document.body);
74
+ // Mancha is a global variable and $ is a shorthand for the renderer context.
75
+ const { $ } = Mancha;
76
+
77
+ // We can use the $ shorthand to access the form data and define variables.
78
+ $.handleForm = function (event) {
79
+ console.log(event);
80
+ this.message = `Hello, ${this.name}!`;
81
+ };
82
+
83
+ // The script tag already contains the `init` attribute. So we don't need
84
+ // to call `$.mount()` explicitly.
85
+ // $.mount(document.body);
85
86
  </script>
86
87
  ```
87
88
 
@@ -92,43 +93,43 @@ the `<script>` tag that imports `Mancha`, and explicitly call the `mount()` func
92
93
  <script src="//unpkg.com/mancha" css="utils"></script>
93
94
 
94
95
  <body>
95
- <form class="flex flex-col max-w-md p-4 bg-white rounded-lg" :on:submit="handleForm($event)">
96
- <label class="w-full mb-4">
97
- <span class="block text-sm font-medium text-gray-700">Name</span>
98
- <input
99
- required
100
- type="text"
101
- :bind="name"
102
- class="w-full mt-1 p-2 border border-gray-300 rounded-md focus:border-indigo-500"
103
- />
104
- </label>
105
- <button
106
- type="submit"
107
- class="w-full py-2 px-4 bg-indigo-600 text-white rounded-md hover:bg-indigo-700"
108
- >
109
- Submit
110
- </button>
111
-
112
- <p class="w-full mt-4 text-gray-700 text-center" :show="message">{{ message }}</p>
113
- </form>
96
+ <form class="flex flex-col max-w-md p-4 bg-white rounded-lg" :on:submit="handleForm($event)">
97
+ <label class="w-full mb-4">
98
+ <span class="block text-sm font-medium text-gray-700">Name</span>
99
+ <input
100
+ required
101
+ type="text"
102
+ :bind="name"
103
+ class="w-full mt-1 p-2 border border-gray-300 rounded-md focus:border-indigo-500"
104
+ />
105
+ </label>
106
+ <button
107
+ type="submit"
108
+ class="w-full py-2 px-4 bg-indigo-600 text-white rounded-md hover:bg-indigo-700"
109
+ >
110
+ Submit
111
+ </button>
112
+
113
+ <p class="w-full mt-4 text-gray-700 text-center" :show="message">{{ message }}</p>
114
+ </form>
114
115
  </body>
115
116
 
116
117
  <script type="module">
117
- // Mancha is a global variable and $ is a shorthand for the renderer context.
118
- const { $ } = Mancha;
118
+ // Mancha is a global variable and $ is a shorthand for the renderer context.
119
+ const { $ } = Mancha;
119
120
 
120
- // We can use the $ shorthand to access the form data and define variables.
121
- $.handleForm = function (event) {
122
- console.log(event);
123
- this.message = `Hello, ${this.name}!`;
124
- };
121
+ // We can use the $ shorthand to access the form data and define variables.
122
+ $.handleForm = function (event) {
123
+ console.log(event);
124
+ this.message = `Hello, ${this.name}!`;
125
+ };
125
126
 
126
- // Define the variables that will be used in the form.
127
- $.name = null;
128
- $.message = null;
127
+ // Define the variables that will be used in the form.
128
+ $.name = null;
129
+ $.message = null;
129
130
 
130
- // Mount the renderer context to the body element.
131
- await $.mount(document.body);
131
+ // Mount the renderer context to the body element.
132
+ await $.mount(document.body);
132
133
  </script>
133
134
  ```
134
135
 
@@ -139,9 +140,9 @@ the `<script>` tag that imports `Mancha`, and explicitly call the `mount()` func
139
140
  ```html
140
141
  <!-- Use <template is="my-component-name"> to register a component. -->
141
142
  <template is="my-red-button">
142
- <button style="background-color: red;">
143
- <slot></slot>
144
- </button>
143
+ <button style="background-color: red;">
144
+ <slot></slot>
145
+ </button>
145
146
  </template>
146
147
  ```
147
148
 
@@ -169,19 +170,184 @@ Then in `index.html`:
169
170
  ```html
170
171
  <!-- src/index.html -->
171
172
  <head>
172
- <!-- ... -->
173
+ <!-- ... -->
173
174
  </head>
174
175
  <body>
175
- <!-- Include the custom component definition before using any of the components -->
176
- <include src="components/registry.tpl.html" />
176
+ <!-- Include the custom component definition before using any of the components -->
177
+ <include src="components/registry.tpl.html" />
177
178
 
178
- <!-- Now you can use any of the custom components -->
179
- <my-red-button>Click Me!</my-red-button>
179
+ <!-- Now you can use any of the custom components -->
180
+ <my-red-button>Click Me!</my-red-button>
180
181
 
181
- <!-- Any other components can also use the custom components, and don't need to re-import them -->
182
- <include src="components/footer.tpl.html"/>
182
+ <!-- Any other components can also use the custom components, and don't need to re-import them -->
183
+ <include src="components/footer.tpl.html" />
184
+ </body>
185
+ ```
186
+
187
+ ## Element Initialization with `:render`
188
+
189
+ The `:render` attribute links any HTML element to a JavaScript ES module for initialization. This is useful when you need to initialize third-party libraries (like charts, maps, or video players) on specific elements.
190
+
191
+ ### Basic Usage
192
+
193
+ ```html
194
+ <canvas :render="./chart-init.js"></canvas>
195
+ ```
196
+
197
+ The module's default export is called with the element and renderer as arguments:
198
+
199
+ ```js
200
+ // chart-init.js
201
+ export default function (elem, renderer) {
202
+ // Initialize a chart library on the canvas element
203
+ const chart = new Chart(elem, {
204
+ type: "bar",
205
+ data: { labels: ["A", "B", "C"], datasets: [{ data: [1, 2, 3] }] },
206
+ });
207
+
208
+ // Optionally store a reference for later access
209
+ elem._chart = chart;
210
+ }
211
+ ```
212
+
213
+ ### Using with Custom Components
214
+
215
+ The `:render` attribute works naturally inside custom component templates. This is the recommended pattern for creating reusable components that need JavaScript initialization:
216
+
217
+ ```
218
+ src/
219
+ ├─ components/
220
+ │ ├─ chart-widget.tpl.html
221
+ │ ├─ chart-widget.js
222
+ │ ├─ registry.tpl.html
223
+ ├─ index.html
224
+ ```
225
+
226
+ ```html
227
+ <!-- components/chart-widget.tpl.html -->
228
+ <template is="chart-widget">
229
+ <div class="chart-container">
230
+ <canvas :render="./chart-widget.js"></canvas>
231
+ <slot></slot>
232
+ </div>
233
+ </template>
234
+ ```
235
+
236
+ ```js
237
+ // components/chart-widget.js
238
+ export default function (elem, renderer) {
239
+ // Access data passed via :data attribute on the component
240
+ const { labels, values } = renderer.$;
241
+
242
+ new Chart(elem, {
243
+ type: "bar",
244
+ data: {
245
+ labels: labels || ["A", "B", "C"],
246
+ datasets: [{ data: values || [1, 2, 3] }],
247
+ },
248
+ });
249
+ }
183
250
  ```
184
251
 
252
+ ```html
253
+ <!-- components/registry.tpl.html -->
254
+ <include src="./chart-widget.tpl.html" />
255
+ ```
256
+
257
+ ```html
258
+ <!-- index.html -->
259
+ <body>
260
+ <include src="components/registry.tpl.html" />
261
+
262
+ <!-- Use the component with custom data -->
263
+ <chart-widget :data="{ labels: ['Jan', 'Feb', 'Mar'], values: [10, 20, 15] }">
264
+ <p>Monthly Sales</p>
265
+ </chart-widget>
266
+
267
+ <!-- Use it again with different data -->
268
+ <chart-widget :data="{ labels: ['Q1', 'Q2', 'Q3', 'Q4'], values: [100, 150, 120, 180] }">
269
+ <p>Quarterly Revenue</p>
270
+ </chart-widget>
271
+ </body>
272
+ ```
273
+
274
+ Relative paths like `./chart-widget.js` are automatically resolved based on where the template is defined (`/components/`), not where the component is used.
275
+
276
+ ### Works on Any Element
277
+
278
+ The `:render` attribute works on any HTML element, not just inside custom components:
279
+
280
+ ```html
281
+ <video :render="./video-player.js" src="movie.mp4"></video>
282
+ <div :render="./carousel.js" class="slides"></div>
283
+ <form :render="./form-validation.js"></form>
284
+ ```
285
+
286
+ ### Accessing Renderer State
287
+
288
+ The init function receives the renderer instance, giving you access to reactive state:
289
+
290
+ ```js
291
+ // counter-canvas.js
292
+ export default function (elem, renderer) {
293
+ const ctx = elem.getContext("2d");
294
+
295
+ // Access current state
296
+ const count = renderer.$.count;
297
+
298
+ // Draw based on state
299
+ ctx.fillText(`Count: ${count}`, 10, 50);
300
+
301
+ // Watch for changes using the renderer's effect system
302
+ renderer.effect(function () {
303
+ ctx.clearRect(0, 0, elem.width, elem.height);
304
+ ctx.fillText(`Count: ${this.$.count}`, 10, 50);
305
+ });
306
+ }
307
+ ```
308
+
309
+ ### Server-Side Rendering Compatibility
310
+
311
+ During server-side rendering (SSR), the `:render` attribute's path is resolved but the JavaScript module is not executed. The module is only executed when the HTML is hydrated in the browser, making this feature fully compatible with SSR workflows.
312
+
313
+ ### Setting Undefined Variables in `:render`
314
+
315
+ A powerful pattern is using `:render` to set variables that are already referenced in your template
316
+ but not pre-defined. When a template references an undefined variable, `mancha` automatically
317
+ initializes it to `undefined` and attaches an observer. Since the `:render` callback receives the
318
+ element's renderer, setting variables through it will trigger reactive updates in the template:
319
+
320
+ ```html
321
+ <div :render="./data-loader.js">
322
+ <h1>{{ pageTitle }}</h1>
323
+ <ul :for="item in dataItems">
324
+ <li>{{ item.name }}: {{ item.value }}</li>
325
+ </ul>
326
+ <p :show="loading">Loading...</p>
327
+ </div>
328
+ ```
329
+
330
+ ```js
331
+ // data-loader.js
332
+ export default async function (elem, renderer) {
333
+ // Set loading state.
334
+ await renderer.set("loading", true);
335
+
336
+ // Fetch data from an API.
337
+ const response = await fetch("/api/data");
338
+ const data = await response.json();
339
+
340
+ // Set the variables - the template will reactively update.
341
+ await renderer.set("pageTitle", data.title);
342
+ await renderer.set("dataItems", data.items);
343
+ await renderer.set("loading", false);
344
+ }
345
+ ```
346
+
347
+ Note that `renderer.set()` is async and returns a Promise. You only need to `await` if subsequent
348
+ code depends on those variables being set. You can also set multiple variables concurrently with
349
+ `Promise.all([renderer.set("a", 1), renderer.set("b", 2)])`.
350
+
185
351
  ## URL Query Parameter Binding
186
352
 
187
353
  `mancha` makes it easy to synchronize your application's state with the URL query parameters. This is particularly useful for maintaining state across page reloads or for creating shareable links.
@@ -192,20 +358,20 @@ Here's how you can use it:
192
358
 
193
359
  ```html
194
360
  <body :data="{ $$search: '' }">
195
- <form class="flex flex-col max-w-md p-4 bg-white rounded-lg">
196
- <label class="w-full mb-4">
197
- <span class="block text-sm font-medium text-gray-700">Search</span>
198
- <input
199
- type="text"
200
- :bind="$$search"
201
- class="w-full mt-1 p-2 border border-gray-300 rounded-md focus:border-indigo-500"
202
- placeholder="Type to see the URL change..."
203
- />
204
- </label>
205
- <div :show="$$search" class="mt-2">
206
- <p>Current search query: <span class="font-mono">{{ $$search }}</span></p>
207
- </div>
208
- </form>
361
+ <form class="flex flex-col max-w-md p-4 bg-white rounded-lg">
362
+ <label class="w-full mb-4">
363
+ <span class="block text-sm font-medium text-gray-700">Search</span>
364
+ <input
365
+ type="text"
366
+ :bind="$$search"
367
+ class="w-full mt-1 p-2 border border-gray-300 rounded-md focus:border-indigo-500"
368
+ placeholder="Type to see the URL change..."
369
+ />
370
+ </label>
371
+ <div :show="$$search" class="mt-2">
372
+ <p>Current search query: <span class="font-mono">{{ $$search }}</span></p>
373
+ </div>
374
+ </form>
209
375
  </body>
210
376
  ```
211
377
 
@@ -220,8 +386,8 @@ Here's an example of how you can test a simple component:
220
386
  ```html
221
387
  <!-- my-component.html -->
222
388
  <body>
223
- <button data-testid="login-button" :show="!user">Login</button>
224
- <button data-testid="logout-button" :show="user">Logout</button>
389
+ <button data-testid="login-button" :show="!user">Login</button>
390
+ <button data-testid="logout-button" :show="user">Logout</button>
225
391
  </body>
226
392
  ```
227
393
 
@@ -237,45 +403,44 @@ const componentPath = path.join(import.meta.dirname, "my-component.html");
237
403
  const findByTestId = (node, testId) => node.querySelector(`[data-testid="${testId}"]`);
238
404
 
239
405
  describe("My Component", () => {
406
+ test("renders correctly when logged out", async () => {
407
+ // 1. Initialize the renderer with the desired state for this test case.
408
+ const renderer = new Renderer({ user: null });
240
409
 
241
- test("renders correctly when logged out", async () => {
242
- // 1. Initialize the renderer with the desired state for this test case.
243
- const renderer = new Renderer({ user: null });
244
-
245
- // 2. Create a clean DOM fragment from the page content.
246
- const fragment = await renderer.preprocessLocal(componentPath);
410
+ // 2. Create a clean DOM fragment from the page content.
411
+ const fragment = await renderer.preprocessLocal(componentPath);
247
412
 
248
- // 3. Mount the renderer to the fragment to apply data bindings.
249
- await renderer.mount(fragment);
413
+ // 3. Mount the renderer to the fragment to apply data bindings.
414
+ await renderer.mount(fragment);
250
415
 
251
- // 4. Find elements and assert their state.
252
- const loginButton = findByTestId(fragment, "login-button");
253
- const logoutButton = findByTestId(fragment, "logout-button");
416
+ // 4. Find elements and assert their state.
417
+ const loginButton = findByTestId(fragment, "login-button");
418
+ const logoutButton = findByTestId(fragment, "logout-button");
254
419
 
255
- assert.ok(loginButton, "Login button should exist");
256
- assert.ok(logoutButton, "Logout button should exist");
420
+ assert.ok(loginButton, "Login button should exist");
421
+ assert.ok(logoutButton, "Logout button should exist");
257
422
 
258
- assert.strictEqual(loginButton.style.display, "", "Login button should be visible");
259
- assert.strictEqual(logoutButton.style.display, "none", "Logout button should be hidden");
260
- });
423
+ assert.strictEqual(loginButton.style.display, "", "Login button should be visible");
424
+ assert.strictEqual(logoutButton.style.display, "none", "Logout button should be hidden");
425
+ });
261
426
 
262
- test("renders correctly when logged in", async () => {
263
- // 1. Initialize the renderer with the desired state for this test case.
264
- const renderer = new Renderer({ user: { name: "John Doe" } });
427
+ test("renders correctly when logged in", async () => {
428
+ // 1. Initialize the renderer with the desired state for this test case.
429
+ const renderer = new Renderer({ user: { name: "John Doe" } });
265
430
 
266
- // 2. Create a clean DOM fragment from the page content.
267
- const fragment = await renderer.preprocessLocal(componentPath);
431
+ // 2. Create a clean DOM fragment from the page content.
432
+ const fragment = await renderer.preprocessLocal(componentPath);
268
433
 
269
- // 3. Mount the renderer to the fragment to apply data bindings.
270
- await renderer.mount(fragment);
434
+ // 3. Mount the renderer to the fragment to apply data bindings.
435
+ await renderer.mount(fragment);
271
436
 
272
- // 4. Find elements and assert their state.
273
- const loginButton = findByTestId(fragment, "login-button");
274
- const logoutButton = findByTestId(fragment, "logout-button");
437
+ // 4. Find elements and assert their state.
438
+ const loginButton = findByTestId(fragment, "login-button");
439
+ const logoutButton = findByTestId(fragment, "logout-button");
275
440
 
276
- assert.strictEqual(loginButton.style.display, "none", "Login button should be hidden");
277
- assert.strictEqual(logoutButton.style.display, "", "Logout button should be visible");
278
- });
441
+ assert.strictEqual(loginButton.style.display, "none", "Login button should be hidden");
442
+ assert.strictEqual(logoutButton.style.display, "", "Logout button should be visible");
443
+ });
279
444
  });
280
445
  ```
281
446
 
@@ -287,15 +452,15 @@ The `Renderer` and `SignalStore` classes support generic type parameters for typ
287
452
  import { Renderer } from "mancha";
288
453
 
289
454
  interface AppState {
290
- user: { name: string; email: string } | null;
291
- count: number;
292
- items: string[];
455
+ user: { name: string; email: string } | null;
456
+ count: number;
457
+ items: string[];
293
458
  }
294
459
 
295
460
  const renderer = new Renderer<AppState>({
296
- user: null,
297
- count: 0,
298
- items: ["a", "b"],
461
+ user: null,
462
+ count: 0,
463
+ items: ["a", "b"],
299
464
  });
300
465
 
301
466
  // Type-safe access via the $ proxy
@@ -327,12 +492,13 @@ Use the `:types` attribute to declare types for variables in your templates:
327
492
 
328
493
  ```html
329
494
  <div :types='{"name": "string", "age": "number"}'>
330
- <span>{{ name.toUpperCase() }}</span>
331
- <span>{{ age.toFixed(0) }}</span>
495
+ <span>{{ name.toUpperCase() }}</span>
496
+ <span>{{ age.toFixed(0) }}</span>
332
497
  </div>
333
498
  ```
334
499
 
335
500
  The type checker will validate that:
501
+
336
502
  - `name.toUpperCase()` is valid (string has toUpperCase method)
337
503
  - `age.toFixed(0)` is valid (number has toFixed method)
338
504
  - Using `name.toFixed()` would be an error (string doesn't have toFixed)
@@ -374,11 +540,11 @@ The type checker understands `:for` loops and infers the item type from the arra
374
540
 
375
541
  ```html
376
542
  <div :types='{"users": "{ name: string, age: number }[]"}'>
377
- <ul :for="user in users">
378
- <!-- 'user' is automatically typed as { name: string, age: number } -->
379
- <li>{{ user.name.toUpperCase() }}</li>
380
- <li>{{ user.age.toFixed(0) }}</li>
381
- </ul>
543
+ <ul :for="user in users">
544
+ <!-- 'user' is automatically typed as { name: string, age: number } -->
545
+ <li>{{ user.name.toUpperCase() }}</li>
546
+ <li>{{ user.age.toFixed(0) }}</li>
547
+ </ul>
382
548
  </div>
383
549
  ```
384
550
 
@@ -388,14 +554,14 @@ Child scopes inherit types from parent scopes:
388
554
 
389
555
  ```html
390
556
  <div :types='{"name": "string", "age": "number"}'>
391
- <span>{{ name.toUpperCase() }}</span>
392
-
393
- <div :types='{"city": "string"}'>
394
- <!-- This scope has access to: name, age, and city -->
395
- <span>{{ name.toLowerCase() }}</span>
396
- <span>{{ city.toUpperCase() }}</span>
397
- <span>{{ age.toFixed(0) }}</span>
398
- </div>
557
+ <span>{{ name.toUpperCase() }}</span>
558
+
559
+ <div :types='{"city": "string"}'>
560
+ <!-- This scope has access to: name, age, and city -->
561
+ <span>{{ name.toLowerCase() }}</span>
562
+ <span>{{ city.toUpperCase() }}</span>
563
+ <span>{{ age.toFixed(0) }}</span>
564
+ </div>
399
565
  </div>
400
566
  ```
401
567
 
@@ -403,15 +569,15 @@ Child scopes can override parent types:
403
569
 
404
570
  ```html
405
571
  <div :types='{"value": "string"}'>
406
- <span>{{ value.toUpperCase() }}</span>
572
+ <span>{{ value.toUpperCase() }}</span>
407
573
 
408
- <div :types='{"value": "number"}'>
409
- <!-- 'value' is now number, not string -->
410
- <span>{{ value.toFixed(2) }}</span>
411
- </div>
574
+ <div :types='{"value": "number"}'>
575
+ <!-- 'value' is now number, not string -->
576
+ <span>{{ value.toFixed(2) }}</span>
577
+ </div>
412
578
 
413
- <!-- Back to string scope -->
414
- <span>{{ value.toLowerCase() }}</span>
579
+ <!-- Back to string scope -->
580
+ <span>{{ value.toLowerCase() }}</span>
415
581
  </div>
416
582
  ```
417
583
 
@@ -424,25 +590,25 @@ You can import TypeScript types from external files using the `@import:` syntax:
424
590
  ```typescript
425
591
  // types/user.ts
426
592
  export interface User {
427
- id: number;
428
- name: string;
429
- email: string;
430
- isAdmin: boolean;
593
+ id: number;
594
+ name: string;
595
+ email: string;
596
+ isAdmin: boolean;
431
597
  }
432
598
 
433
599
  export interface Product {
434
- id: number;
435
- name: string;
436
- price: number;
600
+ id: number;
601
+ name: string;
602
+ price: number;
437
603
  }
438
604
  ```
439
605
 
440
606
  ```html
441
607
  <!-- Import a single type -->
442
608
  <div :types='{"user": "@import:./types/user.ts:User"}'>
443
- <span>{{ user.name.toUpperCase() }}</span>
444
- <span>{{ user.email.toLowerCase() }}</span>
445
- <span :show="user.isAdmin">Admin Badge</span>
609
+ <span>{{ user.name.toUpperCase() }}</span>
610
+ <span>{{ user.email.toLowerCase() }}</span>
611
+ <span :show="user.isAdmin">Admin Badge</span>
446
612
  </div>
447
613
  ```
448
614
 
@@ -456,20 +622,21 @@ The format is: `@import:MODULE_PATH:TYPE_NAME`
456
622
  - **TYPE_NAME**: The exported type/interface name
457
623
 
458
624
  **Examples of external package imports:**
625
+
459
626
  ```html
460
627
  <!-- Import from a standard package -->
461
628
  <div :types='{"program": "@import:typescript:Program"}'>
462
- <span>{{ program }}</span>
629
+ <span>{{ program }}</span>
463
630
  </div>
464
631
 
465
632
  <!-- Import from package subpath (with package.json exports) -->
466
633
  <div :types='{"parser": "@import:yargs/helpers:Parser.Arguments"}'>
467
- <span>{{ parser }}</span>
634
+ <span>{{ parser }}</span>
468
635
  </div>
469
636
 
470
637
  <!-- Import from your own packages in a monorepo -->
471
638
  <div :types='{"data": "@import:my-shared-lib/types:ApiResponse"}'>
472
- <span>{{ data.status }}</span>
639
+ <span>{{ data.status }}</span>
473
640
  </div>
474
641
  ```
475
642
 
@@ -477,23 +644,25 @@ The format is: `@import:MODULE_PATH:TYPE_NAME`
477
644
 
478
645
  ```html
479
646
  <div :types='{"users": "@import:./types/user.ts:User[]"}'>
480
- <ul :for="user in users">
481
- <li>{{ user.name }} - {{ user.email }}</li>
482
- </ul>
647
+ <ul :for="user in users">
648
+ <li>{{ user.name }} - {{ user.email }}</li>
649
+ </ul>
483
650
  </div>
484
651
  ```
485
652
 
486
653
  #### Multiple Imports
487
654
 
488
655
  ```html
489
- <div :types='{
656
+ <div
657
+ :types='{
490
658
  "user": "@import:./types/user.ts:User",
491
659
  "product": "@import:./types/user.ts:Product",
492
660
  "count": "number"
493
- }'>
494
- <span>{{ user.name }}</span>
495
- <span>{{ product.name }} - ${{ product.price.toFixed(2) }}</span>
496
- <span>Total: {{ count }}</span>
661
+ }'
662
+ >
663
+ <span>{{ user.name }}</span>
664
+ <span>{{ product.name }} - ${{ product.price.toFixed(2) }}</span>
665
+ <span>Total: {{ count }}</span>
497
666
  </div>
498
667
  ```
499
668
 
@@ -504,22 +673,22 @@ Use imports anywhere you'd use a type:
504
673
  ```html
505
674
  <!-- In object types -->
506
675
  <div :types='{"response": "{ data: @import:./types/user.ts:User[], total: number }"}'>
507
- <span>Total users: {{ response.total }}</span>
508
- <ul :for="user in response.data">
509
- <li>{{ user.name }}</li>
510
- </ul>
676
+ <span>Total users: {{ response.total }}</span>
677
+ <ul :for="user in response.data">
678
+ <li>{{ user.name }}</li>
679
+ </ul>
511
680
  </div>
512
681
 
513
682
  <!-- With generics -->
514
683
  <div :types='{"response": "@import:./api.ts:ApiResponse<@import:./types/user.ts:User>"}'>
515
- <span>{{ response.data.name }}</span>
516
- <span>Status: {{ response.status }}</span>
684
+ <span>{{ response.data.name }}</span>
685
+ <span>Status: {{ response.status }}</span>
517
686
  </div>
518
687
 
519
688
  <!-- With unions -->
520
689
  <div :types='{"user": "@import:./types/user.ts:User | null"}'>
521
- <span :show="user !== null">{{ user.name }}</span>
522
- <span :show="user === null">Not logged in</span>
690
+ <span :show="user !== null">{{ user.name }}</span>
691
+ <span :show="user === null">Not logged in</span>
523
692
  </div>
524
693
  ```
525
694
 
@@ -529,12 +698,12 @@ Imports are inherited by nested scopes:
529
698
 
530
699
  ```html
531
700
  <div :types='{"user": "@import:./types/user.ts:User"}'>
532
- <span>{{ user.name }}</span>
701
+ <span>{{ user.name }}</span>
533
702
 
534
- <div :types='{"product": "@import:./types/user.ts:Product"}'>
535
- <!-- Has access to both User and Product types -->
536
- <span>{{ user.name }} bought {{ product.name }}</span>
537
- </div>
703
+ <div :types='{"product": "@import:./types/user.ts:Product"}'>
704
+ <!-- Has access to both User and Product types -->
705
+ <span>{{ user.name }} bought {{ product.name }}</span>
706
+ </div>
538
707
  </div>
539
708
  ```
540
709
 
@@ -542,26 +711,26 @@ Imports are inherited by nested scopes:
542
711
 
543
712
  ```html
544
713
  <div :types='{"orders": "@import:./types/orders.ts:Order[]"}'>
545
- <div :for="order in orders">
546
- <h2>Order #{{ order.id }}</h2>
547
- <p>Customer: {{ order.customer.name }}</p>
548
-
549
- <div :types='{"selectedProduct": "@import:./types/products.ts:Product"}'>
550
- <ul :for="item in order.items">
551
- <li>
552
- {{ item.product.name }} x {{ item.quantity }}
553
- = ${{ (item.product.price * item.quantity).toFixed(2) }}
554
- </li>
555
- </ul>
556
-
557
- <div :show="selectedProduct">
558
- <h3>Selected: {{ selectedProduct.name }}</h3>
559
- <p>${{ selectedProduct.price.toFixed(2) }}</p>
560
- </div>
561
- </div>
562
-
563
- <p>Total: ${{ order.total.toFixed(2) }}</p>
564
- </div>
714
+ <div :for="order in orders">
715
+ <h2>Order #{{ order.id }}</h2>
716
+ <p>Customer: {{ order.customer.name }}</p>
717
+
718
+ <div :types='{"selectedProduct": "@import:./types/products.ts:Product"}'>
719
+ <ul :for="item in order.items">
720
+ <li>
721
+ {{ item.product.name }} x {{ item.quantity }} = ${{ (item.product.price *
722
+ item.quantity).toFixed(2) }}
723
+ </li>
724
+ </ul>
725
+
726
+ <div :show="selectedProduct">
727
+ <h3>Selected: {{ selectedProduct.name }}</h3>
728
+ <p>${{ selectedProduct.price.toFixed(2) }}</p>
729
+ </div>
730
+ </div>
731
+
732
+ <p>Total: ${{ order.total.toFixed(2) }}</p>
733
+ </div>
565
734
  </div>
566
735
  ```
567
736
 
@@ -585,36 +754,40 @@ Imports are inherited by nested scopes:
585
754
  #### Form Validation
586
755
 
587
756
  ```html
588
- <div :types='{
757
+ <div
758
+ :types='{
589
759
  "formData": "{ name: string, email: string, age: number }",
590
760
  "errors": "{ name?: string, email?: string, age?: string }"
591
- }'>
592
- <form>
593
- <input type="text" :bind="formData.name" />
594
- <span :show="errors.name" class="error">{{ errors.name }}</span>
595
-
596
- <input type="email" :bind="formData.email" />
597
- <span :show="errors.email" class="error">{{ errors.email }}</span>
598
-
599
- <input type="number" :bind="formData.age" />
600
- <span :show="errors.age" class="error">{{ errors.age }}</span>
601
- </form>
761
+ }'
762
+ >
763
+ <form>
764
+ <input type="text" :bind="formData.name" />
765
+ <span :show="errors.name" class="error">{{ errors.name }}</span>
766
+
767
+ <input type="email" :bind="formData.email" />
768
+ <span :show="errors.email" class="error">{{ errors.email }}</span>
769
+
770
+ <input type="number" :bind="formData.age" />
771
+ <span :show="errors.age" class="error">{{ errors.age }}</span>
772
+ </form>
602
773
  </div>
603
774
  ```
604
775
 
605
776
  #### API Response Handling
606
777
 
607
778
  ```html
608
- <div :types='{
779
+ <div
780
+ :types='{
609
781
  "response": "@import:./api.ts:ApiResponse<@import:./types/user.ts:User>",
610
782
  "loading": "boolean",
611
783
  "error": "string | null"
612
- }'>
613
- <div :show="loading">Loading...</div>
614
- <div :show="error">Error: {{ error }}</div>
615
- <div :show="!loading && !error && response">
616
- <h1>{{ response.data.name }}</h1>
617
- <p>{{ response.data.email }}</p>
618
- </div>
784
+ }'
785
+ >
786
+ <div :show="loading">Loading...</div>
787
+ <div :show="error">Error: {{ error }}</div>
788
+ <div :show="!loading && !error && response">
789
+ <h1>{{ response.data.name }}</h1>
790
+ <p>{{ response.data.email }}</p>
791
+ </div>
619
792
  </div>
620
793
  ```