Javascript обработка ошибок формы

Ошибки — это хорошо. Автор материала, перевод которого мы сегодня публикуем, говорит, что уверен в том, что эта идея известна всем. На первый взгляд ошибки кажутся чем-то страшным. Им могут сопутствовать какие-то потери. Ошибка, сделанная на публике, вредит авторитету того, кто её совершил. Но, совершая ошибки, мы на них учимся, а значит, попадая в следующий раз в ситуацию, в которой раньше вели себя неправильно, делаем всё как нужно.

Выше мы говорили об ошибках, которые люди совершают в обычной жизни. Ошибки в программировании — это нечто иное. Сообщения об ошибках помогают нам улучшать код, они позволяют сообщать пользователям наших проектов о том, что что-то пошло не так, и, возможно, рассказывают пользователям о том, как нужно вести себя для того, чтобы ошибок больше не возникало.

Этот материал, посвящённый обработке ошибок в JavaScript, разбит на три части. Сначала мы сделаем общий обзор системы обработки ошибок в JavaScript и поговорим об объектах ошибок. После этого мы поищем ответ на вопрос о том, что делать с ошибками, возникающими в серверном коде (в частности, при использовании связки Node.js + Express.js). Далее — обсудим обработку ошибок в React.js. Фреймворки, которые будут здесь рассматриваться, выбраны по причине их огромной популярности. Однако рассматриваемые здесь принципы работы с ошибками универсальны, поэтому вы, даже если не пользуетесь Express и React, без труда сможете применить то, что узнали, к тем инструментам, с которыми работаете.

Код демонстрационного проекта, используемого в данном материале, можно найти в этом репозитории.

1. Ошибки в JavaScript и универсальные способы работы с ними

Если в вашем коде что-то пошло не так, вы можете воспользоваться следующей конструкцией.

throw new Error('something went wrong')

В ходе выполнения этой команды будет создан экземпляр объекта Error и будет сгенерировано (или, как говорят, «выброшено») исключение с этим объектом. Инструкция throw может генерировать исключения, содержащие произвольные выражения. При этом выполнение скрипта остановится в том случае, если не были предприняты меры по обработке ошибки.

Начинающие JS-программисты обычно не используют инструкцию throw. Они, как правило, сталкиваются с исключениями, выдаваемыми либо средой выполнения языка, либо сторонними библиотеками. Когда это происходит — в консоль попадает нечто вроде ReferenceError: fs is not defined и выполнение программы останавливается.

▍Объект Error

У экземпляров объекта Error есть несколько свойств, которыми мы можем пользоваться. Первое интересующее нас свойство — message. Именно сюда попадает та строка, которую можно передать конструктору ошибки в качестве аргумента. Например, ниже показано создание экземпляра объекта Error и вывод в консоль переданной конструктором строки через обращение к его свойству message.

const myError = new Error('please improve your code')
console.log(myError.message) // please improve your code

Второе свойство объекта, очень важное, представляет собой трассировку стека ошибки. Это — свойство stack. Обратившись к нему можно просмотреть стек вызовов (историю ошибки), который показывает последовательность операций, приведшую к неправильной работе программы. В частности, это позволяет понять — в каком именно файле содержится сбойный код, и увидеть, какая последовательность вызовов функций привела к ошибке. Вот пример того, что можно увидеть, обратившись к свойству stack.

Error: please improve your code
 at Object.<anonymous> (/Users/gisderdube/Documents/_projects/hacking.nosync/error-handling/src/general.js:1:79)
 at Module._compile (internal/modules/cjs/loader.js:689:30)
 at Object.Module._extensions..js (internal/modules/cjs/loader.js:700:10)
 at Module.load (internal/modules/cjs/loader.js:599:32)
 at tryModuleLoad (internal/modules/cjs/loader.js:538:12)
 at Function.Module._load (internal/modules/cjs/loader.js:530:3)
 at Function.Module.runMain (internal/modules/cjs/loader.js:742:12)
 at startup (internal/bootstrap/node.js:266:19)
 at bootstrapNodeJSCore (internal/bootstrap/node.js:596:3)

Здесь, в верхней части, находится сообщение об ошибке, затем следует указание на тот участок кода, выполнение которого вызвало ошибку, потом описывается то место, откуда был вызван этот сбойный участок. Это продолжается до самого «дальнего» по отношению к ошибке фрагмента кода.

▍Генерирование и обработка ошибок

Создание экземпляра объекта Error, то есть, выполнение команды вида new Error(), ни к каким особым последствиям не приводит. Интересные вещи начинают происходить после применения оператора throw, который генерирует ошибку. Как уже было сказано, если такую ошибку не обработать, выполнение скрипта остановится. При этом нет никакой разницы — был ли оператор throw использован самим программистом, произошла ли ошибка в некоей библиотеке или в среде выполнения языка (в браузере или в Node.js). Поговорим о различных сценариях обработки ошибок.

▍Конструкция try…catch

Блок try...catch представляет собой самый простой способ обработки ошибок, о котором часто забывают. В наши дни, правда, он используется гораздо интенсивнее чем раньше, благодаря тому, что его можно применять для обработки ошибок в конструкциях async/await.

Этот блок можно использовать для обработки любых ошибок, происходящих в синхронном коде. Рассмотрим пример.

const a = 5

try {
    console.log(b) // переменная b не объявлена - возникает ошибка
} catch (err) {
    console.error(err) // в консоль попадает сообщение об ошибке и стек ошибки
}

console.log(a) // выполнение скрипта не останавливается, данная команда выполняется

Если бы в этом примере мы не заключили бы сбойную команду console.log(b) в блок try...catch, то выполнение скрипта было бы остановлено.

▍Блок finally

Иногда случается так, что некий код нужно выполнить независимо от того, произошла ошибка или нет. Для этого можно, в конструкции try...catch, использовать третий, необязательный, блок — finally. Часто его использование эквивалентно некоему коду, который идёт сразу после try...catch, но в некоторых ситуациях он может пригодиться. Вот пример его использования.

const a = 5

try {
    console.log(b) // переменная b не объявлена - возникает ошибка
} catch (err) {
    console.error(err) // в консоль попадает сообщение об ошибке и стек ошибки
} finally {
    console.log(a) // этот код будет выполнен в любом случае
}

▍Асинхронные механизмы — коллбэки

Программируя на JavaScript всегда стоит обращать внимание на участки кода, выполняющиеся асинхронно. Если у вас имеется асинхронная функция и в ней возникает ошибка, скрипт продолжит выполняться. Когда асинхронные механизмы в JS реализуются с использованием коллбэков (кстати, делать так не рекомендуется), соответствующий коллбэк (функция обратного вызова) обычно получает два параметра. Это нечто вроде параметра err, который может содержать ошибку, и result — с результатами выполнения асинхронной операции. Выглядит это примерно так:

myAsyncFunc(someInput, (err, result) => {
    if(err) return console.error(err) // порядок работы с объектом ошибки мы рассмотрим позже
    console.log(result)
})

Если в коллбэк попадает ошибка, она видна там в виде параметра err. В противном случае в этот параметр попадёт значение undefined или null. Если оказалось, что в err что-то есть, важно отреагировать на это, либо так как в нашем примере, воспользовавшись командой return, либо воспользовавшись конструкцией if...else и поместив в блок else команды для работы с результатом выполнения асинхронной операции. Речь идёт о том, чтобы, в том случае, если произошла ошибка, исключить возможность работы с результатом, параметром result, который в таком случае может иметь значение undefined. Работа с таким значением, если предполагается, например, что оно содержит объект, сама может вызвать ошибку. Скажем, это произойдёт при попытке использовать конструкцию result.data или подобную ей.

▍Асинхронные механизмы — промисы

Для выполнения асинхронных операций в JavaScript лучше использовать не коллбэки а промисы. Тут, в дополнение к улучшенной читабельности кода, имеются и более совершенные механизмы обработки ошибок. А именно, возиться с объектом ошибки, который может попасть в функцию обратного вызова, при использовании промисов не нужно. Здесь для этой цели предусмотрен специальный блок catch. Он перехватывает все ошибки, произошедшие в промисах, которые находятся до него, или все ошибки, которые произошли в коде после предыдущего блока catch. Обратите внимание на то, что если в промисе произошла ошибка, для обработки которой нет блока catch, это не остановит выполнение скрипта, но сообщение об ошибке будет не особенно удобочитаемым.

(node:7741) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: something went wrong
(node:7741) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code. */

В результате можно порекомендовать всегда, при работе с промисами, использовать блок catch. Взглянем на пример.

Promise.resolve(1)
    .then(res => {
        console.log(res) // 1

        throw new Error('something went wrong')

        return Promise.resolve(2)
    })
    .then(res => {
        console.log(res) // этот блок выполнен не будет
    })
    .catch(err => {
        console.error(err) // о том, что делать с этой ошибкой, поговорим позже
        return Promise.resolve(3)
    })
    .then(res => {
        console.log(res) // 3
    })
    .catch(err => {
        // этот блок тут на тот случай, если в предыдущем блоке возникнет какая-нибудь ошибка
        console.error(err)
    })

▍Асинхронные механизмы и try…catch

