@sealcode/sealgen 0.8.3 → 0.8.6

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 (107) hide show
  1. package/@types/forms/controls/autocomplete.d.ts +1 -1
  2. package/@types/forms/controls/checkbox-group.d.ts +3 -2
  3. package/@types/forms/controls/collapsible-checkbox-group.d.ts +1 -1
  4. package/@types/forms/controls/collapsible-field-group.d.ts +1 -1
  5. package/@types/forms/controls/controls.d.ts +3 -0
  6. package/@types/forms/controls/dropdown.d.ts +6 -5
  7. package/@types/forms/controls/field-group.d.ts +3 -3
  8. package/@types/forms/controls/file.d.ts +14 -0
  9. package/@types/forms/controls/form-control.d.ts +4 -1
  10. package/@types/forms/controls/form-field-control.d.ts +1 -1
  11. package/@types/forms/controls/multiple-files.d.ts +31 -0
  12. package/@types/forms/controls/number.d.ts +17 -0
  13. package/@types/forms/controls/simple-input.d.ts +20 -27
  14. package/@types/forms/controls/textarea.d.ts +9 -7
  15. package/@types/forms/fields/field.d.ts +4 -1
  16. package/@types/forms/fields/file.d.ts +16 -0
  17. package/@types/forms/fields/files-multiple.d.ts +12 -0
  18. package/@types/forms/fields/multiple-files.d.ts +14 -0
  19. package/@types/forms/fields/number.d.ts +10 -1
  20. package/@types/forms/form.d.ts +7 -4
  21. package/@types/forms/multiform.d.ts +3 -3
  22. package/@types/page/list.d.ts +1 -1
  23. package/@types/page/mountable-with-fields.d.ts +3 -2
  24. package/@types/page/page.d.ts +1 -1
  25. package/@types/sanitize.d.ts +1 -1
  26. package/lib/forms/controls/autocomplete.js.map +1 -1
  27. package/lib/forms/controls/checkbox-group.js +11 -5
  28. package/lib/forms/controls/checkbox-group.js.map +1 -1
  29. package/lib/forms/controls/collapsible-checkbox-group.js +2 -2
  30. package/lib/forms/controls/collapsible-checkbox-group.js.map +1 -1
  31. package/lib/forms/controls/collapsible-field-group.js.map +1 -1
  32. package/lib/forms/controls/controls.js +3 -0
  33. package/lib/forms/controls/controls.js.map +1 -1
  34. package/lib/forms/controls/derived.js +12 -11
  35. package/lib/forms/controls/derived.js.map +1 -1
  36. package/lib/forms/controls/dropdown.js +25 -18
  37. package/lib/forms/controls/dropdown.js.map +1 -1
  38. package/lib/forms/controls/field-group.js +3 -3
  39. package/lib/forms/controls/field-group.js.map +1 -1
  40. package/lib/forms/controls/file.js +38 -0
  41. package/lib/forms/controls/file.js.map +1 -0
  42. package/lib/forms/controls/form-control.js +1 -0
  43. package/lib/forms/controls/form-control.js.map +1 -1
  44. package/lib/forms/controls/form-field-control.js.map +1 -1
  45. package/lib/forms/controls/multiple-files.js +137 -0
  46. package/lib/forms/controls/multiple-files.js.map +1 -0
  47. package/lib/forms/controls/number.js +34 -0
  48. package/lib/forms/controls/number.js.map +1 -0
  49. package/lib/forms/controls/simple-input.js +20 -10
  50. package/lib/forms/controls/simple-input.js.map +1 -1
  51. package/lib/forms/controls/textarea.js +13 -22
  52. package/lib/forms/controls/textarea.js.map +1 -1
  53. package/lib/forms/fields/enum-multiple.js +4 -1
  54. package/lib/forms/fields/enum-multiple.js.map +1 -1
  55. package/lib/forms/fields/field.js +8 -1
  56. package/lib/forms/fields/field.js.map +1 -1
  57. package/lib/forms/fields/file.js +58 -0
  58. package/lib/forms/fields/file.js.map +1 -0
  59. package/lib/forms/fields/files-multiple.js +18 -0
  60. package/lib/forms/fields/files-multiple.js.map +1 -0
  61. package/lib/forms/fields/multiple-files.js +19 -0
  62. package/lib/forms/fields/multiple-files.js.map +1 -0
  63. package/lib/forms/fields/number.js +6 -2
  64. package/lib/forms/fields/number.js.map +1 -1
  65. package/lib/forms/form.js +38 -21
  66. package/lib/forms/form.js.map +1 -1
  67. package/lib/forms/multiform.js +8 -8
  68. package/lib/forms/multiform.js.map +1 -1
  69. package/lib/page/list.js +9 -6
  70. package/lib/page/list.js.map +1 -1
  71. package/lib/page/mountable-with-fields.js +16 -3
  72. package/lib/page/mountable-with-fields.js.map +1 -1
  73. package/lib/page/page.js +1 -1
  74. package/lib/page/page.js.map +1 -1
  75. package/lib/sanitize.js +3 -0
  76. package/lib/sanitize.js.map +1 -1
  77. package/lib/tests/manual/multiform.js +13 -13
  78. package/lib/tests/manual/multiform.js.map +1 -1
  79. package/package.json +2 -2
  80. package/src/forms/controls/autocomplete.ts +1 -5
  81. package/src/forms/controls/checkbox-group.ts +17 -6
  82. package/src/forms/controls/collapsible-checkbox-group.ts +10 -2
  83. package/src/forms/controls/collapsible-field-group.ts +2 -4
  84. package/src/forms/controls/controls.ts +3 -0
  85. package/src/forms/controls/derived.ts +12 -11
  86. package/src/forms/controls/dropdown.ts +51 -34
  87. package/src/forms/controls/field-group.ts +6 -4
  88. package/src/forms/controls/file.ts +50 -0
  89. package/src/forms/controls/form-control.ts +5 -1
  90. package/src/forms/controls/form-field-control.ts +2 -1
  91. package/src/forms/controls/multiple-files.ts +213 -0
  92. package/src/forms/controls/number.ts +52 -0
  93. package/src/forms/controls/simple-input.ts +67 -37
  94. package/src/forms/controls/textarea.ts +25 -24
  95. package/src/forms/fields/enum-multiple.ts +3 -1
  96. package/src/forms/fields/field.ts +12 -2
  97. package/src/forms/fields/file.ts +86 -0
  98. package/src/forms/fields/multiple-files.ts +20 -0
  99. package/src/forms/fields/number.ts +17 -2
  100. package/src/forms/form.ts +84 -42
  101. package/src/forms/forms.scss +4 -1
  102. package/src/forms/multiform.ts +26 -13
  103. package/src/page/list.ts +10 -7
  104. package/src/page/mountable-with-fields.ts +26 -4
  105. package/src/page/page.ts +3 -1
  106. package/src/sanitize.ts +4 -1
  107. 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 class SimpleInput extends FormFieldControl {
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
- public options: {
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 | true>> {
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 | true>) {
102
+ renderAttributes(attrs: Record<string, string | boolean>) {
89
103
  return Object.entries(attrs)
90
104
  .map(([key, value]) => {
91
- if (value === true) {
92
- return key;
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
- <label for="${id}">${label}</label>
125
- <input
126
- ${this.renderAttributes(
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,37 @@ 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 FormFieldControl {
10
+ export class Textarea<Required extends boolean> extends SimpleInput<
11
+ string | null
12
+ > {
10
13
  constructor(
11
- public field: FormField,
12
- public options: { label?: string; rows?: number; cols?: number } = {}
14
+ public field: FormField<boolean, string>,
15
+ public options: SimpleInputOptions & {
16
+ label?: string;
17
+ rows?: number;
18
+ cols?: number;
19
+ description?: string;
20
+ } = {}
13
21
  ) {
14
- super([field]);
22
+ super(field, options);
15
23
  }
16
24
 
17
- async render(
25
+ renderDescription() {
26
+ return this.options.description
27
+ ? /* HTML */ `<div class="form-input__description">
28
+ ${this.options.description}
29
+ </div>`
30
+ : "";
31
+ }
32
+
33
+ async renderInput(
18
34
  ctx: Context,
35
+ attributes: string,
19
36
  data: Record<string, FormDataValue>
20
- ): Promise<FlatTemplatable> {
21
- const { raw } = await this.field.getValue(ctx, data);
22
- const { name, required } = this.field;
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
- );
37
+ ) {
38
+ const { parsed: value } = await this.field.getValue(ctx, data);
39
+ return /* HTML */ `<textarea ${attributes}>${value}</textarea>`;
39
40
  }
40
41
  }
@@ -37,7 +37,9 @@ export class EnumMultipleField<
37
37
  _: Context,
38
38
  value: FormDataValue
39
39
  ): Promise<FieldParseResult<string[]>> {
40
- if (is(value, predicates.array(predicates.string))) {
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(required: Required, public default_value: DefaultValue) {
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: parseFloat(value.toString()),
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 { FieldsToShape, FormField } from "./fields/field";
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(ctx: Context, data: FormData): Promise<FlatTemplatable> {
76
- return tempstream/* HTML */ `${this.makeOpenFormTag(ctx)}
77
- ${!this.controls.some((control) => control.role == "messages")
78
- ? this.renderMessages(ctx, data)
79
- : ""}
80
- ${this.renderControls(ctx, data)}
81
- ${this.controls.some((control) => control.role == "submit")
82
- ? ""
83
- : this.makeSubmitButton()}
84
- ${this.makeCloseFormTag()}`;
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" action="${
94
- this.action
95
- }" class="${this.makeFormClasses(ctx).then((classes) =>
96
- classes.join(" ")
97
- )}" ${this.useTurbo ? "" : `data-turbo="false"`}>`;
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 `</form>`;
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(ctx, {
114
- raw_values: this.extractRawValues(ctx),
115
- messages,
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(ctx, {
137
- raw_values: data.raw_values,
138
- messages: [],
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(ctx, {
160
- raw_values: data.raw_values,
161
- messages,
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 { valid, form_messages } = await this.validate(
197
+ const raw_values = await this.extractRawValues(ctx);
198
+ const { valid, form_messages, field_errors } = await this.validate(
169
199
  ctx,
170
- this.extractRawValues(ctx)
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: this.extractRawValues(ctx),
208
+ raw_values,
179
209
  messages: [],
180
210
  });
181
211
  return this.onSuccess(
182
212
  ctx,
183
213
  {
184
- raw_values: this.extractRawValues(ctx),
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: this.extractRawValues(ctx),
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(ctx, {
232
- raw_values: this.extractRawValues(ctx),
233
- messages: [],
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(ctx: Context): Record<string, FormDataValue> {
288
+ async extractRawValues(
289
+ ctx: Context
290
+ ): Promise<Record<string, FormDataValue>> {
249
291
  return ctx.$body;
250
292
  }
251
293
  }
@@ -22,8 +22,11 @@
22
22
  flex-flow: row nowrap;
23
23
  align-items: center;
24
24
  & > input[type="checkbox"] {
25
- margin-right: 10px;
26
25
  width: 15px;
27
26
  height: 15px;
27
+
28
+ + label {
29
+ padding-left: 10px;
30
+ }
28
31
  }
29
32
  }