Стандартизируем поведение форм в проекте (Angular)


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

    Если код отправки форм в вашем проекте похож на этот, советую заглянуть под кат.

    onSubmit(): void
    // login.component.ts
    // bad practices
    onSubmit(): void {
      this.formSubmitted = true;
      this.isUnhandledServerError = false;
      if (!this.formGroup.valid) return;
      this.isLoading = true;
      const { username, password } = this.formGroup.value;
      this.login(username, password)
        .pipe(finalize(() => (this.isLoading = false)))
        .subscribe({ error: error => this.handleError(error) });
    }
    


    Для тех, кто просто любит код:
    Проект на stackblitz до рефакторинга.
    Проект на stackblitz после рефакторинга.

    Описание проблемы


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

    Предлагаю разобрать пример обработчика отправки формы, хорошего с точки зрения UX, но плохого с точки зрения разработки. Проект на stackblitz до рефакторинга.

    // login.component.ts
    onSubmit(): void {
      this.formSubmitted = true; // подкрашиваем невалидные элементы
      this.isUnhandledServerError = false; // скрываем сообщение об неизвестной ошибке на сервере
      if (!this.formGroup.valid) return; // валидируем форму
      this.isLoading = true; // показываем индикатор загрузки
      const { username, password } = this.formGroup.value;
      this.login(username, password) // отправляем данные на сервер
        .pipe(finalize(() => (this.isLoading = false))) // скрываем индикатор загрузки
        .subscribe({ error: error => this.handleError(error) });
    }
    

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

    Решение


    Чтобы упростить разработку и стандартизировать поведение форм в приложении, необходимо вынести код обработчика отправки формы в отдельный класс. Проект на stackblitz после рефакторинга. (Я намеренно упростил код для примера, в реальном проекте нужно заменить все boolean поля на Observable.)

    class Form<T> {
        submitted = false;
    
        pending = false;
    
        hasUnhandledServerError = false;
    
        constructor(private formGroup: FormGroup, private action: (value: any) => Observable<T>) {}
    
        submit(): Observable<T> {
            if (this.pending) return EMPTY;
            this.submitted = true;
            this.hasUnhandledServerError = false;
            if (this.formGroup.valid) {
                this.pending = true;
                return this.action(this.formGroup.value).pipe(
                    tap({ error: () => (this.hasUnhandledServerError = true) }),
                    finalize(() => (this.pending = false)),
                );
            }
            return EMPTY;
        }
    }
    

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

    Почему не вынести это в библиотеку?


    Требования к UX для каждого проекта уникальны, и в большей степени зависят от дизайнера. Мне уже приходилось переопределять поведение стандартных Material элементов по пожеланию заказчика. Поэтому я не вижу хоть сколько-нибудь возможным стандартизировать поведение форм во всех приложениях с помощью одной библиотеки. Пусть поведение интерфейса остается во власти дизайнера и разработчиков. Тем не менее я считаю, что это неплохая идея — выделять связанную с UX логику в отдельные классы.

    Надеюсь, пример был полезен, и вы попробуете использовать идею в своих проектах. Пока!

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 2

      0

      На мой взгляд вообще весь код Submit тогда уж надо засунуть в pipe от Observable. И пока на него не подписались pending не выставлять и Empty не возвращать. А то получается первый получит Observable, потеряет его и форма сломается?

        0
        Спасибо! Да, будет надёжней обернуть тело метода submit в defer. Поправил в stackblitz.

      Only users with full accounts can post comments. Log in, please.