После того, как в JavaScript появилась конструкция async/await, мы вернулись к классическому способу обработки ошибок — к try...catch...finally. Обрабатывать ошибки при таком подходе оказывается очень легко и удобно. Рассмотрим пример.

;(async function() {
    try {
        await someFuncThatThrowsAnError()
    } catch (err) {
        console.error(err) // об этом поговорим позже
    }

    console.log('Easy!') // будет выполнено
})()

При таком подходе ошибки в асинхронном коде обрабатываются так же, как в синхронном. В результате теперь, при необходимости, в одном блоке catch можно обрабатывать более широкий диапазон ошибок.

2. Генерирование и обработка ошибок в серверном коде

Теперь, когда у нас есть инструменты для работы с ошибками, посмотрим на то, что мы можем с ними делать в реальных ситуациях. Генерирование и правильная обработка ошибок — это важнейший аспект серверного программирования. Существуют разные подходы к работе с ошибками. Здесь будет продемонстрирован подход с использованием собственного конструктора для экземпляров объекта Error и кодов ошибок, которые удобно передавать во фронтенд или любым механизмам, использующим серверные API. Как структурирован бэкенд конкретного проекта — особого значения не имеет, так как при любом подходе можно использовать одни и те же идеи, касающиеся работы с ошибками.

В качестве серверного фреймворка, отвечающего за маршрутизацию, мы будем использовать Express.js. Подумаем о том, какая структура нам нужна для организации эффективной системы обработки ошибок. Итак, вот что нам нужно:

  1. Универсальная обработка ошибок — некий базовый механизм, подходящий для обработки любых ошибок, в ходе работы которого просто выдаётся сообщение наподобие Something went wrong, please try again or contact us, предлагающее пользователю попробовать выполнить операцию, давшую сбой, ещё раз или связаться с владельцем сервера. Эта система не отличается особой интеллектуальностью, но она, по крайней мере, способна сообщить пользователю о том, что что-то пошло не так. Подобное сообщение гораздо лучше, чем «бесконечная загрузка» или нечто подобное.
  2. Обработка конкретных ошибок — механизм, позволяющий сообщить пользователю подробные сведения о причинах неправильного поведения системы и дать ему конкретные советы по борьбе с неполадкой. Например, это может касаться отсутствия неких важных данных в запросе, который пользователь отправляет на сервер, или в том, что в базе данных уже существует некая запись, которую он пытается добавить ещё раз, и так далее.

▍Разработка собственного конструктора объектов ошибок

Здесь мы воспользуемся стандартным классом Error и расширим его. Пользоваться механизмами наследования в JavaScript — дело рискованное, но в данном случае эти механизмы оказываются весьма полезными. Зачем нам наследование? Дело в том, что нам, для того, чтобы код удобно было бы отлаживать, нужны сведения о трассировке стека ошибки. Расширяя стандартный класс Error, мы, без дополнительных усилий, получаем возможности по трассировке стека. Мы добавляем в наш собственный объект ошибки два свойства. Первое — это свойство code, доступ к которому можно будет получить с помощью конструкции вида err.code. Второе — свойство status. В него будет записываться код состояния HTTP, который планируется передавать клиентской части приложения.

Вот как выглядит класс CustomError, код которого оформлен в виде модуля.

class CustomError extends Error {
    constructor(code = 'GENERIC', status = 500, ...params) {
        super(...params)

        if (Error.captureStackTrace) {
            Error.captureStackTrace(this, CustomError)
        }

        this.code = code
        this.status = status
    }
}

module.exports = CustomError

▍Маршрутизация

Теперь, когда наш объект ошибки готов к использованию, нужно настроить структуру маршрутов. Как было сказано выше, нам требуется реализовать унифицированный подход к обработке ошибок, позволяющий одинаково обрабатывать ошибки для всех маршрутов. По умолчанию фреймворк Express.js не вполне поддерживает такую схему работы. Дело в том, что все его маршруты инкапсулированы.

Для того чтобы справиться с этой проблемой, мы можем реализовать собственный обработчик маршрутов и определять логику маршрутов в виде обычных функций. Благодаря такому подходу, если функция маршрута (или любая другая функция) выбрасывает ошибку, она попадёт в обработчик маршрутов, который затем может передать её клиентской части приложения. При возникновении ошибки на сервере мы планируем передавать её во фронтенд в следующем формате, полагая, что для этого будет применяться JSON-API:

{
    error: 'SOME_ERROR_CODE',
    description: 'Something bad happened. Please try again or contact support.'
}

Если на данном этапе происходящие кажется вам непонятным — не беспокойтесь — просто продолжайте читать, пробуйте работать с тем, о чём идёт речь, и постепенно вы во всём разберётесь. На самом деле, если говорить о компьютерном обучении, здесь применяется подход «сверху-вниз», когда сначала обсуждаются общие идеи, а потом осуществляется переход к частностям.

Вот как выглядит код обработчика маршрутов.

const express = require('express')
const router = express.Router()
const CustomError = require('../CustomError')

router.use(async (req, res) => {
    try {
        const route = require(`.${req.path}`)[req.method]

        try {
            const result = route(req) // Передаём запрос функции route
            res.send(result) // Передаём клиенту то, что получено от функции route
        } catch (err) {
            /*
            Сюда мы попадаем в том случае, если в функции route произойдёт ошибка
            */
            if (err instanceof CustomError) {
                /* 
                Если ошибка уже обработана - трансформируем её в 
                возвращаемый объект
                */

                return res.status(err.status).send({
                    error: err.code,
                    description: err.message,
                })
            } else {
                console.error(err) // Для отладочных целей

                // Общая ошибка - вернём универсальный объект ошибки
                return res.status(500).send({
                    error: 'GENERIC',
                    description: 'Something went wrong. Please try again or contact support.',
                })
            }
        }
    } catch (err) {
        /* 
         Сюда мы попадём, если запрос окажется неудачным, то есть,
         либо не будет найдено файла, соответствующего пути, переданному
         в запросе, либо не будет экспортированной функции с заданным
         методом запроса
        */
        res.status(404).send({
            error: 'NOT_FOUND',
            description: 'The resource you tried to access does not exist.',
        })
    }
})

module.exports = router

Полагаем, комментарии в коде достаточно хорошо его поясняют. Надеемся, читать их удобнее, чем объяснения подобного кода, данные после него.

Теперь взглянем на файл маршрутов.

const CustomError = require('../CustomError')

const GET = req => {
    // пример успешного выполнения запроса
    return { name: 'Rio de Janeiro' }
}

const POST = req => {
    // пример ошибки общего характера
    throw new Error('Some unexpected error, may also be thrown by a library or the runtime.')
}

const DELETE = req => {
    // пример ошибки, обрабатываемой особым образом
    throw new CustomError('CITY_NOT_FOUND', 404, 'The city you are trying to delete could not be found.')
}

const PATCH = req => {
    // пример перехвата ошибок и использования CustomError
    try {
        // тут случилось что-то нехорошее
        throw new Error('Some internal error')
    } catch (err) {
        console.error(err) // принимаем решение о том, что нам тут делать

        throw new CustomError(
            'CITY_NOT_EDITABLE',
            400,
            'The city you are trying to edit is not editable.'
        )
    }
}

module.exports = {
    GET,
    POST,
    DELETE,
    PATCH,
}

В этих примерах с самими запросами ничего не делается. Тут просто рассматриваются разные сценарии возникновения ошибок. Итак, например, запрос GET /city попадёт в функцию const GET = req =>..., запрос POST /city попадёт в функцию const POST = req =>... и так далее. Эта схема работает и при использовании параметров запросов. Например — для запроса вида GET /city?startsWith=R. В целом, здесь продемонстрировано, что при обработке ошибок, во фронтенд может попасть либо общая ошибка, содержащая лишь предложение попробовать снова или связаться с владельцем сервера, либо ошибка, сформированная с использованием конструктора CustomError, которая содержит подробные сведения о проблеме.
Данные общей ошибки придут в клиентскую часть приложения в таком виде:

{
    error: 'GENERIC',
    description: 'Something went wrong. Please try again or contact support.'
}

Конструктор CustomError используется так:

throw new CustomError('MY_CODE', 400, 'Error description')

Это даёт следующий JSON-код, передаваемый во фронтенд:

{
    error: 'MY_CODE',
    description: 'Error description'
}

Теперь, когда мы основательно потрудились над серверной частью приложения, в клиентскую часть больше не попадают бесполезные логи ошибок. Вместо этого клиент получает полезные сведения о том, что пошло не так.

Не забудьте о том, что здесь лежит репозиторий с рассматриваемым здесь кодом. Можете его загрузить, поэкспериментировать с ним, и, если надо, адаптировать под нужды вашего проекта.

3. Работа с ошибками на клиенте

Теперь пришла пора описать третью часть нашей системы обработки ошибок, касающуюся фронтенда. Тут нужно будет, во-первых, обрабатывать ошибки, возникающие в клиентской части приложения, а во-вторых, понадобится оповещать пользователя об ошибках, возникающих на сервере. Разберёмся сначала с показом сведений о серверных ошибках. Как уже было сказано, в этом примере будет использована библиотека React.

▍Сохранение сведений об ошибках в состоянии приложения

Как и любые другие данные, ошибки и сообщения об ошибках могут меняться, поэтому их имеет смысл помещать в состояние компонентов. При монтировании компонента данные об ошибке сбрасываются, поэтому, когда пользователь впервые видит страницу, там сообщений об ошибках не будет.

