Page MenuHomeSealhub

form.ts
No OneTemporary

import { Context } from "koa";
import Router from "@koa/router";
import { FlatTemplatable, Templatable, tempstream } from "tempstream";
import {
hasFieldOfType,
hasShape,
is,
predicates,
} from "@sealcode/ts-predicates";
import { FormField } from "./fields/field";
import { Fields, Mountable, PageErrorMessage } from "../page/mountable";
export type FormDataPrimitive = string | string[] | undefined;
export type FormDataValue =
| FormDataPrimitive
| Record<string, FormDataPrimitive>;
export type FieldValueType<F extends FormField> = F extends FormField<infer R>
? R
: never;
export type FormFieldsToValues<F extends Record<string, FormField<keyof F>>> = {
[Property in keyof F]: FieldValueType<F[Property]>;
};
export type FormMessage = { type: "info" | "success" | "error"; text: string };
export type FormData<Fieldnames extends string = string> = {
raw_values: Record<Fieldnames, FormDataValue>;
messages: FormMessage[];
};
export abstract class Form<F extends Fields> extends Mountable<F> {
defaultSuccessMessage = "Done";
submitButtonText = "Wyślij";
async canAccess(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_: Context
): Promise<{ canAccess: boolean; message: string }> {
return { canAccess: true, message: "" };
}
async renderError(
_: Context,
error: PageErrorMessage
): Promise<FlatTemplatable> {
return tempstream/* HTML */ `<div>${error.message}</div>`;
}
async render(ctx: Context, data: FormData): Promise<FlatTemplatable> {
return tempstream/* HTML */ `${this.makeFormTag()} ${
!this.controls.some((control) => control.role == "messages")
? this.renderMessages(ctx, data)
: ""
} ${this.renderControls(ctx, data)}<input type="submit" value="${
this.submitButtonText
}"/></form>`;
}
public makeFormTag(): FlatTemplatable {
return `<form method="POST" action="./">`;
}
public async onValuesInvalid(
ctx: Context,
form_messages: FormMessage[]
): Promise<void> {
ctx.status = 422;
ctx.body = await this.render(ctx, {
raw_values: ctx.$body,
messages: form_messages.length
? form_messages
: [{ type: "error", text: "Some fields are invalid" }],
});
}
public async onError(
ctx: Context,
data: FormData,
error: unknown
): Promise<void> {
ctx.status = 422;
let error_message = "Unknown error has occured";
if (
is(error, predicates.object) &&
hasShape({ message: predicates.string }, error)
) {
error_message = error.message;
}
ctx.body = await this.render(ctx, {
raw_values: data.raw_values,
messages: [{ type: "error", text: error_message }],
});
}
public abstract onSubmit(
ctx: Context,
data: FormData
): void | Promise<void>;
public async onSuccess(ctx: Context, data: FormData): Promise<void> {
ctx.body = await this.render(ctx, {
raw_values: data.raw_values,
messages: [{ type: "success", text: this.defaultSuccessMessage }],
});
ctx.status = 422;
}
public mount(router: Router, path: string): void {
router.use(path, async (ctx, next) => {
const result = await this.canAccess(ctx);
if (!result.canAccess) {
ctx.body = this.renderError(ctx, {
type: "access",
message: result.message,
});
ctx.status = 403;
return;
}
await next();
});
router.get(path, async (ctx) => {
ctx.type = "html";
ctx.body = await this.render(ctx, { raw_values: {}, messages: [] });
});
router.post(path, async (ctx) => {
const { valid, form_messages } = await this.validate(
ctx,
ctx.$body
);
if (!valid) {
await this.onValuesInvalid(ctx, form_messages);
return;
}
try {
ctx.status = 303;
await this.onSubmit(ctx, {
raw_values: ctx.$body,
messages: [],
});
await this.onSuccess(ctx, {
raw_values: ctx.$body,
messages: [],
});
} catch (e: unknown) {
// eslint-disable-next-line no-console
console.dir(e, { depth: 5 });
const message =
is(e, predicates.object) &&
hasFieldOfType(e, "message", predicates.string)
? e?.message
: is(e, predicates.string)
? e
: "Wystąpił błąd";
ctx.status = 422;
await this.onError(
ctx,
{
raw_values: ctx.$body,
messages: [
{
type: "error",
text: message,
},
],
},
e
);
}
});
}
extractRawValues(ctx: Context): Record<string, FormDataValue> {
return ctx.$body;
}
}

File Metadata

Mime Type
text/x-java
Expires
Sat, Oct 11, 08:11 (1 d, 13 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
977817
Default Alt Text
form.ts (4 KB)

Event Timeline