Приклад опису помилок у формі

Опис

Після надсилання форми і отримання помилок валідації відбувається наступне:

  1. Над кожним полем, в якому має місце помилка (далі – помилкові поля), з'являється видимий опис суті помилки.
  2. Ці описи мають унікальні id, які вказуються в атрибуті "aria-describedby" кожного відповідного помилкового поля. Таким чином, при фокусі на полі скрінрідер, окрім його назви, озвучить ще й опис помилки.
  3. Перед формою створюється текст, фокус ставиться на нього. Текст повідомляє про наявність помилок.
  4. У кінці цього текста розміщуються якірні посилання, активація яких переводитиме користувача до відповідних помилкових полів.
  5. Рамки помилкових полей додатково забарвлюються в червоний колір, але це не послаблює видимість маркера фокусу, бо він має відступ 2px і товщину 2px.

При повторному надсиланні форми створені атрибути "aria-describedby" і згенеровані елементи прибираються з DOM.

Демонстрація

Відповідний код HTML

<div id="formCustom">
    <div id="formStatus" style="display: none"></div>
    <div class="mb25" id="anchorsSpawner" style="display: none"></div>
    <div class="mb25">
        <div class="mb4"><label for="inputEmail">Електронна пошта</label></div>
        <input class="input" id="inputEmail" size="30" required aria-required="true">
    </div>
    <div class="mb25">
        <div class="mb4"><label for="inputName">Ім'я</label></div>
        <input class="input" id="inputName" size="30" required aria-required="true">
    </div>
    <div class="flex_row_sta_cen">
        <div class="mr16">
            <button class="button" id="buttonSendWithError">Симулювати помилки</button>
        </div>
        <div>
            <button class="button" id="buttonSendWithSuccess">Симулювати успіх</button>
        </div>
    </div>
</div>

Відповідний код JS

const buttonSendWithError = document.getElementById("buttonSendWithError");
const buttonSendWithSuccess = document.getElementById("buttonSendWithSuccess");
const inputEmail = document.getElementById("inputEmail");
const inputName = document.getElementById("inputName");
const formStatus = document.getElementById("formStatus");
const anchorsSpawner = document.getElementById("anchorsSpawner");

buttonSendWithError.addEventListener("click", () => {
    Form.send("error");
});

buttonSendWithSuccess.addEventListener("click", () => {
    Form.send("success");
});

class Form
{
    static send(simulation="success") {
        // remove previously generated statuses and links:
        formStatus.innerHtml = "";
        formStatus.removeAttribute("tabindex");
        formStatus.style.display = "none";
        anchorsSpawner.style.display = "none";
        while (anchorsSpawner.firstChild) {
            anchorsSpawner.removeChild(anchorsSpawner.firstChild);
        }
        
        // remove previously generated errors:
        let messages = document.getElementsByClassName("error-message");
        Array.from(messages).forEach(message => {
            message.remove();
        });
        
        // remove descriptions and respective attributes:
        let inputsDescribed = document.getElementsByClassName("input-described");
        Array.from(inputsDescribed).forEach(input => {
            input.classList.remove("input-described");
            input.classList.remove("input-with-error");
            input.removeAttribute("aria-describedby");
        });
        
        // just simulation:
        let response = {};
        if (simulation === "error") {
            inputEmail.value = "not_email";
            inputName.value = "";
    
            response = {
    "errors" : {
        "inputEmail" : "Має бути валідною адресою електронної пошти",
        "inputName" : "Не має бути порожнім"
    }
            }
        }
        
        setTimeout(function(){
            Form.handleResponse(response);
        }, 500);
    }
    
    static handleResponse(response) {
        if (!response.hasOwnProperty("errors")) {
            this.showStatus("Форма прийнята без помилок.");
            return true;
        }
        
        let message, input, anchors = [];
        for (const inputId in response["errors"]) {
            console.log(inputId + " = " + response["errors"][inputId]);
    
            message = document.createElement("p");
            message.setAttribute("id", "error-"+inputId);
            message.classList.add("error-message");
            message.textContent = "Помилка: " + response["errors"][inputId];
    
            input = document.getElementById(inputId);
            input.insertAdjacentElement("beforebegin", message);
            input.setAttribute("aria-describedby", "error-"+inputId);
            input.classList.add("input-described");
            input.classList.add("input-with-error");
    
            anchors.push(inputId);
        }
        
        this.showStatus("Форма містить помилки. Відповідні поля нижче мають пояснення.", anchors);
    }
    
    static showStatus(text, anchors=[]) {
        if (anchors.length > 0) {
            for (let anchor of anchors) {
    let label = document.querySelector('label[for="'+anchor+'"]');
    let formAnchorLink = document.createElement("a");
    formAnchorLink.setAttribute("id", "formAnchorLink");
    formAnchorLink.setAttribute("href", "#"+anchor);
    formAnchorLink.classList.add("link");
    formAnchorLink.innerText = 'перейти до виправлення поля "'+label.innerText+'"';
    
    let formAnchorLinkContainer = document.createElement("div");
    formAnchorLinkContainer.appendChild(formAnchorLink);
    anchorsSpawner.appendChild(formAnchorLinkContainer);
            }
        }
        
        anchorsSpawner.style.display = "block";
        formStatus.innerHTML = text;
        formStatus.setAttribute("tabindex", "0");
        formStatus.style.display = "block";
        formStatus.focus();
    }
}

Відповідний код CSS

#formCustom {
    background-color: #efefef;
    padding: 20px;
}

p {
    margin: 0px 0px 2px 0px;
    padding: 0px;
}

.error-message {
    color: #d90000;
}

.input {
    background-color: #ffffff;
    border: 1px solid #cecece;
    border-radius: .25rem;
    color: #000;
    font-family: "font_normal";
    font-size: 16px;
    outline-color: #0984e3;
    padding: 10px;
}.input:focus {
    outline: 2px solid #4444FF !important;
    outline-offset: 2px !important;
}

.input-with-error {
    border: 1px #d90000 solid !important;
}

button {
    cursor: pointer;
}button:focus {
    outline: 2px solid #4444FF;
    outline-offset: 2px;
}