AngularInDepth уходит от Medium. Более свежие статьи размещаются на новой платформе inDepth.dev. Спасибо за то, что участвуете в глубоком движении!

В прошлом блог AngularInDepth включал несколько очень полезных статей, показывающих, как ReactiveFormsModule в @angular/forms может облегчить вашу жизнь.

Сегодня мы поговорим о некоторых проблемах с ReactiveFormsModule и обсудим предложения по устранению многих из этих проблем. Официальное предложение можно найти как проблему в репозитории Angular # 31963 (это кажется самой быстрорастущей проблемой на данный момент¹). Цель этой публикации - побудить сообщество получить отзывы об улучшении ReactiveFormsModule и исправлении некоторых из его давних проблем.

Вам может быть интересно, какие проблемы возникают с ReactiveFormsModule? Некоторые из самых больших проблем:

1. Модуль не строго типизирован.

2. Относительно сложно * отображать * сообщения об ошибках, учитывая, насколько важна эта задача.

  1. См. № 25824 № 24981 № 22319 № 21011 № 2240 № 9121 № 18114.

3. Относительно сложно * добавлять * сообщения об ошибках, включая взаимодействие с асинхронными службами для проверки (отсюда необходимость в различных стратегиях обновления, таких как «на blur" /» на submit").

4. Многочисленные неприятности из-за неудачных решений API.

  • Вы не можете привязать один элемент управления формы к нескольким входам без ControlValueAccessor # 14451
  • Невозможно хранить произвольные метаданные в элементе управления # 19686
  • Вызов reset() на самом деле не сбрасывает элемент управления до его начального значения # 20214 # 19747 # 15741 # 19251
  • Должен вызывать markAsTouched() / markAsUntouched() вместо простого markTouched(boolean), что более удобно с программной точки зрения # 23414 # 23336
  • Создание компонентов пользовательской формы относительно сложно # 12248
  • и др. №11447 №12715 №10468 №10195 №3133

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

  • См. Проблемы # 3009 # 20230, связанные с синтаксическим анализом / форматированием пользовательского ввода.
  • См. Вопросы # 31046 # 24444 # 10887 # 30610, касающиеся изменения флага затронутого / грязного / и т. Д.
  • См. Проблемы №30486 №31070 №21823, связанные с отсутствием отслеживания отправленных ng-изменений.
  • Возможность удалить элемент управления FormGroup без генерации события # 29662
  • Возможность подписаться на добавление / удаление элемента управления формы FormGroup # 16756
  • Возможность пометить ControlValueAccessor как нетронутый # 27315
  • Предоставьте ControlValueAccessors для библиотек, отличных от @angular/forms # 27672

По сути, существующий класс AbstractControl не предлагает расширяемости / простоты использования, которые должен иметь такой важный объект. Маловероятно, что какой-либо один API может постоянно решать проблемы каждого, но хорошо спроектированный API решает большинство проблем людей большую часть времени и может быть расширен для решения проблем произвольной сложности, когда это необходимо.

Далее следует предложение по новому AbstractControl API с ControlEvent интерфейсом. В целом это предложение касается вопросов 1, 3, 4 и 5 выше. Важно отметить, что это предложение полностью инициировано сообществом. Команда Angular не предоставила никаких отзывов по этому предложению.

  • Проблему Angular, связанную с этим предложением, можно увидеть здесь: https://github.com/angular/angular/issues/31963
  • Репозиторий github для этого предложения можно увидеть здесь: https://github.com/jorroll/reactive-forms-2-proposal. Репо включает в себя рабочие реализации всего, что здесь обсуждается.
  • Прототип модуля для предложения был опубликован в npm по адресу reactive-forms-module2-proposal это просто подходит для экспериментов!

Репозиторий github также содержит примеры stackblitz предлагаемого API в действии. Демонстрация stackblitz также содержит пример директивы совместимости, позволяющей использовать новый AbstractControl с существующими компонентами угловых форм (такими как @angular/material компоненты).

Предлагаемый новый AbstractControl