Следующее, с чем надо разобраться, заключается в том, что ошибки одного типа нужно показывать в одном стиле. По аналогии с сервером, здесь можно выделить 3 типа ошибок.

  1. Глобальные ошибки — в эту категорию попадают сообщения об ошибках общего характера, приходящие с сервера, или ошибки, которые, например, возникают в том случае, если пользователь не вошёл в систему и в других подобных ситуациях.
  2. Специфические ошибки, выдаваемые серверной частью приложения — сюда относятся ошибки, сведения о которых приходят с сервера. Например, подобная ошибка возникает, если пользователь попытался войти в систему и отправил на сервер имя и пароль, а сервер сообщил ему о том, что пароль неправильный. Подобные вещи в клиентской части приложения не проверяются, поэтому сообщения о таких ошибках должны приходить с сервера.
  3. Специфические ошибки, выдаваемые клиентской частью приложения. Пример такой ошибки — сообщение о некорректном адресе электронной почты, введённом в соответствующее поле.

Ошибки второго и третьего типов очень похожи, работать с ними можно, используя хранилище состояния компонентов одного уровня. Их главное различие заключается в том, что они исходят из разных источников. Ниже, анализируя код, мы посмотрим на работу с ними.

Здесь будет использоваться встроенная в React система управления состоянием приложения, но, при необходимости, вы можете воспользоваться и специализированными решениями для управления состоянием — такими, как MobX или Redux.

▍Глобальные ошибки

Обычно сообщения о таких ошибках сохраняются в компоненте наиболее высокого уровня, имеющем состояние. Они выводятся в статическом элементе пользовательского интерфейса. Это может быть красное поле в верхней части экрана, модальное окно или что угодно другое. Реализация зависит от конкретного проекта. Вот как выглядит сообщение о такой ошибке.

Сообщение о глобальной ошибке

Теперь взглянем на код, который хранится в файле Application.js.

import React, { Component } from 'react'

import GlobalError from './GlobalError'

class Application extends Component {
    constructor(props) {
        super(props)

        this.state = {
            error: '',
        }

        this._resetError = this._resetError.bind(this)
        this._setError = this._setError.bind(this)
    }

    render() {
        return (
            <div className="container">
                <GlobalError error={this.state.error} resetError={this._resetError} />
                <h1>Handling Errors</h1>
            </div>
        )
    }

    _resetError() {
        this.setState({ error: '' })
    }

    _setError(newError) {
        this.setState({ error: newError })
    }
}

export default Application

Как видно, в состоянии, в Application.js, имеется место для хранения данных ошибки. Кроме того, тут предусмотрены методы для сброса этих данных и для их изменения.

Ошибка и метод для сброса ошибки передаётся компоненту GlobalError, который отвечает за вывод сообщения об ошибке на экран и за сброс ошибки после нажатия на значок x в поле, где выводится сообщение. Вот код компонента GlobalError (файл GlobalError.js).

import React, { Component } from 'react'

class GlobalError extends Component {
    render() {
        if (!this.props.error) return null

        return (
            <div
                style={{
                    position: 'fixed',
                    top: 0,
                    left: '50%',
                    transform: 'translateX(-50%)',
                    padding: 10,
                    backgroundColor: '#ffcccc',
                    boxShadow: '0 3px 25px -10px rgba(0,0,0,0.5)',
                    display: 'flex',
                    alignItems: 'center',
                }}
            >
                {this.props.error}
                 
                <i
                    className="material-icons"
                    style={{ cursor: 'pointer' }}
                    onClick={this.props.resetError}
                >
                    close
                </font></i>
            </div>
        )
    }
}

export default GlobalError

Обратите внимание на строку if (!this.props.error) return null. Она указывает на то, что при отсутствии ошибки компонент ничего не выводит. Это предотвращает постоянный показ красного прямоугольника на странице. Конечно, вы, при желании, можете поменять внешний вид и поведение этого компонента. Например, вместо того, чтобы сбрасывать ошибку по нажатию на x, можно задать тайм-аут в пару секунд, по истечении которого состояние ошибки сбрасывается автоматически.

Теперь, когда всё готово для работы с глобальными ошибками, для задания глобальной ошибки достаточно воспользоваться _setError из Application.js. Например, это можно сделать в том случае, если сервер, после обращения к нему, вернул сообщение об общей ошибке (error: 'GENERIC'). Рассмотрим пример (файл GenericErrorReq.js).

import React, { Component } from 'react'
import axios from 'axios'

class GenericErrorReq extends Component {
    constructor(props) {
        super(props)

        this._callBackend = this._callBackend.bind(this)
    }

    render() {
        return (
            <div>
                <button onClick={this._callBackend}>Click me to call the backend</button>
            </div>
        )
    }

    _callBackend() {
        axios
            .post('/api/city')
            .then(result => {
                // сделать что-нибудь с результатом в том случае, если запрос оказался успешным
            })
            .catch(err => {
                if (err.response.data.error === 'GENERIC') {
                    this.props.setError(err.response.data.description)
                }
            })
    }
}

export default GenericErrorReq

На самом деле, на этом наш разговор об обработке ошибок можно было бы и закончить. Даже если в проекте нужно оповещать пользователя о специфических ошибках, никто не мешает просто поменять глобальное состояние, хранящее ошибку и вывести соответствующее сообщение поверх страницы. Однако тут мы не остановимся и поговорим о специфических ошибках. Во-первых, это руководство по обработке ошибок иначе было бы неполным, а во-вторых, с точки зрения UX-специалистов, неправильно будет показывать сообщения обо всех ошибках так, будто все они — глобальные.

▍Обработка специфических ошибок, возникающих при выполнении запросов

Вот пример специфического сообщения об ошибке, выводимого в том случае, если пользователь пытается удалить из базы данных город, которого там нет.

Сообщение о специфической ошибке

Тут используется тот же принцип, который мы применяли при работе с глобальными ошибками. Только сведения о таких ошибках хранятся в локальном состоянии соответствующих компонентов. Работа с ними очень похожа на работу с глобальными ошибками. Вот код файла SpecificErrorReq.js.

import React, { Component } from 'react'
import axios from 'axios'

import InlineError from './InlineError'

class SpecificErrorRequest extends Component {
    constructor(props) {
        super(props)

        this.state = {
            error: '',
        }

        this._callBackend = this._callBackend.bind(this)
    }

    render() {
        return (
            <div>
                <button onClick={this._callBackend}>Delete your city</button>
                <InlineError error={this.state.error} />
            </div>
        )
    }

    _callBackend() {
        this.setState({
            error: '',
        })

        axios
            .delete('/api/city')
            .then(result => {
                // сделать что-нибудь с результатом в том случае, если запрос оказался успешным
            })
            .catch(err => {
                if (err.response.data.error === 'GENERIC') {
                    this.props.setError(err.response.data.description)
                } else {
                    this.setState({
                        error: err.response.data.description,
                    })
                }
            })
    }
}

export default SpecificErrorRequest

Тут стоит отметить, что для сброса специфических ошибок недостаточно, например, просто нажать на некую кнопку x. То, что пользователь прочёл сообщение об ошибке и закрыл его, не помогает такую ошибку исправить. Исправить её можно, правильно сформировав запрос к серверу, например — введя в ситуации, показанной на предыдущем рисунке, имя города, который есть в базе. В результате очищать сообщение об ошибке имеет смысл, например, после выполнения нового запроса. Сбросить ошибку можно и в том случае, если пользователь внёс изменения в то, что будет использоваться при формировании нового запроса, то есть — при изменении содержимого поля ввода.

▍Ошибки, возникающие в клиентской части приложения

Как уже было сказано, для хранения данных о таких ошибках можно использовать состояние тех же компонентов, которое используется для хранения данных по специфическим ошибкам, поступающим с сервера. Предположим, мы позволяем пользователю отправить на сервер запрос на удаление города из базы только в том случае, если в соответствующем поле ввода есть какой-то текст. Отсутствие или наличие текста в поле можно проверить средствами клиентской части приложения.

В поле ничего нет, мы сообщаем об этом пользователю

Вот код файла SpecificErrorFrontend.js, реализующий вышеописанный функционал.

import React, { Component } from 'react'
import axios from 'axios'

import InlineError from './InlineError'

class SpecificErrorRequest extends Component {
    constructor(props) {
        super(props)

        this.state = {
            error: '',
            city: '',
        }

        this._callBackend = this._callBackend.bind(this)
        this._changeCity = this._changeCity.bind(this)
    }

    render() {
        return (
            <div>
                <input
                    type="text"
                    value={this.state.city}
                    style={{ marginRight: 15 }}
                    onChange={this._changeCity}
                />
                <button onClick={this._callBackend}>Delete your city</button>
                <InlineError error={this.state.error} />
            </div>
        )
    }

    _changeCity(e) {
        this.setState({
            error: '',
            city: e.target.value,
        })
    }

    _validate() {
        if (!this.state.city.length) throw new Error('Please provide a city name.')
    }

