@sealcode/sealgen 0.3.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.
Files changed (101) 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 +5 -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/lib/forms/controls/checkboxed-list-input.js +10 -17
  26. package/lib/forms/controls/checkboxed-list-input.js.map +1 -1
  27. package/lib/forms/controls/dropdown.js +16 -20
  28. package/lib/forms/controls/dropdown.js.map +1 -1
  29. package/lib/forms/controls/editable-collection-subset.js +33 -35
  30. package/lib/forms/controls/editable-collection-subset.js.map +1 -1
  31. package/lib/forms/controls/form-control.js.map +1 -1
  32. package/lib/forms/controls/form-field-control.js +2 -11
  33. package/lib/forms/controls/form-field-control.js.map +1 -1
  34. package/lib/forms/controls/simple-input.js +16 -22
  35. package/lib/forms/controls/simple-input.js.map +1 -1
  36. package/lib/forms/field.js +96 -20
  37. package/lib/forms/field.js.map +1 -1
  38. package/lib/forms/fields/checkboxed-list.js +43 -0
  39. package/lib/forms/fields/checkboxed-list.js.map +1 -0
  40. package/lib/forms/fields/collection-field.js +28 -0
  41. package/lib/forms/fields/collection-field.js.map +1 -0
  42. package/lib/forms/fields/field.js +66 -0
  43. package/lib/forms/fields/field.js.map +1 -0
  44. package/lib/forms/fields/number.js +45 -0
  45. package/lib/forms/fields/number.js.map +1 -0
  46. package/lib/forms/fields/pick-from-list.js +34 -0
  47. package/lib/forms/fields/pick-from-list.js.map +1 -0
  48. package/lib/forms/fields/simple-form-field.js +18 -0
  49. package/lib/forms/fields/simple-form-field.js.map +1 -0
  50. package/lib/forms/form.js +47 -96
  51. package/lib/forms/form.js.map +1 -1
  52. package/lib/forms/simple-form-field.js +18 -0
  53. package/lib/forms/simple-form-field.js.map +1 -0
  54. package/lib/forms/validator.js +0 -10
  55. package/lib/forms/validator.js.map +1 -1
  56. package/lib/generate-routes.js +1 -0
  57. package/lib/generate-routes.js.map +1 -1
  58. package/lib/index.js +3 -3
  59. package/lib/index.js.map +1 -1
  60. package/lib/make-hidden-inputs.js +17 -0
  61. package/lib/make-hidden-inputs.js.map +1 -0
  62. package/lib/page/list.js +64 -79
  63. package/lib/page/list.js.map +1 -1
  64. package/lib/page/mountable.js +56 -0
  65. package/lib/page/mountable.js.map +1 -0
  66. package/lib/page/page.js +11 -1
  67. package/lib/page/page.js.map +1 -1
  68. package/lib/page/props-parser.js +4 -47
  69. package/lib/page/props-parser.js.map +1 -1
  70. package/lib/templates/form.js +32 -17
  71. package/lib/templates/form.js.map +1 -1
  72. package/lib/templates/list.js +30 -20
  73. package/lib/templates/list.js.map +1 -1
  74. package/package.json +3 -3
  75. package/src/forms/controls/checkboxed-list-input.ts +12 -23
  76. package/src/forms/controls/dropdown.ts +27 -32
  77. package/src/forms/controls/editable-collection-subset.ts +57 -54
  78. package/src/forms/controls/form-control.ts +5 -6
  79. package/src/forms/controls/form-field-control.ts +13 -29
  80. package/src/forms/controls/frame.ts +2 -2
  81. package/src/forms/controls/simple-input.ts +26 -23
  82. package/src/forms/fields/checkboxed-list.ts +61 -0
  83. package/src/forms/fields/collection-field.ts +54 -0
  84. package/src/forms/fields/field.ts +106 -0
  85. package/src/forms/fields/number.ts +53 -0
  86. package/src/forms/fields/pick-from-list.ts +49 -0
  87. package/src/forms/fields/simple-form-field.ts +20 -0
  88. package/src/forms/form.ts +94 -162
  89. package/src/generate-routes.ts +1 -0
  90. package/src/index.ts +5 -4
  91. package/src/make-hidden-inputs.ts +22 -0
  92. package/src/page/list.ts +113 -126
  93. package/src/page/mountable.ts +105 -0
  94. package/src/page/page.ts +20 -11
  95. package/src/page/props-parser.ts +9 -67
  96. package/src/templates/form.ts +32 -17
  97. package/src/templates/list.ts +30 -20
  98. package/src/forms/controls.ts +0 -0
  99. package/src/forms/field.ts +0 -119
  100. package/src/forms/form-fields-list.ts +0 -7
  101. package/src/forms/validator.ts +0 -10