Предлагаемый класс AbstractControl имеет свойство source: ControlSource<PartialControlEvent>, которое является источником истины для всех операций с AbstractControl. ControlSource - это просто измененная тема rxjs. Внутри выходные данные source передаются по конвейеру events наблюдаемому, который выполняет все необходимые действия для определения нового состояния AbstractControl перед тем, как испустить новый объект ControlEvent, описывающий любые произошедшие мутации. Это означает, что подписавшись на наблюдаемый events, вы получите все изменения в AbstractControl.

С помощью этого относительно скромного изменения мы можем выполнить целый ряд улучшений API. Давайте рассмотрим некоторые из них на примерах, прежде чем рассматривать сам ControlEvent API.

Кроме того, вы можете прокрутить вниз и перейти к разделу « Погружение в API ControlEvent» ниже.

Пример 1

Новый API знаком пользователям старого API.

Важно, чтобы новый API был хорошо знаком пользователям существующего ReactiveFormsModule и был на 100% пригоден для использования людьми, которые не хотят использовать наблюдаемые объекты.

Пример 2

Подписка на вложенные изменения

Новый API позволяет нам подписаться на изменения любого свойства. Применительно к ControlContainers, таким как FormGroup и FormArray, мы можем подписаться на вложенные дочерние свойства.

Важно отметить, что в этом примере, если address FormGroup удалена, наша подписка будет выдавать undefined. Если добавляется новая address FormGroup, наша подписка будет выдавать новое значение street FormControl.

Это также позволяет нам подписаться на controls изменения FormGroup / FormArray.

Пример 3

Связывание одного FormControl с другим FormControl

Здесь, подписавшись source из controlB на events из controlA, controlB отразит все изменения в controlA.

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

Пример 4

Динамически преобразовывать значение элемента управления

Здесь пользователь предоставляет string значения даты, и нам нужен элемент управления с Date объектами javascript. Мы создаем два элемента управления, один для хранения значений string, а другой для хранения значений Date, и мы синхронизируем все изменения между ними. Однако изменения значений от одного к другому преобразуются в соответствующий формат.

Пример 5

Динамически анализировать вводимые пользователем данные

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

Чтобы упростить этот процесс, FormControlDirective / FormControlNameDirective / etc принимают необязательные функции toControl, toAccessor и accessorValidator.

В этом примере мы предоставляем функцию stringToDate, которая получает входную строку и преобразует ее в javascript Date или null, если строка имеет неправильный формат. Точно так же мы предоставляем функцию dateToString для синхронизации значений Date | null нашего элемента управления с элементом ввода. Мы также предоставляем необязательную функцию accessorValidator для проверки строк элемента ввода и предоставления пользователю полезных сообщений об ошибках.

Пример 6

Проверка значения AbstractControl через службу

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

Некоторые моменты, на которые следует обратить внимание в этом примере:

  1. Когда создается подписка на свойство usernameControl's value, элемент управления уже будет отмечен pending.
  2. API позволяет пользователям связать вызов markPending() с определенным ключом (в данном случае "usernameValidator"). Таким образом, вызов markPending(false) в другом месте (например, другой вызов проверки службы) не будет преждевременно пометить этот вызов службы как "больше не ожидающий". AbstractControl ожидает обработки, пока любой ключ равен true.
  3. Точно так же ошибки сохраняются, связанные с источником. В данном случае источником является 'usernameValidator'. Если эта служба добавляет ошибку, но другая служба позже сообщает, что ошибок нет, эта служба случайно не перезапишет ошибку этой службы. Важно отметить, что свойство errors объединяет все ошибки в один объект.

Погружение в API ControlEvent

Примечание. Важно подчеркнуть, что при стандартном использовании разработчикам не нужно знать о существовании ControlEvent API. Если вам не нравятся наблюдаемые, вы можете продолжать без страха просто использовать setValue(), patchValue() и т. Д. Однако для целей этой публикации давайте заглянем под капот, что происходит!

В основе этого предложения AbstractControl лежит новый API ControlEvent, который контролирует все мутации (изменения состояния) в AbstractControl. Он основан на двух свойствах AbstractControl: source и events.

Чтобы изменить состояние AbstractControl, вы создаете новый объект PartialControlEvent из свойства источника. Этот объект имеет интерфейс

