mancha 0.17.3 → 0.17.5
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/.github/workflows/ci.yml +8 -8
- package/.prettierrc +2 -2
- package/.vscode/extensions.json +1 -1
- package/.vscode/launch.json +33 -43
- package/README.md +94 -94
- package/dist/browser.js.map +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/css_gen_basic.js.map +1 -1
- package/dist/css_gen_utils.d.ts +786 -0
- package/dist/css_gen_utils.js +63 -23
- package/dist/css_gen_utils.js.map +1 -1
- package/dist/dome.js.map +1 -1
- package/dist/expressions/ast.d.ts +16 -16
- package/dist/expressions/ast.test.js +89 -64
- package/dist/expressions/ast.test.js.map +1 -1
- package/dist/expressions/ast_factory.d.ts +1 -1
- package/dist/expressions/ast_factory.js +17 -17
- package/dist/expressions/ast_factory.js.map +1 -1
- package/dist/expressions/ast_factory.test.js +42 -36
- package/dist/expressions/ast_factory.test.js.map +1 -1
- package/dist/expressions/constants.js +56 -56
- package/dist/expressions/constants.js.map +1 -1
- package/dist/expressions/constants.test.js +57 -57
- package/dist/expressions/constants.test.js.map +1 -1
- package/dist/expressions/eval.d.ts +17 -17
- package/dist/expressions/eval.js +58 -60
- package/dist/expressions/eval.js.map +1 -1
- package/dist/expressions/eval.test.js +11 -8
- package/dist/expressions/eval.test.js.map +1 -1
- package/dist/expressions/expressions.test.d.ts +6 -6
- package/dist/expressions/expressions.test.js +6 -6
- package/dist/expressions/index.d.ts +6 -6
- package/dist/expressions/index.js +6 -6
- package/dist/expressions/parser.d.ts +3 -3
- package/dist/expressions/parser.js +37 -42
- package/dist/expressions/parser.js.map +1 -1
- package/dist/expressions/parser.test.js +3 -6
- package/dist/expressions/parser.test.js.map +1 -1
- package/dist/expressions/tokenizer.js +22 -25
- package/dist/expressions/tokenizer.js.map +1 -1
- package/dist/expressions/tokenizer.test.js +40 -15
- package/dist/expressions/tokenizer.test.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/iterator.js.map +1 -1
- package/dist/mancha.js.map +1 -1
- package/dist/plugins.js +2 -2
- package/dist/plugins.js.map +1 -1
- package/dist/query.js.map +1 -1
- package/dist/renderer.js.map +1 -1
- package/dist/safe_browser.js.map +1 -1
- package/dist/store.js +1 -1
- package/dist/store.js.map +1 -1
- package/dist/test_utils.js.map +1 -1
- package/dist/trusted_attributes.js.map +1 -1
- package/dist/type_checker.js +11 -7
- package/dist/type_checker.js.map +1 -1
- package/dist/worker.js.map +1 -1
- package/docs/css.md +419 -0
- package/docs/quickstart.md +305 -296
- package/global.d.ts +2 -2
- package/gulpfile.ts +44 -0
- package/package.json +86 -84
- package/scripts/generate-css-docs.ts +374 -0
- package/tsconfig.json +42 -19
- package/tsec_exemptions.json +8 -3
- package/webpack.config.esmodule.ts +26 -0
- package/webpack.config.ts +21 -0
- package/gulpfile.js +0 -44
- package/webpack.config.esmodule.js +0 -23
- package/webpack.config.js +0 -18
package/docs/quickstart.md
CHANGED
|
@@ -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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
118
|
-
|
|
118
|
+
// Mancha is a global variable and $ is a shorthand for the renderer context.
|
|
119
|
+
const { $ } = Mancha;
|
|
119
120
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
// Define the variables that will be used in the form.
|
|
128
|
+
$.name = null;
|
|
129
|
+
$.message = null;
|
|
129
130
|
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
143
|
+
<button style="background-color: red;">
|
|
144
|
+
<slot></slot>
|
|
145
|
+
</button>
|
|
145
146
|
</template>
|
|
146
147
|
```
|
|
147
148
|
|
|
@@ -169,17 +170,18 @@ Then in `index.html`:
|
|
|
169
170
|
```html
|
|
170
171
|
<!-- src/index.html -->
|
|
171
172
|
<head>
|
|
172
|
-
|
|
173
|
+
<!-- ... -->
|
|
173
174
|
</head>
|
|
174
175
|
<body>
|
|
175
|
-
|
|
176
|
-
|
|
176
|
+
<!-- Include the custom component definition before using any of the components -->
|
|
177
|
+
<include src="components/registry.tpl.html" />
|
|
177
178
|
|
|
178
|
-
|
|
179
|
-
|
|
179
|
+
<!-- Now you can use any of the custom components -->
|
|
180
|
+
<my-red-button>Click Me!</my-red-button>
|
|
180
181
|
|
|
181
|
-
|
|
182
|
-
|
|
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>
|
|
183
185
|
```
|
|
184
186
|
|
|
185
187
|
## Element Initialization with `:render`
|
|
@@ -197,14 +199,14 @@ The module's default export is called with the element and renderer as arguments
|
|
|
197
199
|
```js
|
|
198
200
|
// chart-init.js
|
|
199
201
|
export default function (elem, renderer) {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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;
|
|
208
210
|
}
|
|
209
211
|
```
|
|
210
212
|
|
|
@@ -224,26 +226,26 @@ src/
|
|
|
224
226
|
```html
|
|
225
227
|
<!-- components/chart-widget.tpl.html -->
|
|
226
228
|
<template is="chart-widget">
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
229
|
+
<div class="chart-container">
|
|
230
|
+
<canvas :render="./chart-widget.js"></canvas>
|
|
231
|
+
<slot></slot>
|
|
232
|
+
</div>
|
|
231
233
|
</template>
|
|
232
234
|
```
|
|
233
235
|
|
|
234
236
|
```js
|
|
235
237
|
// components/chart-widget.js
|
|
236
238
|
export default function (elem, renderer) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
+
});
|
|
247
249
|
}
|
|
248
250
|
```
|
|
249
251
|
|
|
@@ -255,17 +257,17 @@ export default function (elem, renderer) {
|
|
|
255
257
|
```html
|
|
256
258
|
<!-- index.html -->
|
|
257
259
|
<body>
|
|
258
|
-
|
|
260
|
+
<include src="components/registry.tpl.html" />
|
|
259
261
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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>
|
|
264
266
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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>
|
|
269
271
|
</body>
|
|
270
272
|
```
|
|
271
273
|
|
|
@@ -288,19 +290,19 @@ The init function receives the renderer instance, giving you access to reactive
|
|
|
288
290
|
```js
|
|
289
291
|
// counter-canvas.js
|
|
290
292
|
export default function (elem, renderer) {
|
|
291
|
-
|
|
293
|
+
const ctx = elem.getContext("2d");
|
|
292
294
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
+
// Access current state
|
|
296
|
+
const count = renderer.$.count;
|
|
295
297
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
+
// Draw based on state
|
|
299
|
+
ctx.fillText(`Count: ${count}`, 10, 50);
|
|
298
300
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
+
});
|
|
304
306
|
}
|
|
305
307
|
```
|
|
306
308
|
|
|
@@ -317,28 +319,28 @@ element's renderer, setting variables through it will trigger reactive updates i
|
|
|
317
319
|
|
|
318
320
|
```html
|
|
319
321
|
<div :render="./data-loader.js">
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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>
|
|
325
327
|
</div>
|
|
326
328
|
```
|
|
327
329
|
|
|
328
330
|
```js
|
|
329
331
|
// data-loader.js
|
|
330
332
|
export default async function (elem, renderer) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
+
// Set loading state.
|
|
334
|
+
await renderer.set("loading", true);
|
|
333
335
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
336
|
+
// Fetch data from an API.
|
|
337
|
+
const response = await fetch("/api/data");
|
|
338
|
+
const data = await response.json();
|
|
337
339
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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);
|
|
342
344
|
}
|
|
343
345
|
```
|
|
344
346
|
|
|
@@ -356,20 +358,20 @@ Here's how you can use it:
|
|
|
356
358
|
|
|
357
359
|
```html
|
|
358
360
|
<body :data="{ $$search: '' }">
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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>
|
|
373
375
|
</body>
|
|
374
376
|
```
|
|
375
377
|
|
|
@@ -384,8 +386,8 @@ Here's an example of how you can test a simple component:
|
|
|
384
386
|
```html
|
|
385
387
|
<!-- my-component.html -->
|
|
386
388
|
<body>
|
|
387
|
-
|
|
388
|
-
|
|
389
|
+
<button data-testid="login-button" :show="!user">Login</button>
|
|
390
|
+
<button data-testid="logout-button" :show="user">Logout</button>
|
|
389
391
|
</body>
|
|
390
392
|
```
|
|
391
393
|
|
|
@@ -401,45 +403,44 @@ const componentPath = path.join(import.meta.dirname, "my-component.html");
|
|
|
401
403
|
const findByTestId = (node, testId) => node.querySelector(`[data-testid="${testId}"]`);
|
|
402
404
|
|
|
403
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 });
|
|
404
409
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const renderer = new Renderer({ user: null });
|
|
408
|
-
|
|
409
|
-
// 2. Create a clean DOM fragment from the page content.
|
|
410
|
-
const fragment = await renderer.preprocessLocal(componentPath);
|
|
410
|
+
// 2. Create a clean DOM fragment from the page content.
|
|
411
|
+
const fragment = await renderer.preprocessLocal(componentPath);
|
|
411
412
|
|
|
412
|
-
|
|
413
|
-
|
|
413
|
+
// 3. Mount the renderer to the fragment to apply data bindings.
|
|
414
|
+
await renderer.mount(fragment);
|
|
414
415
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
416
|
+
// 4. Find elements and assert their state.
|
|
417
|
+
const loginButton = findByTestId(fragment, "login-button");
|
|
418
|
+
const logoutButton = findByTestId(fragment, "logout-button");
|
|
418
419
|
|
|
419
|
-
|
|
420
|
-
|
|
420
|
+
assert.ok(loginButton, "Login button should exist");
|
|
421
|
+
assert.ok(logoutButton, "Logout button should exist");
|
|
421
422
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
423
|
+
assert.strictEqual(loginButton.style.display, "", "Login button should be visible");
|
|
424
|
+
assert.strictEqual(logoutButton.style.display, "none", "Logout button should be hidden");
|
|
425
|
+
});
|
|
425
426
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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" } });
|
|
429
430
|
|
|
430
|
-
|
|
431
|
-
|
|
431
|
+
// 2. Create a clean DOM fragment from the page content.
|
|
432
|
+
const fragment = await renderer.preprocessLocal(componentPath);
|
|
432
433
|
|
|
433
|
-
|
|
434
|
-
|
|
434
|
+
// 3. Mount the renderer to the fragment to apply data bindings.
|
|
435
|
+
await renderer.mount(fragment);
|
|
435
436
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
437
|
+
// 4. Find elements and assert their state.
|
|
438
|
+
const loginButton = findByTestId(fragment, "login-button");
|
|
439
|
+
const logoutButton = findByTestId(fragment, "logout-button");
|
|
439
440
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
441
|
+
assert.strictEqual(loginButton.style.display, "none", "Login button should be hidden");
|
|
442
|
+
assert.strictEqual(logoutButton.style.display, "", "Logout button should be visible");
|
|
443
|
+
});
|
|
443
444
|
});
|
|
444
445
|
```
|
|
445
446
|
|
|
@@ -451,15 +452,15 @@ The `Renderer` and `SignalStore` classes support generic type parameters for typ
|
|
|
451
452
|
import { Renderer } from "mancha";
|
|
452
453
|
|
|
453
454
|
interface AppState {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
455
|
+
user: { name: string; email: string } | null;
|
|
456
|
+
count: number;
|
|
457
|
+
items: string[];
|
|
457
458
|
}
|
|
458
459
|
|
|
459
460
|
const renderer = new Renderer<AppState>({
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
461
|
+
user: null,
|
|
462
|
+
count: 0,
|
|
463
|
+
items: ["a", "b"],
|
|
463
464
|
});
|
|
464
465
|
|
|
465
466
|
// Type-safe access via the $ proxy
|
|
@@ -491,12 +492,13 @@ Use the `:types` attribute to declare types for variables in your templates:
|
|
|
491
492
|
|
|
492
493
|
```html
|
|
493
494
|
<div :types='{"name": "string", "age": "number"}'>
|
|
494
|
-
|
|
495
|
-
|
|
495
|
+
<span>{{ name.toUpperCase() }}</span>
|
|
496
|
+
<span>{{ age.toFixed(0) }}</span>
|
|
496
497
|
</div>
|
|
497
498
|
```
|
|
498
499
|
|
|
499
500
|
The type checker will validate that:
|
|
501
|
+
|
|
500
502
|
- `name.toUpperCase()` is valid (string has toUpperCase method)
|
|
501
503
|
- `age.toFixed(0)` is valid (number has toFixed method)
|
|
502
504
|
- Using `name.toFixed()` would be an error (string doesn't have toFixed)
|
|
@@ -538,11 +540,11 @@ The type checker understands `:for` loops and infers the item type from the arra
|
|
|
538
540
|
|
|
539
541
|
```html
|
|
540
542
|
<div :types='{"users": "{ name: string, age: number }[]"}'>
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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>
|
|
546
548
|
</div>
|
|
547
549
|
```
|
|
548
550
|
|
|
@@ -552,14 +554,14 @@ Child scopes inherit types from parent scopes:
|
|
|
552
554
|
|
|
553
555
|
```html
|
|
554
556
|
<div :types='{"name": "string", "age": "number"}'>
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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>
|
|
563
565
|
</div>
|
|
564
566
|
```
|
|
565
567
|
|
|
@@ -567,15 +569,15 @@ Child scopes can override parent types:
|
|
|
567
569
|
|
|
568
570
|
```html
|
|
569
571
|
<div :types='{"value": "string"}'>
|
|
570
|
-
|
|
572
|
+
<span>{{ value.toUpperCase() }}</span>
|
|
571
573
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
574
|
+
<div :types='{"value": "number"}'>
|
|
575
|
+
<!-- 'value' is now number, not string -->
|
|
576
|
+
<span>{{ value.toFixed(2) }}</span>
|
|
577
|
+
</div>
|
|
576
578
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
+
<!-- Back to string scope -->
|
|
580
|
+
<span>{{ value.toLowerCase() }}</span>
|
|
579
581
|
</div>
|
|
580
582
|
```
|
|
581
583
|
|
|
@@ -588,25 +590,25 @@ You can import TypeScript types from external files using the `@import:` syntax:
|
|
|
588
590
|
```typescript
|
|
589
591
|
// types/user.ts
|
|
590
592
|
export interface User {
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
593
|
+
id: number;
|
|
594
|
+
name: string;
|
|
595
|
+
email: string;
|
|
596
|
+
isAdmin: boolean;
|
|
595
597
|
}
|
|
596
598
|
|
|
597
599
|
export interface Product {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
600
|
+
id: number;
|
|
601
|
+
name: string;
|
|
602
|
+
price: number;
|
|
601
603
|
}
|
|
602
604
|
```
|
|
603
605
|
|
|
604
606
|
```html
|
|
605
607
|
<!-- Import a single type -->
|
|
606
608
|
<div :types='{"user": "@import:./types/user.ts:User"}'>
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
609
|
+
<span>{{ user.name.toUpperCase() }}</span>
|
|
610
|
+
<span>{{ user.email.toLowerCase() }}</span>
|
|
611
|
+
<span :show="user.isAdmin">Admin Badge</span>
|
|
610
612
|
</div>
|
|
611
613
|
```
|
|
612
614
|
|
|
@@ -620,20 +622,21 @@ The format is: `@import:MODULE_PATH:TYPE_NAME`
|
|
|
620
622
|
- **TYPE_NAME**: The exported type/interface name
|
|
621
623
|
|
|
622
624
|
**Examples of external package imports:**
|
|
625
|
+
|
|
623
626
|
```html
|
|
624
627
|
<!-- Import from a standard package -->
|
|
625
628
|
<div :types='{"program": "@import:typescript:Program"}'>
|
|
626
|
-
|
|
629
|
+
<span>{{ program }}</span>
|
|
627
630
|
</div>
|
|
628
631
|
|
|
629
632
|
<!-- Import from package subpath (with package.json exports) -->
|
|
630
633
|
<div :types='{"parser": "@import:yargs/helpers:Parser.Arguments"}'>
|
|
631
|
-
|
|
634
|
+
<span>{{ parser }}</span>
|
|
632
635
|
</div>
|
|
633
636
|
|
|
634
637
|
<!-- Import from your own packages in a monorepo -->
|
|
635
638
|
<div :types='{"data": "@import:my-shared-lib/types:ApiResponse"}'>
|
|
636
|
-
|
|
639
|
+
<span>{{ data.status }}</span>
|
|
637
640
|
</div>
|
|
638
641
|
```
|
|
639
642
|
|
|
@@ -641,23 +644,25 @@ The format is: `@import:MODULE_PATH:TYPE_NAME`
|
|
|
641
644
|
|
|
642
645
|
```html
|
|
643
646
|
<div :types='{"users": "@import:./types/user.ts:User[]"}'>
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
+
<ul :for="user in users">
|
|
648
|
+
<li>{{ user.name }} - {{ user.email }}</li>
|
|
649
|
+
</ul>
|
|
647
650
|
</div>
|
|
648
651
|
```
|
|
649
652
|
|
|
650
653
|
#### Multiple Imports
|
|
651
654
|
|
|
652
655
|
```html
|
|
653
|
-
<div
|
|
656
|
+
<div
|
|
657
|
+
:types='{
|
|
654
658
|
"user": "@import:./types/user.ts:User",
|
|
655
659
|
"product": "@import:./types/user.ts:Product",
|
|
656
660
|
"count": "number"
|
|
657
|
-
}'
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
+
}'
|
|
662
|
+
>
|
|
663
|
+
<span>{{ user.name }}</span>
|
|
664
|
+
<span>{{ product.name }} - ${{ product.price.toFixed(2) }}</span>
|
|
665
|
+
<span>Total: {{ count }}</span>
|
|
661
666
|
</div>
|
|
662
667
|
```
|
|
663
668
|
|
|
@@ -668,22 +673,22 @@ Use imports anywhere you'd use a type:
|
|
|
668
673
|
```html
|
|
669
674
|
<!-- In object types -->
|
|
670
675
|
<div :types='{"response": "{ data: @import:./types/user.ts:User[], total: number }"}'>
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
676
|
+
<span>Total users: {{ response.total }}</span>
|
|
677
|
+
<ul :for="user in response.data">
|
|
678
|
+
<li>{{ user.name }}</li>
|
|
679
|
+
</ul>
|
|
675
680
|
</div>
|
|
676
681
|
|
|
677
682
|
<!-- With generics -->
|
|
678
683
|
<div :types='{"response": "@import:./api.ts:ApiResponse<@import:./types/user.ts:User>"}'>
|
|
679
|
-
|
|
680
|
-
|
|
684
|
+
<span>{{ response.data.name }}</span>
|
|
685
|
+
<span>Status: {{ response.status }}</span>
|
|
681
686
|
</div>
|
|
682
687
|
|
|
683
688
|
<!-- With unions -->
|
|
684
689
|
<div :types='{"user": "@import:./types/user.ts:User | null"}'>
|
|
685
|
-
|
|
686
|
-
|
|
690
|
+
<span :show="user !== null">{{ user.name }}</span>
|
|
691
|
+
<span :show="user === null">Not logged in</span>
|
|
687
692
|
</div>
|
|
688
693
|
```
|
|
689
694
|
|
|
@@ -693,12 +698,12 @@ Imports are inherited by nested scopes:
|
|
|
693
698
|
|
|
694
699
|
```html
|
|
695
700
|
<div :types='{"user": "@import:./types/user.ts:User"}'>
|
|
696
|
-
|
|
701
|
+
<span>{{ user.name }}</span>
|
|
697
702
|
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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>
|
|
702
707
|
</div>
|
|
703
708
|
```
|
|
704
709
|
|
|
@@ -706,26 +711,26 @@ Imports are inherited by nested scopes:
|
|
|
706
711
|
|
|
707
712
|
```html
|
|
708
713
|
<div :types='{"orders": "@import:./types/orders.ts:Order[]"}'>
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
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>
|
|
729
734
|
</div>
|
|
730
735
|
```
|
|
731
736
|
|
|
@@ -749,36 +754,40 @@ Imports are inherited by nested scopes:
|
|
|
749
754
|
#### Form Validation
|
|
750
755
|
|
|
751
756
|
```html
|
|
752
|
-
<div
|
|
757
|
+
<div
|
|
758
|
+
:types='{
|
|
753
759
|
"formData": "{ name: string, email: string, age: number }",
|
|
754
760
|
"errors": "{ name?: string, email?: string, age?: string }"
|
|
755
|
-
}'
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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>
|
|
766
773
|
</div>
|
|
767
774
|
```
|
|
768
775
|
|
|
769
776
|
#### API Response Handling
|
|
770
777
|
|
|
771
778
|
```html
|
|
772
|
-
<div
|
|
779
|
+
<div
|
|
780
|
+
:types='{
|
|
773
781
|
"response": "@import:./api.ts:ApiResponse<@import:./types/user.ts:User>",
|
|
774
782
|
"loading": "boolean",
|
|
775
783
|
"error": "string | null"
|
|
776
|
-
}'
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
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>
|
|
783
792
|
</div>
|
|
784
793
|
```
|