    _callBackend() {
        this.setState({
            error: '',
        })

        try {
            this._validate()
        } catch (err) {
            return this.setState({ error: err.message })
        }

        axios
            .delete('/api/city')
            .then(result => {
                // сделать что-нибудь с результатом в том случае, если запрос оказался успешным
            })
            .catch(err => {
                if (err.response.data.error === 'GENERIC') {
                    this.props.setError(err.response.data.description)
                } else {
                    this.setState({
                        error: err.response.data.description,
                    })
                }
            })
    }
}

export default SpecificErrorRequest

▍Интернационализация сообщений об ошибках с использованием кодов ошибок

Возможно, сейчас вы задаётесь вопросом о том, зачем нам нужны коды ошибок (наподобие GENERIC), если мы показываем пользователю только сообщения об ошибках, полученных с сервера. Дело в том, что, по мере роста и развития приложения, оно, вполне возможно, выйдет на мировой рынок, а это означает, что настанет время, когда создателям приложения нужно будет задуматься о поддержке им нескольких языков. Коды ошибок позволяют отличать их друг от друга и выводить сообщения о них на языке пользователя сайта.

Итоги

Надеемся, теперь у вас сформировалось понимание того, как можно работать с ошибками в веб-приложениях. Нечто вроде console.error(err) следует использовать только в отладочных целях, в продакшн подобные вещи, забытые программистом, проникать не должны. Упрощает решение задачи логирования использование какой-нибудь подходящей библиотеки наподобие loglevel.

Уважаемые читатели! Как вы обрабатываете ошибки в своих проектах?

Веб-формы стали неотъемлемой частью веб-приложений. Они часто используется для сбора такой информации о пользователе, как имя, адрес электронной почты, местоположение, возраст и т. д. Но вполне возможно, что какой-то пользователь может ввести данные не в том формате, в котором вы ожидаете.

Чтобы сэкономить время и избежать ненужной нагрузки на ресурсы вашего сервера, вы можете проверить данные формы на стороне клиента (то есть в системе пользователя), используя JavaScript, прежде чем передавать их на веб-сервер для дальнейшей обработки.

Проверка на стороне клиента также полезна для создания лучшего пользовательского взаимодействия; она быстрее, потому что проверка происходит в веб-браузере пользователя, тогда как проверка на стороне сервера происходит на сервере, который требует, чтобы вводимые пользователем данные были сначала отправлены на сервер, где происходит обработка; пользователь также должен дождаться ответа сервера, чтобы узнать, что именно пошло не так.

В этом разделе мы более подробно рассмотрим, как выполнять валидацию форм на JavaScript и обрабатывать любые ошибки ввода.

Проверка на стороне клиента не является заменой или альтернативой проверке на стороне сервера. Вы должны всегда проверять данные формы на стороне сервера, даже если они уже проверены на стороне клиента, потому что пользователь может отключить JavaScript в своем браузере.

Процесс проверки формы обычно состоит из двух частей — проверки обязательных полей и проверки формата данных, которая выполняется для обеспечения того, чтобы тип и формат данных, введенных в форму был валидным.

Что ж, давайте посмотрим как это работает.

Создание HTML-формы

Давайте сначала создадим простую HTML-форму, которую мы будем проверять на стороне клиента, используя JavaScript, когда пользователь нажимает кнопку отправки (submit). Для этого создадим HTML-файл с именем application-form.html и поместим в него следующий код, а затем сохраним его где-нибудь в вашей системе.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Simple HTML Form</title>
    <link rel="stylesheet" href="style.css">
    <script src="validator.js"></script>
</head>
<body>
<form name="contactForm" onsubmit="return validateForm()" action="confirmation.php" method="post">
    <h2>Форма заявки</h2>
    <div class="row">
        <label>Имя</label>
        <input type="text" name="name">
        <div class="error" id="nameErr"></div>
    </div>
    <div class="row">
        <label>Email</label>
        <input type="text" name="email">
        <div class="error" id="emailErr"></div>
    </div>
    <div class="row">
        <label>Номер телефона</label>
        <input type="text" name="mobile" maxlength="10">
        <div class="error" id="mobileErr"></div>
    </div>
    <div class="row">
        <label>Страна</label>
        <select name="country">
            <option>Select</option>
            <option>Australia</option>
            <option>India</option>
            <option>United States</option>
            <option>United Kingdom</option>
        </select> 
        <div class="error" id="countryErr"></div>
    </div>
    <div class="row">
        <label>Пол</label>
        <div class="form-inline">
            <label><input type="radio" name="gender" value="male">Мужской</label>
            <label><input type="radio" name="gender" value="female">Женский</label> 
        </div>
        <div class="error" id="genderErr"></div>
    </div>
    <div class="row">
        <label>Хобби <i>(не обязательно)</i></label>
        <div class="form-inline">
            <label><input type="checkbox" name="hobbies[]" value="sports">Спорт</label>
            <label><input type="checkbox" name="hobbies[]" value="movies">Кино</label>
            <label><input type="checkbox" name="hobbies[]" value="music">Музыка</label>  
        </div>
    </div>        
    <div class="row">
        <input type="submit" value="Submit">
    </div>
</form>
</body>
</html>

Создание скрипта валидации формы

Теперь мы собираемся создать файл JavaScript, который содержит наш полный скрипт проверки.

Что ж, давайте создадим файл JavaScript с именем validator.js и поместим в него следующий код, а затем сохраним его в том же месте, где вы сохранили предыдущий HTML-файл. Просмотрите каждую строку следующего примера кода, чтобы понять, как работает проверка JavaScript:

// Определяем функции для отображения сообщения об ошибке
function printError(elemId, hintMsg) {
    document.getElementById(elemId).innerHTML = hintMsg;
}

// Определяем функции для проверки формы
function validateForm() {
    // Получение значений элементов формы
    var name = document.contactForm.name.value;
    var email = document.contactForm.email.value;
    var mobile = document.contactForm.mobile.value;
    var country = document.contactForm.country.value;
    var gender = document.contactForm.gender.value;
    var hobbies = [];
    var checkboxes = document.getElementsByName("hobbies[]");
    for(var i=0; i < checkboxes.length; i++) {
        if(checkboxes[i].checked) {
            // Заполняем массив хобби выбранными значениями
            hobbies.push(checkboxes[i].value);
        }
    }
    
    // Определяем переменные ошибок со значением по умолчанию
    var nameErr = emailErr = mobileErr = countryErr = genderErr = true;
    
    // Проверяем имя
    if(name == "") {
        printError("nameErr", "Пожалуйста, введите ваше имя");
    } else {
        var regex = /^[a-zA-Z\s]+$/;                
        if(regex.test(name) === false) {
            printError("nameErr", "Пожалуйста, введите правильное имя");
        } else {
            printError("nameErr", "");
            nameErr = false;
        }
    }
    
    // Проверяем адрес электронной почты
    if(email == "") {
        printError("emailErr", "Пожалуйста, введите адрес вашей электронной почты");
    } else {
        // Регулярное выражение для базовой проверки электронной почты
        var regex = /^\S+@\S+\.\S+$/;
        if(regex.test(email) === false) {
            printError("emailErr", "Пожалуйста, введите действительный адрес электронной почты");
        } else{
            printError("emailErr", "");
            emailErr = false;
        }
    }
    
    // Проверяем номер мобильного телефона
    if(mobile == "") {
        printError("mobileErr", "Пожалуйста, введите номер вашего мобильного телефона");
    } else {
        var regex = /^[1-9]\d{9}$/;
        if(regex.test(mobile) === false) {
            printError("mobileErr", "Пожалуйста, введите действительный 10-значный номер мобильного телефона");
        } else{
            printError("mobileErr", "");
            mobileErr = false;
        }
    }
    
    // Проверяем страну
    if(country == "Select") {
        printError("countryErr", "Пожалуйста, выберите вашу страну");
    } else {
        printError("countryErr", "");
        countryErr = false;
    }
    
    // Проверяем пол
    if(gender == "") {
        printError("genderErr", "Пожалуйста, выберите ваш пол");
    } else {
        printError("genderErr", "");
        genderErr = false;
    }
    
    // Запрещаем отправку формы в случае ошибок
    if((nameErr || emailErr || mobileErr || countryErr || genderErr) == true) {
       return false;
    } else {
        // Создаем строки из входных данных для предварительного просмотра
        var dataPreview = "Вы ввели следующие данные: \n" +
                          "Имя: " + name + "\n" +
                          "Email: " + email + "\n" +
                          "Номер: " + mobile + "\n" +
                          "Страна: " + country + "\n" +
                          "Пол: " + gender + "\n";
        if(hobbies.length) {
            dataPreview += "Hobbies: " + hobbies.join(", ");
        }
        // Отображаем входные данные в диалоговом окне перед отправкой формы
        alert(dataPreview);
    }
};

Доступ к значению отдельного поля формы можно получить с помощью синтаксиса document.formName.fieldName.value или document.getElementsByName(name).value. Но, чтобы получить значения из поля формы, которое поддерживает множественный выбор, например, группу чекбоксов, вам нужно использовать оператор цикла, как показано в примере выше (строки с 14 по 21).

Чтобы проверить, является ли формат входных данных правильным или нет, мы должны использовать регулярные выражения. Это один из наиболее эффективных методов проверки пользовательских данных.

Вышеприведенный скрипт также отображает данные, введенные пользователем в диалоговом окне для предварительного просмотра перед отправкой формы на веб-сервер.

