import { bubble } from "@web3r/flowerkit/evt"; import { wait } from "@web3r/flowerkit/fn"; import type { TGetFromServerArgs } from "@web3r/flowerkit/net"; import { getFromServer } from "@web3r/flowerkit/net"; import { getObjFromFormData } from "@web3r/flowerkit/obj"; import { getDocument } from "ssr-window"; import { constants } from "../../../constants"; import { getCheckedBoxes } from "../../../utils/getCheckedBoxes"; import { _getFormExtendedCfg } from "../utils/_getFormExtendedCfg"; import type { IFormExtendedCfg } from "../utils/_getFormExtendedCfg"; type TRequestMethod = NonNullable["method"]>; type TRequestContentType = NonNullable["contentType"]>; const getRequestMethod = (method: unknown): TRequestMethod => { switch (method) { case "POST": case "GET": case "PUT": case "DELETE": case "HEAD": case "CONNECT": case "OPTIONS": case "TRACE": return method; case "PATCH": return "POST"; default: return "POST"; } }; const getRequestContentType = (contentType: unknown): TRequestContentType => { switch (contentType) { case "auto": case "application/json": case "multipart/form-data": case "application/x-www-form-urlencoded": return contentType; default: return "auto"; } }; /** * Singleton. Плагин для отправки форм. * Автоматически отправляет валидную форму с атрибутом `data-js-form` в зависимости от ее параметров. * @module FormSend */ export class FormSend { static #instance: FormSend | null = null; constructor() { if (FormSend.#instance) { return FormSend.#instance; } this.#bindEvents(); FormSend.#instance = this; } /** * Получает данные формы * @param form{HTMLFormElement} форма * @param isGetAll{Boolean=} получение всех input, включая disabled * @param excludedParams{Array=} исключаемые из данных названия имён * @param isTrim{Boolean=} удаление пустых значений * @returns {FormData} */ static getFormData(form: IFormExtendedCfg["form"], isGetAll = false, excludedParams: string[] = [], isTrim = false) { const cfg = _getFormExtendedCfg(form); const fd = new FormData(form); if (cfg.isSerializeDisabledInputs || isGetAll) { const disabledInputs = form.querySelectorAll("[disabled]"); disabledInputs.forEach(({ name, value, type }) => { const set = (name: string, value: string) => fd.set(name, value); if (type === "radio") { const radios = getCheckedBoxes(getDocument().querySelectorAll(`[name=${name}]`), `[type="radio"]`); if (radios.length) { set(name, (radios[0] as HTMLInputElement).value); } } else { set(name, value); } }); } if (cfg.isRemoveEmptyFiles) { const filesInput = form.querySelectorAll(`[type='file'][name]`); filesInput.forEach(({ name, value }) => { if (!value) { fd.delete(name); } }); } [ ...fd.entries() ].forEach(([ name, value ]) => { if ((isTrim && typeof value === "string" && !value.trim().length) || excludedParams.includes(name as string)) { fd.delete(name as string); } }); return fd; } /** * Стандартный обработчик успешной отправки формы */ static async onSend(resp: unknown, cfg: IFormExtendedCfg) { const { delay, form } = cfg || { delay: 0, form: null, }; if (form) { await wait(delay) .finally(() => { console.debug("[FormSend] Form sent successfully: ", form, resp); bubble(form as HTMLFormElement, constants.formBubbles.formSuccess, { resp, cfg, }); FormSend.#setLoadingState(form as IFormExtendedCfg["form"], false); }); } } /** * Стандартный обработчик неудачной отправки формы */ static async onFail(resp: unknown, cfg: IFormExtendedCfg) { const { form, delay } = cfg || { delay: 0, form: null, }; if (form) { await wait(delay) .finally(() => { console.debug("[FormSend] Form error on sending: ", form, resp); bubble(form as HTMLFormElement, constants.formBubbles.formError, { resp, cfg }); FormSend.#setLoadingState(form as IFormExtendedCfg["form"], false); }); } } /** * Управление состоянием блокировки формы во время отправки данных * @private * @param form{HTMLFormElement} форма * @param isLoading{Boolean=} состояние блокировки * @param timeOut{Number=} задержка перехода в состояние */ static #setLoadingState(form: IFormExtendedCfg["form"], isLoading = true, timeOut = 0) { const cfg = _getFormExtendedCfg(form); const { successTimeOut, handler } = cfg; timeOut = timeOut || (successTimeOut as number); const fn = () => { form.classList.toggle(constants.stateClasses.loading, isLoading); if (handler && (handler.matches("button") || handler.matches("[type=\"submit\"]"))) { (handler as HTMLButtonElement).disabled = isLoading; } }; (typeof timeOut === "number" && timeOut > 0) ? wait(timeOut).then(() => fn()) : fn(); } /** * Отправка данных формы на сервер * @param form{HTMLFormElement} форма * @param data{FormData|Object=} данные формы * @returns {Promise} */ static async getSendResult(form: IFormExtendedCfg["form"], data?: FormData) { const { url, isShowLoader, contentType, method, getFromServerOptions, } = _getFormExtendedCfg(form); const body = data || FormSend.getFormData(form); const requestContentType = getRequestContentType(contentType); const requestMethod = getRequestMethod(method); const requestData = requestContentType === "application/json" ? getObjFromFormData(body) : body; if (isShowLoader) { FormSend.#setLoadingState(form); } console.debug("[FormSend] Form sending:", form, "with data:", [ ...body.entries() ], "type:", contentType, "and method:", method); bubble(form, constants.formBubbles.formSending); return await getFromServer({ data: requestData, url: typeof url === "string" ? url : "./", method: requestMethod, contentType: requestContentType, timeout: 30000, ...(getFromServerOptions || {}), }); } #bindEvents() { getDocument().addEventListener(constants.formBubbles.formValid, (e: Event) => this.#handleFormValid(e)); } async #handleFormValid(e: Event) { const { target } = e; const cfg = _getFormExtendedCfg(target as IFormExtendedCfg["form"]); const { isIgnoreAutoSend, isUseFetch, isConfirmBeforeSubmit } = cfg; const send = async (fd?: FormData) => { if (isUseFetch) { return FormSend.getSendResult(target as IFormExtendedCfg["form"], fd) .then((resp: unknown) => { FormSend.onSend(resp, cfg); return resp; }, (resp: unknown) => { FormSend.onFail(resp, cfg); return resp; }); } else { (target as HTMLFormElement).submit(); return Promise.resolve(); } }; if (!isIgnoreAutoSend) { if (isConfirmBeforeSubmit) { const fd = FormSend.getFormData(target as IFormExtendedCfg["form"]); bubble(target as HTMLFormElement, constants.formBubbles.formNeedsConfirm, { getSendResult: async () => send(fd), cfg, fd, }); } else { await send(); } } } }