Page MenuHomeSealhub

form.ts
No OneTemporary

import { Context } from "koa";
import { randomUUID } from "crypto";
import Router from "@koa/router";
import { FlatTemplatable, tempstream } from "tempstream";
import {
hasFieldOfType,
hasShape,
is,
predicates,
} from "@sealcode/ts-predicates";
import {
FormData,
FormDataValue,
FormMessage,
FormReaction,
} from "./form-types.js";
import {
Fields,
MountableWithFields,
PageErrorMessage,
} from "../page/mountable-with-fields.js";
export abstract class Form<
F extends Fields,
SubmitResult
> extends MountableWithFields<F> {
defaultSuccessMessage = "Done";
submitButtonText = "Wyślij";
action = "./";
useTurbo = true;
form_id = randomUUID() as string;
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>`;
}
makeSubmitButton(): FlatTemplatable {
return /* HTML */ `<input
type="submit"
value="${this.submitButtonText}"
formaction="${this.action}"
${this.form_id ? `form="${this.form_id}"` : ""}
/>`;
}
async render(
ctx: Context,
data: FormData,
show_field_errors: boolean
): Promise<FlatTemplatable> {
return tempstream/* HTML */ `<div class="form-container">
${this.makeOpenFormTag(ctx)}
${!this.controls.some((control) => control.role == "messages")
? this.renderMessages(ctx, data)
: ""}
${this.renderControls(
this.makeFormControlContext(ctx, data, show_field_errors)
)}
${this.controls.some((control) => control.role == "submit")
? ""
: this.makeSubmitButton()}
${this.makeCloseFormTag()}
</div>`;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async makeFormClasses(_ctx: Context): Promise<string[]> {
return [];
}
public makeOpenFormTag(ctx: Context): FlatTemplatable {
return tempstream`<form enctype="multipart/form-data" method="POST" id="${
this.form_id
}" action="${this.action}" class="${this.makeFormClasses(ctx).then(
(classes) => classes.join(" ")
)}" ${
this.useTurbo ? "" : `data-turbo="false"`
}></form><div class="form">`;
}
public makeCloseFormTag(): FlatTemplatable {
return `</div>`;
}
public async onValuesInvalid(
ctx: Context,
form_messages: FormMessage[],
_field_errors: Partial<Record<keyof Fields, string>>
): Promise<FormReaction> {
const messages = form_messages.length
? form_messages
: [<const>{ type: "error", text: "Some fields are invalid" }];
return {
action: "stay",
content: await this.render(
ctx,
{
raw_values: await this.extractRawValues(ctx),
messages,
},
"show_field_errors" && true
),
messages,
};
}
public async onError(
ctx: Context,
data: FormData,
error: unknown
): Promise<FormReaction> {
let error_message = "Unknown error has occured";
if (
is(error, predicates.object) &&
hasShape({ message: predicates.string }, error)
) {
error_message = error.message;
}
const messages = [<const>{ type: "error", text: error_message }];
return {
action: "stay",
content: await this.render(
ctx,
{
raw_values: data.raw_values,
messages,
},
"show_field_errors" && true
),
};
}
public abstract onSubmit(
ctx: Context,
data: FormData
): SubmitResult | Promise<SubmitResult>;
public async onSuccess(
ctx: Context,
_data: FormData,
_submitResult: SubmitResult
): Promise<FormReaction> {
const messages = [
<const>{ type: "success", text: this.defaultSuccessMessage },
];
return {
action: "stay",
content: await this.render(
ctx,
{
raw_values: await this.getInitialValues(ctx),
messages,
},
"show_field_errors" && false
),
messages,
};
}
async getRawValuesOnSuccess(ctx: Context) {
return this.extractRawValues(ctx);
}
async handlePost(ctx: Context): Promise<FormReaction> {
const raw_values = await this.extractRawValues(ctx);
const { valid, form_messages, field_errors } = await this.validate(
ctx,
raw_values
);
if (!valid) {
console.debug("Form values invalid: ", {
form_messages,
field_errors,
});
return this.onValuesInvalid(ctx, form_messages, field_errors);
}
try {
ctx.status = 303;
const result = await this.onSubmit(ctx, {
raw_values,
messages: [],
});
return this.onSuccess(
ctx,
{
raw_values: await this.getRawValuesOnSuccess(ctx),
messages: [],
},
result
);
} 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";
return this.onError(
ctx,
{
raw_values,
messages: [
{
type: "error",
text: message,
},
],
},
e
);
}
}
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();
});
// for use with other subroutes or middlewares that individual controls
// might register
const subrouter = new Router();
for (const control of this.controls) {
control.mount(subrouter, this);
}
router.use(path, subrouter.routes(), subrouter.allowedMethods());
router.get(path, async (ctx) => {
ctx.type = "html";
ctx.body = await this.render(
ctx,
{
raw_values: await this.extractRawValues(ctx),
messages: [],
},
"show_field_errors" && false
);
});
router.post(path, async (ctx) => {
console.log("REGULAR FORM HANDLE");
const reaction =
(ctx.override_reaction as FormReaction | undefined) ||
(await this.handlePost(ctx));
if (reaction.action == "stay") {
ctx.status = 422;
ctx.body = reaction.content;
} else if (reaction.action == "redirect") {
ctx.status = 303;
ctx.redirect(reaction.url);
}
});
}
async extractRawValues(
ctx: Context
): Promise<Record<string, FormDataValue>> {
return Object.keys(ctx.$body).length
? ctx.$body
: this.getInitialValues(ctx);
}
}

File Metadata

Mime Type
text/x-java
Expires
Tue, Dec 24, 14:02 (17 h, 48 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
557202
Default Alt Text
form.ts (6 KB)

Event Timeline