/** * Acquires all data from the given form and POSTs it as JSON to the target URL. * In case of failure this function will throw an error. * In case of success a parsed JSON body of the response will be returned, * unless the body contains a `location` field, * in that case the page will be redirected to that location. * * @param formId - ID of the form. * @param target - Target URL to POST to. Defaults to the current URL. * @returns {Promise} - The response JSON. */ async function postJsonForm(formId, target = '') { const form = document.getElementById(formId); const formData = new FormData(form); const res = await fetch(target, { method: 'POST', credentials: 'include', headers: { 'accept': 'application/json', 'content-type': 'application/json' }, body: JSON.stringify(Object.fromEntries(formData)), }); if (res.status >= 400) { const error = await res.json(); throw new Error(`${error.statusCode} - ${error.name}: ${error.message}`) } else if (res.status === 200 || res.status === 201) { const body = await res.json(); if (body.location) { location.href = body.location; } else { return body; } } } /** * Redirects the page to the given target with the key/value pairs of the JSON body as query parameters. * Controls will be deleted from the JSON to prevent very large URLs. * `false` values will be deleted to prevent incorrect serializations to "false". * @param json - JSON to convert. * @param target - URL to redirect to. */ function redirectJsonResponse(json, target) { // These would cause the URL to get very large, can be acquired later if needed delete json.controls; // Remove false parameters since these would be converted to "false" strings for (const [key, val] of Object.entries(json)) { if (typeof val === 'boolean' && !val) { delete json[key]; } } const searchParams = new URLSearchParams(Object.entries(json)); location.href = `${target}?${searchParams.toString()}`; } /** * Adds a listener to the given form to catch the form submission and do an API call instead. * In case of an error, the inner text of the given error block will be updated with the message. * In case of success the callback function will be called. * * @param formId - ID of the form. * @param errorId - ID of the error block. * @param apiTarget - Target URL to send the POST request to. Defaults to the current URL. * @param callback - Callback function that will be called with the response JSON. */ async function addPostListener(formId, errorId, apiTarget, callback) { const form = document.getElementById(formId); const errorBlock = document.getElementById(errorId); form.addEventListener('submit', async(event) => { event.preventDefault(); try { const json = await postJsonForm(formId, apiTarget); if (json) { callback(json); } } catch (error) { errorBlock.innerText = error.message; } }); } /** * Updates links on a page based on the controls received from the API. * @param url - API URL that will return the controls * @param controlMap - Key/value map with keys being element IDs and values being the control field names. */ async function addControlLinks(url, controlMap) { const json = await fetchJson(url); for (let [ id, control ] of Object.entries(controlMap)) { updateElement(id, json.controls[control], { href: true }); } } /** * Shows or hides the given element. * @param id - ID of the element. * @param visible - If it should be visible. */ function setVisibility(id, visible) { const element = document.getElementById(id); element.classList[visible ? 'remove' : 'add']('hidden'); // Disable children of hidden elements, // such that the browser does not expect input for them for (const child of getDescendants(element)) { if ('disabled' in child) child.disabled = !visible; } } /** * Obtains all children, grandchildren, etc. of the given element. * @param element - Element to get all descendants from. */ function getDescendants(element) { return [...element.querySelectorAll("*")]; } /** * Updates the inner text and href field of an element. * @param id - ID of the element. * @param text - Text to put in the field(s). * @param options - Indicates which fields should be updated. * Keys should be `innerText` and/or `href`, values should be booleans. */ function updateElement(id, text, options) { const element = document.getElementById(id); if (options.innerText) { element.innerText = text; } if (options.href) { element.href = text; } } /** * Fetches JSON from the url and converts it to an object. * @param url - URL to fetch JSON from. */ async function fetchJson(url) { const res = await fetch(url, { headers: { accept: 'application/json' } }); return res.json(); }