Вы можете проверить формат электронной почты с помощью регулярного выражения. Но пользователь может ввести адрес электронной почты, который правильно отформатирован, но не существует. Поэтому для проверки подлинности электронной почты отправьте пользователю электронное письмо с подтверждением и убедитесь, что электронное письмо действительно существует.

Добавление стилей для формы

Наконец, создайте файл с именем style.css и поместите в него следующий код, затем сохраните его также в том же месте, где вы сохранили два предыдущих файла. Это правила для визуального оформления нашей формы.

body {
    font-size: 16px;
    background: #f9f9f9;
    font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
}
h2 {
    text-align: center;
    text-decoration: underline;
}
form {
    width: 300px;
    background: #fff;
    padding: 15px 40px 40px;
    border: 1px solid #ccc;
    margin: 50px auto 0;
    border-radius: 5px;
}
label {
    display: block;
    margin-bottom: 5px
}
label i {
    color: #999;
    font-size: 80%;
}
input, select {
    border: 1px solid #ccc;
    padding: 10px;
    display: block;
    width: 100%;
    box-sizing: border-box;
    border-radius: 2px;
}
.row {
    padding-bottom: 10px;
}
.form-inline {
    border: 1px solid #ccc;
    padding: 8px 10px 4px;
    border-radius: 2px;
}
.form-inline label, .form-inline input {
    display: inline-block;
    width: auto;
    padding-right: 15px;
}
.error {
    color: red;
    font-size: 90%;
}
input[type="submit"] {
    font-size: 110%;
    font-weight: 100;
    background: #006dcc;
    border-color: #016BC1;
    box-shadow: 0 3px 0 #0165b6;
    color: #fff;
    margin-top: 10px;
    cursor: pointer;
}
input[type="submit"]:hover {
    background: #0165b6;
}

Теперь откройте файл application-form.html в веб-браузере и попробуйте заполнить некоторые данные и посмотреть, как скрипт реагирует на ввод недопустимых данных в поле формы.

Чтобы узнать о проверке на стороне сервера, ознакомьтесь с Руководством по валидации форм в PHP.

Похожие посты

  • 1099

Сортировка — обычная задача при работе с массивами. Она будет использоваться, например, если вы захотите отобразить названия городов или регионов в алфавитном порядке. JavaScript массив (array) имеет встроенный метод sort() для сортировки элементов массива в алфавитном порядке. Следующий пример демонстрирует, как это работает: Реверсирование массива Вы можете использовать метод reverse(), чтобы изменить порядок элементов массива…

  • 746

Массивы — это сложные переменные, которые позволяют нам хранить группы значений под одним именем переменной. Массивы JavaScript могут хранить любое допустимое значение, включая строки, числа, объекты, функции и даже другие массивы, что позволяет создавать более сложные структуры данных, такие как массив объектов или массив массивов. Предположим, вы хотите сохранить название цветов в своем коде JavaScript….

  • 312

Атрибуты — это специальные слова, используемые внутри начального тега HTML-элемента для управления поведением тега или предоставления дополнительной информации о теге. JavaScript предоставляет несколько методов для добавления, удаления или изменения атрибутов HTML-элемента. В этом разделе мы узнаем об этих методах подробно. Получение значения атрибута элемента Метод getAttribute() используется для получения текущего значения атрибута элемента. Если указанный…

В современном веб-разработке важнейшей задачей является обработка и валидация форм, которые пользователи заполняют на сайте. Ошибки в этих формах могут привести к нежелательным последствиям, таким как ошибки в базе данных, нарушение безопасности сайта и неудовлетворительный пользовательский опыт. Одним из способов решения этой проблемы является применение JavaScript – мощного языка программирования, который позволяет осуществлять проверку вводимых пользователем данных и уведомлять его об ошибках.

Что такое валидация формы?

Валидация формы – это процесс проверки правильности заполнения данных пользователя на сайте. При валидации формы JavaScript анализирует данные, введенные пользователем, и проверяет их на соответствие заданным критериям. Эта проверка может быть выполнена на сервере или на клиенте. Варианты валидации на сервере позволяют произвести более глубокую проверку, чем валидация на клиенте, поскольку они имеют доступ к базе данных. Однако, проверка на клиенте может быть полезна для повышения удобства использования сайта, поскольку пользователь может получать мгновенную обратную связь о правильности его ввода.

Введение в обработку форм на JavaScript

Прежде чем мы начнем рассматривать способы осуществления валидации, стоит упомянуть, что обработка и валидация форм с помощью JavaScript – это необходимо стилизовать с формой HTML. Для этого можно использовать CSS или любую библиотеку CSS, например, Bootstrap, для создания красивых и функциональных форм.

Для начала надо настроить HTML-разметку формы. Вот пример простой формы в HTML:

<form>
  <label for="name">Имя:</label>
<input type="text" id="name" name="name" required>

<label for="email">Email:</label>
<input type="email" id="email" name="email" required>

<label for="password">Пароль:</label>
<input type="password" id="password" name="password" required>

<button type="submit">Отправить</button>
</form>

Это минимальный набор полей, который может содержать форма, но в зависимости от сложности вашего сайта формы могут быть значительно более сложными.

В этой разметке три основных элемента для обработки формы:

1. Тег `form` — обертка для всей формы.
2. Тег `input` – элементы ввода, такие как текстовые поля, флажки (checkbox) и радиокнопки (radio).
3. Тег `button` – кнопка отправки формы.

С помощью JavaScript мы можем обработать эти элементы и выполнить валидацию их значений.

Обработка и валидация данных формы с помощью JavaScript

В этом разделе мы рассмотрим несколько примеров обработки форм. Первый пример – валидация имени пользователя.

let nameInput = document.getElementById('name');
let name = nameInput.value.trim();

if (name.length < 2) {
  alert('Имя должно содержать не менее двух символов');
}

Этот скрипт извлекает значение, введенное пользователем в поле имени, и проверяет, было ли оно введено правильно. Если имя меньше двух символов, появится сообщение об ошибке. Эта проверка может быть дополнена другими условиями, такими как проверка на специальные символы, цифры и т.д.

Другой пример проверки формы – валидация электронной почты:

let emailInput = document.getElementById('email');
let email = emailInput.value.trim();

let regex = /^\S+@\S+\.\S+$/;
if (!regex.test(email)) {
  alert('Неправильный формат email');
}

Этот скрипт сначала извлекает значение из поля электронной почты, а затем использует регулярное выражение для проверки правильности его формата. Если значение не соответствует формату email, появится сообщение об ошибке.

Вы также можете создавать свои собственные методы проверки. К примеру, можно написать метод, который проверяет одинаковы ли пароли, введенные пользователем:

function checkPasswords() {
  let passwordInput = document.getElementById('password');
  let confirmPasswordInput = document.getElementById('confirmPassword');

  let password = passwordInput.value.trim();
  let confirmPassword = confirmPasswordInput.value.trim();

  if (password !== confirmPassword) {
    alert('Пароли не совпадают');
    return false;
  }

  return true;
}

Этот метод отличается от предыдущих: он проверяет сразу два значения – поля пароля и поля подтверждения пароля. Если значения не совпадают, пользователь получит сообщение об ошибке.

Надо отметить, что все эти примеры работают только при активации событий на элементах формы, таких как изменение значения или отправка формы.

Обработка ошибок формы

Валидация формы – это не единственный момент, на который следует обратить внимание. Когда пользователь вводит неправильные данные в форму, ему следует предоставить мгновенную обратную связь. Например, при вводе неверного адреса электронной почты или пароля, изначально следует подсветить поля ошибок, чтобы пользователь понимал, где произошла ошибка.

Здесь нужно использовать CSS-классы для определения полей с ошибками. К примеру:

input.error {
  border: 1px solid red;
}

Когда пользователь вводит данные неправильно и фронтенд обнаруживает ошибку, он может использовать этот класс для определения и подсветки поля ошибок. Вот пример скрипта, который находит все поля с ошибками на странице и выделяет их:

let inputs = document.querySelectorAll('input');
let errorInputs = [];

for (let i = 0; i < inputs.length; i++) {
  let input = inputs[i];
  let value = input.value.trim();

  if (input.required && value === '') {
    errorInputs.push(input);
  } else if (input.type === 'email' && !regex.test(value)) {
    errorInputs.push(input);
  } else if (input.type === 'password' && !checkPasswords()) {
    errorInputs.push(input);
  }
}

for (let i = 0; i < errorInputs.length; i++) {
  errorInputs[i].classList.add('error');
}

После нахождения полей с ошибками, скрипт добавляет им класс ‘error’, который используется в CSS для подсветки полей.

Заключение

Обработка и валидация форм – это очень важный аспект веб-разработки. Без них пользователи могут вводить неправильные данные, что может привести к ненужным ошибкам и ухудшению пользовательского опыта. С помощью JavaScript можно производить различные проверки на правильность данных, что обеспечивает повышение удобства использования сайта. Не забывайте обратить внимание на обработку ошибок, что позволяет пользователям получать оперативную обратную связь при вводе неправильных данных.

Summary: in this tutorial, you’ll learn about JavaScript form validation by building a signup form from scratch.

What is form validation

Before submitting data to the server, you should check the data in the web browser to ensure that the submitted data is in the correct format.

