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.
- package/.github/workflows/ci.yml +11 -10
- package/.prettierrc +2 -2
- package/.vscode/extensions.json +1 -1
- package/.vscode/launch.json +33 -43
- package/README.md +144 -84
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +1 -1
- 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 +350 -399
- package/dist/css_gen_utils.js.map +1 -1
- package/dist/dome.d.ts +1 -0
- package/dist/dome.js +15 -0
- 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 +45 -45
- package/dist/expressions/parser.js.map +1 -1
- package/dist/expressions/parser.test.js +43 -2
- 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.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/interfaces.d.ts +1 -1
- package/dist/iterator.js.map +1 -1
- package/dist/mancha.js +1 -1
- package/dist/mancha.js.map +1 -1
- package/dist/plugins.js +96 -25
- package/dist/plugins.js.map +1 -1
- package/dist/query.js.map +1 -1
- package/dist/renderer.js +4 -2
- package/dist/renderer.js.map +1 -1
- package/dist/safe_browser.js +1 -1
- package/dist/safe_browser.js.map +1 -1
- package/dist/store.d.ts +10 -4
- package/dist/store.js +26 -10
- package/dist/store.js.map +1 -1
- package/dist/test_utils.d.ts +2 -0
- package/dist/test_utils.js +14 -1
- package/dist/test_utils.js.map +1 -1
- package/dist/trusted_attributes.js +2 -0
- package/dist/trusted_attributes.js.map +1 -1
- package/dist/type_checker.js +57 -22
- package/dist/type_checker.js.map +1 -1
- package/dist/worker.d.ts +2 -0
- package/dist/worker.js +1 -0
- package/dist/worker.js.map +1 -1
- package/docs/css.md +309 -0
- package/docs/quickstart.md +414 -241
- package/global.d.ts +2 -2
- package/gulpfile.ts +44 -0
- package/package.json +86 -84
- package/scripts/generate-css-docs.ts +263 -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,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
|
-
|
|
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>
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
224
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
|
|
249
|
-
|
|
413
|
+
// 3. Mount the renderer to the fragment to apply data bindings.
|
|
414
|
+
await renderer.mount(fragment);
|
|
250
415
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
256
|
-
|
|
420
|
+
assert.ok(loginButton, "Login button should exist");
|
|
421
|
+
assert.ok(logoutButton, "Logout button should exist");
|
|
257
422
|
|
|
258
|
-
|
|
259
|
-
|
|
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
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
267
|
-
|
|
431
|
+
// 2. Create a clean DOM fragment from the page content.
|
|
432
|
+
const fragment = await renderer.preprocessLocal(componentPath);
|
|
268
433
|
|
|
269
|
-
|
|
270
|
-
|
|
434
|
+
// 3. Mount the renderer to the fragment to apply data bindings.
|
|
435
|
+
await renderer.mount(fragment);
|
|
271
436
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
331
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
572
|
+
<span>{{ value.toUpperCase() }}</span>
|
|
407
573
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
574
|
+
<div :types='{"value": "number"}'>
|
|
575
|
+
<!-- 'value' is now number, not string -->
|
|
576
|
+
<span>{{ value.toFixed(2) }}</span>
|
|
577
|
+
</div>
|
|
412
578
|
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
593
|
+
id: number;
|
|
594
|
+
name: string;
|
|
595
|
+
email: string;
|
|
596
|
+
isAdmin: boolean;
|
|
431
597
|
}
|
|
432
598
|
|
|
433
599
|
export interface Product {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
|
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
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
|
|
516
|
-
|
|
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
|
-
|
|
522
|
-
|
|
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
|
-
|
|
701
|
+
<span>{{ user.name }}</span>
|
|
533
702
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
|
757
|
+
<div
|
|
758
|
+
:types='{
|
|
589
759
|
"formData": "{ name: string, email: string, age: number }",
|
|
590
760
|
"errors": "{ name?: string, email?: string, age?: string }"
|
|
591
|
-
}'
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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
|
|
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
```
|