@@ -1,48 +1,37 @@
1
1
  import { is, predicates } from "@sealcode/ts-predicates";
2
2
  import { BaseContext } from "koa";
3
3
  import { tempstream } from "tempstream";
4
- import { ChekboxedListField, FormField } from "../field";
5
- import { Form, FormData } from "../form";
6
- import { FormFieldsList } from "../form-fields-list";
4
+ import { ChekboxedListField } from "../fields/field";
5
+ import { FormDataValue } from "../form";
7
6
  import { FormFieldControl } from "./controls";
8
7
 
9
8
  export class CheckboxedListInput extends FormFieldControl {
10
9
  constructor(
11
- public fieldname: string,
12
- public options: { label: string } = { label: fieldname }
10
+ public field: ChekboxedListField,
11
+ public options: { label: string } = { label: field.name }
13
12
  ) {
14
- super([fieldname]);
13
+ super([field]);
15
14
  }
16
15
 
17
- isValidFieldName(form: Form) {
18
- return form.fields.some((f) => f.name == this.fieldname);
19
- }
20
-
21
- async _render(ctx: BaseContext, fields: FormField[], data: FormData) {
22
- // safe to disable this as isValidFieldName takes care of checking if the field is of this type
23
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
24
- const field = FormFieldsList.getField(
25
- fields,
26
- this.fieldname
27
- ) as ChekboxedListField;
28
- const pickedValues = data.values[field.name] || "";
16
+ async render(ctx: BaseContext, data: Record<string, FormDataValue>) {
17
+ const { parsed: pickedValues } = await this.field.getValue(ctx, data);
29
18
  if (!is(pickedValues, predicates.array(predicates.string))) {
30
19
  throw new Error("picked values is not an array of strings");
31
20
  }
32
21
  const [options, isVisible] = await Promise.all([
33
- field.generateOptions(ctx),
34
- field.isVisible(ctx),
22
+ this.field.generateOptions(ctx),
23
+ this.field.isVisible(ctx),
35
24
  ]);
36
25
  return tempstream/* HTML */ `${isVisible
37
26
  ? Object.entries(options).map(
38
27
  ([value, text]) => /* HTML */ `<div>
39
28
  <input
40
29
  type="checkbox"
41
- id="${field.name}.${value}"
42
- name="${field.name}.${value}"
30
+ id="${this.field.name}.${value}"
31
+ name="${this.field.name}.${value}"
43
32
  ${pickedValues.includes(value) ? "checked" : ""}
44
33
  />
45
- <label for="${field.name}.${value}">${text}</label>
34
+ <label for="${this.field.name}.${value}">${text}</label>
46
35
  </div>`
47
36
  )
48
37
  : ""}`;
@@ -1,56 +1,51 @@
1
1
  import { BaseContext } from "koa";
2
2
  import { tempstream } from "tempstream";
3
- import { FormField, PickFromListField } from "../field";
4
- import { FormData } from "../form";
5
- import { FormFieldsList } from "../form-fields-list";
3
+ import { FormField, PickFromListField } from "../fields/field";
4
+ import { FormDataValue } from "../form";
6
5
  import { FormFieldControl } from "./controls";
7
6
 
8
7
  export class Dropdown extends FormFieldControl {
9
8
  constructor(
10
- public fieldname: string,
9
+ public field: PickFromListField,
11
10
  public options: {
12
11
  label: string;
13
12
  autosubmit?: boolean;
14
13
  autocomplete?: boolean;
15
14
  } = {
16
- label: fieldname,
15
+ label: field.name,
17
16
  autosubmit: false,
18
17
  autocomplete: true,
19
18
  }
20
19
  ) {
21
- super([fieldname]);
20
+ super([field]);
22
21
  }
23
22
 
24
- areFieldNamesValid(fields: FormField[]) {
25
- return (
26
- super.areFieldNamesValid(fields) &&
27
- FormFieldsList.getField(fields, this.fieldname) instanceof PickFromListField
28
- );
29
- }
30
-
31
- _render(ctx: BaseContext, fields: FormField[], data: FormData) {
32
- // safe to disable this as isValidFieldName takes care of checking if the field is of this type
33
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
34
- const field = FormFieldsList.getField(
35
- fields,
36
- this.fieldname
37
- ) as PickFromListField;
38
- const picked_value = data.values[field.name] || "";
39
- const id = field.name;
40
- return tempstream/* HTML */ `<label for="${id}">${this.options.label}</label
23
+ async render<Fields extends Record<string, FormField>>(
24
+ ctx: BaseContext,
25
+ data: Record<string, FormDataValue>
26
+ ) {
27
+ const { parsed: picked_value } = await this.field.getValue(ctx, data);
28
+ const id = this.field.name;
29
+ return tempstream/* HTML */ `<label for="${id}"
30
+ >${this.options.label}</label
41
31
  ><select
42
- name="${this.fieldnames}"
32
+ name="${this.field.name}"
43
33
  id="${id}"
44
- ${this.options.autosubmit ? `onchange='this.form.submit()'` : ""}
34
+ ${this.options.autosubmit
35
+ ? `onchange='this.form.submit()'`
36
+ : ""}
45
37
  ${!this.options.autocomplete ? `autocomplete="off"` : ""}
46
38
  >
47
- ${Promise.resolve(field.generateOptions(ctx)).then((options) =>
48
- Object.entries(options).map(
49
- ([value, text]) =>
50
- `<option value="${value}" ${
51
- (value || "") == picked_value ? "selected" : ""
52
- }>${text}</option>`
53
- )
39
+ ${Promise.resolve(this.field.generateOptions(ctx)).then(
40
+ (options) =>
41
+ Object.entries(options).map(
42
+ ([value, text]) =>
43
+ `<option value="${value}" ${
44
+ (value || "") == picked_value
45
+ ? "selected"
46
+ : ""
47
+ }>${text}</option>`
48
+ )
54
49
  )}
55
50
  </select>`;
56
51
  }
@@ -1,8 +1,7 @@
1
1
  import { BaseContext } from "koa";
2
- import { Templatable, tempstream } from "tempstream";
3
- import { FormField, PickFromListField } from "../field";
4
- import { FormData } from "../form";
5
- import { FormFieldsList } from "../form-fields-list";
2
+ import { FlatTemplatable, tempstream } from "tempstream";
3
+ import { PickFromListField } from "../fields/field";
4
+ import { FormDataValue } from "../form";
6
5
  import { FormFieldControl } from "./controls";
7
6
 
8
7
  /**
@@ -14,69 +13,73 @@ import { FormFieldControl } from "./controls";
14
13
  */
15
14
  export class EditableCollectionSubset extends FormFieldControl {
16
15
  constructor(
17
- public fieldname: string,
16
+ public field: PickFromListField,
18
17
  public actionname: string,
19
18
  public listLabel?: string,
20
19
  public selectLabel?: string
21
20
  ) {
22
- super([fieldname]);
21
+ super([field]);
23
22
  }
24
23
 
25
- _render(
24
+ async render(
26
25
  ctx: BaseContext,
27
- fields: FormField[],
28
- data: FormData<string>
29
- ): Templatable | Promise<Templatable> {
30
- // safe to disable this as isValidFieldName takes care of checking if the field is of this type
31
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
32
- const field = FormFieldsList.getField(
33
- fields,
34
- this.fieldname
35
- ) as PickFromListField;
36
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
37
- const values = data.values[this.fieldname] as string[];
26
+ data: Record<string, FormDataValue>
27
+ ): Promise<FlatTemplatable> {
28
+ const { parsed: values } = await this.field.getValue(ctx, data);
38
29
 
39
30
  return tempstream/* HTML */ `<div>
40
31
  <ul>
41
- ${Promise.resolve(field.generateOptions(ctx)).then((options) =>
42
- Object.entries(options)
43
- .filter(([value]) => values.includes(value))
44
- .map(
45
- ([value, text]) => /* HTML */ `
46
- <li>
47
- <form method="POST" action="${ctx.path}">
48
- <span>${text}</span>
49
- <input
50
- type="hidden"
51
- name="${this.fieldname}"
52
- value="${value}"
53
- />
54
- <input
55
- type="hidden"
56
- name="${this.actionname}"
57
- value="list"
58
- />
59
- <input
60
- type="submit"
61
- ${this.listLabel
62
- ? `value="${this.listLabel}"`
63
- : ""}
64
- />
65
- </form>
66
- </li>
67
- `
68
- )
69
- )}
70
- </ul>
71
- <form method="POST" action="${ctx.path}">
72
- <select name="${this.fieldname}">
73
- ${Promise.resolve(field.generateOptions(ctx)).then((options) =>
32
+ ${Promise.resolve(this.field.generateOptions(ctx)).then(
33
+ (options) =>
74
34
  Object.entries(options)
75
- .filter(([value]) => !values.includes(value))
35
+ .filter(([value]) =>
36
+ (values || ([] as string[])).includes(value)
37
+ )
76
38
  .map(
77
- ([value, text]) =>
78
- `<option value="${value}">${text}</option>`
39
+ ([value, text]) => /* HTML */ `
40
+ <li>
41
+ <form
42
+ method="POST"
43
+ action="${ctx.path}"
44
+ >
45
+ <span>${text}</span>
46
+ <input
47
+ type="hidden"
48
+ name="${this.field.name}"
49
+ value="${value}"
50
+ />
51
+ <input
52
+ type="hidden"
53
+ name="${this.actionname}"
54
+ value="list"
55
+ />
56
+ <input
57
+ type="submit"
58
+ ${this.listLabel
59
+ ? `value="${this.listLabel}"`
60
+ : ""}
61
+ />
62
+ </form>
63
+ </li>
64
+ `
79
65
  )
66
+ )}
67
+ </ul>
68
+ <form method="POST" action="${ctx.path}">
69
+ <select name="${this.field.name}">
70
+ ${Promise.resolve(this.field.generateOptions(ctx)).then(
71
+ (options) =>
72
+ Object.entries(options)
73
+ .filter(
74
+ ([value]) =>
75
+ !(values || ([] as string[])).includes(
76
+ value
77
+ )
78
+ )
79
+ .map(
80
+ ([value, text]) =>
81
+ `<option value="${value}">${text}</option>`
82
+ )
80
83
  )}
81
84
  </select>
82
85
  <input type="hidden" name="${this.actionname}" value="select" />
@@ -1,13 +1,12 @@
1
1
  import { BaseContext } from "koa";
2
- import { Templatable } from "tempstream";
3
- import { FormField } from "../field";
4
- import { FormData } from "../form";
2
+ import { FlatTemplatable } from "tempstream";
3
+ import { FormDataValue, FormMessage } from "../form";
5
4
 
6
5
  export abstract class FormControl {
7
6
  abstract render(
8
7
  ctx: BaseContext,
9
- formFields: FormField[],
10
- data: FormData
11
- ): Templatable | Promise<Templatable>;
8
+ data: Record<string, FormDataValue>,
9
+ messages: FormMessage[]
10
+ ): FlatTemplatable | Promise<FlatTemplatable>;
12
11
  abstract role: "input" | "decoration" | "messages" | "submit";
13
12
  }
@@ -1,39 +1,23 @@
1
1
  import { BaseContext } from "koa";
2
- import { Templatable } from "tempstream";
3
- import { FormField } from "../field";
4
- import { FormData } from "../form";
2
+ import { FlatTemplatable } from "tempstream";
3
+ import { FormField } from "../fields/field";
4
+ import { FieldValueType, FormDataValue } from "../form";
5
5
  import { FormControl } from "./form-control";
6
6
 
7
+ export type FormFieldsToTypes<FormFields extends readonly FormField[]> = {
8
+ [Index in keyof FormFields]: FormFields[Index] extends FormField
9
+ ? FieldValueType<FormFields[Index]>
10
+ : never;
11
+ };
12
+
7
13
  export abstract class FormFieldControl extends FormControl {
8
14
  role = <const>"input";
9
- constructor(public fieldnames: string[]) {
15
+ constructor(public fields: FormField[]) {
10
16
  super();
11
17
  }
12
18
 
13
- areFieldNamesValid(fields: FormField[]) {
14
- return this.fieldnames.every((fieldname) =>
15
- fields.some((f) => f.name == fieldname)
16
- );
17
- }
18
-
19
- abstract _render(
19
+ abstract render(
20
20
  ctx: BaseContext,
21
- fields: FormField[],
22
- data: FormData
23
- ): Templatable | Promise<Templatable>;
24
-
25
- render(
26
- ctx: BaseContext,
27
- fields: FormField[],
28
- data: FormData
29
- ): Templatable | Promise<Templatable> {
30
- if (!this.areFieldNamesValid(fields)) {
31
- throw new Error(
32
- `Invalid field names given to form control: "${this.fieldnames.join(
33
- ", "
34
- )}". Allowed fields are: ${fields.map((f) => f.name).join(", ")}`
35
- );
36
- }
37
- return this._render(ctx, fields, data);
38
- }
21
+ data: Record<string, FormDataValue>
22
+ ): FlatTemplatable | Promise<FlatTemplatable>;
39
23
  }
@@ -1,4 +1,4 @@
1
- import { Templatable } from "tempstream";
1
+ import { FlatTemplatable, Templatable } from "tempstream";
2
2
  import { FormControl } from "./controls";
3
3
 
4
4
  /**
@@ -15,7 +15,7 @@ export class Frame extends FormControl {
15
15
  super();
16
16
  }
17
17
 
18
- render(): Templatable | Promise<Templatable> {
18
+ render(): FlatTemplatable | Promise<FlatTemplatable> {
19
19
  return /* HTML */ `<turbo-frame
20
20
  id="contrahents"
21
21
  loading="lazy"
@@ -1,12 +1,13 @@
1
1
  import { BaseContext } from "koa";
2
- import { FormField } from "../field";
3
- import { FormData } from "../form";
4
- import { FormFieldsList } from "../form-fields-list";
2
+ import { FormField } from "../fields/field";
3
+ import { NumberField } from "../fields/number";
4
+ import { SimpleFormField } from "../fields/simple-form-field";
5
+ import { FormDataValue } from "../form";
5
6
  import { FormFieldControl } from "./controls";
6
7
 
7
8
  export class SimpleInput extends FormFieldControl {
8
9
  constructor(
9
- public fieldname: string,
10
+ public field: FormField,
10
11
  public options: {
11
12
  id?: string;
12
13
  label?: string;
@@ -31,43 +32,45 @@ export class SimpleInput extends FormFieldControl {
31
32
  step?: number;
32
33
  } = {}
33
34
  ) {
34
- super([fieldname]);
35
+ super([field]);
35
36
  }
36
37
 
37
- _render(_: BaseContext, fields: FormField[], data: FormData) {
38
- const field = FormFieldsList.getField(fields, this.fieldname);
39
- if (!field) {
40
- throw new Error("wrong field name");
41
- }
42
- const id = this.options.id || field.name;
43
- const label = this.options.label || field.name;
38
+ async render<Fields extends Record<string, FormField>>(
39
+ ctx: BaseContext,
40
+ data: Record<string, FormDataValue>
41
+ ) {
42
+ const id = this.options.id || this.field.name;
43
+ const label = this.options.label || this.field.name;
44
44
  const type = this.options.type || "text";
45
45
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
46
- const value = data.values[field.name] as string;
46
+ const { parsed, raw, valid, message } = await this.field.getValue(
47
+ ctx,
48
+ data
49
+ );
47
50
  // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
48
- const raw_value = data.raw_values[field.name] as string;
49
51
  const placeholder = this.options.placeholder || type;
50
52
  const readonly = this.options.readonly || false;
51
- const required = field.required;
52
- const error = data.errors[field.name];
53
+ const required = this.field.required;
54
+
53
55
  return /* HTML */ `<div class="input">
54
56
  <label for="${id}">${label}</label>
55
57
  <input
56
58
  id="${id}"
57
59
  type="${type}"
58
- name="${field.name}"
59
- value="${value === undefined
60
- ? raw_value == undefined
61
- ? ""
62
- : raw_value
63
- : value}"
60
+ name="${this.field.name}"
61
+ value="${!valid && raw
62
+ ? raw.toString()
63
+ : parsed !== undefined && parsed !== null
64
+ ? // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
65
+ ((parsed as any)?.toString() as string) || ""
66
+ : ""}"
64
67
  placeholder="${placeholder}"
65
68
  ${readonly ? "readonly" : ""}
66
69
  ${required ? "required" : ""}
67
70
  ${!this.options.autocomplete ? `autocomplete="off"` : ""}
68
71
  ${this.options.step ? `step="${this.options.step}"` : ""}
69
72
  />
70
- ${error ? `<div class="input__error">${error}</div>` : ""}
73
+ ${~valid ? `<div class="input__error">${message}</div>` : ""}
71
74
  </div>`;
72
75
  }
73
76
  }
@@ -0,0 +1,61 @@
1
+ import { is, predicates } from "@sealcode/ts-predicates";
2
+ import { BaseContext } from "koa";
3
+ import { FormDataValue } from "../form";
4
+ import {
5
+ FieldParseResult,
6
+ FormField,
7
+ FormFieldValidationResponse,
8
+ } from "./field";
9
+
10
+ export class ChekboxedListField extends FormField<Record<string, "on">> {
11
+ constructor(
12
+ public required: boolean = false,
13
+ public generateOptions: (
14
+ ctx: BaseContext
15
+ ) => Promise<Record<string, string> | { [i: string]: string }>,
16
+ public isVisible: (ctx: BaseContext) => Promise<boolean> = () =>
17
+ Promise.resolve(true)
18
+ ) {
19
+ super(required);
20
+ }
21
+
22
+ public async isValueValid(
23
+ _: BaseContext,
24
+ value: unknown
25
+ ): Promise<FormFieldValidationResponse> {
26
+ if (is(value, predicates.string)) {
27
+ return { valid: false, message: "you need an array" };
28
+ }
29
+ if (is(value, predicates.null)) {
30
+ return { valid: false, message: "you need an array" };
31
+ }
32
+ return { valid: true, message: "" };
33
+ }
34
+
35
+ async parse(
36
+ _: BaseContext,
37
+ raw_value: FormDataValue
38
+ ): Promise<FieldParseResult<Record<string, "on">>> {
39
+ if (is(raw_value, predicates.object)) {
40
+ return {
41
+ parsable: true,
42
+ parsed_value: Object.fromEntries(
43
+ Object.entries(raw_value).filter(
44
+ ([, value]) => value === "on"
45
+ )
46
+ ) as Record<string, "on">,
47
+ error: null,
48
+ };
49
+ } else {
50
+ return {
51
+ parsed_value: null,
52
+ parsable: false,
53
+ error: "Expected an object",
54
+ };
55
+ }
56
+ }
57
+
58
+ public getEmptyValue(): Record<string, "on"> {
59
+ return {};
60
+ }
61
+ }
@@ -0,0 +1,54 @@
1
+ import { BaseContext } from "koa";
2
+ import { Depromisify, Field, FieldOutput } from "sealious";
3
+ import { FormDataValue } from "../form";
4
+
5
+ import {
6
+ FieldParseResult,
7
+ FormField,
8
+ FormFieldValidationResponse,
9
+ } from "./field";
10
+
11
+ export class CollectionField<F extends Field> extends FormField<
12
+ FieldOutput<F>
13
+ > {
14
+ constructor(required: boolean, public sealiousField: F) {
15
+ super(required);
16
+ }
17
+
18
+ async parse(
19
+ ctx: BaseContext,
20
+ raw_value: FormDataValue
21
+ ): Promise<FieldParseResult<FieldOutput<F>>> {
22
+ const encoded_value = (await this.sealiousField.encode(
23
+ ctx.$context,
24
+ raw_value
25
+ )) as Depromisify<ReturnType<F["encode"]>>;
26
+ const parsed_value = (await this.sealiousField.decode(
27
+ ctx.$context,
28
+ encoded_value,
29
+ null,
30
+ {}
31
+ )) as Depromisify<ReturnType<F["encode"]>>;
32
+ return {
33
+ parsable: true,
34
+ error: null,
35
+ parsed_value,
36
+ };
37
+ }
38
+
39
+ public async isValueValid(
40
+ ctx: BaseContext,
41
+ value: FieldOutput<F>
42
+ ): Promise<FormFieldValidationResponse> {
43
+ const { valid, reason } = await this.sealiousField.checkValue(
44
+ ctx.$context,
45
+ value,
46
+ undefined
47
+ );
48
+ return { valid, message: reason || (!valid ? "Wrong value" : "") };
49
+ }
50
+
51
+ public getEmptyValue(): Depromisify<ReturnType<F["decode"]>> {
52
+ return "" as unknown as Depromisify<ReturnType<F["decode"]>>;
53
+ }
54
+ }
@@ -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";