История URL и доступная навигация
Htmx уже заменяет область списка, но интерактивный поиск пока не фиксирует новое состояние в адресной строке. Добавим историю URL и явную обработку ошибочных ответов формы. Пользователь должен иметь возможность открыть результат поиска отдельно, нажать «назад» и исправить неправильный ввод. Эти действия относятся к единому договору страницы; современная замена DOM не должна превращать список в состояние, которое невозможно восстановить по адресу.
В этом уроке сохраняется серверное представление panel. Его содержимое включает условие поиска, строки, страницы и форму создания. Добавляем атрибут hx-push-url="true" к чтению списка, а формы записи получают отдельный hx-post. HTTP-маршруты и предметные проверки не меняются. Это позволяет увидеть, какие решения относятся к навигации браузера, а какие по-прежнему определяют допустимое изменение базы.
Адрес описывает представление
После интерактивного поиска htmx записывает URL в историю. Условие q=MON теперь можно скопировать и открыть в новой вкладке. Сервер должен вернуть полный документ при таком обычном GET, потому что у нового окна нет прежней оболочки. В обработчике учитывается заголовок восстановления истории: если библиотека запросила восстановление полной страницы, выбираем page даже при наличии других признаков htmx. Иначе история получит фрагмент без окружения.
К ссылкам страниц добавлены hx-boost, цель main и замена outerHTML. Они остаются обычными ссылками с href. При доступном htmx библиотека перехватывает действие и обновляет область, а без неё браузер переходит целиком. Благодаря этому URL, обычная навигация и частичная загрузка используют один путь. Не нужно создавать отдельный /ajax/items, который постепенно начинает отличаться сортировкой или правилами фильтрации от страницы списка.
document.body.addEventListener('htmx:beforeSwap',event=>{if([409,422].includes(event.detail.xhr.status)&&event.detail.xhr.getResponseHeader('content-type')?.startsWith('text/html')){event.detail.shouldSwap=true;event.detail.isError=false}});
Этот код из static/feedback.js не отвечает за поиск. Он разрешает замену только для выбранных ожидаемых ошибок формы и конфликта, когда ответ действительно является HTML. Параметры истории заданы в view.html, а выбор полного шаблона находится в main.go. Полное состояние lesson-09 включает все три файла. Листинг является небольшим самостоятельным обработчиком события, а не скрытой заменой серверной проверки.
Ошибочный статус и содержимое
По умолчанию htmx не обязан вставлять тело любого ответа с ошибочным статусом. Для 422 это мешает нашему договору: сервер уже подготовил форму с сохранёнными значениями и объяснением, но человек может не увидеть её. В событии htmx:beforeSwap устанавливаем shouldSwap и снимаем признак библиотечной ошибки для 409 и 422. Это не меняет реальный HTTP-статус и не превращает отказ записи в успех. Решение лишь сообщает интерфейсу, что тело является полезным представлением.
Дополнительно проверяется Content-Type. Будущий отказ движения с текстовым сообщением не должен внезапно заменять весь main как HTML-фрагмент. Белый список статусов без проверки формы тела слишком широк: разные маршруты могут использовать одинаковый код для разных представлений. В нашем приложении ошибки формы возвращают HTML, а техническая ошибка SQL получает отдельный короткий ответ. Такой договор нужно сохранять при расширении новых действий.
Не разрешаем автоматическую замену для всех 500. Ответ внутренней ошибки может не иметь ни формы, ни навигации, а подробное сообщение исключения вообще не предназначено посетителю. Интерфейсу полезно различать исправимый ввод и неисправность сервера. Разрешение 422 — содержательное решение продукта; глобальное «показывать любое тело» было бы только маскировкой различий, которые сервер аккуратно обозначил статусами.
Запись и повторное чтение
Обычная POST-форма по-прежнему получает 303 после успешной записи. Для htmx-обращения обработчик выполняет чтение списка и возвращает актуальный panel непосредственно. В обоих случаях INSERT происходит ровно в одном месте и подчиняется одной проверке. Различается только способ показать последующее состояние: полный GET браузера либо подготовленный фрагмент в ответе интерактивному клиенту. Не следует дублировать INSERT внутри отдельного «AJAX-обработчика».
На этом шаге после записи список возвращается без прежнего фильтра. Такое поведение явно задано очисткой RawQuery перед чтением. Новая карточка становится доступна в общем представлении, но пользователь может потерять предыдущее условие. Это допустимый учебный договор, а не единственный правильный UX. Для сохранения фильтра следует передавать проверенное состояние возврата или хранить его отдельно, не принимая произвольный внешний URL перенаправления.
POST не получает автоматическое создание записи в истории каждого изменения. Адрес отображает представление списка, а не уникальный журнал всех действий пользователя. Внешне похожие операции могут иметь разные правила: поиск заслуживает сохраняемого URL, а исправление количества не должно создавать ложную страницу с повторяемым действием. Сохранять браузерную историю и сохранять предметную историю движения — разные механизмы, второй появится через два урока.
Ожидаемое восстановление
Рассмотрим маршрут: общий список, поиск монитора, следующая страница при достаточном количестве строк, затем «назад». Ожидается восстановление соответствующего представления с согласованными условием и номером страницы. Важный дополнительный случай — прямое открытие того же URL без ранее загруженного JavaScript. Сервер умеет подготовить page, поэтому адрес не зависит от наличия конкретного снимка истории в браузере. Учебный стенд при подготовке не запускался; сценарий приведён для самостоятельного чтения и прохождения.
Вторая последовательность касается ошибки: отправьте артикул с кириллическими буквами. Сервер должен вернуть 422 с формой, библиотека — заменить panel, а хранилище — остаться прежним. Исправление SKU приводит к успешной записи и общей таблице. Можно проследить каждую границу отдельно: сеть сообщает статус, тело содержит представление, DOM получает замену, SQL принимает только допустимую операцию. Одно красное сообщение не доказывает правильность всех четырёх частей.
Не следует считать push URL гарантией доступности. Пользователю по-прежнему нужны понятные подписи ссылок, видимое состояние загрузки и сохранение смысловой структуры. В текущем шаблоне используются обычные формы, таблица с заголовками и сообщение role="alert", но автоматическое управление фокусом после замены остаётся направлением дальнейшего улучшения. Честная граница позволяет развивать интерфейс предметно, а не объявлять его полностью доступным из-за одного атрибута.
Теперь список имеет единый договор обычной и интерактивной навигации. Следующий урок добавит редактирование карточки, где возникает новая проблема: две открытые формы могут предлагать изменения на основании одной старой версии. Ни история URL, ни htmx, ни ограничение одной SQL-сессии сами по себе не предотвращают потерю одного изменения другим. Для этого потребуется условие на стороне базы.
Исходники урока
Оглавление серии · Скачать исходники. Полное самостоятельное состояние находится в lesson-09. Учебные приложения, зависимости, база и проверки при подготовке не запускались; описанный результат ожидается по исходникам. Материалы пока находятся в локальной редакционной версии.
Первичные источники: документация 1.