@sealcode/sealgen 0.2.2 → 0.4.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/@types/forms/controls/checkboxed-list-input.d.ts +5 -6
- package/@types/forms/controls/dropdown.d.ts +5 -6
- package/@types/forms/controls/editable-collection-subset.d.ts +6 -6
- package/@types/forms/controls/form-control.d.ts +3 -4
- package/@types/forms/controls/form-field-control.d.ts +9 -8
- package/@types/forms/controls/frame.d.ts +2 -2
- package/@types/forms/controls/simple-input.d.ts +5 -5
- package/@types/forms/field.d.ts +47 -16
- package/@types/forms/fields/checkboxed-list.d.ts +16 -0
- package/@types/forms/fields/collection-field.d.ts +11 -0
- package/@types/forms/fields/field.d.ts +38 -0
- package/@types/forms/fields/number.d.ts +13 -0
- package/@types/forms/fields/pick-from-list.d.ts +16 -0
- package/@types/forms/fields/simple-form-field.d.ts +7 -0
- package/@types/forms/form-fields-list.d.ts +1 -1
- package/@types/forms/form.d.ts +23 -28
- package/@types/forms/simple-form-field.d.ts +6 -0
- package/@types/forms/validator.d.ts +0 -3
- package/@types/index.d.ts +6 -4
- package/@types/make-hidden-inputs.d.ts +4 -0
- package/@types/page/list.d.ts +23 -42
- package/@types/page/mountable.d.ts +36 -0
- package/@types/page/page.d.ts +9 -16
- package/@types/page/props-parser.d.ts +5 -10
- package/@types/templates/page.d.ts +1 -1
- package/lib/build.js +11 -3
- package/lib/build.js.map +1 -1
- package/lib/forms/controls/checkboxed-list-input.js +10 -17
- package/lib/forms/controls/checkboxed-list-input.js.map +1 -1
- package/lib/forms/controls/dropdown.js +16 -20
- package/lib/forms/controls/dropdown.js.map +1 -1
- package/lib/forms/controls/editable-collection-subset.js +33 -35
- package/lib/forms/controls/editable-collection-subset.js.map +1 -1
- package/lib/forms/controls/form-control.js.map +1 -1
- package/lib/forms/controls/form-field-control.js +2 -11
- package/lib/forms/controls/form-field-control.js.map +1 -1
- package/lib/forms/controls/simple-input.js +16 -22
- package/lib/forms/controls/simple-input.js.map +1 -1
- package/lib/forms/field.js +96 -20
- package/lib/forms/field.js.map +1 -1
- package/lib/forms/fields/checkboxed-list.js +43 -0
- package/lib/forms/fields/checkboxed-list.js.map +1 -0
- package/lib/forms/fields/collection-field.js +28 -0
- package/lib/forms/fields/collection-field.js.map +1 -0
- package/lib/forms/fields/field.js +66 -0
- package/lib/forms/fields/field.js.map +1 -0
- package/lib/forms/fields/number.js +45 -0
- package/lib/forms/fields/number.js.map +1 -0
- package/lib/forms/fields/pick-from-list.js +34 -0
- package/lib/forms/fields/pick-from-list.js.map +1 -0
- package/lib/forms/fields/simple-form-field.js +18 -0
- package/lib/forms/fields/simple-form-field.js.map +1 -0
- package/lib/forms/form.js +47 -96
- package/lib/forms/form.js.map +1 -1
- package/lib/forms/simple-form-field.js +18 -0
- package/lib/forms/simple-form-field.js.map +1 -0
- package/lib/forms/validator.js +0 -10
- package/lib/forms/validator.js.map +1 -1
- package/lib/generate-routes.js +11 -3
- package/lib/generate-routes.js.map +1 -1
- package/lib/index.js +4 -3
- package/lib/index.js.map +1 -1
- package/lib/make-hidden-inputs.js +17 -0
- package/lib/make-hidden-inputs.js.map +1 -0
- package/lib/page/list.js +64 -79
- package/lib/page/list.js.map +1 -1
- package/lib/page/mountable.js +56 -0
- package/lib/page/mountable.js.map +1 -0
- package/lib/page/page.js +11 -1
- package/lib/page/page.js.map +1 -1
- package/lib/page/props-parser.js +4 -47
- package/lib/page/props-parser.js.map +1 -1
- package/lib/templates/form.js +38 -18
- package/lib/templates/form.js.map +1 -1
- package/lib/templates/list.js +30 -20
- package/lib/templates/list.js.map +1 -1
- package/lib/templates/page.js +5 -4
- package/lib/templates/page.js.map +1 -1
- package/lib/utils/escape-url-params.js +1 -0
- package/lib/utils/escape-url-params.js.map +1 -1
- package/package.json +3 -3
- package/src/build.ts +13 -3
- package/src/forms/controls/checkboxed-list-input.ts +12 -23
- package/src/forms/controls/dropdown.ts +27 -32
- package/src/forms/controls/editable-collection-subset.ts +57 -54
- package/src/forms/controls/form-control.ts +5 -6
- package/src/forms/controls/form-field-control.ts +13 -29
- package/src/forms/controls/frame.ts +2 -2
- package/src/forms/controls/simple-input.ts +26 -23
- package/src/forms/fields/checkboxed-list.ts +61 -0
- package/src/forms/fields/collection-field.ts +54 -0
- package/src/forms/fields/field.ts +106 -0
- package/src/forms/fields/number.ts +53 -0
- package/src/forms/fields/pick-from-list.ts +49 -0
- package/src/forms/fields/simple-form-field.ts +20 -0
- package/src/forms/form.ts +94 -162
- package/src/generate-routes.ts +11 -7
- package/src/index.ts +6 -4
- package/src/make-hidden-inputs.ts +22 -0
- package/src/page/list.ts +113 -126
- package/src/page/mountable.ts +105 -0
- package/src/page/page.ts +20 -11
- package/src/page/props-parser.ts +9 -67
- package/src/templates/form.ts +38 -18
- package/src/templates/list.ts +30 -20
- package/src/templates/page.ts +6 -6
- package/src/utils/escape-url-params.ts +1 -0
- package/.log/ti-101379.log +0 -524
- package/.log/ti-131883.log +0 -505
- package/.log/ti-13490.log +0 -652
- package/.log/ti-138140.log +0 -505
- package/.log/ti-138288.log +0 -526
- package/.log/ti-292609.log +0 -508
- package/.log/ti-304933.log +0 -524
- package/.log/ti-312312.log +0 -508
- package/.log/ti-314240.log +0 -1617
- package/.log/ti-46589.log +0 -675
- package/.log/ti-62627.log +0 -508
- package/.log/ti-64785.log +0 -508
- package/.log/ti-65911.log +0 -533
- package/.log/ti-69885.log +0 -548
- package/.log/ti-72106.log +0 -551
- package/.log/ti-74858.log +0 -508
- package/.log/tsserver.log +0 -1651
- package/src/forms/controls.ts +0 -0
- package/src/forms/field.ts +0 -119
- package/src/forms/form-fields-list.ts +0 -7
- package/src/forms/validator.ts +0 -10
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { BaseContext } from "koa";
|
|
2
|
+
import { FormDataValue } from "../form";
|
|
3
|
+
|
|
4
|
+
export type FormFieldValidationResponse = { valid: boolean; message: string };
|
|
5
|
+
|
|
6
|
+
export type FormFieldValidationFn<ValueType> = (
|
|
7
|
+
ctx: BaseContext,
|
|
8
|
+
value: ValueType,
|
|
9
|
+
field: FormField<ValueType>
|
|
10
|
+
) => Promise<FormFieldValidationResponse>;
|
|
11
|
+
|
|
12
|
+
export type FieldParsedValue<T> = T extends FormField<infer R> ? R : never;
|
|
13
|
+
|
|
14
|
+
export type FieldParseResult<T> =
|
|
15
|
+
| {
|
|
16
|
+
parsable: false;
|
|
17
|
+
error: string;
|
|
18
|
+
parsed_value: null;
|
|
19
|
+
}
|
|
20
|
+
| { parsable: true; error: null; parsed_value: T };
|
|
21
|
+
|
|
22
|
+
export abstract class FormField<ParsedValue extends unknown = unknown> {
|
|
23
|
+
name: string;
|
|
24
|
+
constructor(public required: boolean = false) {}
|
|
25
|
+
|
|
26
|
+
init(fieldname: string): void {
|
|
27
|
+
this.name = fieldname;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public async _validate(
|
|
31
|
+
ctx: BaseContext,
|
|
32
|
+
value: ParsedValue
|
|
33
|
+
): Promise<FormFieldValidationResponse> {
|
|
34
|
+
if (
|
|
35
|
+
this.required &&
|
|
36
|
+
(value === "" || value === null || value === undefined)
|
|
37
|
+
) {
|
|
38
|
+
return { valid: false, message: "This field is required" };
|
|
39
|
+
}
|
|
40
|
+
return this.isValueValid(ctx, value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public abstract getEmptyValue(): ParsedValue;
|
|
44
|
+
|
|
45
|
+
public async isValueValid(
|
|
46
|
+
ctx: BaseContext,
|
|
47
|
+
value: ParsedValue
|
|
48
|
+
): Promise<FormFieldValidationResponse> {
|
|
49
|
+
return { valid: true, message: "" };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
abstract parse(
|
|
53
|
+
ctx: BaseContext,
|
|
54
|
+
raw_value: FormDataValue
|
|
55
|
+
): Promise<FieldParseResult<ParsedValue>>;
|
|
56
|
+
|
|
57
|
+
async getValue(
|
|
58
|
+
ctx: BaseContext,
|
|
59
|
+
all_form_values: Record<string, FormDataValue>
|
|
60
|
+
): Promise<{
|
|
61
|
+
valid: boolean;
|
|
62
|
+
message: string;
|
|
63
|
+
parsed: ParsedValue;
|
|
64
|
+
raw: FormDataValue;
|
|
65
|
+
}> {
|
|
66
|
+
if (Object.keys(all_form_values).length === 0) {
|
|
67
|
+
// don't validate on virgin form
|
|
68
|
+
return {
|
|
69
|
+
parsed: this.getEmptyValue(),
|
|
70
|
+
valid: true,
|
|
71
|
+
message: "",
|
|
72
|
+
raw: "",
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const raw = all_form_values[this.name];
|
|
76
|
+
const {
|
|
77
|
+
parsable,
|
|
78
|
+
parsed_value,
|
|
79
|
+
error: parse_error,
|
|
80
|
+
} = await this.parse(ctx, raw);
|
|
81
|
+
if (!parsable) {
|
|
82
|
+
return {
|
|
83
|
+
valid: false,
|
|
84
|
+
parsed: this.getEmptyValue(),
|
|
85
|
+
raw,
|
|
86
|
+
message: parse_error || "Error",
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
const { valid, message } = await this._validate(
|
|
90
|
+
ctx,
|
|
91
|
+
parsed_value as ParsedValue
|
|
92
|
+
);
|
|
93
|
+
return {
|
|
94
|
+
parsed: parsed_value as ParsedValue,
|
|
95
|
+
valid,
|
|
96
|
+
message,
|
|
97
|
+
raw: all_form_values[this.name],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export * from "./simple-form-field";
|
|
103
|
+
export * from "./checkboxed-list";
|
|
104
|
+
export * from "./collection-field";
|
|
105
|
+
export * from "./number";
|
|
106
|
+
export * from "./pick-from-list";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { is, predicates } from "@sealcode/ts-predicates";
|
|
2
|
+
import { BaseContext } from "koa";
|
|
3
|
+
import { FormDataValue } from "../form";
|
|
4
|
+
import { FieldParseResult, FormField } from "./field";
|
|
5
|
+
|
|
6
|
+
export class NumberField<
|
|
7
|
+
DefaultValue extends number | null = null
|
|
8
|
+
> extends FormField<number | DefaultValue> {
|
|
9
|
+
constructor(required = false, public default_value: DefaultValue) {
|
|
10
|
+
super(required);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public async isValueValid(_: BaseContext, value: unknown) {
|
|
14
|
+
if (
|
|
15
|
+
(is(value, predicates.string) &&
|
|
16
|
+
!isNaN(parseFloat(value)) &&
|
|
17
|
+
parseFloat(value).toString() == value.trim()) ||
|
|
18
|
+
is(value, predicates.number) ||
|
|
19
|
+
((is(value, predicates.undefined) || value == "") && !this.required)
|
|
20
|
+
) {
|
|
21
|
+
return { valid: true, message: "" };
|
|
22
|
+
}
|
|
23
|
+
return { valid: false, message: "Proszę wprowadzić liczbę" };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async parse(
|
|
27
|
+
_: BaseContext,
|
|
28
|
+
value: FormDataValue
|
|
29
|
+
): Promise<FieldParseResult<number>> {
|
|
30
|
+
if (
|
|
31
|
+
(is(value, predicates.string) &&
|
|
32
|
+
!isNaN(parseFloat(value)) &&
|
|
33
|
+
parseFloat(value).toString() == value.trim()) ||
|
|
34
|
+
is(value, predicates.number)
|
|
35
|
+
) {
|
|
36
|
+
return {
|
|
37
|
+
parsable: true,
|
|
38
|
+
parsed_value: parseFloat(value.toString()),
|
|
39
|
+
error: null,
|
|
40
|
+
};
|
|
41
|
+
} else {
|
|
42
|
+
return {
|
|
43
|
+
parsable: false,
|
|
44
|
+
parsed_value: null,
|
|
45
|
+
error: "Please enter a number",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public getEmptyValue(): DefaultValue {
|
|
51
|
+
return this.default_value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { is, predicates } from "@sealcode/ts-predicates";
|
|
2
|
+
import { BaseContext } from "koa";
|
|
3
|
+
import { FormFieldValidationResponse } from "./field";
|
|
4
|
+
import { SimpleFormField } from "./simple-form-field";
|
|
5
|
+
|
|
6
|
+
export class PickFromListField extends SimpleFormField {
|
|
7
|
+
constructor(
|
|
8
|
+
public required: boolean = false,
|
|
9
|
+
public generateOptions: (
|
|
10
|
+
ctx: BaseContext
|
|
11
|
+
) => Promise<Record<string, string> | { [i: string]: string }>,
|
|
12
|
+
public customValidation: (
|
|
13
|
+
ctx: BaseContext,
|
|
14
|
+
value: unknown,
|
|
15
|
+
instance: PickFromListField
|
|
16
|
+
) => Promise<FormFieldValidationResponse> = (ctx, value, instance) =>
|
|
17
|
+
instance.valueInList(ctx, value)
|
|
18
|
+
) {
|
|
19
|
+
super(required);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public isValueValid(
|
|
23
|
+
ctx: BaseContext,
|
|
24
|
+
value: string
|
|
25
|
+
): Promise<FormFieldValidationResponse> {
|
|
26
|
+
return this.customValidation(ctx, value, this);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async valueInList(
|
|
30
|
+
ctx: BaseContext,
|
|
31
|
+
value: unknown
|
|
32
|
+
): Promise<FormFieldValidationResponse> {
|
|
33
|
+
const options = await this.generateOptions(ctx);
|
|
34
|
+
if (!is(value, predicates.string)) {
|
|
35
|
+
return { valid: false, message: "not a string" };
|
|
36
|
+
}
|
|
37
|
+
if (!Object.keys(options).includes(value)) {
|
|
38
|
+
return {
|
|
39
|
+
valid: false,
|
|
40
|
+
message: `"${value}" is not one of the options`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return { valid: true, message: "" };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public getEmptyValue(): string {
|
|
47
|
+
return "";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { FieldParseResult, FormField } from "./field";
|
|
2
|
+
import { FormDataValue } from "../form";
|
|
3
|
+
import { BaseContext } from "koa";
|
|
4
|
+
|
|
5
|
+
export class SimpleFormField extends FormField<string | null> {
|
|
6
|
+
async parse(
|
|
7
|
+
_: BaseContext,
|
|
8
|
+
raw_value: FormDataValue
|
|
9
|
+
): Promise<FieldParseResult<string>> {
|
|
10
|
+
return {
|
|
11
|
+
parsed_value: (raw_value || "").toString(),
|
|
12
|
+
parsable: true,
|
|
13
|
+
error: null,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
public getEmptyValue(): string {
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
}
|
package/src/forms/form.ts
CHANGED
|
@@ -1,25 +1,38 @@
|
|
|
1
1
|
import { BaseContext } from "koa";
|
|
2
2
|
import Router from "@koa/router";
|
|
3
|
-
import { Templatable, tempstream } from "tempstream";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import { FlatTemplatable, Templatable, tempstream } from "tempstream";
|
|
4
|
+
import {
|
|
5
|
+
hasFieldOfType,
|
|
6
|
+
hasShape,
|
|
7
|
+
is,
|
|
8
|
+
predicates,
|
|
9
|
+
} from "@sealcode/ts-predicates";
|
|
10
|
+
import { FormField } from "./fields/field";
|
|
11
|
+
import { Fields, Mountable, PageErrorMessage } from "../page/mountable";
|
|
12
|
+
|
|
13
|
+
export type FormDataPrimitive = string | string[] | undefined;
|
|
14
|
+
export type FormDataValue =
|
|
15
|
+
| FormDataPrimitive
|
|
16
|
+
| Record<string, FormDataPrimitive>;
|
|
17
|
+
|
|
18
|
+
export type FieldValueType<F extends FormField> = F extends FormField<infer R>
|
|
19
|
+
? R
|
|
20
|
+
: never;
|
|
21
|
+
|
|
22
|
+
export type FormFieldsToValues<F extends Record<string, FormField<keyof F>>> = {
|
|
23
|
+
[Property in keyof F]: FieldValueType<F[Property]>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type FormMessage = { type: "info" | "success" | "error"; text: string };
|
|
8
27
|
|
|
9
28
|
export type FormData<Fieldnames extends string = string> = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
errors: Partial<Record<Fieldnames, string>>;
|
|
13
|
-
messages: { type: "info" | "success" | "error"; text: string }[];
|
|
29
|
+
raw_values: Record<Fieldnames, FormDataValue>;
|
|
30
|
+
messages: FormMessage[];
|
|
14
31
|
};
|
|
15
32
|
|
|
16
|
-
export abstract class Form<
|
|
17
|
-
implements Mountable
|
|
18
|
-
{
|
|
19
|
-
abstract fields: FormField<Fieldnames>[];
|
|
20
|
-
abstract controls: FormControl[];
|
|
33
|
+
export abstract class Form<F extends Fields> extends Mountable<F> {
|
|
21
34
|
defaultSuccessMessage = "Done";
|
|
22
|
-
submitButtonText = "
|
|
35
|
+
submitButtonText = "Wyślij";
|
|
23
36
|
|
|
24
37
|
async canAccess(
|
|
25
38
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -35,134 +48,39 @@ export abstract class Form<Fieldnames extends string = string>
|
|
|
35
48
|
return tempstream/* HTML */ `<div>${error.message}</div>`;
|
|
36
49
|
}
|
|
37
50
|
|
|
38
|
-
async
|
|
39
|
-
|
|
40
|
-
_: BaseContext,
|
|
41
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
42
|
-
__: Record<string, unknown>
|
|
43
|
-
): Promise<{ valid: boolean; error: string }> {
|
|
44
|
-
return {
|
|
45
|
-
valid: true,
|
|
46
|
-
error: "",
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
private async _validate(
|
|
51
|
-
ctx: BaseContext,
|
|
52
|
-
values: Record<string, unknown>
|
|
53
|
-
): Promise<{
|
|
54
|
-
valid: boolean;
|
|
55
|
-
errors: Record<Fieldnames | "form", string>;
|
|
56
|
-
}> {
|
|
57
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
58
|
-
const errors = {} as Record<Fieldnames | "form", string>;
|
|
59
|
-
let valid = true;
|
|
60
|
-
await Promise.all(
|
|
61
|
-
this.fields.map(async (field) => {
|
|
62
|
-
const { valid: fieldvalid, message: fieldmessage } =
|
|
63
|
-
await field._validate(ctx, values[field.name]);
|
|
64
|
-
if (!fieldvalid) {
|
|
65
|
-
valid = false;
|
|
66
|
-
errors[field.name] = fieldmessage;
|
|
67
|
-
}
|
|
68
|
-
})
|
|
69
|
-
);
|
|
70
|
-
const formValidationResult = await this.validate(ctx, values);
|
|
71
|
-
if (!formValidationResult.valid) {
|
|
72
|
-
valid = false;
|
|
73
|
-
errors.form = formValidationResult.error;
|
|
74
|
-
}
|
|
75
|
-
return { valid, errors };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async render(
|
|
79
|
-
ctx: BaseContext,
|
|
80
|
-
data: FormData,
|
|
81
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
82
|
-
_: string
|
|
83
|
-
): Promise<Templatable> {
|
|
84
|
-
return tempstream/* HTML */ `${this.makeFormTag(
|
|
85
|
-
`${ctx.URL.pathname}/`
|
|
86
|
-
)} ${
|
|
51
|
+
async render(ctx: BaseContext, data: FormData): Promise<Templatable> {
|
|
52
|
+
return tempstream/* HTML */ `${this.makeFormTag()} ${
|
|
87
53
|
!this.controls.some((control) => control.role == "messages")
|
|
88
54
|
? this.renderMessages(ctx, data)
|
|
89
55
|
: ""
|
|
90
|
-
} ${
|
|
91
|
-
data.errors.form !== undefined
|
|
92
|
-
? `<div class="form__error">${data.errors.form}</div>`
|
|
93
|
-
: ""
|
|
94
56
|
} ${this.renderControls(ctx, data)}<input type="submit" value="${
|
|
95
57
|
this.submitButtonText
|
|
96
58
|
}"/></form>`;
|
|
97
59
|
}
|
|
98
60
|
|
|
99
|
-
public
|
|
100
|
-
return
|
|
101
|
-
${data.messages.map(
|
|
102
|
-
(message) =>
|
|
103
|
-
`<div class="form-message form-message--${message.type}">${message.text}</div>`
|
|
104
|
-
)}
|
|
105
|
-
</div>`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
public renderControls(ctx: BaseContext, data: FormData): Templatable {
|
|
109
|
-
return tempstream/* HTML */ `${this.controls.map((control) =>
|
|
110
|
-
control.render(ctx, this.fields, data)
|
|
111
|
-
)}`;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
public makeFormTag(path: string) {
|
|
115
|
-
return `<form method="POST" action="${path}">`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
private generateData(rawData: Record<string, unknown> = {}): FormData {
|
|
119
|
-
// generates a FormData object that has the correct shape to be passed to
|
|
120
|
-
// render(), so for example it makes sure that all fields either have values or
|
|
121
|
-
// are empty string (the aren't undefined, for example). If no argument is passed,
|
|
122
|
-
// creates an object that represents an empty state of the form. If some data
|
|
123
|
-
// object is passed in the first argument, then the values in that data object are
|
|
124
|
-
// incorporated into the generated object
|
|
125
|
-
|
|
126
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
127
|
-
const values = Object.fromEntries(
|
|
128
|
-
this.fields.map((f) => [
|
|
129
|
-
f.name,
|
|
130
|
-
rawData[f.name] || f.getEmptyValue(),
|
|
131
|
-
])
|
|
132
|
-
) as Record<Fieldnames, string>;
|
|
133
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
134
|
-
const errors = Object.fromEntries(
|
|
135
|
-
this.fields.map((f) => [f.name, ""])
|
|
136
|
-
) as Record<Fieldnames, string>;
|
|
137
|
-
return {
|
|
138
|
-
values,
|
|
139
|
-
errors,
|
|
140
|
-
messages: [],
|
|
141
|
-
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
142
|
-
raw_values: rawData as Record<string, string>,
|
|
143
|
-
};
|
|
61
|
+
public makeFormTag(): FlatTemplatable {
|
|
62
|
+
return `<form method="POST" action="./">`;
|
|
144
63
|
}
|
|
145
64
|
|
|
146
65
|
public async onValuesInvalid(
|
|
147
66
|
ctx: BaseContext,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
) {
|
|
67
|
+
form_messages: FormMessage[]
|
|
68
|
+
): Promise<void> {
|
|
151
69
|
ctx.status = 422;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
messages: [{ type: "error", text: "Some fields are invalid" }],
|
|
160
|
-
},
|
|
161
|
-
form_path
|
|
162
|
-
);
|
|
70
|
+
ctx.body = await this.render(ctx, {
|
|
71
|
+
raw_values: ctx.$body,
|
|
72
|
+
|
|
73
|
+
messages: form_messages.length
|
|
74
|
+
? form_messages
|
|
75
|
+
: [{ type: "error", text: "Some fields are invalid" }],
|
|
76
|
+
});
|
|
163
77
|
}
|
|
164
78
|
|
|
165
|
-
public async onError(
|
|
79
|
+
public async onError(
|
|
80
|
+
ctx: BaseContext,
|
|
81
|
+
data: FormData,
|
|
82
|
+
error: unknown
|
|
83
|
+
): Promise<void> {
|
|
166
84
|
ctx.status = 422;
|
|
167
85
|
let error_message = "Unknown error has occured";
|
|
168
86
|
if (
|
|
@@ -171,42 +89,26 @@ export abstract class Form<Fieldnames extends string = string>
|
|
|
171
89
|
) {
|
|
172
90
|
error_message = error.message;
|
|
173
91
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
values,
|
|
179
|
-
raw_values,
|
|
180
|
-
errors: {},
|
|
181
|
-
messages: [{ type: "error", text: error_message }],
|
|
182
|
-
},
|
|
183
|
-
form_path
|
|
184
|
-
);
|
|
92
|
+
ctx.body = await this.render(ctx, {
|
|
93
|
+
raw_values: data.raw_values,
|
|
94
|
+
messages: [{ type: "error", text: error_message }],
|
|
95
|
+
});
|
|
185
96
|
}
|
|
186
97
|
|
|
187
98
|
public abstract onSubmit(
|
|
188
99
|
ctx: BaseContext,
|
|
189
|
-
|
|
100
|
+
data: FormData
|
|
190
101
|
): void | Promise<void>;
|
|
191
102
|
|
|
192
|
-
public async onSuccess(ctx: BaseContext
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
values,
|
|
198
|
-
raw_values,
|
|
199
|
-
errors: {},
|
|
200
|
-
messages: [
|
|
201
|
-
{ type: "success", text: this.defaultSuccessMessage },
|
|
202
|
-
],
|
|
203
|
-
},
|
|
204
|
-
form_path
|
|
205
|
-
);
|
|
103
|
+
public async onSuccess(ctx: BaseContext): Promise<void> {
|
|
104
|
+
ctx.body = await this.render(ctx, {
|
|
105
|
+
raw_values: ctx.$body,
|
|
106
|
+
messages: [{ type: "success", text: this.defaultSuccessMessage }],
|
|
107
|
+
});
|
|
206
108
|
ctx.status = 422;
|
|
207
109
|
}
|
|
208
110
|
|
|
209
|
-
public mount(router: Router, path: string) {
|
|
111
|
+
public mount(router: Router, path: string): void {
|
|
210
112
|
router.use(path, async (ctx, next) => {
|
|
211
113
|
const result = await this.canAccess(ctx);
|
|
212
114
|
if (!result.canAccess) {
|
|
@@ -221,22 +123,52 @@ export abstract class Form<Fieldnames extends string = string>
|
|
|
221
123
|
});
|
|
222
124
|
router.get(path, async (ctx) => {
|
|
223
125
|
ctx.type = "html";
|
|
224
|
-
ctx.body = await this.render(ctx,
|
|
126
|
+
ctx.body = await this.render(ctx, { raw_values: {}, messages: [] });
|
|
225
127
|
});
|
|
226
128
|
router.post(path, async (ctx) => {
|
|
227
|
-
const { valid,
|
|
129
|
+
const { valid, form_messages } = await this.validate(
|
|
130
|
+
ctx,
|
|
131
|
+
ctx.$body
|
|
132
|
+
);
|
|
228
133
|
if (!valid) {
|
|
229
|
-
await this.onValuesInvalid(ctx,
|
|
134
|
+
await this.onValuesInvalid(ctx, form_messages);
|
|
230
135
|
return;
|
|
231
136
|
}
|
|
232
137
|
try {
|
|
233
|
-
await this.onSubmit(ctx,
|
|
234
|
-
|
|
235
|
-
|
|
138
|
+
await this.onSubmit(ctx, {
|
|
139
|
+
raw_values: ctx.$body,
|
|
140
|
+
messages: [],
|
|
141
|
+
});
|
|
142
|
+
await this.onSuccess(ctx);
|
|
143
|
+
} catch (e: unknown) {
|
|
236
144
|
// eslint-disable-next-line no-console
|
|
237
145
|
console.dir(e, { depth: 5 });
|
|
238
|
-
|
|
146
|
+
const message =
|
|
147
|
+
is(e, predicates.object) &&
|
|
148
|
+
hasFieldOfType(e, "message", predicates.string)
|
|
149
|
+
? e?.message
|
|
150
|
+
: is(e, predicates.string)
|
|
151
|
+
? e
|
|
152
|
+
: "Wystąpił błąd";
|
|
153
|
+
|
|
154
|
+
await this.onError(
|
|
155
|
+
ctx,
|
|
156
|
+
{
|
|
157
|
+
raw_values: ctx.$body,
|
|
158
|
+
messages: [
|
|
159
|
+
{
|
|
160
|
+
type: "error",
|
|
161
|
+
text: message,
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
e
|
|
166
|
+
);
|
|
239
167
|
}
|
|
240
168
|
});
|
|
241
169
|
}
|
|
170
|
+
|
|
171
|
+
extractRawValues(ctx: BaseContext): Record<string, FormDataValue> {
|
|
172
|
+
return ctx.$body;
|
|
173
|
+
}
|
|
242
174
|
}
|
package/src/generate-routes.ts
CHANGED
|
@@ -52,6 +52,7 @@ export async function generateRoutes(): Promise<void> {
|
|
|
52
52
|
|
|
53
53
|
import Router from "@koa/router";
|
|
54
54
|
import { Middlewares } from "sealious";
|
|
55
|
+
import { Mountable } from "@sealcode/sealgen";
|
|
55
56
|
|
|
56
57
|
${files
|
|
57
58
|
.map(
|
|
@@ -67,15 +68,18 @@ ${files
|
|
|
67
68
|
.map(({ actionName, url }) => `export const ${actionName}URL = "${url}";`)
|
|
68
69
|
.join("\n")}
|
|
69
70
|
|
|
71
|
+
function mount(router: Router, URL: string, mountable: Mountable){
|
|
72
|
+
router.use(URL, Middlewares.extractContext(), Middlewares.parseBody());
|
|
73
|
+
mountable.init();
|
|
74
|
+
mountable.mount(router, URL);
|
|
75
|
+
// to automatically add trailing slashes:
|
|
76
|
+
router.get(URL.slice(0, -1), (ctx) => ctx.redirect(URL));
|
|
77
|
+
}
|
|
78
|
+
|
|
70
79
|
export default function mountAutoRoutes(router: Router) {
|
|
71
80
|
${files
|
|
72
|
-
.map(
|
|
73
|
-
|
|
74
|
-
actionName,
|
|
75
|
-
}) => ` router.use(${actionName}URL, Middlewares.extractContext(), Middlewares.parseBody());
|
|
76
|
-
${actionName}.mount(router, ${actionName}URL);`
|
|
77
|
-
)
|
|
78
|
-
.join("\n\n")}
|
|
81
|
+
.map(({ actionName }) => ` mount(router, ${actionName}URL, ${actionName});`)
|
|
82
|
+
.join("\n")}
|
|
79
83
|
}
|
|
80
84
|
`
|
|
81
85
|
);
|
package/src/index.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import Sealious from "sealious";
|
|
2
|
+
import { FormDataValue } from "./forms/form";
|
|
2
3
|
|
|
3
4
|
declare module "koa" {
|
|
4
5
|
interface BaseContext {
|
|
5
6
|
$context: Sealious.Context;
|
|
6
|
-
$body: Record<string,
|
|
7
|
+
$body: Record<string, FormDataValue>;
|
|
7
8
|
}
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
export * from "./forms/form";
|
|
11
11
|
export * from "./forms/controls/controls";
|
|
12
|
-
export * from "./forms/field";
|
|
12
|
+
export * from "./forms/fields/field";
|
|
13
|
+
export * from "./page/mountable";
|
|
13
14
|
export * from "./page/page";
|
|
15
|
+
export * from "./forms/form";
|
|
14
16
|
export * from "./page/props-parser";
|
|
15
17
|
export * from "./utils/utils";
|
|
16
|
-
export * from "./
|
|
18
|
+
export * from "./page/list";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { BaseContext } from "koa";
|
|
2
|
+
import { Templatable, tempstream } from "tempstream";
|
|
3
|
+
import { FormDataValue, FormField } from ".";
|
|
4
|
+
|
|
5
|
+
export function makeHiddenInputs(
|
|
6
|
+
ctx: BaseContext,
|
|
7
|
+
fields: Record<string, FormField>,
|
|
8
|
+
values: Record<string, FormDataValue>,
|
|
9
|
+
fields_to_skip: string[]
|
|
10
|
+
): Templatable {
|
|
11
|
+
return tempstream`${Object.entries(fields)
|
|
12
|
+
.filter(([key]) => !fields_to_skip.includes(key))
|
|
13
|
+
.map(
|
|
14
|
+
([key, field]) => tempstream/* HTML */ `<input
|
|
15
|
+
type="hidden"
|
|
16
|
+
name="${key}"
|
|
17
|
+
value="${field
|
|
18
|
+
.getValue(ctx, values)
|
|
19
|
+
.then((r) => JSON.stringify(r.raw))}"
|
|
20
|
+
/>`
|
|
21
|
+
)}`;
|
|
22
|
+
}
|