Когда вы вызываете такой метод, как AbstractControl#markTouched(), этот метод просто создает для вас соответствующий объект ControlEvent и испускает этот объект из элемента управления ControlSource (который сам по себе является просто измененным rxjs Subject).

На внутреннем уровне AbstractControl подписывается на вывод свойства source и направляет этот вывод в метод protected processEvent(). После обработки новый объект ControlEvent, содержащий любые изменения, испускается из свойства events элемента управления (поэтому, когда подписчик получает ControlEvent из свойства events, любые изменения уже были применены к AbstractControl).

Вы заметите, что обрабатываются только события, которые еще не были обработаны этим AbstractControl (т. Е. !event.processed.includes(this.id)). Это позволяет двум AbstractControls подписываться на события друг друга, не входя в бесконечный цикл (подробнее об этом позже).

Вы можете заглянуть в репозиторий github, чтобы увидеть полное предложение интерфейса AbstractControl, а также рабочие реализации FormControl, FormGroup, FormArray, etc.

Теперь, когда мы немного больше знаем об API ControlEvent, давайте рассмотрим несколько примеров, которые он позволяет…

Пример 7

Синхронизация одного значения FormControl с другим

Допустим, у нас есть два FormControl, и мы хотим, чтобы у них было одинаковое состояние. Новый API предоставляет удобный AbstractControl#replayState() метод, который возвращает наблюдаемые ControlEvent изменения состояния, которые описывают текущее состояние AbstractControl.

Если вы подписываете один источник FormControl на replayState() другого элемента управления формы, их значения будут равны.

Метод replayState() также обеспечивает гибкий способ «сохранения» состояния управления и повторного применения всего или части его позже.

Пример 8

Настройка изменений состояния AbstractControl

Допустим, вы программно меняете значение элемента управления через «службу А». Отдельно у вас есть другой компонент, «компонент B», который наблюдает за изменениями значений элемента управления и реагирует на них. По какой-то причине вы хотите, чтобы компонент B игнорировал изменения значений, которые были программно инициированы службой A.

В текущем ReactiveFormsModule вы можете изменить значение элемента управления и подавить соответствующее наблюдаемое излучение, передав параметр noEmit. К сожалению, это повлияет на все, наблюдая за изменением значения элемента управления. Если мы хотим, чтобы componentB игнорировал выдачу значений, нам не повезло.

С этим новым API мы можем достичь нашей цели. Каждый метод, который изменяет состояние AbstractControl, принимает параметр meta, в который вы можете передать произвольный объект. Если вы подписываетесь непосредственно на events элемента управления, мы можем просматривать любые переданные метаданные.

Здесь подписка в ловушке ngOnInit() игнорирует изменения метасвойства myService: true.

Пример 9

Создание «крючков жизненного цикла» из AbstractControl

Давайте воспользуемся реализацией этого предложения FormControlDirective в качестве примера (полный код можно увидеть в репозитории github). Предположим, вы создаете настраиваемую директиву, которая предоставляет общедоступный FormControl, и хотите предоставить крючки жизненного цикла для подписчиков этого FormControl.

В конкретном случае FormControlDirective я хотел, чтобы ControlValueAccessor, подключенный к FormControlDirective, получал уведомление при изменении элемента управления «вход» FormControlDirective.

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

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

ControlValueAccessor

Пока что мы сосредоточились на изменениях в AbstractControl API. Но некоторые проблемы с ReactiveFormsModule проистекают из ControlValueAccessor API. Хотя представленный до сих пор ControlEvent API не основывается на каких-либо предположениях относительно ControlValueAccessor API и отлично работает с существующим интерфейсом ControlValueAccessor, он также позволяет значительно улучшить ControlValueAccessor API.

Рискуя представить слишком много новых идей одновременно, давайте посмотрим, как мы можем улучшить ControlValueAccessor с помощью нового ControlEvent API ...

Напоминаем, что существующий интерфейс ControlValueAccessor выглядит как

Предлагаемый ControlEvent API позволяет использовать новый ControlAccessor API, который выглядит так:

В этом обновлении свойство control директивы, реализующей ControlAccessor, содержит AbstractControl, представляющее состояние формы директивы (в качестве напоминания, компоненты являются директивами).

