Page MenuHomeSealhub

multiform.ts
No OneTemporary

multiform.ts

import Router from "@koa/router";
import { is, predicates } from "@sealcode/ts-predicates";
import { Context } from "koa";
import { FlatTemplatable, tempstream } from "tempstream";
import { Form, FormDataValue, FormMessage } from "..";
import { Fields, MountableWithFields } from "../page/mountable-with-fields";
import { attribute } from "../sanitize";
const FIELD_PREFIX_SEPARATOR = "___";
export class Multiform extends MountableWithFields {
controls = [];
public name: string;
public forms: Record<string, Form<Fields>>;
init(): void {
super.init();
for (const [key, form] of Object.entries(this.forms)) {
if (key !== attribute(key)) {
throw new Error(
`Form name "${key}" is not url-safe. Try: "${attribute(
key
)}"`
);
}
form.makeOpenFormTag = () => `<div>`;
form.makeCloseFormTag = () => `</div>`;
form.field_names_prefix = key + FIELD_PREFIX_SEPARATOR;
form.form_id = this.name;
form.action = "./" + key;
form.init();
form.extractRawValues = (context) =>
this.getSubformRawValues(context, key);
const submit_button_id = this.name + "_" + key + "_submit";
form.makeSubmitButton = () => /* HTML */ `<input
type="submit"
value="${form.submitButtonText}"
formaction="${form.action}"
id="${submit_button_id}"
${form.form_id ? `form="${form.form_id}"` : ""}
/> `;
}
for (const form of Object.values(this.forms)) {
for (const field of Object.values(form.fields)) {
if (field.name.includes(FIELD_PREFIX_SEPARATOR)) {
throw new Error(
`A field name within multiform cannot contain '${FIELD_PREFIX_SEPARATOR}'`
);
}
}
}
}
extractRawValues(ctx: Context): Record<string, FormDataValue> {
return ctx.$body || {};
}
async canAccess(): Promise<{ canAccess: boolean; message: string }> {
return {
canAccess: true,
message:
"this view includes multiple forms and each of them have their unique access rules",
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getSubformsToRender(ctx: Context): Array<readonly [string, Form<any>]> {
const requested_frame = ctx.headers["turbo-frame"];
if (
!is(
requested_frame,
predicates.or(predicates.undefined, predicates.string)
)
) {
throw new Error("Wrong turbo-frame header value type");
}
const forms_to_render =
requested_frame && this.forms[requested_frame]
? [<const>[requested_frame, this.forms[requested_frame]]]
: Object.entries(this.forms);
return forms_to_render;
}
async renderSubform(
ctx: Context,
sub_form_name: string
): Promise<FlatTemplatable> {
const form = this.forms[sub_form_name];
const result = await form.canAccess(ctx);
if (!result.canAccess) {
return result.message;
}
return form.render(ctx, {
raw_values: this.getSubformRawValues(ctx, sub_form_name),
messages: [],
});
}
getSubformRawValues(
ctx: Context,
sub_form_name: string
): Record<string, FormDataValue> {
const result = Object.fromEntries(
Object.entries(this.extractRawValues(ctx))
.filter(([key]) =>
key.startsWith(sub_form_name + FIELD_PREFIX_SEPARATOR)
)
.map(([key, value]) => [
key.slice((sub_form_name + FIELD_PREFIX_SEPARATOR).length),
value,
])
);
return result;
}
async render(
ctx: Context,
messages: FormMessage[],
prerenderedForms: Record<string, FlatTemplatable | undefined> = {}
): Promise<FlatTemplatable> {
return tempstream/* HTML */ `${this.renderMessages(ctx, {
raw_values: {},
messages,
})}
<div class="forms">
<form
id="${this.name}"
novalidate
${
/* novalidate is here because all the subforms are actuallly one form and errors in one will prevent submitting all of the forms */ ""
}
method="POST"
></form>
${this.getSubformsToRender(ctx).map(([form_name]) => {
const frame_form_id =
this.name + "_" + form_name + "_frame_form";
return tempstream/* HTML */ `<turbo-frame
id="${form_name}"
contains-subform
subform-id="${frame_form_id}"
>
${prerenderedForms[form_name] ||
this.renderSubform(ctx, form_name)}
<form id="${frame_form_id}" method="POST"></form>
</turbo-frame>`;
})}
</div>
${this.makeBottomScript()}`;
}
makeBottomScript(): string {
// this script assigns each field to its corresponding form, instead of
// the html-only version where all fields are attached to one meta-form
return /* HTML */ ` <script>
(function () {
if (!window.subform_handlers) {
window.subform_handlers = {};
}
if (window.subform_handlers["${this.name}"]) {
return;
}
const handler = () => {
document
.querySelectorAll("turbo-frame[contains-subform]")
.forEach((frame) => {
frame_form_id = frame.getAttribute("subform-id");
frame.querySelectorAll("input").forEach((input) => {
input.setAttribute("form", frame_form_id);
});
});
};
window.subform_handlers["${this.name}"] = handler;
document.documentElement.addEventListener(
"turbo:load",
handler
);
document.documentElement.addEventListener(
"turbo:frame-render",
handler
);
})();
</script>`;
}
mount(router: Router, path: string): void {
router.get(path, async (ctx) => {
ctx.type = "html";
ctx.body = await this.render(ctx, [], {});
});
for (const [key, form] of Object.entries(this.forms)) {
router.post(
path + (path.endsWith("/") ? "" : "/") + key,
async (ctx) => {
const reaction = await form.handlePost(ctx);
if (reaction.action == "stay") {
ctx.status = 422;
const form_content = reaction.content;
ctx.body = this.render(ctx, [], {
[key]: form_content,
});
} else if (reaction.action == "redirect") {
ctx.status = 303;
ctx.redirect(reaction.url);
}
}
);
}
}
}

File Metadata

Mime Type
text/x-java
Expires
Tue, Dec 24, 14:02 (20 h, 55 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
557103
Default Alt Text
multiform.ts (5 KB)

Event Timeline