To provide quick feedback, you can use JavaScript to validate data. This is called client-side validation.

If you don’t carry the client-side validation, it may cause a bad user experience. In this case, you may feel a noticeable delay because it takes time for the form data to transfer between the web browsers and the server.

Unlike the client-side validation that performs in the web browser, the server-side validation is performed on the server. It’s critical always to implement the server-side validation.

The reason is that client-side validation is quite easy to bypass. Malicious users can disable JavaScript and submit bad data to your server.

In this tutorial, you’re going to focus on the client-side validation only.

Client-side validation options

When it comes to client-side validation, you’ll have two options:

  • JavaScript validation: you develop the validation logic using JavaScript. Or you can use a library to do so.
  • Built-in form validation: you can use the HTML5 form validation features. This validation has a better performance than JavaScript validation. However, it isn’t as customizable as JavaScript validation.

You will create a simple signup form with four input fields: username, email, password, and confirm password.

When you click the sign up without filling anything or filling incorrect data format, the form will show error messages:

You’ll validate the following:

  • Username cannot be blank and has at least 3 characters and cannot be longer than 25 characters.
  • Email is mandatory and valid.
  • Password has eight characters or longer. And it must contain 1 lowercase character, 1 uppercase character, 1 number, and at least one special character in this set (!@#$%^&*).
  • The confirm password must be the same as the password.

Create the project structure

First, create the form-validation folder that stores all the source code files of the project.

Second, create the js and css folders inside the form-validation folder.

Third, create the style.css in the css folder, the app.js in the js folder, and index.html directly in the form-validation folder.

The final project structure will look like this:

Build the HTML form

First, open the index.html file and enter the following code:

<!DOCTYPE html>
<html>
<head>
    <title>JavaScript Form Validation Demo</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>

    <script src="js/app.js"></script>
</body>

</html>Code language: HTML, XML (xml)

In this HTML file, we place the style.css file in the head section and app.js file in the body section before the closing </body> tag.

Second, add the following HTML markup to create the signup form. The final index.html file will look like the following:

 <!DOCTYPE html>
<html>
<head>
    <title>JavaScript Form Validation Demo</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <div class="container">
        <form id="signup" class="form">
            <h1>Sign Up</h1>
            <div class="form-field">
                <label for="username">Username:</label>
                <input type="text" name="username" id="username" autocomplete="off">
                <small></small>
            </div>

            <div class="form-field">
                <label for="email">Email:</label>
                <input type="text" name="email" id="email" autocomplete="off">
                <small></small>
            </div>

            <div class="form-field">
                <label for="password">Password:</label>
                <input type="password" name="password" id="password" autocomplete="off">
                <small></small>
            </div>


            <div class="form-field">
                <label for="confirm-password">Confirm Password:</label>
                <input type="password" name="confirm-password" id="confirm-password" autocomplete="off">
                <small></small>
            </div>

            <div class="form-field">
                <input type="submit" value="Sign Up">
            </div>
        </form>
    </div>

    <script src="js/app.js"></script>
</body>
</html>Code language: HTML, XML (xml)

The notable thing about the signup form is that each field is wrapped in a div with the class form-field.

Each form field has three elements:

  • A label
  • An input field
  • A <small> element

You’ll use the <small> tag to show the error message to the users.

If an input field isn’t valid, we’ll make its border color red by adding the error class to the form-field element. It’ll look like this:

<div class="form-field error">
   <label for="username">Username:</label>
   <input type="text" name="username" id="username" autocomplete="off">
   <small></small>
</div>Code language: JavaScript (javascript)

If the value of an input field is valid, then we’ll make its border color green by adding the success class to the form-field element as follows:

<div class="form-field success">
   <label for="username">Username:</label>
   <input type="text" name="username" id="username" autocomplete="off">
   <small></small>
</div>Code language: JavaScript (javascript)

Check out the style.css for details of the.error and .success classes.

Select the form fields and add the submit event listener

In the app.js file, you’ll first use the document.querySelector() method to select the input fields and the form:

const usernameEl = document.querySelector('#username');
const emailEl = document.querySelector('#email');
const passwordEl = document.querySelector('#password');
const confirmPasswordEl = document.querySelector('#confirm-password');

const form = document.querySelector('#signup');Code language: JavaScript (javascript)

And then you attach the submit event listener to the form by using the addEventListener() method:

form.addEventListener('submit', function (e) {
    // prevent the form from submitting
    e.preventDefault();

});Code language: JavaScript (javascript)

In the event listener, you need to call the e.preventDefault() to prevent the form from submitting once the submit button is clicked.

Develop utility functions

Before validating the form, you can develop some reusable utility functions to check if:

  • A field is required.
  • The length of a field is between min and max.
  • The email is in a valid format.
  • The password is strong.

The following isRequired() function returns true if the input argument is empty:

const isRequired = value => value === '' ? false : true;Code language: JavaScript (javascript)

The following isBetween() function returns false if the length argument is not between the min and max argument:

const isBetween = (length, min, max) => length < min || length > max ? false : true;Code language: JavaScript (javascript)

To check the email is valid, you’ll use a regular expression:

const isEmailValid = (email) => {
    const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
    return re.test(email);
};Code language: JavaScript (javascript)

To check if a password is strong, which match a specified pattern, you’ll also use a regular expression:

const isPasswordSecure = (password) => {
    const re = new RegExp("^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})");
    return re.test(password);
};Code language: JavaScript (javascript)

The following table illustrates the meaning of each part of the regular expression used to validate the password:

Password RegEx Meaning
^ The password starts
(?=.*[a-z]) The password must contain at least one lowercase character
(?=.*[A-Z]) The password must contain at least one uppercase character
(?=.*[0-9]) The password must contain at least one number
(?=.*[!@#$%^&*]) The password must contain at least one special character.
(?=.{8,}) The password must be eight characters or longer

Develop functions that show the error / success

The following showError() function highlights the border of the input field and displays an error message if the input field is invalid:

const showError = (input, message) => {
    // get the form-field element
    const formField = input.parentElement;
    // add the error class
    formField.classList.remove('success');
    formField.classList.add('error');

    // show the error message
    const error = formField.querySelector('small');
    error.textContent = message;
};Code language: JavaScript (javascript)

How it works.

First, get the parent element of the input field, which is the <div> element that contains the form-field class:

const formField = input.parentElement;Code language: JavaScript (javascript)

Second, remove the success class and add the error class to the form-field element:

formField.classList.remove('success');
formField.classList.add('error');Code language: JavaScript (javascript)

Third, select the <small> element inside the form-field element:

const error = formField.querySelector('small');Code language: JavaScript (javascript)

Notice that you use the formField.querySelector() instead of the document.querySelector().

Finally, set the error message to its textContent property of the <small> element:

error.textContent = message;

The function that shows the success indicator is similar to the showError() function:

const showSuccess = (input) => {
    // get the form-field element
    const formField = input.parentElement;

    // remove the error class
    formField.classList.remove('error');
    formField.classList.add('success');

    // hide the error message
    const error = formField.querySelector('small');
    error.textContent = '';
}Code language: JavaScript (javascript)

Unlike the showError() function, the showSuccess() function removes the error class, adds the success class, and set the error message to blank.

Now, you can use the utility function above to check for each field.

Develop input field validating functions

You’ll develop four functions for validating values of the form fields:

1) Validate the username field

The following checkUsername() function uses:

  • The isRequired() function to check if the username is provided.
  • The isBetween() function to check if the length of the username is between 3 and 25 characters.
  • The showError() and showSuccess() functions to show the error and success indicator.

The function returns true if the field passes the checks.

const checkUsername = () => {

    let valid = false;
    const min = 3,
        max = 25;
    const username = usernameEl.value.trim();

    if (!isRequired(username)) {
        showError(usernameEl, 'Username cannot be blank.');
    } else if (!isBetween(username.length, min, max)) {
        showError(usernameEl, `Username must be between ${min} and ${max} characters.`)
    } else {
        showSuccess(usernameEl);
        valid = true;
    }
    return valid;
}Code language: JavaScript (javascript)

2) Validate the email field

The checkEmail() function returns true if the email is provided and valid.

It uses the isRequired() and isEmailValid() functions for checking. And it uses the showError() and showSuccess() functions to provide feedback in case of error and success.

const checkEmail = () => {
    let valid = false;
    const email = emailEl.value.trim();
    if (!isRequired(email)) {
        showError(emailEl, 'Email cannot be blank.');
    } else if (!isEmailValid(email)) {
        showError(emailEl, 'Email is not valid.')
    } else {
        showSuccess(emailEl);
        valid = true;
    }
    return valid;
}Code language: JavaScript (javascript)

3) Validate the password field

The following checkPassword() function checks the password field if it is provided and match the required format:

const checkPassword = () => {

    let valid = false;

    const password = passwordEl.value.trim();

    if (!isRequired(password)) {
        showError(passwordEl, 'Password cannot be blank.');
    } else if (!isPasswordSecure(password)) {
        showError(passwordEl, 'Password must has at least 8 characters that include at least 1 lowercase character, 1 uppercase characters, 1 number, and 1 special character in (!@#$%^&*)');
    } else {
        showSuccess(passwordEl);
        valid = true;
    }

    return valid;
};Code language: JavaScript (javascript)

