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

Требования к продукту для таких форм:

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

Вот иллюстрация того, чего мы хотим достичь:

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

Создание каркаса

Во-первых, нам нужно создать фиктивный источник данных, чтобы нам было с чем поработать. В нашем случае мы будем использовать простой BehaviorSubject. В реальной жизни это, вероятно, будет Observable из магазина Акиты, ngrx или любого другого государственного управления.

Затем мы создаем SettingsComponent и инициализируем представление наших данных FormGroup:

Мы подписываемся на store$ наблюдаемое; Каждый раз, когда он излучает, мы вызываем метод patchValue() формы, чтобы обновить значения формы на основе данных, которые мы получаем с сервера.

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

Создание оператора isDirty

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

Первая наблюдаемая - это store$, которая служит нашим единственным источником истины. Второй - это valueChanges наблюдаемый объект формы, который выдает текущее значение формы при каждом изменении формы. Вот к чему мы стремимся:

Посмотрим на реализацию оператора:

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

В нашем случае мы используем наблюдаемый combineLatest(), чтобы получить как текущее хранилище, так и значение формы - когда одно из них изменяется, мы выполняем глубокую проверку с использованием библиотеки fast-deep-equal и возвращаем результат.

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

🦊 Возможности для улучшения

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

Нам нужно всегда иметь одну подписку на источник (то есть Subject), которая делится последним результатом с каждым подписчиком.

Оператор shareReplay создает ReplaySubject, который является единственным подписанным на источник. Каждый раз, когда мы звоним subscribe(), мы всегда подписываемся на эту тему, которая имеет последнее значение.

Большой! Мы выполнили наше первое требование, теперь перейдем к следующему.

Обработка навигации в приложении

Маршрутизатор Angular предоставляет CanDeactivate защиту, где мы можем реализовать canDeactivate метод, который будет вызываться при любой навигации в приложении и предоставить ссылку на компонент, из которого мы выполняем навигацию.

Этот метод должен возвращать boolean, Observable<boolean> или Promise<boolean>. Когда мы используем Promise или Observable, маршрутизатор будет ждать, пока это не разрешится как истинное (навигация) или ложное (отменить навигацию).

В нашем случае мы потребуем, чтобы каждый компонент, использующий наш оператор dirtyCheck, реализовал свойство isDirty$, на которое мы можем подписаться в нашей защите:

Мы подписываемся на isDirty$ observable. Когда компонент не загрязнен, мы возвращаем of(true), что означает, что мы можем перемещаться; в противном случае мы открываем модальный компонент и в зависимости от ответа пользователя решаем, следует ли разрешить навигацию. Обратите внимание, что мы также используем take(1), потому что Angular ожидает, что первое значение из наблюдаемого укажет результат.

Последний шаг - активировать DirtyCheckGuard:

Обработка формы отправления

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

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

Когда возникает событие beforeunload, мы проверяем, не загрязнен ли компонент. Если это так, мы устанавливаем для returnValue значение false, что заставит браузер отображать диалоговое окно подтверждения по умолчанию, с которым мы все знакомы.

Нам не нужны утечки памяти, поэтому мы очищаем beforeunload подписку, вызывая переданную функцию обратного вызова из оператора finalize() источника, который будет вызываться, когда источник завершится или возникнет ошибка.

Теперь мы выполнили все требования, наш продукт доволен 😀.



🚀 На случай, если вы это пропустили

Вот несколько моих проектов с открытым исходным кодом:

  • Акита: государственное управление, специально разработанное для JS-приложений
  • Spectator: мощный инструмент для упрощения ваших угловых тестов
  • Transloco: библиотека интернационализации Angular
  • Forms Manger: основа правильного управления формами в Angular
  • Кешью: гибкая и простая библиотека, которая кэширует HTTP-запросы.

Подпишитесь на меня в Medium или Twitter, чтобы узнать больше об Angular, Akita и JS!