@sealcode/sealgen 0.3.1 → 0.4.1

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.
Files changed (111) hide show
  1. package/@types/forms/controls/checkboxed-list-input.d.ts +5 -6
  2. package/@types/forms/controls/dropdown.d.ts +5 -6
  3. package/@types/forms/controls/editable-collection-subset.d.ts +6 -6
  4. package/@types/forms/controls/form-control.d.ts +3 -4
  5. package/@types/forms/controls/form-field-control.d.ts +9 -8
  6. package/@types/forms/controls/frame.d.ts +2 -2
  7. package/@types/forms/controls/simple-input.d.ts +5 -5
  8. package/@types/forms/field.d.ts +47 -16
  9. package/@types/forms/fields/checkboxed-list.d.ts +16 -0
  10. package/@types/forms/fields/collection-field.d.ts +11 -0
  11. package/@types/forms/fields/field.d.ts +38 -0
  12. package/@types/forms/fields/number.d.ts +13 -0
  13. package/@types/forms/fields/pick-from-list.d.ts +16 -0
  14. package/@types/forms/fields/simple-form-field.d.ts +7 -0
  15. package/@types/forms/form-fields-list.d.ts +1 -1
  16. package/@types/forms/form.d.ts +23 -28
  17. package/@types/forms/simple-form-field.d.ts +6 -0
  18. package/@types/forms/validator.d.ts +0 -3
  19. package/@types/index.d.ts +6 -4
  20. package/@types/make-hidden-inputs.d.ts +4 -0
  21. package/@types/page/list.d.ts +23 -42
  22. package/@types/page/mountable.d.ts +36 -0
  23. package/@types/page/page.d.ts +9 -16
  24. package/@types/page/props-parser.d.ts +5 -10
  25. package/@types/templates/page.d.ts +1 -1
  26. package/lib/build.js +12 -3
  27. package/lib/build.js.map +1 -1
  28. package/lib/forms/controls/checkboxed-list-input.js +10 -17
  29. package/lib/forms/controls/checkboxed-list-input.js.map +1 -1
  30. package/lib/forms/controls/dropdown.js +16 -20
  31. package/lib/forms/controls/dropdown.js.map +1 -1
  32. package/lib/forms/controls/editable-collection-subset.js +33 -35
  33. package/lib/forms/controls/editable-collection-subset.js.map +1 -1
  34. package/lib/forms/controls/form-control.js.map +1 -1
  35. package/lib/forms/controls/form-field-control.js +2 -11
  36. package/lib/forms/controls/form-field-control.js.map +1 -1
  37. package/lib/forms/controls/simple-input.js +16 -22
  38. package/lib/forms/controls/simple-input.js.map +1 -1
  39. package/lib/forms/field.js +96 -20
  40. package/lib/forms/field.js.map +1 -1
  41. package/lib/forms/fields/checkboxed-list.js +43 -0
  42. package/lib/forms/fields/checkboxed-list.js.map +1 -0
  43. package/lib/forms/fields/collection-field.js +28 -0
  44. package/lib/forms/fields/collection-field.js.map +1 -0
  45. package/lib/forms/fields/field.js +66 -0
  46. package/lib/forms/fields/field.js.map +1 -0
  47. package/lib/forms/fields/number.js +45 -0
  48. package/lib/forms/fields/number.js.map +1 -0
  49. package/lib/forms/fields/pick-from-list.js +34 -0
  50. package/lib/forms/fields/pick-from-list.js.map +1 -0
  51. package/lib/forms/fields/simple-form-field.js +18 -0
  52. package/lib/forms/fields/simple-form-field.js.map +1 -0
  53. package/lib/forms/form.js +50 -96
  54. package/lib/forms/form.js.map +1 -1
  55. package/lib/forms/simple-form-field.js +18 -0
  56. package/lib/forms/simple-form-field.js.map +1 -0
  57. package/lib/forms/validator.js +0 -10
  58. package/lib/forms/validator.js.map +1 -1
  59. package/lib/generate-routes.js +18 -3
  60. package/lib/generate-routes.js.map +1 -1
  61. package/lib/index.js +4 -3
  62. package/lib/index.js.map +1 -1
  63. package/lib/make-hidden-inputs.js +17 -0
  64. package/lib/make-hidden-inputs.js.map +1 -0
  65. package/lib/page/list.js +64 -79
  66. package/lib/page/list.js.map +1 -1
  67. package/lib/page/mountable.js +56 -0
  68. package/lib/page/mountable.js.map +1 -0
  69. package/lib/page/page.js +11 -1
  70. package/lib/page/page.js.map +1 -1
  71. package/lib/page/props-parser.js +4 -47
  72. package/lib/page/props-parser.js.map +1 -1
  73. package/lib/templates/form.js +38 -18
  74. package/lib/templates/form.js.map +1 -1
  75. package/lib/templates/list.js +30 -20
  76. package/lib/templates/list.js.map +1 -1
  77. package/lib/templates/page.js +5 -4
  78. package/lib/templates/page.js.map +1 -1
  79. package/lib/utils/escape-url-params.js +1 -0
  80. package/lib/utils/escape-url-params.js.map +1 -1
  81. package/package.json +4 -3
  82. package/src/build.ts +14 -3
  83. package/src/forms/controls/checkboxed-list-input.ts +12 -23
  84. package/src/forms/controls/dropdown.ts +27 -32
  85. package/src/forms/controls/editable-collection-subset.ts +57 -54
  86. package/src/forms/controls/form-control.ts +5 -6
  87. package/src/forms/controls/form-field-control.ts +13 -29
  88. package/src/forms/controls/frame.ts +2 -2
  89. package/src/forms/controls/simple-input.ts +26 -23
  90. package/src/forms/fields/checkboxed-list.ts +61 -0
  91. package/src/forms/fields/collection-field.ts +54 -0
  92. package/src/forms/fields/field.ts +106 -0
  93. package/src/forms/fields/number.ts +53 -0
  94. package/src/forms/fields/pick-from-list.ts +49 -0
  95. package/src/forms/fields/simple-form-field.ts +20 -0
  96. package/src/forms/form.ts +97 -162
  97. package/src/generate-routes.ts +18 -7
  98. package/src/index.ts +6 -4
  99. package/src/make-hidden-inputs.ts +22 -0
  100. package/src/page/list.ts +113 -126
  101. package/src/page/mountable.ts +105 -0
  102. package/src/page/page.ts +20 -11
  103. package/src/page/props-parser.ts +9 -67
  104. package/src/templates/form.ts +38 -18
  105. package/src/templates/list.ts +30 -20
  106. package/src/templates/page.ts +6 -6
  107. package/src/utils/escape-url-params.ts +1 -0
  108. package/src/forms/controls.ts +0 -0
  109. package/src/forms/field.ts +0 -119
  110. package/src/forms/form-fields-list.ts +0 -7
  111. 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 { FormControl } from "./controls/controls";
