@sealcode/sealgen 0.15.49 → 0.15.50
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/@types/src/forms/controls/checkboxed-list-input.d.ts +8 -0
- package/@types/src/forms/fields/checkboxed-list.d.ts +7 -8
- package/lib/forms/controls/checkboxed-list-input.js +71 -25
- package/lib/forms/controls/checkboxed-list-input.test.js +49 -0
- package/lib/templates/jdd-component.js +2 -6
- package/package.json +1 -1
- package/src/forms/controls/checkboxed-list-input.test.ts +53 -0
- package/src/forms/controls/checkboxed-list-input.ts +87 -27
- package/src/forms/fields/checkboxed-list.ts +7 -3
- package/src/templates/jdd-component.ts +2 -6
|
@@ -9,5 +9,13 @@ export declare class CheckboxedListInput extends FormFieldControl {
|
|
|
9
9
|
constructor(field: CheckboxedListField<boolean>, options?: {
|
|
10
10
|
label: string;
|
|
11
11
|
});
|
|
12
|
+
generateCountBasedClasses(n: number): string[];
|
|
12
13
|
render(fctx: FormControlContext): Promise<string>;
|
|
14
|
+
renderOption({ name, value, form_id, label, checked, }: {
|
|
15
|
+
name: string;
|
|
16
|
+
form_id: string;
|
|
17
|
+
value: string;
|
|
18
|
+
label?: string;
|
|
19
|
+
checked: boolean;
|
|
20
|
+
}): Promise<string>;
|
|
13
21
|
}
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { Context } from "koa";
|
|
2
2
|
import { FormDataValue } from "../form-types.js";
|
|
3
3
|
import { FieldParseResult, FormField, FormFieldValidationResponse } from "./field.js";
|
|
4
|
+
export type CheckboxedListItem = {
|
|
5
|
+
value: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
group?: string;
|
|
8
|
+
};
|
|
4
9
|
export declare class CheckboxedListField<Required extends boolean> extends FormField<Required, Record<string, "on">> {
|
|
5
10
|
required: Required;
|
|
6
|
-
generateOptions: (ctx: Context) => Promise<
|
|
7
|
-
value: string;
|
|
8
|
-
label?: string;
|
|
9
|
-
}[]>;
|
|
11
|
+
generateOptions: (ctx: Context) => Promise<CheckboxedListItem[]>;
|
|
10
12
|
isVisible: (ctx: Context) => Promise<boolean>;
|
|
11
|
-
constructor(required: Required, generateOptions: (ctx: Context) => Promise<
|
|
12
|
-
value: string;
|
|
13
|
-
label?: string;
|
|
14
|
-
}[]>, isVisible?: (ctx: Context) => Promise<boolean>);
|
|
13
|
+
constructor(required: Required, generateOptions: (ctx: Context) => Promise<CheckboxedListItem[]>, isVisible?: (ctx: Context) => Promise<boolean>);
|
|
15
14
|
predicate: (v: unknown, w: unknown) => {
|
|
16
15
|
readonly match: boolean;
|
|
17
16
|
readonly wildcard: string[];
|
|
@@ -7,6 +7,15 @@ class CheckboxedListInput extends FormFieldControl {
|
|
|
7
7
|
this.field = field;
|
|
8
8
|
this.options = options;
|
|
9
9
|
}
|
|
10
|
+
generateCountBasedClasses(n) {
|
|
11
|
+
return [
|
|
12
|
+
"options-count--" + n.toString(),
|
|
13
|
+
...n >= 5 ? ["options-count--5-or-more"] : [],
|
|
14
|
+
...n >= 10 ? ["options-count--10-or-more"] : [],
|
|
15
|
+
...n >= 15 ? ["options-count--15-or-more"] : [],
|
|
16
|
+
...n >= 20 ? ["options-count--20-or-more"] : []
|
|
17
|
+
];
|
|
18
|
+
}
|
|
10
19
|
async render(fctx) {
|
|
11
20
|
const {
|
|
12
21
|
valid,
|
|
@@ -18,47 +27,84 @@ class CheckboxedListInput extends FormFieldControl {
|
|
|
18
27
|
this.field.generateOptions(fctx.ctx),
|
|
19
28
|
this.field.isVisible(fctx.ctx)
|
|
20
29
|
]);
|
|
30
|
+
const ungroupped = [];
|
|
31
|
+
const groups = {};
|
|
32
|
+
for (const option of options) {
|
|
33
|
+
if (!option.group) {
|
|
34
|
+
ungroupped.push(option);
|
|
35
|
+
} else {
|
|
36
|
+
if (!groups[option.group]) {
|
|
37
|
+
groups[option.group] = [];
|
|
38
|
+
}
|
|
39
|
+
groups[option.group].push(option);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
21
42
|
const { name, required } = this.field;
|
|
22
43
|
return inputWrapper(
|
|
23
44
|
"checkboxed-list",
|
|
24
45
|
[
|
|
25
46
|
name,
|
|
26
47
|
getRequiredClass(required),
|
|
27
|
-
|
|
28
|
-
...options.length >= 5 ? ["options-count--5-or-more"] : [],
|
|
29
|
-
...options.length >= 10 ? ["options-count--10-or-more"] : [],
|
|
30
|
-
...options.length >= 15 ? ["options-count--15-or-more"] : [],
|
|
31
|
-
...options.length >= 20 ? ["options-count--20-or-more"] : []
|
|
48
|
+
...this.generateCountBasedClasses(options.length)
|
|
32
49
|
],
|
|
33
50
|
tempstream`<label
|
|
34
51
|
>${this.options.label || this.field.name}</label
|
|
35
52
|
>
|
|
36
53
|
${~valid && fctx.validate ? `<div class="input__error">${message}</div>` : ""}
|
|
37
54
|
<ul>
|
|
38
|
-
${isVisible ?
|
|
39
|
-
(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
${isVisible ? Object.entries(groups).map(
|
|
56
|
+
([group_label, options2]) => tempstream` <h3>
|
|
57
|
+
${group_label}
|
|
58
|
+
</h3>
|
|
59
|
+
${options2.map(
|
|
60
|
+
({ value, label }) => this.renderOption({
|
|
61
|
+
name,
|
|
62
|
+
value,
|
|
63
|
+
label,
|
|
64
|
+
form_id: fctx.form_id,
|
|
65
|
+
checked: pickedValues.includes(
|
|
66
|
+
value
|
|
67
|
+
)
|
|
68
|
+
})
|
|
69
|
+
)}`
|
|
70
|
+
) : ""}
|
|
71
|
+
${isVisible ? ungroupped.map(
|
|
72
|
+
({ value, label }) => this.renderOption({
|
|
73
|
+
name,
|
|
74
|
+
value,
|
|
75
|
+
label,
|
|
76
|
+
form_id: fctx.form_id,
|
|
77
|
+
checked: pickedValues.includes(value)
|
|
78
|
+
})
|
|
58
79
|
) : ""}
|
|
59
80
|
</ul>`
|
|
60
81
|
);
|
|
61
82
|
}
|
|
83
|
+
renderOption({
|
|
84
|
+
name,
|
|
85
|
+
value,
|
|
86
|
+
form_id,
|
|
87
|
+
label,
|
|
88
|
+
checked
|
|
89
|
+
}) {
|
|
90
|
+
return tempstream`<li>
|
|
91
|
+
${inputWrapper(
|
|
92
|
+
"checkbox",
|
|
93
|
+
[name],
|
|
94
|
+
/* HTML */
|
|
95
|
+
`
|
|
96
|
+
<input
|
|
97
|
+
type="checkbox"
|
|
98
|
+
id="${name}.${value}"
|
|
99
|
+
name="${name}.${value}"
|
|
100
|
+
${checked ? "checked" : ""}
|
|
101
|
+
form=${form_id}
|
|
102
|
+
/>
|
|
103
|
+
<label for="${name}.${value}">${label || value}</label>
|
|
104
|
+
`
|
|
105
|
+
)}
|
|
106
|
+
</li>`;
|
|
107
|
+
}
|
|
62
108
|
}
|
|
63
109
|
export {
|
|
64
110
|
CheckboxedListInput
|
|
@@ -231,6 +231,55 @@ describe("checkboxed-list", () => {
|
|
|
231
231
|
await page.getByRole("button", { name: "Wy\u015Blij" }).click();
|
|
232
232
|
await page.getByText(/ABC/).click({ timeout: 500 });
|
|
233
233
|
});
|
|
234
|
+
it("groups checkboxes based on the value of the 'group' attribute", async () => {
|
|
235
|
+
const port = await getPort();
|
|
236
|
+
console.log(`Using port ${port} form checkboxed-list-input.test.ts`);
|
|
237
|
+
const app = new Koa();
|
|
238
|
+
const router = new Router();
|
|
239
|
+
const fields = {
|
|
240
|
+
syllables: new CheckboxedListField(true, async () => {
|
|
241
|
+
return [
|
|
242
|
+
...["e", "y", "u", "i", "o", "a"].map((s, index) => ({
|
|
243
|
+
value: "vowel" + index,
|
|
244
|
+
label: s,
|
|
245
|
+
group: "vowels"
|
|
246
|
+
})),
|
|
247
|
+
...["q", "w", "r", "t", "p", "s", "test_label"].map((s, index) => ({
|
|
248
|
+
value: "consonant" + index,
|
|
249
|
+
label: s,
|
|
250
|
+
group: "consonants"
|
|
251
|
+
}))
|
|
252
|
+
];
|
|
253
|
+
})
|
|
254
|
+
};
|
|
255
|
+
mount(
|
|
256
|
+
router,
|
|
257
|
+
"/",
|
|
258
|
+
new class extends Form {
|
|
259
|
+
constructor() {
|
|
260
|
+
super(...arguments);
|
|
261
|
+
this.fields = fields;
|
|
262
|
+
this.controls = [new CheckboxedListInput(fields.syllables, { label: "Letters" })];
|
|
263
|
+
}
|
|
264
|
+
async onSubmit(ctx) {
|
|
265
|
+
}
|
|
266
|
+
async canAccess(_) {
|
|
267
|
+
return { canAccess: true, message: "" };
|
|
268
|
+
}
|
|
269
|
+
}(),
|
|
270
|
+
true
|
|
271
|
+
);
|
|
272
|
+
app.use(router.routes()).use(router.allowedMethods());
|
|
273
|
+
server = app.listen(port);
|
|
274
|
+
browser = await getBrowser();
|
|
275
|
+
const context = await browser.newContext();
|
|
276
|
+
page = await context.newPage();
|
|
277
|
+
await page.goto(`http://localhost:${port}`);
|
|
278
|
+
await page.getByRole("button", { name: "Wy\u015Blij" }).click();
|
|
279
|
+
await page.getByText(/vowels/).click({ timeout: 500 });
|
|
280
|
+
await page.getByText(/consonants/).click({ timeout: 500 });
|
|
281
|
+
await page.getByText("test_label").click({ timeout: 500 });
|
|
282
|
+
});
|
|
234
283
|
});
|
|
235
284
|
});
|
|
236
285
|
//# sourceMappingURL=checkboxed-list-input.test.js.map
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { toKebabCase, toPascalCase } from "js-convert-case";
|
|
2
2
|
function jddComponentTemplate(component_name) {
|
|
3
|
-
return `import
|
|
4
|
-
import {
|
|
5
|
-
import type {
|
|
6
|
-
ExtractStructuredComponentArgumentsParsed,
|
|
7
|
-
JDDContext,
|
|
8
|
-
} from "@sealcode/jdd";
|
|
3
|
+
return `import { TempstreamJSX } from "tempstream";
|
|
4
|
+
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
|
|
9
5
|
import { Component, ComponentArguments } from "@sealcode/jdd";
|
|
10
6
|
|
|
11
7
|
const component_arguments = {
|
package/package.json
CHANGED
|
@@ -255,5 +255,58 @@ describe("checkboxed-list", () => {
|
|
|
255
255
|
await page.getByRole("button", { name: "Wyślij" }).click();
|
|
256
256
|
await page.getByText(/ABC/).click({ timeout: 500 }); // should display an error message for that field
|
|
257
257
|
});
|
|
258
|
+
|
|
259
|
+
it("groups checkboxes based on the value of the 'group' attribute", async () => {
|
|
260
|
+
const port = await getPort();
|
|
261
|
+
console.log(`Using port ${port} form checkboxed-list-input.test.ts`);
|
|
262
|
+
const app = new Koa();
|
|
263
|
+
const router = new Router();
|
|
264
|
+
|
|
265
|
+
const fields = {
|
|
266
|
+
syllables: new CheckboxedListField(true, async () => {
|
|
267
|
+
return [
|
|
268
|
+
...["e", "y", "u", "i", "o", "a"].map((s, index) => ({
|
|
269
|
+
value: "vowel" + index,
|
|
270
|
+
label: s,
|
|
271
|
+
group: "vowels",
|
|
272
|
+
})),
|
|
273
|
+
...["q", "w", "r", "t", "p", "s", "test_label"].map((s, index) => ({
|
|
274
|
+
value: "consonant" + index,
|
|
275
|
+
label: s,
|
|
276
|
+
group: "consonants",
|
|
277
|
+
})),
|
|
278
|
+
];
|
|
279
|
+
}),
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
mount(
|
|
283
|
+
router,
|
|
284
|
+
"/",
|
|
285
|
+
new (class extends Form<typeof fields, unknown> {
|
|
286
|
+
fields = fields;
|
|
287
|
+
controls = [new CheckboxedListInput(fields.syllables, { label: "Letters" })];
|
|
288
|
+
|
|
289
|
+
async onSubmit(ctx: Context) {
|
|
290
|
+
// noop
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async canAccess(_: Context) {
|
|
294
|
+
return { canAccess: true, message: "" };
|
|
295
|
+
}
|
|
296
|
+
})(),
|
|
297
|
+
true
|
|
298
|
+
);
|
|
299
|
+
app.use(router.routes()).use(router.allowedMethods());
|
|
300
|
+
server = app.listen(port);
|
|
301
|
+
|
|
302
|
+
browser = await getBrowser();
|
|
303
|
+
const context = await browser.newContext();
|
|
304
|
+
page = await context.newPage();
|
|
305
|
+
await page.goto(`http://localhost:${port}`);
|
|
306
|
+
await page.getByRole("button", { name: "Wyślij" }).click();
|
|
307
|
+
await page.getByText(/vowels/).click({ timeout: 500 }); // should display an error message for that field
|
|
308
|
+
await page.getByText(/consonants/).click({ timeout: 500 }); // should display an error message for that field
|
|
309
|
+
await page.getByText("test_label").click({ timeout: 500 });
|
|
310
|
+
});
|
|
258
311
|
});
|
|
259
312
|
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { tempstream } from "tempstream";
|
|
2
2
|
import { getRequiredClass, inputWrapper } from "../../utils/input-wrapper.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
CheckboxedListField,
|
|
5
|
+
CheckboxedListItem,
|
|
6
|
+
} from "../fields/checkboxed-list.js";
|
|
4
7
|
import { FormControlContext } from "./form-control.js";
|
|
5
8
|
import { FormFieldControl } from "./form-field-control.js";
|
|
6
9
|
|
|
@@ -12,6 +15,16 @@ export class CheckboxedListInput extends FormFieldControl {
|
|
|
12
15
|
super([field]);
|
|
13
16
|
}
|
|
14
17
|
|
|
18
|
+
generateCountBasedClasses(n: number) {
|
|
19
|
+
return [
|
|
20
|
+
"options-count--" + n.toString(),
|
|
21
|
+
...(n >= 5 ? ["options-count--5-or-more"] : []),
|
|
22
|
+
...(n >= 10 ? ["options-count--10-or-more"] : []),
|
|
23
|
+
...(n >= 15 ? ["options-count--15-or-more"] : []),
|
|
24
|
+
...(n >= 20 ? ["options-count--20-or-more"] : []),
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
async render(fctx: FormControlContext) {
|
|
16
29
|
const {
|
|
17
30
|
valid,
|
|
@@ -26,17 +39,26 @@ export class CheckboxedListInput extends FormFieldControl {
|
|
|
26
39
|
this.field.isVisible(fctx.ctx),
|
|
27
40
|
]);
|
|
28
41
|
|
|
42
|
+
const ungroupped = [];
|
|
43
|
+
const groups: Record<string, CheckboxedListItem[] | undefined> = {};
|
|
44
|
+
for (const option of options) {
|
|
45
|
+
if (!option.group) {
|
|
46
|
+
ungroupped.push(option);
|
|
47
|
+
} else {
|
|
48
|
+
if (!groups[option.group]) {
|
|
49
|
+
groups[option.group] = [];
|
|
50
|
+
}
|
|
51
|
+
groups[option.group]!.push(option);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
29
55
|
const { name, required } = this.field;
|
|
30
56
|
return inputWrapper(
|
|
31
57
|
"checkboxed-list",
|
|
32
58
|
[
|
|
33
59
|
name,
|
|
34
60
|
getRequiredClass(required),
|
|
35
|
-
|
|
36
|
-
...(options.length >= 5 ? ["options-count--5-or-more"] : []),
|
|
37
|
-
...(options.length >= 10 ? ["options-count--10-or-more"] : []),
|
|
38
|
-
...(options.length >= 15 ? ["options-count--15-or-more"] : []),
|
|
39
|
-
...(options.length >= 20 ? ["options-count--20-or-more"] : []),
|
|
61
|
+
...this.generateCountBasedClasses(options.length),
|
|
40
62
|
],
|
|
41
63
|
tempstream/* HTML */ `<label
|
|
42
64
|
>${this.options.label || this.field.name}</label
|
|
@@ -46,30 +68,68 @@ export class CheckboxedListInput extends FormFieldControl {
|
|
|
46
68
|
: ""}
|
|
47
69
|
<ul>
|
|
48
70
|
${isVisible
|
|
49
|
-
?
|
|
50
|
-
(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
? Object.entries(groups).map(
|
|
72
|
+
([group_label, options]) =>
|
|
73
|
+
tempstream/* HTML */ ` <h3>
|
|
74
|
+
${group_label}
|
|
75
|
+
</h3>
|
|
76
|
+
${options!.map(({ value, label }) =>
|
|
77
|
+
this.renderOption({
|
|
78
|
+
name,
|
|
79
|
+
value,
|
|
80
|
+
label,
|
|
81
|
+
form_id: fctx.form_id,
|
|
82
|
+
checked:
|
|
83
|
+
pickedValues.includes(
|
|
84
|
+
value
|
|
85
|
+
),
|
|
86
|
+
})
|
|
87
|
+
)}`
|
|
88
|
+
)
|
|
89
|
+
: ""}
|
|
90
|
+
${isVisible
|
|
91
|
+
? ungroupped.map(({ value, label }) =>
|
|
92
|
+
this.renderOption({
|
|
93
|
+
name,
|
|
94
|
+
value,
|
|
95
|
+
label,
|
|
96
|
+
form_id: fctx.form_id,
|
|
97
|
+
checked: pickedValues.includes(value),
|
|
98
|
+
})
|
|
70
99
|
)
|
|
71
100
|
: ""}
|
|
72
101
|
</ul>`
|
|
73
102
|
);
|
|
74
103
|
}
|
|
104
|
+
|
|
105
|
+
renderOption({
|
|
106
|
+
name,
|
|
107
|
+
value,
|
|
108
|
+
form_id,
|
|
109
|
+
label,
|
|
110
|
+
checked,
|
|
111
|
+
}: {
|
|
112
|
+
name: string;
|
|
113
|
+
form_id: string;
|
|
114
|
+
value: string;
|
|
115
|
+
label?: string;
|
|
116
|
+
checked: boolean;
|
|
117
|
+
}) {
|
|
118
|
+
return tempstream/* HTML */ `<li>
|
|
119
|
+
${inputWrapper(
|
|
120
|
+
"checkbox",
|
|
121
|
+
[name],
|
|
122
|
+
/* HTML */ `
|
|
123
|
+
<input
|
|
124
|
+
type="checkbox"
|
|
125
|
+
id="${name}.${value}"
|
|
126
|
+
name="${name}.${value}"
|
|
127
|
+
${checked ? "checked" : ""}
|
|
128
|
+
form=${form_id}
|
|
129
|
+
/>
|
|
130
|
+
<label for="${name}.${value}">${label || value}</label>
|
|
131
|
+
`
|
|
132
|
+
)}
|
|
133
|
+
</li>`;
|
|
134
|
+
}
|
|
75
135
|
}
|
|
@@ -7,15 +7,19 @@ import {
|
|
|
7
7
|
FormFieldValidationResponse,
|
|
8
8
|
} from "./field.js";
|
|
9
9
|
|
|
10
|
+
export type CheckboxedListItem = {
|
|
11
|
+
value: string;
|
|
12
|
+
label?: string;
|
|
13
|
+
group?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
10
16
|
export class CheckboxedListField<Required extends boolean> extends FormField<
|
|
11
17
|
Required,
|
|
12
18
|
Record<string, "on">
|
|
13
19
|
> {
|
|
14
20
|
constructor(
|
|
15
21
|
public required: Required,
|
|
16
|
-
public generateOptions: (
|
|
17
|
-
ctx: Context
|
|
18
|
-
) => Promise<{ value: string; label?: string }[]>,
|
|
22
|
+
public generateOptions: (ctx: Context) => Promise<CheckboxedListItem[]>,
|
|
19
23
|
public isVisible: (ctx: Context) => Promise<boolean> = () =>
|
|
20
24
|
Promise.resolve(true)
|
|
21
25
|
) {
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { toKebabCase, toPascalCase } from "js-convert-case";
|
|
2
2
|
|
|
3
3
|
export function jddComponentTemplate(component_name: string) {
|
|
4
|
-
return `import
|
|
5
|
-
import {
|
|
6
|
-
import type {
|
|
7
|
-
ExtractStructuredComponentArgumentsParsed,
|
|
8
|
-
JDDContext,
|
|
9
|
-
} from "@sealcode/jdd";
|
|
4
|
+
return `import { TempstreamJSX } from "tempstream";
|
|
5
|
+
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
|
|
10
6
|
import { Component, ComponentArguments } from "@sealcode/jdd";
|
|
11
7
|
|
|
12
8
|
const component_arguments = {
|