@sealcode/sealgen 0.8.2 → 0.8.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/@types/forms/controls/autocomplete.d.ts +3 -1
- package/@types/forms/controls/checkbox-group.d.ts +3 -2
- package/@types/forms/controls/collapsible-checkbox-group.d.ts +1 -1
- package/@types/forms/controls/collapsible-field-group.d.ts +1 -1
- package/@types/forms/controls/controls.d.ts +3 -0
- package/@types/forms/controls/dropdown.d.ts +6 -5
- package/@types/forms/controls/field-group.d.ts +3 -3
- package/@types/forms/controls/file.d.ts +14 -0
- package/@types/forms/controls/form-control.d.ts +4 -1
- package/@types/forms/controls/form-field-control.d.ts +1 -1
- package/@types/forms/controls/multiple-files.d.ts +31 -0
- package/@types/forms/controls/number.d.ts +17 -0
- package/@types/forms/controls/simple-input.d.ts +20 -27
- package/@types/forms/controls/textarea.d.ts +9 -7
- package/@types/forms/fields/field.d.ts +4 -1
- package/@types/forms/fields/file.d.ts +16 -0
- package/@types/forms/fields/files-multiple.d.ts +12 -0
- package/@types/forms/fields/multiple-files.d.ts +14 -0
- package/@types/forms/fields/number.d.ts +10 -1
- package/@types/forms/form.d.ts +7 -4
- package/@types/forms/multiform.d.ts +3 -3
- package/@types/page/list.d.ts +1 -1
- package/@types/page/mountable-with-fields.d.ts +3 -2
- package/@types/page/page.d.ts +1 -1
- package/@types/sanitize.d.ts +1 -1
- package/lib/forms/controls/autocomplete.js.map +1 -1
- package/lib/forms/controls/checkbox-group.js +11 -5
- package/lib/forms/controls/checkbox-group.js.map +1 -1
- package/lib/forms/controls/collapsible-checkbox-group.js +2 -2
- package/lib/forms/controls/collapsible-checkbox-group.js.map +1 -1
- package/lib/forms/controls/collapsible-field-group.js.map +1 -1
- package/lib/forms/controls/controls.js +3 -0
- package/lib/forms/controls/controls.js.map +1 -1
- package/lib/forms/controls/derived.js +12 -11
- package/lib/forms/controls/derived.js.map +1 -1
- package/lib/forms/controls/dropdown.js +25 -18
- package/lib/forms/controls/dropdown.js.map +1 -1
- package/lib/forms/controls/field-group.js +3 -3
- package/lib/forms/controls/field-group.js.map +1 -1
- package/lib/forms/controls/file.js +38 -0
- package/lib/forms/controls/file.js.map +1 -0
- package/lib/forms/controls/form-control.js +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/multiple-files.js +137 -0
- package/lib/forms/controls/multiple-files.js.map +1 -0
- package/lib/forms/controls/number.js +34 -0
- package/lib/forms/controls/number.js.map +1 -0
- package/lib/forms/controls/simple-input.js +20 -10
- package/lib/forms/controls/simple-input.js.map +1 -1
- package/lib/forms/controls/textarea.js +13 -22
- package/lib/forms/controls/textarea.js.map +1 -1
- package/lib/forms/fields/enum-multiple.js +4 -1
- package/lib/forms/fields/enum-multiple.js.map +1 -1
- package/lib/forms/fields/field.js +8 -1
- package/lib/forms/fields/field.js.map +1 -1
- package/lib/forms/fields/file.js +58 -0
- package/lib/forms/fields/file.js.map +1 -0
- package/lib/forms/fields/files-multiple.js +18 -0
- package/lib/forms/fields/files-multiple.js.map +1 -0
- package/lib/forms/fields/multiple-files.js +19 -0
- package/lib/forms/fields/multiple-files.js.map +1 -0
- package/lib/forms/fields/number.js +6 -2
- package/lib/forms/fields/number.js.map +1 -1
- package/lib/forms/form.js +38 -21
- package/lib/forms/form.js.map +1 -1
- package/lib/forms/multiform.js +8 -8
- package/lib/forms/multiform.js.map +1 -1
- package/lib/page/list.js +9 -6
- package/lib/page/list.js.map +1 -1
- package/lib/page/mountable-with-fields.js +16 -3
- package/lib/page/mountable-with-fields.js.map +1 -1
- package/lib/page/page.js +1 -1
- package/lib/page/page.js.map +1 -1
- package/lib/sanitize.js +3 -0
- package/lib/sanitize.js.map +1 -1
- package/lib/tests/manual/multiform.js +13 -13
- package/lib/tests/manual/multiform.js.map +1 -1
- package/package.json +2 -2
- package/src/forms/controls/autocomplete.ts +2 -6
- package/src/forms/controls/checkbox-group.ts +17 -6
- package/src/forms/controls/collapsible-checkbox-group.ts +10 -2
- package/src/forms/controls/collapsible-field-group.ts +2 -4
- package/src/forms/controls/controls.ts +3 -0
- package/src/forms/controls/derived.ts +12 -11
- package/src/forms/controls/dropdown.ts +51 -34
- package/src/forms/controls/field-group.ts +6 -4
- package/src/forms/controls/file.ts +50 -0
- package/src/forms/controls/form-control.ts +5 -1
- package/src/forms/controls/form-field-control.ts +2 -1
- package/src/forms/controls/multiple-files.ts +213 -0
- package/src/forms/controls/number.ts +52 -0
- package/src/forms/controls/simple-input.ts +67 -37
- package/src/forms/controls/textarea.ts +23 -24
- package/src/forms/fields/enum-multiple.ts +3 -1
- package/src/forms/fields/field.ts +12 -2
- package/src/forms/fields/file.ts +86 -0
- package/src/forms/fields/multiple-files.ts +20 -0
- package/src/forms/fields/number.ts +17 -2
- package/src/forms/form.ts +84 -42
- package/src/forms/forms.scss +4 -1
- package/src/forms/multiform.ts +26 -13
- package/src/page/list.ts +10 -7
- package/src/page/mountable-with-fields.ts +26 -4
- package/src/page/page.ts +3 -1
- package/src/sanitize.ts +4 -1
- package/src/tests/manual/multiform.ts +19 -13
|
@@ -6,35 +6,49 @@ import { FormField } from "../fields/field";
|
|
|
6
6
|
import { FormDataValue, FormMessage } from "../form";
|
|
7
7
|
import { FormFieldControl } from "./controls";
|
|
8
8
|
|
|
9
|
-
export
|
|
9
|
+
export type SimpleInputOptions = {
|
|
10
|
+
id?: string;
|
|
11
|
+
label?: string;
|
|
12
|
+
autocomplete?: boolean;
|
|
13
|
+
hide_errors?: boolean;
|
|
14
|
+
type?:
|
|
15
|
+
| "color"
|
|
16
|
+
| "date"
|
|
17
|
+
| "email"
|
|
18
|
+
| "file"
|
|
19
|
+
| "month"
|
|
20
|
+
| "number"
|
|
21
|
+
| "password"
|
|
22
|
+
| "search"
|
|
23
|
+
| "tel"
|
|
24
|
+
| "text"
|
|
25
|
+
| "time"
|
|
26
|
+
| "url"
|
|
27
|
+
| "week";
|
|
28
|
+
value?: string;
|
|
29
|
+
placeholder?: string;
|
|
30
|
+
readonly?: boolean;
|
|
31
|
+
step?: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export class SimpleInput<
|
|
35
|
+
ParsedValue,
|
|
36
|
+
Options extends SimpleInputOptions = SimpleInputOptions
|
|
37
|
+
> extends FormFieldControl {
|
|
38
|
+
options: SimpleInputOptions;
|
|
10
39
|
constructor(
|
|
11
|
-
public field: FormField,
|
|
12
|
-
|
|
13
|
-
id?: string;
|
|
14
|
-
label?: string;
|
|
15
|
-
autocomplete?: boolean;
|
|
16
|
-
hide_errors?: boolean;
|
|
17
|
-
type?:
|
|
18
|
-
| "color"
|
|
19
|
-
| "date"
|
|
20
|
-
| "email"
|
|
21
|
-
| "file"
|
|
22
|
-
| "month"
|
|
23
|
-
| "number"
|
|
24
|
-
| "password"
|
|
25
|
-
| "search"
|
|
26
|
-
| "tel"
|
|
27
|
-
| "text"
|
|
28
|
-
| "time"
|
|
29
|
-
| "url"
|
|
30
|
-
| "week";
|
|
31
|
-
value?: string;
|
|
32
|
-
placeholder?: string;
|
|
33
|
-
readonly?: boolean;
|
|
34
|
-
step?: number;
|
|
35
|
-
} = {}
|
|
40
|
+
public field: FormField<boolean, ParsedValue>,
|
|
41
|
+
options?: Options
|
|
36
42
|
) {
|
|
37
43
|
super([field]);
|
|
44
|
+
this.options = options || {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async preInput(
|
|
48
|
+
_ctx: Context,
|
|
49
|
+
_data: Record<string, FormDataValue>
|
|
50
|
+
): Promise<FlatTemplatable> {
|
|
51
|
+
return "";
|
|
38
52
|
}
|
|
39
53
|
|
|
40
54
|
getType(): string {
|
|
@@ -60,7 +74,7 @@ export class SimpleInput extends FormFieldControl {
|
|
|
60
74
|
data: Record<string, FormDataValue>,
|
|
61
75
|
field_prefix: string,
|
|
62
76
|
form_id: string
|
|
63
|
-
): Promise<Record<string, string |
|
|
77
|
+
): Promise<Record<string, string | boolean>> {
|
|
64
78
|
const readonly = this.options.readonly || false;
|
|
65
79
|
const required = this.field.required;
|
|
66
80
|
const { parsed, raw, valid } = await this.field.getValue(ctx, data);
|
|
@@ -85,11 +99,15 @@ export class SimpleInput extends FormFieldControl {
|
|
|
85
99
|
]);
|
|
86
100
|
}
|
|
87
101
|
|
|
88
|
-
renderAttributes(attrs: Record<string, string |
|
|
102
|
+
renderAttributes(attrs: Record<string, string | boolean>) {
|
|
89
103
|
return Object.entries(attrs)
|
|
90
104
|
.map(([key, value]) => {
|
|
91
|
-
if (value ===
|
|
92
|
-
|
|
105
|
+
if (typeof value === "boolean") {
|
|
106
|
+
if (value) {
|
|
107
|
+
return key;
|
|
108
|
+
} else {
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
93
111
|
} else {
|
|
94
112
|
return `${key}="${attribute(value)}"`;
|
|
95
113
|
}
|
|
@@ -101,12 +119,21 @@ export class SimpleInput extends FormFieldControl {
|
|
|
101
119
|
return "";
|
|
102
120
|
}
|
|
103
121
|
|
|
122
|
+
async renderInput(
|
|
123
|
+
ctx: Context,
|
|
124
|
+
attributes_str: string,
|
|
125
|
+
data: Record<string, FormDataValue>
|
|
126
|
+
): Promise<FlatTemplatable> {
|
|
127
|
+
return `<input ${attributes_str} />`;
|
|
128
|
+
}
|
|
129
|
+
|
|
104
130
|
async render<Fields extends Record<string, FormField>>(
|
|
105
131
|
ctx: Context,
|
|
106
132
|
data: Record<string, FormDataValue>,
|
|
107
133
|
_messages: FormMessage[],
|
|
108
134
|
field_prefix: string,
|
|
109
|
-
form_id: string
|
|
135
|
+
form_id: string,
|
|
136
|
+
show_errors: boolean
|
|
110
137
|
): Promise<FlatTemplatable> {
|
|
111
138
|
const id = this.getID();
|
|
112
139
|
const label = this.getLabel();
|
|
@@ -121,19 +148,22 @@ export class SimpleInput extends FormFieldControl {
|
|
|
121
148
|
return inputWrapper(
|
|
122
149
|
[id, type, getRequiredClass(this.field.required)],
|
|
123
150
|
tempstream/* HTML */ `
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
151
|
+
${label ? `<label for="${id}">${label}</label>` : ""}
|
|
152
|
+
${await this.preInput(ctx, data)}
|
|
153
|
+
${this.renderInput(
|
|
154
|
+
ctx,
|
|
155
|
+
this.renderAttributes(
|
|
127
156
|
await this.getInputAttributes(
|
|
128
157
|
ctx,
|
|
129
158
|
data,
|
|
130
159
|
field_prefix,
|
|
131
160
|
form_id
|
|
132
161
|
)
|
|
133
|
-
)
|
|
134
|
-
|
|
162
|
+
),
|
|
163
|
+
data
|
|
164
|
+
)}
|
|
135
165
|
${this.postInput(ctx)}
|
|
136
|
-
${~valid && !this.options.hide_errors
|
|
166
|
+
${~valid && !this.options.hide_errors && show_errors
|
|
137
167
|
? `<div class="input__error">${message}</div>`
|
|
138
168
|
: ""}
|
|
139
169
|
`
|
|
@@ -5,36 +5,35 @@ import { getRequiredClass, inputWrapper } from "../../utils/input-wrapper";
|
|
|
5
5
|
import { FormField } from "../fields/field";
|
|
6
6
|
import { FormDataValue } from "../form";
|
|
7
7
|
import { FormFieldControl } from "./form-field-control";
|
|
8
|
+
import { SimpleInput, SimpleInputOptions } from "./simple-input";
|
|
8
9
|
|
|
9
|
-
export class Textarea extends
|
|
10
|
+
export class Textarea<Required extends boolean> extends SimpleInput<string> {
|
|
10
11
|
constructor(
|
|
11
|
-
public field: FormField,
|
|
12
|
-
public options:
|
|
12
|
+
public field: FormField<boolean, string>,
|
|
13
|
+
public options: SimpleInputOptions & {
|
|
14
|
+
label?: string;
|
|
15
|
+
rows?: number;
|
|
16
|
+
cols?: number;
|
|
17
|
+
description?: string;
|
|
18
|
+
} = {}
|
|
13
19
|
) {
|
|
14
|
-
super(
|
|
20
|
+
super(field, options);
|
|
15
21
|
}
|
|
16
22
|
|
|
17
|
-
|
|
23
|
+
renderDescription() {
|
|
24
|
+
return this.options.description
|
|
25
|
+
? /* HTML */ `<div class="form-input__description">
|
|
26
|
+
${this.options.description}
|
|
27
|
+
</div>`
|
|
28
|
+
: "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async renderInput(
|
|
18
32
|
ctx: Context,
|
|
33
|
+
attributes: string,
|
|
19
34
|
data: Record<string, FormDataValue>
|
|
20
|
-
)
|
|
21
|
-
const {
|
|
22
|
-
|
|
23
|
-
return inputWrapper(
|
|
24
|
-
[name, getRequiredClass(required)],
|
|
25
|
-
/* HTML */ ` <label for="${name}"
|
|
26
|
-
>${this.options.label != undefined
|
|
27
|
-
? this.options.label
|
|
28
|
-
: name}</label
|
|
29
|
-
>
|
|
30
|
-
<textarea
|
|
31
|
-
name="${name}"
|
|
32
|
-
id="${name}"
|
|
33
|
-
rows="${this.options.rows || 5}"
|
|
34
|
-
cols="${this.options.cols || 33}"
|
|
35
|
-
>
|
|
36
|
-
${attribute((raw || "").toString())}</textarea
|
|
37
|
-
>`
|
|
38
|
-
);
|
|
35
|
+
) {
|
|
36
|
+
const { parsed: value } = await this.field.getValue(ctx, data);
|
|
37
|
+
return /* HTML */ `<textarea ${attributes}>${value}</textarea>`;
|
|
39
38
|
}
|
|
40
39
|
}
|
|
@@ -37,7 +37,9 @@ export class EnumMultipleField<
|
|
|
37
37
|
_: Context,
|
|
38
38
|
value: FormDataValue
|
|
39
39
|
): Promise<FieldParseResult<string[]>> {
|
|
40
|
-
if (is(value, predicates.
|
|
40
|
+
if (is(value, predicates.string)) {
|
|
41
|
+
return { parsable: true, parsed_value: [value], error: null };
|
|
42
|
+
} else if (is(value, predicates.array(predicates.string))) {
|
|
41
43
|
return {
|
|
42
44
|
parsable: true,
|
|
43
45
|
parsed_value: value,
|
|
@@ -2,7 +2,6 @@ import { Predicate, predicates } from "@sealcode/ts-predicates";
|
|
|
2
2
|
import { Context } from "koa";
|
|
3
3
|
import { t } from "../../utils/translate";
|
|
4
4
|
import { FormDataValue } from "../form";
|
|
5
|
-
import { SimpleFormField } from "./simple-form-field";
|
|
6
5
|
|
|
7
6
|
export type FormFieldValidationResponse = { valid: boolean; message: string };
|
|
8
7
|
|
|
@@ -57,7 +56,7 @@ export abstract class FormField<
|
|
|
57
56
|
predicate: Predicate = predicates.unknown;
|
|
58
57
|
mapToFilter: (parsed: ParsedValue) => unknown = (value) => value;
|
|
59
58
|
|
|
60
|
-
init(fieldname: string): void {
|
|
59
|
+
async init(fieldname: string): Promise<void> {
|
|
61
60
|
this.name = fieldname;
|
|
62
61
|
}
|
|
63
62
|
|
|
@@ -145,6 +144,15 @@ export abstract class FormField<
|
|
|
145
144
|
this.mapToFilter = fn;
|
|
146
145
|
return this;
|
|
147
146
|
}
|
|
147
|
+
|
|
148
|
+
//return undefined here to delete a value
|
|
149
|
+
async getDatabaseValue(
|
|
150
|
+
ctx: Context,
|
|
151
|
+
data: Record<string, FormDataValue>
|
|
152
|
+
): Promise<unknown> {
|
|
153
|
+
const { parsed } = await this.getValue(ctx, data);
|
|
154
|
+
return parsed;
|
|
155
|
+
}
|
|
148
156
|
}
|
|
149
157
|
|
|
150
158
|
export * from "./simple-form-field";
|
|
@@ -158,3 +166,5 @@ export * from "./enum";
|
|
|
158
166
|
export * from "./boolean";
|
|
159
167
|
export * from "./enum-multiple";
|
|
160
168
|
export * from "./existing-sealious-value";
|
|
169
|
+
export * from "./file";
|
|
170
|
+
export * from "./multiple-files";
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasField,
|
|
3
|
+
hasFieldOfType,
|
|
4
|
+
hasShape,
|
|
5
|
+
is,
|
|
6
|
+
predicates,
|
|
7
|
+
} from "@sealcode/ts-predicates";
|
|
8
|
+
import { Context } from "koa";
|
|
9
|
+
import { File as SealiousFile } from "sealious";
|
|
10
|
+
import { FormDataValue } from "../form";
|
|
11
|
+
import { FieldParseResult, FormField } from "./field";
|
|
12
|
+
|
|
13
|
+
export type FileContainer = {
|
|
14
|
+
old: SealiousFile | null;
|
|
15
|
+
new: SealiousFile | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export class File<Required extends boolean> extends FormField<
|
|
19
|
+
Required,
|
|
20
|
+
FileContainer
|
|
21
|
+
> {
|
|
22
|
+
async parse(
|
|
23
|
+
ctx: Context,
|
|
24
|
+
raw_value: FormDataValue
|
|
25
|
+
): Promise<FieldParseResult<FileContainer>> {
|
|
26
|
+
let result;
|
|
27
|
+
if (
|
|
28
|
+
raw_value instanceof SealiousFile ||
|
|
29
|
+
(is(raw_value, predicates.object) &&
|
|
30
|
+
hasShape(
|
|
31
|
+
{
|
|
32
|
+
filename: predicates.string,
|
|
33
|
+
id: predicates.string,
|
|
34
|
+
},
|
|
35
|
+
raw_value
|
|
36
|
+
) &&
|
|
37
|
+
raw_value.id)
|
|
38
|
+
) {
|
|
39
|
+
const file = await SealiousFile.fromID(
|
|
40
|
+
ctx.$app,
|
|
41
|
+
raw_value.id as string
|
|
42
|
+
);
|
|
43
|
+
result = {
|
|
44
|
+
parsed_value: { old: file, new: null },
|
|
45
|
+
error: null,
|
|
46
|
+
parsable: true as true,
|
|
47
|
+
};
|
|
48
|
+
} else if (!is(raw_value, predicates.object)) {
|
|
49
|
+
result = {
|
|
50
|
+
parsed_value: null,
|
|
51
|
+
error: "Expected a field with old.id:string, old.filename:string and new?:File",
|
|
52
|
+
parsable: false as false,
|
|
53
|
+
};
|
|
54
|
+
} else {
|
|
55
|
+
const old_file =
|
|
56
|
+
hasFieldOfType(raw_value, "old", predicates.object) &&
|
|
57
|
+
hasShape(
|
|
58
|
+
{ id: predicates.string, filename: predicates.string },
|
|
59
|
+
raw_value.old
|
|
60
|
+
) &&
|
|
61
|
+
raw_value.old.id
|
|
62
|
+
? await SealiousFile.fromID(ctx.$app, raw_value.old.id)
|
|
63
|
+
: null;
|
|
64
|
+
const new_file =
|
|
65
|
+
hasField("new", raw_value) &&
|
|
66
|
+
raw_value.new instanceof SealiousFile
|
|
67
|
+
? raw_value.new
|
|
68
|
+
: null;
|
|
69
|
+
result = {
|
|
70
|
+
parsable: true as true,
|
|
71
|
+
error: null,
|
|
72
|
+
parsed_value: { old: old_file, new: new_file },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async getDatabaseValue(ctx: Context, data: Record<string, FormDataValue>) {
|
|
79
|
+
const { parsed } = await this.getValue(ctx, data);
|
|
80
|
+
return parsed.new || undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getEmptyValue() {
|
|
84
|
+
return { old: null, new: null };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Context } from "koa";
|
|
2
|
+
import { FieldTypes as SealiousFieldTypes } from "sealious";
|
|
3
|
+
import { FormField } from "./field";
|
|
4
|
+
|
|
5
|
+
export class MultipleFiles extends FormField<false> {
|
|
6
|
+
constructor(
|
|
7
|
+
public collection_field: SealiousFieldTypes.ReverseSingleReference,
|
|
8
|
+
public getItemId: (ctx: Context) => Promise<string> | string
|
|
9
|
+
) {
|
|
10
|
+
super(false);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
getEmptyValue() {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async parse() {
|
|
18
|
+
return <const>{ parsed_value: undefined, parsable: true, error: null }; // the control is supposed to do the heavy lifting here
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -7,7 +7,15 @@ export class NumberField<
|
|
|
7
7
|
DefaultValue extends number | null = null,
|
|
8
8
|
Required extends boolean = boolean
|
|
9
9
|
> extends FormField<Required, number | DefaultValue> {
|
|
10
|
-
constructor(
|
|
10
|
+
constructor(
|
|
11
|
+
required: Required,
|
|
12
|
+
public default_value: DefaultValue,
|
|
13
|
+
public options: Partial<{
|
|
14
|
+
max: number;
|
|
15
|
+
min: number;
|
|
16
|
+
decimalDigits: number;
|
|
17
|
+
}> = {}
|
|
18
|
+
) {
|
|
11
19
|
super(required);
|
|
12
20
|
}
|
|
13
21
|
|
|
@@ -36,9 +44,16 @@ export class NumberField<
|
|
|
36
44
|
parseFloat(value).toString() == value.trim()) ||
|
|
37
45
|
is(value, predicates.number)
|
|
38
46
|
) {
|
|
47
|
+
console.log(this.name, this.options.decimalDigits);
|
|
39
48
|
return {
|
|
40
49
|
parsable: true,
|
|
41
|
-
parsed_value:
|
|
50
|
+
parsed_value: this.options.decimalDigits
|
|
51
|
+
? parseFloat(
|
|
52
|
+
parseFloat(value.toString()).toFixed(
|
|
53
|
+
this.options.decimalDigits
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
: parseFloat(value.toString()),
|
|
42
57
|
error: null,
|
|
43
58
|
};
|
|
44
59
|
} else {
|
package/src/forms/form.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Context } from "koa";
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { File as SealiousFile } from "sealious";
|
|
2
4
|
import Router from "@koa/router";
|
|
3
5
|
import { FlatTemplatable, tempstream } from "tempstream";
|
|
4
6
|
import {
|
|
@@ -7,7 +9,7 @@ import {
|
|
|
7
9
|
is,
|
|
8
10
|
predicates,
|
|
9
11
|
} from "@sealcode/ts-predicates";
|
|
10
|
-
import {
|
|
12
|
+
import { FormField } from "./fields/field";
|
|
11
13
|
import {
|
|
12
14
|
Fields,
|
|
13
15
|
MountableWithFields,
|
|
@@ -17,7 +19,8 @@ import {
|
|
|
17
19
|
export type FormDataPrimitive = string | string[] | number | undefined;
|
|
18
20
|
export type FormDataValue =
|
|
19
21
|
| FormDataPrimitive
|
|
20
|
-
| Record<string, FormDataPrimitive
|
|
22
|
+
| Record<string, FormDataPrimitive>
|
|
23
|
+
| SealiousFile;
|
|
21
24
|
|
|
22
25
|
export type FieldValueType<F extends FormField> = F extends FormField<infer R>
|
|
23
26
|
? R
|
|
@@ -48,6 +51,7 @@ export abstract class Form<
|
|
|
48
51
|
submitButtonText = "Wyślij";
|
|
49
52
|
action = "./";
|
|
50
53
|
useTurbo = true;
|
|
54
|
+
form_id = randomUUID();
|
|
51
55
|
|
|
52
56
|
async canAccess(
|
|
53
57
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -72,16 +76,22 @@ export abstract class Form<
|
|
|
72
76
|
/>`;
|
|
73
77
|
}
|
|
74
78
|
|
|
75
|
-
async render(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
async render(
|
|
80
|
+
ctx: Context,
|
|
81
|
+
data: FormData,
|
|
82
|
+
show_field_errors: boolean
|
|
83
|
+
): Promise<FlatTemplatable> {
|
|
84
|
+
return tempstream/* HTML */ `<div class="form-container">
|
|
85
|
+
${this.makeOpenFormTag(ctx)}
|
|
86
|
+
${!this.controls.some((control) => control.role == "messages")
|
|
87
|
+
? this.renderMessages(ctx, data)
|
|
88
|
+
: ""}
|
|
89
|
+
${this.renderControls(ctx, data, show_field_errors)}
|
|
90
|
+
${this.controls.some((control) => control.role == "submit")
|
|
91
|
+
? ""
|
|
92
|
+
: this.makeSubmitButton()}
|
|
93
|
+
${this.makeCloseFormTag()}
|
|
94
|
+
</div>`;
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -90,30 +100,37 @@ export abstract class Form<
|
|
|
90
100
|
}
|
|
91
101
|
|
|
92
102
|
public makeOpenFormTag(ctx: Context): FlatTemplatable {
|
|
93
|
-
return tempstream`<form method="POST"
|
|
94
|
-
this.
|
|
95
|
-
}" class="${this.makeFormClasses(ctx).then(
|
|
96
|
-
classes.join(" ")
|
|
97
|
-
)}" ${
|
|
103
|
+
return tempstream`<form enctype="multipart/form-data" method="POST" id="${
|
|
104
|
+
this.form_id
|
|
105
|
+
}" action="${this.action}" class="${this.makeFormClasses(ctx).then(
|
|
106
|
+
(classes) => classes.join(" ")
|
|
107
|
+
)}" ${
|
|
108
|
+
this.useTurbo ? "" : `data-turbo="false"`
|
|
109
|
+
}></form><div class="form">`;
|
|
98
110
|
}
|
|
99
111
|
|
|
100
112
|
public makeCloseFormTag(): FlatTemplatable {
|
|
101
|
-
return `</
|
|
113
|
+
return `</div>`;
|
|
102
114
|
}
|
|
103
115
|
|
|
104
116
|
public async onValuesInvalid(
|
|
105
117
|
ctx: Context,
|
|
106
|
-
form_messages: FormMessage[]
|
|
118
|
+
form_messages: FormMessage[],
|
|
119
|
+
_field_errors: Partial<Record<keyof Fields, string>>
|
|
107
120
|
): Promise<FormReaction> {
|
|
108
121
|
const messages = form_messages.length
|
|
109
122
|
? form_messages
|
|
110
123
|
: [<const>{ type: "error", text: "Some fields are invalid" }];
|
|
111
124
|
return {
|
|
112
125
|
action: "stay",
|
|
113
|
-
content: await this.render(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
126
|
+
content: await this.render(
|
|
127
|
+
ctx,
|
|
128
|
+
{
|
|
129
|
+
raw_values: await this.extractRawValues(ctx),
|
|
130
|
+
messages,
|
|
131
|
+
},
|
|
132
|
+
"show_field_errors" && true
|
|
133
|
+
),
|
|
117
134
|
messages,
|
|
118
135
|
};
|
|
119
136
|
}
|
|
@@ -133,10 +150,14 @@ export abstract class Form<
|
|
|
133
150
|
const messages = [<const>{ type: "error", text: error_message }];
|
|
134
151
|
return {
|
|
135
152
|
action: "stay",
|
|
136
|
-
content: await this.render(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
153
|
+
content: await this.render(
|
|
154
|
+
ctx,
|
|
155
|
+
{
|
|
156
|
+
raw_values: data.raw_values,
|
|
157
|
+
messages: [],
|
|
158
|
+
},
|
|
159
|
+
"show_field_errors" && true
|
|
160
|
+
),
|
|
140
161
|
messages,
|
|
141
162
|
};
|
|
142
163
|
}
|
|
@@ -156,32 +177,41 @@ export abstract class Form<
|
|
|
156
177
|
];
|
|
157
178
|
return {
|
|
158
179
|
action: "stay",
|
|
159
|
-
content: await this.render(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
180
|
+
content: await this.render(
|
|
181
|
+
ctx,
|
|
182
|
+
{
|
|
183
|
+
raw_values: data.raw_values,
|
|
184
|
+
messages,
|
|
185
|
+
},
|
|
186
|
+
"show_field_errors" && false
|
|
187
|
+
),
|
|
163
188
|
messages,
|
|
164
189
|
};
|
|
165
190
|
}
|
|
166
191
|
|
|
192
|
+
async getRawValuesOnSuccess(ctx: Context) {
|
|
193
|
+
return this.extractRawValues(ctx);
|
|
194
|
+
}
|
|
195
|
+
|
|
167
196
|
async handlePost(ctx: Context): Promise<FormReaction> {
|
|
168
|
-
const
|
|
197
|
+
const raw_values = await this.extractRawValues(ctx);
|
|
198
|
+
const { valid, form_messages, field_errors } = await this.validate(
|
|
169
199
|
ctx,
|
|
170
|
-
|
|
200
|
+
raw_values
|
|
171
201
|
);
|
|
172
202
|
if (!valid) {
|
|
173
|
-
return this.onValuesInvalid(ctx, form_messages);
|
|
203
|
+
return this.onValuesInvalid(ctx, form_messages, field_errors);
|
|
174
204
|
}
|
|
175
205
|
try {
|
|
176
206
|
ctx.status = 303;
|
|
177
207
|
const result = await this.onSubmit(ctx, {
|
|
178
|
-
raw_values
|
|
208
|
+
raw_values,
|
|
179
209
|
messages: [],
|
|
180
210
|
});
|
|
181
211
|
return this.onSuccess(
|
|
182
212
|
ctx,
|
|
183
213
|
{
|
|
184
|
-
raw_values: this.
|
|
214
|
+
raw_values: await this.getRawValuesOnSuccess(ctx),
|
|
185
215
|
messages: [],
|
|
186
216
|
},
|
|
187
217
|
result
|
|
@@ -200,7 +230,7 @@ export abstract class Form<
|
|
|
200
230
|
return this.onError(
|
|
201
231
|
ctx,
|
|
202
232
|
{
|
|
203
|
-
raw_values
|
|
233
|
+
raw_values,
|
|
204
234
|
messages: [
|
|
205
235
|
{
|
|
206
236
|
type: "error",
|
|
@@ -228,10 +258,14 @@ export abstract class Form<
|
|
|
228
258
|
});
|
|
229
259
|
router.get(path, async (ctx) => {
|
|
230
260
|
ctx.type = "html";
|
|
231
|
-
ctx.body = await this.render(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
261
|
+
ctx.body = await this.render(
|
|
262
|
+
ctx,
|
|
263
|
+
{
|
|
264
|
+
raw_values: await this.extractRawValues(ctx),
|
|
265
|
+
messages: [],
|
|
266
|
+
},
|
|
267
|
+
"show_field_errors" && false
|
|
268
|
+
);
|
|
235
269
|
});
|
|
236
270
|
router.post(path, async (ctx) => {
|
|
237
271
|
const reaction = await this.handlePost(ctx);
|
|
@@ -243,9 +277,17 @@ export abstract class Form<
|
|
|
243
277
|
ctx.redirect(reaction.url);
|
|
244
278
|
}
|
|
245
279
|
});
|
|
280
|
+
const subrouter = new Router(); // for use with other subroutes that individual controls might register
|
|
281
|
+
for (const control of this.controls) {
|
|
282
|
+
control.mount(subrouter);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
router.use(path, subrouter.routes(), subrouter.allowedMethods());
|
|
246
286
|
}
|
|
247
287
|
|
|
248
|
-
extractRawValues(
|
|
288
|
+
async extractRawValues(
|
|
289
|
+
ctx: Context
|
|
290
|
+
): Promise<Record<string, FormDataValue>> {
|
|
249
291
|
return ctx.$body;
|
|
250
292
|
}
|
|
251
293
|
}
|