2022-02-16 12:47:45 +01:00

145 lines
4.8 KiB
JavaScript

/**
* 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<unknown>} - 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();
}