4) Validate the confirm password field

The checkConfirmPassword() function checks if the confirm password is the same as the password.

const checkConfirmPassword = () => {
    let valid = false;
    // check confirm password
    const confirmPassword = confirmPasswordEl.value.trim();
    const password = passwordEl.value.trim();

    if (!isRequired(confirmPassword)) {
        showError(confirmPasswordEl, 'Please enter the password again');
    } else if (password !== confirmPassword) {
        showError(confirmPasswordEl, 'Confirm password does not match');
    } else {
        showSuccess(confirmPasswordEl);
        valid = true;
    }

    return valid;
};Code language: JavaScript (javascript)

Modifying the submit event handler

Now, you can use the functions that validate the input fields in the submit event handler:

form.addEventListener('submit', function (e) {
    // prevent the form from submitting
    e.preventDefault();

    // validate forms
    let isUsernameValid = checkUsername(),
        isEmailValid = checkEmail(),
        isPasswordValid = checkPassword(),
        isConfirmPasswordValid = checkConfirmPassword();

    let isFormValid = isUsernameValid &&
        isEmailValid &&
        isPasswordValid &&
        isConfirmPasswordValid;

    // submit to the server if the form is valid
    if (isFormValid) {

    }
});Code language: JavaScript (javascript)

How it works:

  • First, call each individual function to validate username, email, password, and confirm password fields.
  • Second, use the && operator to determine if the form is valid. The form is valid only if all fields are valid.
  • Finally, submit data to the server if the form is valid specified the isFormValid flag. Note that submitting form data to the server isn’t covered in this tutorial.

Now, you can open the index.html file, enter some values and click the submit button to test it.

Add Instant feedback feature

The form only shows the error or success when you click the Sign Up button.

To provide instant feedback, you can attach an event listener to the input event of each field and validate it.

It’s even better to use the event delegation so that you attach the input event listener to the form and validate each field based on the current field id, like this:

form.addEventListener('input', function (e) {
    switch (e.target.id) {
        case 'username':
            checkUsername();
            break;
        case 'email':
            checkEmail();
            break;
        case 'password':
            checkPassword();
            break;
        case 'confirm-password':
            checkConfirmPassword();
            break;
    }
});Code language: JavaScript (javascript)

If you open the index.html and enter some data, you’ll see that the form shows the feedback either error or success instantly.

Also, you improve the performance of the form by using the debouncing technique.

Technically, you’ll wait for the users to pause the typing for a small amount of time or stop typing before validating the input.

For the detail of the debouncing technique, check it out in this tutorial.

The following illustrates the debounce() function:

const debounce = (fn, delay = 500) => {
    let timeoutId;
    return (...args) => {
        // cancel the previous timer
        if (timeoutId) {
            clearTimeout(timeoutId);
        }
        // setup a new timer
        timeoutId = setTimeout(() => {
            fn.apply(null, args)
        }, delay);
    };
};Code language: JavaScript (javascript)

Now, you can pass the input event handler to the debounce() function to debounce it:

form.addEventListener('input', debounce(function (e) {
    switch (e.target.id) {
        case 'username':
            checkUsername();
            break;
        case 'email':
            checkEmail();
            break;
        case 'password':
            checkPassword();
            break;
        case 'confirm-password':
            checkConfirmPassword();
            break;
    }
}));Code language: JavaScript (javascript)

If you enter data to a form field to trigger the input event, you’ll see that the error or success message will have a bit of delay.

The following shows the complete app.js file:

const usernameEl = document.querySelector('#username');
const emailEl = document.querySelector('#email');
const passwordEl = document.querySelector('#password');
const confirmPasswordEl = document.querySelector('#confirm-password');

const form = document.querySelector('#signup');


const checkUsername = () => {

    let valid = false;

    const min = 3,
        max = 25;

    const username = usernameEl.value.trim();

    if (!isRequired(username)) {
        showError(usernameEl, 'Username cannot be blank.');
    } else if (!isBetween(username.length, min, max)) {
        showError(usernameEl, `Username must be between ${min} and ${max} characters.`)
    } else {
        showSuccess(usernameEl);
        valid = true;
    }
    return valid;
};


const checkEmail = () => {
    let valid = false;
    const email = emailEl.value.trim();
    if (!isRequired(email)) {
        showError(emailEl, 'Email cannot be blank.');
    } else if (!isEmailValid(email)) {
        showError(emailEl, 'Email is not valid.')
    } else {
        showSuccess(emailEl);
        valid = true;
    }
    return valid;
};

const checkPassword = () => {
    let valid = false;


    const password = passwordEl.value.trim();

    if (!isRequired(password)) {
        showError(passwordEl, 'Password cannot be blank.');
    } else if (!isPasswordSecure(password)) {
        showError(passwordEl, 'Password must has at least 8 characters that include at least 1 lowercase character, 1 uppercase characters, 1 number, and 1 special character in (!@#$%^&*)');
    } else {
        showSuccess(passwordEl);
        valid = true;
    }

    return valid;
};

const checkConfirmPassword = () => {
    let valid = false;
    // check confirm password
    const confirmPassword = confirmPasswordEl.value.trim();
    const password = passwordEl.value.trim();

    if (!isRequired(confirmPassword)) {
        showError(confirmPasswordEl, 'Please enter the password again');
    } else if (password !== confirmPassword) {
        showError(confirmPasswordEl, 'The password does not match');
    } else {
        showSuccess(confirmPasswordEl);
        valid = true;
    }

    return valid;
};

const isEmailValid = (email) => {
    const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
    return re.test(email);
};

const isPasswordSecure = (password) => {
    const re = new RegExp("^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})");
    return re.test(password);
};

const isRequired = value => value === '' ? false : true;
const isBetween = (length, min, max) => length < min || length > max ? false : true;


const showError = (input, message) => {
    // get the form-field element
    const formField = input.parentElement;
    // add the error class
    formField.classList.remove('success');
    formField.classList.add('error');

    // show the error message
    const error = formField.querySelector('small');
    error.textContent = message;
};

const showSuccess = (input) => {
    // get the form-field element
    const formField = input.parentElement;

    // remove the error class
    formField.classList.remove('error');
    formField.classList.add('success');

    // hide the error message
    const error = formField.querySelector('small');
    error.textContent = '';
}


form.addEventListener('submit', function (e) {
    // prevent the form from submitting
    e.preventDefault();

    // validate fields
    let isUsernameValid = checkUsername(),
        isEmailValid = checkEmail(),
        isPasswordValid = checkPassword(),
        isConfirmPasswordValid = checkConfirmPassword();

    let isFormValid = isUsernameValid &&
        isEmailValid &&
        isPasswordValid &&
        isConfirmPasswordValid;

    // submit to the server if the form is valid
    if (isFormValid) {

    }
});


const debounce = (fn, delay = 500) => {
    let timeoutId;
    return (...args) => {
        // cancel the previous timer
        if (timeoutId) {
            clearTimeout(timeoutId);
        }
        // setup a new timer
        timeoutId = setTimeout(() => {
            fn.apply(null, args)
        }, delay);
    };
};

form.addEventListener('input', debounce(function (e) {
    switch (e.target.id) {
        case 'username':
            checkUsername();
            break;
        case 'email':
            checkEmail();
            break;
        case 'password':
            checkPassword();
            break;
        case 'confirm-password':
            checkConfirmPassword();
            break;
    }
}));Code language: JavaScript (javascript)

And here is the final form.

Summary

  • What client-side validation is and the difference between the client-side vs. server-side validation.
  • How to compose a form and combine JavaScript and CSS to validate input fields.
  • How to use regular expressions to check if field values are in the correct format.
  • How to use the event delegation technique.
  • How to use the debouncing technique to improve the performance for the form validation.

Was this tutorial helpful ?

Cover image for Form validation using javascript

Adam Nagy

Adam Nagy

Posted on

• Updated on



 



 



 



 



 

Working with forms is an every day task for almost every web developer. No matter what site you’ll create it will use forms. Validating the form data on the client side is a nice-to-have feature when it comes to user experience. In this tutorial we’ll create a simple form validation using javascript.

While client-side form validation gives a nice user experience, it can be tricked and bypassed really easily. To prevent malicious use, you should always validate form data on the server side

Video Tutorial

If you would watch a detailed step-by-step video instead you can check out the video I made covering this project on my Youtube Channel:

HTML

Let’s start with the HTML markup. We’ll have a container div, that we’ll use to position and style our form. Inside that, not surprisingly, we’ll create a form, we also set an id for it, and set the action to / since we don’t really want to submit this form.

We’ll create four input fields, for the username, email, password, and password confirmation. For styling and control purposes we’ll wrap these input tags into divs with the class input control. Each of these input controls will contain a label, an input, and a div with the class error. Every input should have an id and name attribute. The label’s should have a matching for property with the corresponding input tag’s name attribute. For the input type we will use text for the username and email, and use password for the password and the password confirmation. The div with the error class will hold the error messages for the specific input field. It will be empty for now, we will modify it from javascript.

Lastly, we have to add a button to «submit» our form. In this example we won’t really submit the form just simulate it. For the submit button I’ll use a button with a type of submit.

