import "./linq.js"; import { Template } from "./template.js"; export function formToObject(form) { const data = new FormData(form); const object = {}; data.forEach((value, key) => { setNestedValue(object, key, value); }); return object; } function setNestedValue(obj, path, value) { path .split(".") .asEnumerable() .isLast() .forEach((key, isLast) => { if (isLast) { obj[key] = value; } else { if (!obj[key]) { obj[key] = {}; } obj = obj[key]; } }); } export async function sendFormAsync(form, url, method) { url = url || form.action; method = method || form.method || "post"; const data = formToObject(form); const response = await sendJsonAsync(url, data, method); if (response.ok && response.redirected) { window.location.href = response.url; return null; } const responseText = await response.text(); if ( response.ok == false && handleValidationError(response, responseText, form) ) { return null; } if (response.ok == false) { handleGenericFormError(response, responseText, form); return null; } else { return responseText.length == 0 ? null : JSON.parse(responseText); } } export async function sendJsonAsync(url, data, method = "post") { const response = await fetch(url, { method: method.toUpperCase(), headers: { "Content-Type": "application/json", }, body: JSON.stringify(data), }); return response; } export async function postAndRenderAsync(url, data, template, targetElement) { const response = await sendJsonAsync(url, data); if (response.ok) { const responseText = await response.text(); targetElement.innerHTML = template.render( responseText.length == 0 ? undefined : JSON.parse(responseText), ); } } export async function postFormAndRenderAsync( url, form, template, targetElement, ) { const object = formToObject(form); const data = await postFormAsync(url, object, template, targetElement); if (data) { targetElement.innerHTML = template.render(data); } } const genericFormErrorTemplate = new Template(` `); function handleGenericFormError(response, responseText, form) { if (!response.ok) { // Remove all existing form-level errors before adding a new one form.querySelectorAll(":scope > .form-error").forEach((el) => el.remove()); let message = responseText; try { message = JSON.parse(responseText); } catch (_) {} const html = genericFormErrorTemplate.render(message); form.insertAdjacentHTML("beforeend", html); } } const validationErrorTemplate = new Template(` `); const unknownInputErrorTemplate = new Template(` `); function toCamelCase(str) { str = str.replace(/([-_][a-z])/gi, (match) => { return match.toUpperCase().replace("-", "").replace("_", ""); }); str = str[0].toLowerCase() + str.substring(1); return str; } function handleValidationError(response, responseText, form) { if (response.status !== 400) return false; const responseObject = JSON.parse(responseText); const unknownInputErrors = {}; if ( responseObject.type === "https://tools.ietf.org/html/rfc9110#section-15.5.1" && responseObject.errors ) { for (const [field, messages] of Object.entries(responseObject.errors)) { const input = form.querySelector(`[name="${toCamelCase(field)}"]`); if (input) { const parent = input.parentElement; const errorHtml = validationErrorTemplate.render(messages); let errorContainer = parent.querySelector(".form-error"); // Check if an error container already exists if (errorContainer) { errorContainer.outerHTML = errorHtml; // Replace existing error container } else { parent.insertAdjacentHTML("beforeend", errorHtml); } } else { unknownInputErrors[field] = messages; } } if (Object.keys(unknownInputErrors).length > 0) { form .querySelectorAll(":scope > .form-error") .forEach((el) => el.remove()); const html = unknownInputErrorTemplate.render(unknownInputErrors); form.insertAdjacentHTML("beforeend", html); } return true; } return false; }