Turbo 处理的 303 重定向不保持或滚动到位置定位点

Turbo processed 303 redirect does not maintain or scroll to location anchor

提问人:krsyoung 提问时间:10/27/2023 更新时间:10/31/2023 访问量:41

问:

在 Turbo/Rails (7.1) 应用程序中,Turbo 不遵循方法中指定的具有状态的值。我希望页面会自动滚动到锚点的位置,但是,我认为由于某些问题,锚点丢失了,我最终位于页面顶部。anchorredirect_to:see_otherfetch()

这适用于 GET 和 PATCH (POST) 请求。不起作用的方案如下所示:

  1. link_to turbo_method补丁
  2. 控制器接收请求,然后使用锚点重定向到新页面
  3. 重定向发生,但锚点丢失,页面不滚动

这特别有问题的一个例子是通知(我们通过 PATCH 完成)到类似讨论的帖子。我们希望用户单击通知,将通知标记为已读,然后重定向到与通知关联的页面上的特定评论。

在上面的场景中,我希望单击链接,然后使用锚点进行redirect_to将导致用户登陆页面并自动滚动到具有锚点 ID 的元素。

我创建了一个 repo 来进一步演示这个问题,并希望发现其他人也面临同样的问题和/或找到了解决它的方法:https://github.com/harled/turbo-anchor-issue

通过此处的 Turbo GitHub 问题提出了相同的主题:https://github.com/hotwired/turbo/issues/211

我花了大量时间使用浏览器调试器跟踪 Turbo 代码,我的结论是获取(和不透明重定向)是问题所在,这意味着某种类型的应用程序平台解决方法是必要的。

Ruby-on-Rails fetch-API 涡轮增压

评论


答:

0赞 krsyoung 10/27/2023 #1

下面是一个解决方法,它使用包含锚点的redirect_to来处理 Turbo GET/PATCH 请求,如下所示:

redirect_to(discussion_path(anchor: dom_id(@comment), status: :see_other))

它的要点是将锚点切换到查询参数(转发),然后使用一点 javascript 来清理 URL 并滚动页面。

它感觉不干净,但它似乎确实有效。它还避免了让开发人员学习与参数交互的新方式。redirect_toanchor:

# 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