/* eslint-disable @typescript-eslint/no-explicit-any */ import Router from "@koa/router"; import { is, predicates } from "@sealcode/ts-predicates"; import { Context } from "koa"; import { FlatTemplatable, tempstream } from "tempstream"; import { Fields, MountableWithFields } from "../page/mountable-with-fields.js"; import { attribute } from "../sanitize.js"; import type { FormDataValue, FormMessage } from "./form-types.js"; import { Form } from "./form.js"; const FIELD_PREFIX_SEPARATOR = "___"; export class Multiform extends MountableWithFields { getControls = () => []; public name: string; public forms: Record>; init(path: string, router: Router): void { if (this.initialized) { return; } super.init(path, router); 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 = () => `
`; form.makeCloseFormTag = () => `
`; form.field_names_prefix = key + FIELD_PREFIX_SEPARATOR; form.form_id = this.name; form.action = "./" + key; form.init(path, router); form.extractRawValues = async (context) => this.getSubformRawValues(context, key); const submit_button_id = this.name + "_" + key + "_submit"; form.makeSubmitButton = () => /* HTML */ ` `; } 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}'` ); } } } } async extractRawValues( ctx: Context ): Promise> { 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", }; } getSubformsToRender( ctx: Context ): Array]> { 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] ? [[requested_frame, this.forms[requested_frame]]] : Object.entries(this.forms); return forms_to_render; } async renderSubform( ctx: Context, sub_form_name: string, show_field_errors: boolean ): Promise { 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: await this.getSubformRawValues(ctx, sub_form_name), messages: [], field_messages: {}, }, show_field_errors ); } async getSubformRawValues( ctx: Context, sub_form_name: string ): Promise> { const result = Object.fromEntries( Object.entries(await 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 = {}, show_field_errors: boolean ): Promise { return tempstream /* HTML */ `${this.renderMessages(ctx, { raw_values: {}, messages, field_messages: {}, })}
${this.getSubformsToRender(ctx).map(([form_name]) => { const frame_form_id = this.name + "_" + form_name + "_frame_form"; return tempstream /* HTML */ ` ${prerenderedForms[form_name] || this.renderSubform(ctx, form_name, show_field_errors)}
`; })}
${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 */ ` `; } mount(router: Router, path: string): void { router.get(path, async (ctx) => { ctx.type = "html"; ctx.body = await this.render(ctx, [], {}, false); }); for (const [key, form] of Object.entries(this.forms)) { router.post( path + (path.endsWith("/") ? "" : "/") + key, async (ctx) => { const result = await form.canAccess(ctx); if (!result.canAccess) { ctx.body = this.renderError(ctx, { type: "access", message: result.message, }); ctx.status = 403; return; } 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, }, true ); } else if (reaction.action == "redirect") { ctx.status = 303; ctx.redirect(reaction.url); } } ); } } }