提问人:krsyoung 提问时间:10/27/2023 更新时间:10/31/2023 访问量:41
Turbo 处理的 303 重定向不保持或滚动到位置定位点
Turbo processed 303 redirect does not maintain or scroll to location anchor
问:
在 Turbo/Rails (7.1) 应用程序中,Turbo 不遵循方法中指定的具有状态的值。我希望页面会自动滚动到锚点的位置,但是,我认为由于某些问题,锚点丢失了,我最终位于页面顶部。anchor
redirect_to
:see_other
fetch()
这适用于 GET 和 PATCH (POST) 请求。不起作用的方案如下所示:
- link_to turbo_method补丁
- 控制器接收请求,然后使用锚点重定向到新页面
- 重定向发生,但锚点丢失,页面不滚动
这特别有问题的一个例子是通知(我们通过 PATCH 完成)到类似讨论的帖子。我们希望用户单击通知,将通知标记为已读,然后重定向到与通知关联的页面上的特定评论。
在上面的场景中,我希望单击链接,然后使用锚点进行redirect_to将导致用户登陆页面并自动滚动到具有锚点 ID 的元素。
我创建了一个 repo 来进一步演示这个问题,并希望发现其他人也面临同样的问题和/或找到了解决它的方法:https://github.com/harled/turbo-anchor-issue
通过此处的 Turbo GitHub 问题提出了相同的主题:https://github.com/hotwired/turbo/issues/211
我花了大量时间使用浏览器调试器跟踪 Turbo 代码,我的结论是获取(和不透明重定向)是问题所在,这意味着某种类型的应用程序平台解决方法是必要的。
答:
下面是一个解决方法,它使用包含锚点的redirect_to来处理 Turbo GET/PATCH 请求,如下所示:
redirect_to(discussion_path(anchor: dom_id(@comment), status: :see_other))
它的要点是将锚点切换到查询参数(转发),然后使用一点 javascript 来清理 URL 并滚动页面。
它感觉不干净,但它似乎确实有效。它还避免了让开发人员学习与参数交互的新方式。redirect_to
anchor:
# application_controller.rb
class ApplicationController < ActionController::Base
# Custom redirect_to logic to transparently support redirects with anchors so Turbo
# works as expected. The general approach is to leverage a query parameter to proxy the anchor value
# (as the anchor/fragment is lost when using Turbo and the browser fetch() follow code).
#
# This code looks for an anchor (#comment_100), if it finds one it will add a new query parameter of
# "_anchor=comment_100" and then remove the anchor value.
#
# The resulting URL is then passed through to the redirect_to call
def redirect_to(options = {}, response_options = {})
# https://edgeapi.rubyonrails.org/classes/ActionController/Redirecting.html
# We want to be conservative on when this is applied. Only a string path is allowed,
# a limited set of methods and only the 303/see_other status code
if options.is_a?(String) &&
%w[GET PATCH PUT POST DELETE].include?(request.request_method) &&
[:see_other, 303].include?(response_options[:status])
# parse the uri, where options is the string of the url
uri = URI.parse(options)
# check if there is a fragment present
if uri.fragment.present?
params = uri.query.present? ? CGI.parse(uri.query) : {}
# set a new query parameter of _anchor, with the anchor value
params["_anchor"] = uri.fragment
# re-encode the query parameters
uri.query = URI.encode_www_form(params)
# clear the fragment
uri.fragment = ""
end
options = uri.to_s
end
# call the regular redirect_to method
super
end
end
// application.js
// Whenever render is called, we want to see if there is a rails _anchor query parameter,
// if so, we want to transform it into a proper hash and then try to scroll to it. Find
// the associated server side code in a custom "redirect_to" method.
addEventListener('turbo:load', transformAnchorParamToHash)
function transformAnchorParamToHash (event) {
const url = new URL(location.href)
const urlParams = new URLSearchParams(url.search)
// _anchor is a special query parameter added by a custom rails redirect_to
const anchorParam = urlParams.get('_anchor')
// only continue if we found a rails anchor
if (anchorParam) {
urlParams.delete('_anchor')
// update the hash to be the custom anchor
url.hash = anchorParam
// create a new URL with the new parameters
let searchString = ''
if (urlParams.size > 0) {
searchString = '?' + urlParams.toString()
}
// the new relative path
const newPath = url.pathname + searchString + url.hash
// rewrite the history to remove the custom _anchor query parameter and include the hash
history.replaceState({}, document.title, newPath)
}
// scroll to the anchor
if (location.hash) {
const anchorId = location.hash.replace('#', '')
const element = document.getElementById(anchorId)
if (element) {
const stickyHeaderHeight = calculcateStickyHeaderHeight()
const elementTop = element.getBoundingClientRect().top
const elementTopWithHeaderOffset = elementTop + window.scrollY - stickyHeaderHeight
// for whatever reason we can't scroll to the element immediately, giving in a slight
// delay corrects the issue
setTimeout(function () {
window.scrollTo({ top: elementTopWithHeaderOffset, behavior: 'smooth' })
}, 100)
} else {
console.error(`scrollToAnchor: element was not found with id ${anchorId}`)
}
}
}
// take into account any possible sticky elements (which are assumed to be headers) and sum up their
// heights to use as an offset
function calculcateStickyHeaderHeight () {
let stickyHeaderHeight = 0
const allElements = document.querySelectorAll('*')
const stickyElements = [].filter.call(allElements, el => getComputedStyle(el).position === 'sticky')
stickyElements.forEach(el => { stickyHeaderHeight += el.getBoundingClientRect().height })
return stickyHeaderHeight
}
完整的 repo 在此分支中可用: https://github.com/harled/turbo-anchor-issue/tree/turbo-anchors
评论