Это будет иметь несколько преимуществ по сравнению с текущим API ControlValueAccessor:

1. Проще реализовать

  • При касании формы отметьте элемент управления как прикосновенный.
  • Когда значение формы обновляется, установитеValue в элементе управления.
  • так далее

2. Легче осмыслять (по общему признанию, субъективно)

3. Позволяет ControlAccessor представлять FormGroup / FormArray / и т. Д., А не просто FormControl.

  • ControlAccessor может представлять адрес с помощью FormGroup.
  • ControlAccessor может представлять людей с помощью FormArray.
  • так далее

4. Очень гибкий

  • Вы можете передать метаданные, связанные с изменениями, в ControlAccessor с помощью мета-параметра, найденного в новом AbstractControl.
  • Вы можете создавать собственные события ControlEvents для ControlAccessor.
  • При необходимости вы можете получить доступ к текущему состоянию формы ControlAccessor через стандартный интерфейс (и вы можете использовать метод replayState() для применения этого состояния к другому AbstractControl)
  • При необходимости ControlAccessor может использовать объект настраиваемого элемента управления, расширяющий AbstractControl.

Пример 10

Простой пример с использованием * существующего * ControlValueAccessor API

Напоминаем, что вот простой пользовательский ControlValueAccessor, реализованный с использованием существующего интерфейса:

Пример 11

Простой пример с использованием * предлагаемого * ControlAccessor API

Вот тот же компонент, реализованный с использованием предложенного интерфейса ControlAccessor:

Если мы хотим программно пометить этот ControlAccessor как затронутый, мы можем просто вызвать this.control.markTouched(true). Если мы хотим программно обновить значение, мы можем просто setValue() и т. Д.

Давайте рассмотрим еще несколько примеров преимуществ нового ControlAccessor API:

Пример 12

Ввод адреса электронной почты с асинхронной проверкой

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

Пример 13

Аксессор управления группой форм

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

  • Также отмечу, что, поскольку этот компонент также является ControlContainerAccessor, использование formControlName будет вытягиваться непосредственно из свойства control компонента app-user-form. Т.е. в этом случае нам не нужно использовать директиву [formGroup]='control' внутри шаблона компонента.

Пример 14

Вложение нескольких групп форм

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

Заключение

Хотя исправление существующего ReactiveFormsModule возможно, это потребует множества критических изменений. Как показал Renderer -> Renderer2, более удобным для пользователя решением является создание нового модуля ReactiveFormsModule2, отказ от старого модуля и обеспечение уровня совместимости, позволяющего использовать два бок о бок (включая использование нового FormControl с компонентом, ожидающим старый ControlValueAccessor).

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

Вещи, на которые не распространяется: API валидаторов

Многие проблемы с текущим API FormControl в конечном итоге связаны с текущим ValidatorFn / ValidationErrors API.

Примеры включают:

1. Если требуется элемент управления, атрибут [required] не добавляется автоматически к соответствующему элементу в модели DOM.

  • Точно так же другие валидаторы также должны включать изменения DOM (например, валидатор maxLength должен добавить атрибут [maxlength] для доступности, есть атрибуты ARIA, которые следует добавить для доступности, и т. Д.).
  • Если вы проверяете, является ли введенное число числом, целесообразно добавить атрибут type="number" в базовый <input>.

2. Создание и отображение сообщений об ошибках намного сложнее, чем должно быть, для такой фундаментальной части, как Forms API.

В конечном счете, я считаю это недостатками текущего ValidatorFn / ValidationErrors API, и их следует исправить в исправлении этого API. Любое такое исправление должно быть включено в любой ReactiveFormsModule2 и может быть включено в этот API AbstractControl, но в настоящее время оно выходит за рамки данного конкретного предложения.

Чтобы поддержать или отклонить предложение:

переходите к Угловому выпуску № 31963.

Сноски

  1. Утверждение о «самой быстрорастущей проблеме» основано на том факте, что за 3 месяца проблема поднялась до второй страницы проблем репозитория Angular, если отсортировать их по реакции «Нравится». Это единственный выпуск из первых 4 страниц, созданный в 2019 году.