Перейти к содержанию

Фильтры списка в URL

Фильтр уже определяет результат чтения, но локальные controls исчезают при перезагрузке страницы. Перенесём применённые q и status в URL: /tickets?q=проектор&status=new. Тогда ссылка описывает выбранный список, а форма становится способом подготовить следующее условие. Результат главы — один источник применённого фильтра, который одинаково работает при прямом открытии, навигации и возвращении через историю браузера.

Различим набираемый текст и применённое условие. Диспетчер может написать новый запрос, но до нажатия «Применить» список ещё относится к прежнему URL. Это ожидаемая политика формы с submit. Если отправлять каждый символ, потребовались бы debounce и другое поведение истории. В конечной лаборатории такой режим не включён. Мы сохраняем явное действие, которое помогает изучить Router state без смешения с отдельной темой частоты сетевых запросов.

Чтение queryParamMap

Ниже фрагмент определения url в ListPage. ActivatedRoute получен в поле route, а statuses импортирован из модели. Значение q приводится к допустимой локальной форме: trim и ограничение восьмьюдесятью единицами длины строки. Неизвестный status интерпретируется как отсутствие фильтра. Это клиентская политика нормализации адреса; сервер отдельно отклонит такой status, если запрос отправлен напрямую. Она не превращает изменённый URL в доверенные входные данные API.

const url = this.route.queryParamMap.pipe(
  map(params => ({
    q:(params.get('q') ?? '').trim().slice(0,80),
    status:statuses.includes(params.get('status') as Status) ? params.get('status')! : ''
  })),
  distinctUntilChanged((a,b) => a.q === b.q && a.status === b.status)
);

DistinctUntilChanged сравнивает содержательные q/status, а не ссылку на новый объект map. QueryParamMap способен измениться из-за параметра, который наша страница не использует. Без сравнения новая пара объектов могла бы инициировать одинаковое чтение. Сравнение подчёркивает, что источником запроса является выбранный предметный фильтр. При этом неизвестные query-параметры самого адреса не передаются TicketsApi: сервис строит только q/status. Прямые неизвестные параметры API DTO отклонит, как описано в контракте.

Ожидаемое прямое открытие q «проектор» и status new покажет rq-101 и заполнит оба поля формы. Constructor сначала переносит фильтр в controls через setValue с emitEvent false, затем инициирует API-чтение. Подавление события полезно для избежания вторичного цикла, если в будущем появится наблюдение valueChanges. Сейчас запросы запускает url, а не такое наблюдение, поэтому поток имеет одно ясное направление: адрес → форма отображения и операция → state списка.

Применение нового условия

Метод apply читает getRawValue, удаляет внешние пробелы title-поиска и вызывает router.navigate относительно текущего маршрута. Пустые q/status передаются как null, чтобы убрать параметр из следующего адреса. Query-параметры заменяются текущим набором; лаборатория не обещает сохранение посторонних параметров через merge. Если добавить, например, page или sort, потребуется включить их в явную модель фильтра и договор API. Нельзя случайно сохранить старую page при новом условии, которое меняет число результатов.

Здесь apply не вызывает api.list непосредственно. Иначе одно действие могло бы отправить запрос вручную, а изменение URL затем инициировало бы второй. Сам URL-поток уже знает, когда читать; обработчик формы меняет только источник. Это снижает число путей, которые следует согласовывать при кнопке Back. Когда история возвращает прежний адрес, queryParamMap снова формирует условие и выполняется обычная операция, независимо от того, нажимал ли человек submit именно в этот момент.

Если применённый фильтр содержательно не изменился, distinctUntilChanged не запускает повторное чтение. Для случая серверного отказа существует отдельная кнопка retry. Такое разделение удерживает смысл событий: «Применить» задаёт условие, «Повторить» заново читает уже выбранное. Если хотите, чтобы повторный submit также обновлял данные, это можно сделать явным дополнением, но тогда нужно определить его взаимодействие с router navigation и не создавать дублирующиеся HTTP-подписки.

Адрес как часть интерфейса

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

Параметры читаются как строки и могут быть неожиданными. Например, status=archived не является новым четвёртым состоянием Ticket лишь потому, что оказался в URL. В клиентском каноническом наборе он превратится в пустой фильтр. При необходимости можно также обновить адрес на нормализованный, однако архив этого не делает. Это означает, что адресная строка может содержать неизвестный status, тогда как поле «Статус» показывает «Все». При передаче проекта такую политику важно назвать, чтобы UX не казался полностью однозначным.

Повторяющийся q в адресе также не означает два независимых запроса. QueryParamMap.get берёт значение по своей модели; лаборатория не создаёт многозначный фильтр. Когда TicketsApi строит HttpParams, он отправляет одно q и одно status. Nest DTO отклонит массив, полученный из повторяющегося параметра прямого API-вызова. Эти границы отличаются сознательно: страница интерпретирует пользовательский адрес, сервер валидирует свой внешний HTTP-вход. Для строгой симметрии URL потребуется собственная проверка getAll и сообщение об ошибке адреса.

Разберите историю действий: открыли /tickets, применили new, применили active, нажали Back. Ожидается восстановление new в controls и чтение новых заявок. В таком сценарии не следует восстанавливать status из отдельного localStorage поверх адреса: возникнут два источника и непонятный приоритет. Если когда-нибудь появится запоминание предпочитаемого фильтра, оно может задавать лишь начальный адрес при отсутствии параметров. После этого текущим источником снова должен стать URL.

Проверка в этой сессии ограничена чтением структуры. Фактическая работа Back, encodeURIComponent и proxy не исполнялась. Тем не менее договор результата определён: applied-фильтр восстанавливается из маршрута, форма меняет маршрут, запрос слушает маршрут и принимает только актуальный ответ. Далее разберём сообщения загрузки и отказа с точки зрения пользователя, сохранив эту однонаправленную связь. Она позволит понять, к какому условию относится каждое ожидаемое сообщение.

Статус примера — not_run. Зависимости, приложение, SQL, HTTP и тесты не запускались. Описанные ответы и изменения интерфейса ожидаемые; чтение документации не является проверкой выполнением. Полная конечная версия находится в архиве исходников, а её ограничения и действия читателя — в README и CONTRACT.md.

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