5
- import { hasShape, is, predicates } from "@sealcode/ts-predicates";
6
- import { Mountable, PageErrorMessage } from "../page/page";
7
- import { FormField } from "./field";
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
- values: Record<Fieldnames, string | string[] | number>;
11
- raw_values: Record<Fieldnames, string | string[] | number>;
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<Fieldnames extends string = string>
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 = "Wyslij";
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 validate(
39
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
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 renderMessages(_: BaseContext, data: FormData): Templatable {
100
- return tempstream/* HTML */ `<div class="form-messages">
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
- errors: Record<Fieldnames, string>,
149
- form_path: string
150
- ) {
67
+ form_messages: FormMessage[]
68
+ ): Promise<void> {
151
69
  ctx.status = 422;
152
- const { values, raw_values } = this.generateData(ctx.$body);
153
- ctx.body = await this.render(
154
- ctx,
155
- {
156
- values,
157
- raw_values,
158
- errors,
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(ctx: BaseContext, error: unknown, form_path: string) {
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
- const { values, raw_values } = this.generateData(ctx.$body);
175
- ctx.body = await this.render(
176
- ctx,
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
- values: Record<Fieldnames, string | string[] | number>
100
+ data: FormData
190
101
  ): void | Promise<void>;
191
102
 
192
- public async onSuccess(ctx: BaseContext, form_path: string): Promise<void> {
193
- const { values, raw_values } = this.generateData(ctx.$body);
194
- ctx.body = await this.render(
195
- ctx,
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, data: FormData): Promise<void> {
104
+ ctx.body = await this.render(ctx, {
105
+ raw_values: data.raw_values,
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,55 @@ 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, this.generateData(), path);
126
+ ctx.body = await this.render(ctx, { raw_values: {}, messages: [] });
225
127
  });
226
128
  router.post(path, async (ctx) => {
227
- const { valid, errors } = await this._validate(ctx, ctx.$body);
129
+ const { valid, form_messages } = await this.validate(
130
+ ctx,
131
+ ctx.$body
132
+ );
228
133
  if (!valid) {
229
- await this.onValuesInvalid(ctx, errors, path);
134
+ await this.onValuesInvalid(ctx, form_messages);
230
135
  return;
231
136
  }
232
137
  try {
233
- await this.onSubmit(ctx, this.generateData(ctx.$body).values);
234
- await this.onSuccess(ctx, path);
235
- } catch (e) {
138
+ await this.onSubmit(ctx, {
139
+ raw_values: ctx.$body,
140
+ messages: [],
141
+ });
142
+ await this.onSuccess(ctx, {
143
+ raw_values: ctx.$body,
144
+ messages: [],
145
+ });
146
+ } catch (e: unknown) {
236
147
  // eslint-disable-next-line no-console
237
148
  console.dir(e, { depth: 5 });
238
- await this.onError(ctx, e, path);
149
+ const message =
150
+ is(e, predicates.object) &&
151
+ hasFieldOfType(e, "message", predicates.string)
152
+ ? e?.message
153
+ : is(e, predicates.string)
154
+ ? e
155
+ : "Wystąpił błąd";
156
+
157
+ await this.onError(
158
+ ctx,
159
+ {
160
+ raw_values: ctx.$body,
161
+ messages: [
162
+ {
163
+ type: "error",
164
+ text: message,
165
+ },
166
+ ],
167
+ },
168
+ e
169
+ );
239
170
  }
240
171
  });
241
172
  }
173
+
174
+ extractRawValues(ctx: BaseContext): Record<string, FormDataValue> {
175
+ return ctx.$body;
176
+ }
242
177
  }
@@ -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,25 @@ ${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) {
80
+ router.use(async (ctx, next) => {
81
+ await next();
82
+ if (ctx.body instanceof Promise) {
83
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
84
+ ctx.body = await ctx.body;
85
+ }
86
+ });
71
87
  ${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")}
88
+ .map(({ actionName }) => ` mount(router, ${actionName}URL, ${actionName});`)
89
+ .join("\n")}
79
90
  }
80
91
  `
81
92
  );
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, unknown>;
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 "./forms/validator";
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
+ }