@sealcode/sealgen 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.nyc_output/1c5dff80-7861-427a-b1bb-d0686211f098.json +1 -0
- package/.nyc_output/a38a0d08-1f1f-4973-b5e2-efc434c1593d.json +1 -0
- package/.nyc_output/processinfo/1c5dff80-7861-427a-b1bb-d0686211f098.json +1 -0
- package/.nyc_output/processinfo/a38a0d08-1f1f-4973-b5e2-efc434c1593d.json +1 -0
- package/.nyc_output/processinfo/index.json +1 -0
- package/.xunit +3 -0
- package/@types/forms/controls/field-group.d.ts +17 -0
- package/@types/forms/controls/form-control.d.ts +1 -1
- package/@types/forms/controls/form-field-control.d.ts +1 -1
- package/@types/forms/controls/simple-input.d.ts +2 -1
- package/@types/forms/form.d.ts +19 -6
- package/@types/forms/multiform.d.ts +23 -0
- package/@types/index.d.ts +1 -0
- package/@types/page/mountable-with-fields.d.ts +32 -0
- package/@types/page/mountable.d.ts +2 -27
- package/@types/page/page.d.ts +2 -2
- package/@types/templates/multiform.d.ts +1 -0
- package/@types/templates/redirect.d.ts +1 -0
- package/@types/tests/manual/multiform.d.ts +1 -0
- package/@types/utils/input-wrapper.d.ts +4 -0
- package/coverage/clover.xml +79 -0
- package/lib/add-route.js +27 -8
- package/lib/add-route.js.map +1 -1
- package/lib/forms/controls/checkboxed-list-input.js +12 -9
- package/lib/forms/controls/checkboxed-list-input.js.map +1 -1
- package/lib/forms/controls/dropdown.js +14 -12
- package/lib/forms/controls/dropdown.js.map +1 -1
- package/lib/forms/controls/field-group.js +26 -0
- package/lib/forms/controls/field-group.js.map +1 -0
- package/lib/forms/controls/form-control.js.map +1 -1
- package/lib/forms/controls/form-field-control.js.map +1 -1
- package/lib/forms/controls/simple-input.js +6 -8
- package/lib/forms/controls/simple-input.js.map +1 -1
- package/lib/forms/controls/textarea.js +13 -15
- package/lib/forms/controls/textarea.js.map +1 -1
- package/lib/forms/form.js +95 -57
- package/lib/forms/form.js.map +1 -1
- package/lib/forms/multiform.js +169 -0
- package/lib/forms/multiform.js.map +1 -0
- package/lib/generate-collections.js +5 -3
- package/lib/generate-collections.js.map +1 -1
- package/lib/generate-routes.js +7 -3
- package/lib/generate-routes.js.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/page/list.js +1 -1
- package/lib/page/list.js.map +1 -1
- package/lib/page/mountable-with-fields.js +63 -0
- package/lib/page/mountable-with-fields.js.map +1 -0
- package/lib/page/mountable.js +2 -49
- package/lib/page/mountable.js.map +1 -1
- package/lib/page/page.js +2 -2
- package/lib/page/page.js.map +1 -1
- package/lib/templates/multiform.js +59 -0
- package/lib/templates/multiform.js.map +1 -0
- package/lib/templates/redirect.js +27 -0
- package/lib/templates/redirect.js.map +1 -0
- package/lib/tests/manual/multiform.js +95 -0
- package/lib/tests/manual/multiform.js.map +1 -0
- package/lib/utils/input-wrapper.js +20 -0
- package/lib/utils/input-wrapper.js.map +1 -0
- package/package.json +4 -3
- package/src/add-route.ts +30 -11
- package/src/forms/controls/checkboxed-list-input.ts +17 -10
- package/src/forms/controls/dropdown.ts +28 -23
- package/src/forms/controls/form-control.ts +3 -1
- package/src/forms/controls/form-field-control.ts +4 -1
- package/src/forms/controls/simple-input.ts +19 -12
- package/src/forms/controls/textarea.ts +15 -14
- package/src/forms/form.ts +122 -76
- package/src/forms/multiform.ts +211 -0
- package/src/generate-collections.ts +6 -3
- package/src/generate-routes.ts +7 -3
- package/src/index.ts +1 -0
- package/src/page/list.ts +1 -1
- package/src/page/mountable-with-fields.ts +112 -0
- package/src/page/mountable.ts +4 -96
- package/src/page/page.ts +7 -3
- package/src/templates/multiform.ts +58 -0
- package/src/templates/redirect.ts +22 -0
- package/src/tests/manual/multiform.ts +106 -0
- package/src/utils/input-wrapper.ts +18 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Context } from "koa";
|
|
2
|
+
import { FlatTemplatable } from "tempstream";
|
|
2
3
|
import { attribute } from "../../sanitize";
|
|
4
|
+
import { getRequiredClass, inputWrapper } from "../../utils/input-wrapper";
|
|
3
5
|
import { FormField } from "../fields/field";
|
|
4
6
|
import { FormDataValue } from "../form";
|
|
5
7
|
import { FormFieldControl } from "./controls";
|
|
@@ -37,8 +39,11 @@ export class SimpleInput extends FormFieldControl {
|
|
|
37
39
|
|
|
38
40
|
async render<Fields extends Record<string, FormField>>(
|
|
39
41
|
ctx: Context,
|
|
40
|
-
data: Record<string, FormDataValue
|
|
41
|
-
|
|
42
|
+
data: Record<string, FormDataValue>,
|
|
43
|
+
_: unknown[],
|
|
44
|
+
field_prefix: string,
|
|
45
|
+
form_id: string
|
|
46
|
+
): Promise<FlatTemplatable> {
|
|
42
47
|
const id = this.options.id || this.field.name;
|
|
43
48
|
const label = this.options.label || this.field.name;
|
|
44
49
|
const type = this.options.type || "text";
|
|
@@ -52,16 +57,14 @@ export class SimpleInput extends FormFieldControl {
|
|
|
52
57
|
const readonly = this.options.readonly || false;
|
|
53
58
|
const required = this.field.required;
|
|
54
59
|
|
|
55
|
-
return /* HTML */
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
: ""}"
|
|
59
|
-
>
|
|
60
|
+
return /* HTML */ inputWrapper(
|
|
61
|
+
[id, type, getRequiredClass(required)],
|
|
62
|
+
`
|
|
60
63
|
<label for="${id}">${label}</label>
|
|
61
64
|
<input
|
|
62
65
|
id="${id}"
|
|
63
66
|
type="${type}"
|
|
64
|
-
name="${this.field.name}"
|
|
67
|
+
name="${field_prefix}${this.field.name}"
|
|
65
68
|
value="${attribute(
|
|
66
69
|
!valid && raw
|
|
67
70
|
? raw.toString()
|
|
@@ -70,15 +73,19 @@ export class SimpleInput extends FormFieldControl {
|
|
|
70
73
|
((parsed as any)?.toString() as string) || ""
|
|
71
74
|
: ""
|
|
72
75
|
)}"
|
|
76
|
+
${form_id ? `form="${form_id}"` : ""}
|
|
73
77
|
placeholder="${placeholder}"
|
|
74
78
|
${readonly ? "readonly" : ""}
|
|
75
79
|
${required ? "required" : ""}
|
|
76
80
|
${!this.options.autocomplete ? `autocomplete="off"` : ""}
|
|
77
81
|
${this.options.step ? `step="${this.options.step}"` : ""}
|
|
78
82
|
/>
|
|
79
|
-
${
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
+
${
|
|
84
|
+
~valid && !this.options.hide_errors
|
|
85
|
+
? `<div class="input__error">${message}</div>`
|
|
86
|
+
: ""
|
|
87
|
+
}
|
|
88
|
+
`
|
|
89
|
+
);
|
|
83
90
|
}
|
|
84
91
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Context } from "koa";
|
|
2
2
|
import { FlatTemplatable, tempstream } from "tempstream";
|
|
3
3
|
import { attribute } from "../../sanitize";
|
|
4
|
+
import { getRequiredClass, inputWrapper } from "../../utils/input-wrapper";
|
|
4
5
|
import { FormField } from "../fields/field";
|
|
5
6
|
import { FormDataValue } from "../form";
|
|
6
7
|
import { FormFieldControl } from "./form-field-control";
|
|
@@ -18,20 +19,20 @@ export class Textarea extends FormFieldControl {
|
|
|
18
19
|
data: Record<string, FormDataValue>
|
|
19
20
|
): Promise<FlatTemplatable> {
|
|
20
21
|
const { raw } = await this.field.getValue(ctx, data);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
<label for="${
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
22
|
+
const { name, required } = this.field;
|
|
23
|
+
return inputWrapper(
|
|
24
|
+
[name, getRequiredClass(required)],
|
|
25
|
+
/* HTML */ ` <label for="${name}"
|
|
26
|
+
>${this.options.label || name}</label
|
|
27
|
+
>
|
|
28
|
+
<textarea
|
|
29
|
+
name="${name}"
|
|
30
|
+
id="${name}"
|
|
31
|
+
rows="${this.options.rows || 5}"
|
|
32
|
+
cols="${this.options.cols || 33}"
|
|
33
|
+
>
|
|
33
34
|
${attribute((raw || "").toString())}</textarea
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
>`
|
|
36
|
+
);
|
|
36
37
|
}
|
|
37
38
|
}
|
package/src/forms/form.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Context } from "koa";
|
|
2
2
|
import Router from "@koa/router";
|
|
3
|
-
import { FlatTemplatable,
|
|
3
|
+
import { FlatTemplatable, tempstream } from "tempstream";
|
|
4
4
|
import {
|
|
5
5
|
hasFieldOfType,
|
|
6
6
|
hasShape,
|
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
predicates,
|
|
9
9
|
} from "@sealcode/ts-predicates";
|
|
10
10
|
import { FormField } from "./fields/field";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
Fields,
|
|
13
|
+
MountableWithFields,
|
|
14
|
+
PageErrorMessage,
|
|
15
|
+
} from "../page/mountable-with-fields";
|
|
12
16
|
|
|
13
17
|
export type FormDataPrimitive = string | string[] | undefined;
|
|
14
18
|
export type FormDataValue =
|
|
@@ -30,9 +34,14 @@ export type FormData<Fieldnames extends string = string> = {
|
|
|
30
34
|
messages: FormMessage[];
|
|
31
35
|
};
|
|
32
36
|
|
|
33
|
-
export
|
|
37
|
+
export type FormReaction =
|
|
38
|
+
| { action: "stay"; content: FlatTemplatable; messages?: FormMessage[] }
|
|
39
|
+
| { action: "redirect"; url: string; messages?: FormMessage[] };
|
|
40
|
+
|
|
41
|
+
export abstract class Form<F extends Fields> extends MountableWithFields<F> {
|
|
34
42
|
defaultSuccessMessage = "Done";
|
|
35
43
|
submitButtonText = "Wyślij";
|
|
44
|
+
action = "./";
|
|
36
45
|
|
|
37
46
|
async canAccess(
|
|
38
47
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -48,40 +57,54 @@ export abstract class Form<F extends Fields> extends Mountable<F> {
|
|
|
48
57
|
return tempstream/* HTML */ `<div>${error.message}</div>`;
|
|
49
58
|
}
|
|
50
59
|
|
|
60
|
+
makeSubmitButton(): FlatTemplatable {
|
|
61
|
+
return /* HTML */ `<input
|
|
62
|
+
type="submit"
|
|
63
|
+
value="${this.submitButtonText}"
|
|
64
|
+
formaction="${this.action}"
|
|
65
|
+
${this.form_id ? `form="${this.form_id}"` : ""}
|
|
66
|
+
/>`;
|
|
67
|
+
}
|
|
68
|
+
|
|
51
69
|
async render(ctx: Context, data: FormData): Promise<FlatTemplatable> {
|
|
52
|
-
return tempstream/* HTML */ `${this.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}"/></form>`;
|
|
70
|
+
return tempstream/* HTML */ `${this.makeOpenFormTag()}
|
|
71
|
+
${!this.controls.some((control) => control.role == "messages")
|
|
72
|
+
? this.renderMessages(ctx, data)
|
|
73
|
+
: ""}
|
|
74
|
+
${this.renderControls(ctx, data)} ${this.makeSubmitButton()}
|
|
75
|
+
${this.makeCloseFormTag()}`;
|
|
59
76
|
}
|
|
60
77
|
|
|
61
|
-
public
|
|
62
|
-
return `<form method="POST" action="
|
|
78
|
+
public makeOpenFormTag(): FlatTemplatable {
|
|
79
|
+
return `<form method="POST" action="${this.action}">`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public makeCloseFormTag(): FlatTemplatable {
|
|
83
|
+
return `</form>`;
|
|
63
84
|
}
|
|
64
85
|
|
|
65
86
|
public async onValuesInvalid(
|
|
66
87
|
ctx: Context,
|
|
67
88
|
form_messages: FormMessage[]
|
|
68
|
-
): Promise<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
:
|
|
76
|
-
|
|
89
|
+
): Promise<FormReaction> {
|
|
90
|
+
const messages = form_messages.length
|
|
91
|
+
? form_messages
|
|
92
|
+
: [<const>{ type: "error", text: "Some fields are invalid" }];
|
|
93
|
+
return {
|
|
94
|
+
action: "stay",
|
|
95
|
+
content: await this.render(ctx, {
|
|
96
|
+
raw_values: this.extractRawValues(ctx),
|
|
97
|
+
messages,
|
|
98
|
+
}),
|
|
99
|
+
messages,
|
|
100
|
+
};
|
|
77
101
|
}
|
|
78
102
|
|
|
79
103
|
public async onError(
|
|
80
104
|
ctx: Context,
|
|
81
105
|
data: FormData,
|
|
82
106
|
error: unknown
|
|
83
|
-
): Promise<
|
|
84
|
-
ctx.status = 422;
|
|
107
|
+
): Promise<FormReaction> {
|
|
85
108
|
let error_message = "Unknown error has occured";
|
|
86
109
|
if (
|
|
87
110
|
is(error, predicates.object) &&
|
|
@@ -89,10 +112,15 @@ export abstract class Form<F extends Fields> extends Mountable<F> {
|
|
|
89
112
|
) {
|
|
90
113
|
error_message = error.message;
|
|
91
114
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
115
|
+
const messages = [<const>{ type: "error", text: error_message }];
|
|
116
|
+
return {
|
|
117
|
+
action: "stay",
|
|
118
|
+
content: await this.render(ctx, {
|
|
119
|
+
raw_values: data.raw_values,
|
|
120
|
+
messages: [],
|
|
121
|
+
}),
|
|
122
|
+
messages,
|
|
123
|
+
};
|
|
96
124
|
}
|
|
97
125
|
|
|
98
126
|
public abstract onSubmit(
|
|
@@ -100,12 +128,66 @@ export abstract class Form<F extends Fields> extends Mountable<F> {
|
|
|
100
128
|
data: FormData
|
|
101
129
|
): void | Promise<void>;
|
|
102
130
|
|
|
103
|
-
public async onSuccess(
|
|
104
|
-
ctx
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
131
|
+
public async onSuccess(
|
|
132
|
+
ctx: Context,
|
|
133
|
+
data: FormData
|
|
134
|
+
): Promise<FormReaction> {
|
|
135
|
+
const messages = [
|
|
136
|
+
<const>{ type: "success", text: this.defaultSuccessMessage },
|
|
137
|
+
];
|
|
138
|
+
return {
|
|
139
|
+
action: "stay",
|
|
140
|
+
content: await this.render(ctx, {
|
|
141
|
+
raw_values: data.raw_values,
|
|
142
|
+
messages,
|
|
143
|
+
}),
|
|
144
|
+
messages,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async handlePost(ctx: Context): Promise<FormReaction> {
|
|
149
|
+
const { valid, form_messages } = await this.validate(
|
|
150
|
+
ctx,
|
|
151
|
+
this.extractRawValues(ctx)
|
|
152
|
+
);
|
|
153
|
+
if (!valid) {
|
|
154
|
+
return this.onValuesInvalid(ctx, form_messages);
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
ctx.status = 303;
|
|
158
|
+
await this.onSubmit(ctx, {
|
|
159
|
+
raw_values: this.extractRawValues(ctx),
|
|
160
|
+
messages: [],
|
|
161
|
+
});
|
|
162
|
+
return this.onSuccess(ctx, {
|
|
163
|
+
raw_values: this.extractRawValues(ctx),
|
|
164
|
+
messages: [],
|
|
165
|
+
});
|
|
166
|
+
} catch (e: unknown) {
|
|
167
|
+
// eslint-disable-next-line no-console
|
|
168
|
+
console.dir(e, { depth: 5 });
|
|
169
|
+
const message =
|
|
170
|
+
is(e, predicates.object) &&
|
|
171
|
+
hasFieldOfType(e, "message", predicates.string)
|
|
172
|
+
? e?.message
|
|
173
|
+
: is(e, predicates.string)
|
|
174
|
+
? e
|
|
175
|
+
: "Wystąpił błąd";
|
|
176
|
+
|
|
177
|
+
return this.onError(
|
|
178
|
+
ctx,
|
|
179
|
+
{
|
|
180
|
+
raw_values: this.extractRawValues(ctx),
|
|
181
|
+
messages: [
|
|
182
|
+
{
|
|
183
|
+
type: "error",
|
|
184
|
+
text: message,
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
},
|
|
188
|
+
e
|
|
189
|
+
);
|
|
190
|
+
}
|
|
109
191
|
}
|
|
110
192
|
|
|
111
193
|
public mount(router: Router, path: string): void {
|
|
@@ -124,54 +206,18 @@ export abstract class Form<F extends Fields> extends Mountable<F> {
|
|
|
124
206
|
router.get(path, async (ctx) => {
|
|
125
207
|
ctx.type = "html";
|
|
126
208
|
ctx.body = await this.render(ctx, {
|
|
127
|
-
raw_values: ctx
|
|
209
|
+
raw_values: this.extractRawValues(ctx),
|
|
128
210
|
messages: [],
|
|
129
211
|
});
|
|
130
212
|
});
|
|
131
213
|
router.post(path, async (ctx) => {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
ctx.$body
|
|
135
|
-
);
|
|
136
|
-
if (!valid) {
|
|
137
|
-
await this.onValuesInvalid(ctx, form_messages);
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
ctx.status = 303;
|
|
142
|
-
await this.onSubmit(ctx, {
|
|
143
|
-
raw_values: ctx.$body,
|
|
144
|
-
messages: [],
|
|
145
|
-
});
|
|
146
|
-
await this.onSuccess(ctx, {
|
|
147
|
-
raw_values: ctx.$body,
|
|
148
|
-
messages: [],
|
|
149
|
-
});
|
|
150
|
-
} catch (e: unknown) {
|
|
151
|
-
// eslint-disable-next-line no-console
|
|
152
|
-
console.dir(e, { depth: 5 });
|
|
153
|
-
const message =
|
|
154
|
-
is(e, predicates.object) &&
|
|
155
|
-
hasFieldOfType(e, "message", predicates.string)
|
|
156
|
-
? e?.message
|
|
157
|
-
: is(e, predicates.string)
|
|
158
|
-
? e
|
|
159
|
-
: "Wystąpił błąd";
|
|
160
|
-
|
|
214
|
+
const reaction = await this.handlePost(ctx);
|
|
215
|
+
if (reaction.action == "stay") {
|
|
161
216
|
ctx.status = 422;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
messages: [
|
|
167
|
-
{
|
|
168
|
-
type: "error",
|
|
169
|
-
text: message,
|
|
170
|
-
},
|
|
171
|
-
],
|
|
172
|
-
},
|
|
173
|
-
e
|
|
174
|
-
);
|
|
217
|
+
ctx.body = reaction.content;
|
|
218
|
+
} else if (reaction.action == "redirect") {
|
|
219
|
+
ctx.status = 303;
|
|
220
|
+
ctx.redirect(reaction.url);
|
|
175
221
|
}
|
|
176
222
|
});
|
|
177
223
|
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import Router from "@koa/router";
|
|
2
|
+
import { is, predicates } from "@sealcode/ts-predicates";
|
|
3
|
+
import { Context } from "koa";
|
|
4
|
+
import { FlatTemplatable, tempstream } from "tempstream";
|
|
5
|
+
import { Form, FormDataValue, FormMessage } from "..";
|
|
6
|
+
import { Fields, MountableWithFields } from "../page/mountable-with-fields";
|
|
7
|
+
import { attribute } from "../sanitize";
|
|
8
|
+
|
|
9
|
+
const FIELD_PREFIX_SEPARATOR = "___";
|
|
10
|
+
|
|
11
|
+
export class Multiform extends MountableWithFields {
|
|
12
|
+
controls = [];
|
|
13
|
+
public name: string;
|
|
14
|
+
public forms: Record<string, Form<Fields>>;
|
|
15
|
+
|
|
16
|
+
init(): void {
|
|
17
|
+
super.init();
|
|
18
|
+
for (const [key, form] of Object.entries(this.forms)) {
|
|
19
|
+
if (key !== attribute(key)) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Form name "${key}" is not url-safe. Try: "${attribute(
|
|
22
|
+
key
|
|
23
|
+
)}"`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
form.makeOpenFormTag = () => `<div>`;
|
|
27
|
+
form.makeCloseFormTag = () => `</div>`;
|
|
28
|
+
form.field_names_prefix = key + FIELD_PREFIX_SEPARATOR;
|
|
29
|
+
form.form_id = this.name;
|
|
30
|
+
form.action = "./" + key;
|
|
31
|
+
form.init();
|
|
32
|
+
form.extractRawValues = (context) =>
|
|
33
|
+
this.getSubformRawValues(context, key);
|
|
34
|
+
const submit_button_id = this.name + "_" + key + "_submit";
|
|
35
|
+
|
|
36
|
+
form.makeSubmitButton = () => /* HTML */ `<input
|
|
37
|
+
type="submit"
|
|
38
|
+
value="${form.submitButtonText}"
|
|
39
|
+
formaction="${form.action}"
|
|
40
|
+
id="${submit_button_id}"
|
|
41
|
+
${form.form_id ? `form="${form.form_id}"` : ""}
|
|
42
|
+
/> `;
|
|
43
|
+
}
|
|
44
|
+
for (const form of Object.values(this.forms)) {
|
|
45
|
+
for (const field of Object.values(form.fields)) {
|
|
46
|
+
if (field.name.includes(FIELD_PREFIX_SEPARATOR)) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`A field name within multiform cannot contain '${FIELD_PREFIX_SEPARATOR}'`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
extractRawValues(ctx: Context): Record<string, FormDataValue> {
|
|
56
|
+
return ctx.$body || {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async canAccess(): Promise<{ canAccess: boolean; message: string }> {
|
|
60
|
+
return {
|
|
61
|
+
canAccess: true,
|
|
62
|
+
message:
|
|
63
|
+
"this view includes multiple forms and each of them have their unique access rules",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
68
|
+
getSubformsToRender(ctx: Context): Array<readonly [string, Form<any>]> {
|
|
69
|
+
const requested_frame = ctx.headers["turbo-frame"];
|
|
70
|
+
if (
|
|
71
|
+
!is(
|
|
72
|
+
requested_frame,
|
|
73
|
+
predicates.or(predicates.undefined, predicates.string)
|
|
74
|
+
)
|
|
75
|
+
) {
|
|
76
|
+
throw new Error("Wrong turbo-frame header value type");
|
|
77
|
+
}
|
|
78
|
+
const forms_to_render =
|
|
79
|
+
requested_frame && this.forms[requested_frame]
|
|
80
|
+
? [<const>[requested_frame, this.forms[requested_frame]]]
|
|
81
|
+
: Object.entries(this.forms);
|
|
82
|
+
return forms_to_render;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async renderSubform(
|
|
86
|
+
ctx: Context,
|
|
87
|
+
sub_form_name: string
|
|
88
|
+
): Promise<FlatTemplatable> {
|
|
89
|
+
const form = this.forms[sub_form_name];
|
|
90
|
+
const result = await form.canAccess(ctx);
|
|
91
|
+
if (!result.canAccess) {
|
|
92
|
+
return result.message;
|
|
93
|
+
}
|
|
94
|
+
return form.render(ctx, {
|
|
95
|
+
raw_values: this.getSubformRawValues(ctx, sub_form_name),
|
|
96
|
+
messages: [],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getSubformRawValues(
|
|
101
|
+
ctx: Context,
|
|
102
|
+
sub_form_name: string
|
|
103
|
+
): Record<string, FormDataValue> {
|
|
104
|
+
const result = Object.fromEntries(
|
|
105
|
+
Object.entries(this.extractRawValues(ctx))
|
|
106
|
+
.filter(([key]) =>
|
|
107
|
+
key.startsWith(sub_form_name + FIELD_PREFIX_SEPARATOR)
|
|
108
|
+
)
|
|
109
|
+
.map(([key, value]) => [
|
|
110
|
+
key.slice((sub_form_name + FIELD_PREFIX_SEPARATOR).length),
|
|
111
|
+
value,
|
|
112
|
+
])
|
|
113
|
+
);
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async render(
|
|
118
|
+
ctx: Context,
|
|
119
|
+
messages: FormMessage[],
|
|
120
|
+
prerenderedForms: Record<string, FlatTemplatable | undefined> = {}
|
|
121
|
+
): Promise<FlatTemplatable> {
|
|
122
|
+
return tempstream/* HTML */ `${this.renderMessages(ctx, {
|
|
123
|
+
raw_values: {},
|
|
124
|
+
messages,
|
|
125
|
+
})}
|
|
126
|
+
<div class="forms">
|
|
127
|
+
<form
|
|
128
|
+
id="${this.name}"
|
|
129
|
+
novalidate
|
|
130
|
+
${
|
|
131
|
+
/* novalidate is here because all the subforms are actuallly one form and errors in one will prevent submitting all of the forms */ ""
|
|
132
|
+
}
|
|
133
|
+
method="POST"
|
|
134
|
+
></form>
|
|
135
|
+
${this.getSubformsToRender(ctx).map(([form_name]) => {
|
|
136
|
+
const frame_form_id =
|
|
137
|
+
this.name + "_" + form_name + "_frame_form";
|
|
138
|
+
return tempstream/* HTML */ `<turbo-frame
|
|
139
|
+
id="${form_name}"
|
|
140
|
+
contains-subform
|
|
141
|
+
subform-id="${frame_form_id}"
|
|
142
|
+
>
|
|
143
|
+
${prerenderedForms[form_name] ||
|
|
144
|
+
this.renderSubform(ctx, form_name)}
|
|
145
|
+
<form id="${frame_form_id}" method="POST"></form>
|
|
146
|
+
</turbo-frame>`;
|
|
147
|
+
})}
|
|
148
|
+
</div>
|
|
149
|
+
${this.makeBottomScript()}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
makeBottomScript(): string {
|
|
153
|
+
// this script assigns each field to its corresponding form, instead of
|
|
154
|
+
// the html-only version where all fields are attached to one meta-form
|
|
155
|
+
return /* HTML */ ` <script>
|
|
156
|
+
(function () {
|
|
157
|
+
if (!window.subform_handlers) {
|
|
158
|
+
window.subform_handlers = {};
|
|
159
|
+
}
|
|
160
|
+
if (window.subform_handlers["${this.name}"]) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const handler = () => {
|
|
164
|
+
document
|
|
165
|
+
.querySelectorAll("turbo-frame[contains-subform]")
|
|
166
|
+
.forEach((frame) => {
|
|
167
|
+
frame_form_id = frame.getAttribute("subform-id");
|
|
168
|
+
frame.querySelectorAll("input").forEach((input) => {
|
|
169
|
+
input.setAttribute("form", frame_form_id);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
window.subform_handlers["${this.name}"] = handler;
|
|
174
|
+
document.documentElement.addEventListener(
|
|
175
|
+
"turbo:load",
|
|
176
|
+
handler
|
|
177
|
+
);
|
|
178
|
+
document.documentElement.addEventListener(
|
|
179
|
+
"turbo:frame-render",
|
|
180
|
+
handler
|
|
181
|
+
);
|
|
182
|
+
})();
|
|
183
|
+
</script>`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
mount(router: Router, path: string): void {
|
|
187
|
+
router.get(path, async (ctx) => {
|
|
188
|
+
ctx.type = "html";
|
|
189
|
+
ctx.body = await this.render(ctx, [], {});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
for (const [key, form] of Object.entries(this.forms)) {
|
|
193
|
+
router.post(
|
|
194
|
+
path + (path.endsWith("/") ? "" : "/") + key,
|
|
195
|
+
async (ctx) => {
|
|
196
|
+
const reaction = await form.handlePost(ctx);
|
|
197
|
+
if (reaction.action == "stay") {
|
|
198
|
+
ctx.status = 422;
|
|
199
|
+
const form_content = reaction.content;
|
|
200
|
+
ctx.body = this.render(ctx, [], {
|
|
201
|
+
[key]: form_content,
|
|
202
|
+
});
|
|
203
|
+
} else if (reaction.action == "redirect") {
|
|
204
|
+
ctx.status = 303;
|
|
205
|
+
ctx.redirect(reaction.url);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -10,21 +10,24 @@ async function extractCollectionClassname(
|
|
|
10
10
|
full_file_path: string
|
|
11
11
|
): Promise<string> {
|
|
12
12
|
const file_content = await fs.readFile(full_file_path, "utf-8");
|
|
13
|
-
const result =
|
|
13
|
+
const result = /export default class (\w+)/.exec(file_content);
|
|
14
14
|
if (result === null) {
|
|
15
15
|
throw new Error("Missing 'export default class' statement?");
|
|
16
16
|
}
|
|
17
17
|
return assertType(
|
|
18
18
|
result[1],
|
|
19
19
|
predicates.string,
|
|
20
|
-
|
|
20
|
+
`Missing export default class in '${full_file_path}'?`
|
|
21
21
|
);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export async function generateCollections(): Promise<void> {
|
|
25
25
|
const collections_dir = target_locreq.resolve("src/back/collections");
|
|
26
26
|
const files = (await fs.readdir(collections_dir)).filter(
|
|
27
|
-
(f) =>
|
|
27
|
+
(f) =>
|
|
28
|
+
!/\.(sub)?test\./.exec(f) &&
|
|
29
|
+
f != "collections.ts" &&
|
|
30
|
+
f.endsWith(".ts")
|
|
28
31
|
);
|
|
29
32
|
const file_data = await Promise.all(
|
|
30
33
|
files.map(async (file) => {
|
package/src/generate-routes.ts
CHANGED
|
@@ -12,7 +12,7 @@ const target_locreq = _locreq(process.cwd());
|
|
|
12
12
|
|
|
13
13
|
async function extractActionName(full_file_path: string): Promise<string> {
|
|
14
14
|
const file_content = await fs.readFile(full_file_path, "utf-8");
|
|
15
|
-
const result =
|
|
15
|
+
const result = /export const actionName = "(\w+)"/.exec(file_content);
|
|
16
16
|
if (result === null) {
|
|
17
17
|
throw new Error("Missing 'export const actionName' statement?");
|
|
18
18
|
}
|
|
@@ -32,7 +32,9 @@ export async function generateRoutes(): Promise<void> {
|
|
|
32
32
|
(f) =>
|
|
33
33
|
f.endsWith(".page.ts") ||
|
|
34
34
|
f.endsWith(".form.ts") ||
|
|
35
|
-
f.endsWith(".
|
|
35
|
+
f.endsWith(".multiform.ts") ||
|
|
36
|
+
f.endsWith(".list.ts") ||
|
|
37
|
+
f.endsWith(".redirect.ts")
|
|
36
38
|
)
|
|
37
39
|
.map(async (fullpath) => ({
|
|
38
40
|
fullpath,
|
|
@@ -85,7 +87,9 @@ function mount(router: Router, URL: string, mountable: Mountable) {
|
|
|
85
87
|
mountable.init();
|
|
86
88
|
mountable.mount(router, URL);
|
|
87
89
|
// to automatically add trailing slashes:
|
|
88
|
-
router.get(URL.slice(0, -1), (ctx) =>
|
|
90
|
+
router.get(URL.slice(0, -1), (ctx) => {
|
|
91
|
+
ctx.redirect(ctx.originalUrl + "/");
|
|
92
|
+
});
|
|
89
93
|
router.use(URL, (ctx) => ctx.set("content-type", "text/html;charset=utf-8"));
|
|
90
94
|
}
|
|
91
95
|
|
package/src/index.ts
CHANGED
package/src/page/list.ts
CHANGED
|
@@ -142,7 +142,7 @@ export abstract class ListPage<
|
|
|
142
142
|
])}
|
|
143
143
|
${this.filterControls.map((control) =>
|
|
144
144
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
145
|
-
control.render(ctx, values)
|
|
145
|
+
control.render(ctx, values, [], "", "")
|
|
146
146
|
)}
|
|
147
147
|
</form>`;
|
|
148
148
|
}
|