Page MenuHomeSealhub

mountable-with-fields.ts
No OneTemporary

mountable-with-fields.ts

import { ShapeToType } from "@sealcode/ts-predicates";
import { Context } from "koa";
import { FlatTemplatable, tempstream } from "tempstream";
import {
FormControl,
FormControlContext,
} from "../forms/controls/form-control.js";
import type { FieldsToShape, FormField } from "../forms/fields/field.js";
import type {
FormDataValue,
FormMessage,
FormData,
} from "../forms/form-types.js";
import { Mountable } from "./mountable.js";
export type PageErrorMessage = { type: "access" | "internal"; message: string };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type Fields = Record<string, FormField<boolean, any>>;
type Resolved<T> = T extends Promise<infer X> ? X : never;
type ParsedValue<T extends FormField> = Resolved<
ReturnType<T["getValue"]>
>["parsed"];
export abstract class MountableWithFields<
F extends Fields = Fields
> extends Mountable {
fields: F;
field_names_prefix = ""; // useful for multiform, where many forms are merged into one and field assignment is made using the prefix
form_id = ""; // all fields within this mountable will be tied to form of this id
abstract controls: FormControl[];
constructor() {
super();
if (!this.fields) this.fields = {} as F;
}
init(): void {
for (const [name, field] of Object.entries(this.fields)) {
void field.init(name);
}
}
makeFormControlContext(
ctx: Context,
data: FormData,
validate: boolean,
field_name_prefix = this.field_names_prefix,
form_id = this.form_id
) {
return new FormControlContext(
ctx,
data.raw_values,
data.messages,
field_name_prefix,
form_id,
validate
);
}
// this one is meant to be overwritten
async validateValues(
_ctx: Context,
_data: Record<string, FormDataValue>
): Promise<{ valid: boolean; error: string }> {
return {
valid: true,
error: "",
};
}
async getInitialValues(
_ctx: Context
): Promise<Record<string, FormDataValue>> {
return {};
}
async validate(
ctx: Context,
values: Record<string, FormDataValue>
): Promise<{
valid: boolean;
field_errors: Partial<Record<keyof Fields, string>>;
form_messages: FormMessage[];
}> {
const field_errors = {} as Record<keyof Fields, string>;
let valid = true;
const form_messages = [] as FormMessage[];
await Promise.all(
Object.keys(this.fields).map(async (key: keyof F) => {
const field = this.fields[key];
const { valid: fieldvalid, message: fieldmessage } =
await field.getValue(ctx, values, true);
if (!fieldvalid) {
valid = false;
field_errors[field.name] = fieldmessage;
}
})
);
const formValidationResult = await this.validateValues(ctx, values);
if (!formValidationResult.valid) {
form_messages.push({
type: "error",
text: formValidationResult.error,
});
valid = false;
}
return { valid, field_errors, form_messages };
}
public renderControls(fctx: FormControlContext): FlatTemplatable {
return tempstream/* HTML */ `${this.controls.map((control) =>
control.render(fctx)
)}`;
}
async renderError(
_: Context,
error: PageErrorMessage
): Promise<FlatTemplatable> {
return error.message;
}
public renderMessages(
_: Context,
data: FormData<keyof Fields>
): FlatTemplatable {
return tempstream/* HTML */ `<div class="form-messages">
${data.messages.map(
(message) =>
`<div class="form-message form-message--${message.type}">${message.text}</div>`
)}
</div>`;
}
abstract extractRawValues(
ctx: Context
): Promise<Record<string, FormDataValue>>;
async getParsedValues(ctx: Context): Promise<{
[field in keyof F]: F[field] extends FormField<true>
? Exclude<ParsedValue<F[field]>, null>
: ParsedValue<F[field]>;
}> {
const raw_values = await this.extractRawValues(ctx);
const result: Record<string, unknown> = {};
const promises = Object.entries(this.fields).map(
async ([key, field]) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { parsed } = await field.getValue(ctx, raw_values, false);
result[key] = parsed;
}
);
await Promise.all(promises);
// TODO: remove this any. I don't have the strenght to deal with it now.
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return
return result as any;
}
async getDatabaseValues(
ctx: Context
): Promise<ShapeToType<FieldsToShape<F>>> {
const raw_values = await this.extractRawValues(ctx);
const result: Record<string, unknown> = {};
const promises = Object.entries(this.fields).map(
async ([key, field]) => {
const db_value = await field.getDatabaseValue(ctx, raw_values);
if (db_value !== undefined) {
result[key] = db_value;
}
}
);
await Promise.all(promises);
return result as ShapeToType<FieldsToShape<F>>;
}
}

File Metadata

Mime Type
text/x-java
Expires
Fri, Jan 24, 15:15 (1 d, 9 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
600355
Default Alt Text
mountable-with-fields.ts (4 KB)

Event Timeline