<div class="container">
        <form id="form" action="/">
            <h1>Registration</h1>
            <div class="input-control">
                <label for="username">Username</label>
                <input id="username" name="username" type="text">
                <div class="error"></div>
            </div>
            <div class="input-control">
                <label for="email">Email</label>
                <input id="email" name="email" type="text">
                <div class="error"></div>
            </div>
            <div class="input-control">
                <label for="password">Password</label>
                <input id="password"name="password" type="password">
                <div class="error"></div>
            </div>
            <div class="input-control">
                <label for="password2">Password again</label>
                <input id="password2"name="password2" type="password">
                <div class="error"></div>
            </div>
            <button type="submit">Sign Up</button>
        </form>
    </div>

Enter fullscreen mode

Exit fullscreen mode

That is the HTML markup that we need for our form. Let’s style it a bit with CSS.

CSS

We’ll give a simple clean spacious design for this tutorial. I’ll set a linear gradient as the background and I’ll use a custom google font, that you can install from here.

body {
    background: linear-gradient(to right, #0f2027, #203a43, #2c5364);
    font-family: 'Poppins', sans-serif;
}

Enter fullscreen mode

Exit fullscreen mode

We’ll give a fix width to our form, and center it with margins, also I’ll give it a top margin to move it down a bit vertically. To have more space we apply 20px of padding. We’ll set a fixed font size, a light background color and also set a border radius to have rounded corners.

#form {
    width: 300px;
    margin: 20vh auto 0 auto;
    padding: 20px;
    background-color: whitesmoke;
    border-radius: 4px;
    font-size: 12px;
}

Enter fullscreen mode

Exit fullscreen mode

For the form title, we’ll use a dark text color, and center it horizontally using text-align: center. The submit button should stand out so we’ll use a blue background color, and white text color. We also remove the browser default borders and give it a little border-radius. We’ll give it a little spacing with paddings and margins, and make it full-width by applying 100% width.

#form h1 {
    color: #0f2027;
    text-align: center;
}

#form button {
    padding: 10px;
    margin-top: 10px;
    width: 100%;
    color: white;
    background-color: rgb(41, 57, 194);
    border: none;
    border-radius: 4px;
}

Enter fullscreen mode

Exit fullscreen mode

To have the inputs stacked below each other we’ll use flexbox. To do that we’ll set display: flex; and flex-direction: column. For the inputs we’ll set a grey border, with a little border-radius. We’ll set the display property to block, and make them full-width, by applying width 100%. We’ll also set a little padding, so it’ll be more spacious. I’ll also remove the outline when the input is in focus, by setting outline: 0.

.input-control {
    display: flex;
    flex-direction: column;
}

.input-control input {
    border: 2px solid #f0f0f0;
    border-radius: 4px;
    display: block;
    font-size: 12px;
    padding: 10px;
    width: 100%;
}

.input-control input:focus {
    outline: 0;
}

Enter fullscreen mode

Exit fullscreen mode

We’ll use two classes («success» and «error») to give visual feedback to the user on whether the input’s value is valid or not. We’ll apply these classes from javascript to the input-control div which contains the specific input field. When the success class is present we will set a green border color, otherwise if error is present we’ll use a red border color instead. For the error div we’ll use a smaller font-size and a red color to show the error messages.

.input-control.success input {
    border-color: #09c372;
}

.input-control.error input {
    border-color: #ff3860;
}

.input-control .error {
    color: #ff3860;
    font-size: 9px;
    height: 13px;
}

Enter fullscreen mode

Exit fullscreen mode

Let’s do the validation in javascript next!

Javascript

The first thing we have to do is to save references for the form, and the input fields. As we gave id for every input and the form we can easily to do by using getElementById.

const form = document.getElementById('form');
const username = document.getElementById('username');
const email = document.getElementById('email');
const password = document.getElementById('password');
const password2 = document.getElementById('password2');

Enter fullscreen mode

Exit fullscreen mode

To prevent the form for automatically submit we have to attach and event listener to our form’s submit event. In this event handler function we have to call preventDefault() function to prevent the form from submitting automatically. Instead of submitting we’ll call the validateInputs function, which will validate the inputs and if we want to we can submit the form in there after every check passes, but we won’t do that in this tutorial. We’ll create this validateInputs shortly.

form.addEventListener('submit', e => {
    e.preventDefault();

    validateInputs();
});

Enter fullscreen mode

Exit fullscreen mode

We’ll also create two helper functions: setErrorsetSuccess. We’ll use these helper functions to set the error or success states of the input controls. Let’s start with the setError one. It receives two parameters: element, and message. The element will be the input element that is in the specific input-control. So first we have to get the input control parent div. We’ll save it into the inputControl variable, and get the input control div by using the parent property of the input element. Next we have to gather the error div, and save it into a variable. We can do that by querying the input control with the error class.
Now we have to set the error div’s innerText to be the message that we got in parameters, and remove the success class from the input control (if it exists) and add the error class.

const setError = (element, message) => {
    const inputControl = element.parentElement;
    const errorDisplay = inputControl.querySelector('.error');

    errorDisplay.innerText = message;
    inputControl.classList.add('error');
    inputControl.classList.remove('success')
}

Enter fullscreen mode

Exit fullscreen mode

The setSuccess method will be really similar. The first difference is that it won’t receive a message as a parameter. We have to clear the error display by setting its innerText to an empty string. Lastly we have to reverse the class application. We’ll add the success class to the inputControl and remove the error class (if present).

const setSuccess = element => {
    const inputControl = element.parentElement;
    const errorDisplay = inputControl.querySelector('.error');

    errorDisplay.innerText = '';
    inputControl.classList.add('success');
    inputControl.classList.remove('error');
};

Enter fullscreen mode

Exit fullscreen mode

We will create one last helper function to validate emails. This is an optional step, if you don’t want to use regular expressions, feel free to just set the input type of the email field to email. The isValidEmail function will take a string as a parameter and use this weird looking regular expression to check whether it is a valid email or not. We’ll use String.test() function to test the string against the regex. We’ll also convert the email to a string and make it lowercase.

const isValidEmail = email => {
    const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
    return re.test(String(email).toLowerCase());
}

Enter fullscreen mode

Exit fullscreen mode

Now we should create the validator validateInputs function. First we will get the value of all the input fields. We can do that by getting the value property’s value of the input field references. We’ll call the String.trim() function to remove the trailing empty spaces (if any) from the start and end of the values.
Then we can start validating inputs. We’ll use if, else statements to do the validation. For the username we will check whether if it is empty or not, by comparing the value with an empty string. If it empty, we’ll call the setError function and provide the username element to it, with our error message. Otherwise we’ll call the setSuccess method with the username element. Now we have to do this for the other input fields, but the approach will be the same.

const validateInputs = () => {
    const usernameValue = username.value.trim();
    const emailValue = email.value.trim();
    const passwordValue = password.value.trim();
    const password2Value = password2.value.trim();

    if(usernameValue === '') {
        setError(username, 'Username is required');
    } else {
        setSuccess(username);
    }
};

Enter fullscreen mode

Exit fullscreen mode

For the email we’ll check if it is provided or not, and set an error if it is empty. If it is not empty we’ll check whether it is a valid email address, and if not we’ll set an error, otherwise we set success for the field.

if(emailValue === '') {
        setError(email, 'Email is required');
    } else if (!isValidEmail(emailValue)) {
        setError(email, 'Provide a valid email address');
    } else {
        setSuccess(email);
    }
}

Enter fullscreen mode

Exit fullscreen mode

For the password we’ll check whether it is empty or not, and if it is not empty we’ll check if it is longer than 7 characters. If not, well set an error, otherwise we’ll set it as success.

if(passwordValue === '') {
        setError(password, 'Password is required');
    } else if (passwordValue.length < 8 ) {
        setError(password, 'Password must be at least 8 character.')
    } else {
        setSuccess(password);
    }
}

Enter fullscreen mode

Exit fullscreen mode

For the password confirmation we’ll check if it is empty, and we should also check if the password confirmation’s value is equal to the password’s value.

if(password2Value === '') {
        setError(password2, 'Please confirm your password');
    } else if (password2Value !== passwordValue) {
        setError(password2, "Passwords doesn't match");
    } else {
        setSuccess(password2);
    }
}

Enter fullscreen mode

Exit fullscreen mode

Now we have every input validated, if we wanted to we could submit our form now to a specific endpoint.

Good job now you have a working form validation Javascript. Please note that you always have to validate the form inputs on the server-side as client-side validation can be easily bypassed. There are way more advanced form validation methods and libraries that we use in modern web development, but this project is a really good way to start and learn the fundamentals.

Where you can learn more from me?

I create education content covering web-development on several platforms, feel free to 👀 check them out.

I also create a newsletter where I share the week’s or 2 week’s educational content that I created. No bull💩 just educational content.

🔗 Links:

  • 🍺 Support free education and buy me a beer
  • 💬 Join our community on Discord
  • 📧 Newsletter Subscribe here
  • 🎥 YouTube Javascript Academy
  • 🐦 Twitter: @dev_adamnagy
  • 📷 Instagram @javascriptacademy

Понравилась статья? Поделить с друзьями:
  • Jam9110 kyocera ошибка
  • Java типичные ошибки
  • Jam9011 ошибка kyocera
  • Java поднять ошибку
  • Jam 4209 ошибка kyocera