如何在 Elm 中自动滚动到 div 的底部

How to auto-scroll to the bottom of a div in Elm

我将 <div>s 添加到包装器 <div> 并且我需要能够滚动到每次添加的最后一个。我如何在 Elm 中执行此操作?

<div class="messages" style="height: 7em; overflow: scroll">
  <div>Anonymous: Hello</div>
  <div>John: Hi</div>
</div>

直觉上,我似乎可以调用 运行 的 JavaScript 代码 element.scrollTop = element.scrollHeight:

AddChatMessage chatMessage ->
  ( { model | chatMessages = model.chatMessages ++ [ chatMessage ] } , scrollToBottomPort "div.messages" )

问题是 scrollToBottom 在 更新 model 之前被调用 。所以没什么大不了的,我将其转换为 Task。但是,尽管 model 现在首先更新,但 view 还没有更新。所以我最终滚动到底部的第二个项目!

这可能会引出一个我在 Elm 中很好奇的更普遍的问题,你如何 运行 在视图因模型更改而更新后 Cmd

您可以使用端口滚动到列表底部。
在这种情况下,您需要设置一个 javascript 以在滚动之前等待 1 个 AnimationFrame。确保您的列表已呈现。

一个更简单的方法是使用 Elm 的 Dom.Scroll 库。
并使用 toBottom 函数。

如果您像这样在代码中包含它:

case msg of
    Add ->
        ( model ++ [ newItem <| List.length model ]
        , Task.attempt (always NoOp) <| Scroll.toBottom "idOfContainer"
        )

    NoOp ->
        model ! []

应该可以。 我做了一个工作示例 here in runelm.io

Dom.Scroll 绝对是可行的方法,而且易于实施。

  1. 将要滚动的封闭 HTML 元素设置为具有不同的 ID。
  2. 确保所述元素的样式为 overflow-y: scroll 或 溢出-x:滚动。
  3. 从实际的任何消息发送命令 导致该元素为 displayed/opened(消息 opens/displays 元素,而您发出的命令将执行 滚动)。请注意,您使用命令是因为更改 DOM 在技术上是一种副作用。

查看说明滚动到底部的文档: toBottom : Id -> Task Error (),可以看到需要是一个Task,所以可以wrap成Task.attempt。例如: Task.attempt (\_ -> NoOp) (Dom.Scroll.toBottom Id)

然后您将需要一个将 Result Error () 转换为 Msg 的函数,但是由于滚动不太可能失败或者如果失败也几乎没有缺点,您可以设置一个虚拟函数,例如 (\_ -> NoOp) 作为一个快速破解。

自版本 0.19 Dom 已弃用,您可以使用 Html.Attribute.property 函数设置滚动位置。

例子

import Html exposing (Html, text)
import Html.Attributes exposing (class, property)
import Json.Encode as Encode


view : Model -> Html Msg
view model =
    div [ class "flow-editor"
        , property "scrollLeft" (Encode.float model.scrollLeft)
        , property "scrollTop" (Encode.float model.scrollTop)
        ]
        [ text "placeholder" ]

从 Elm 0.19 开始,使用函数 Browser.Dom.getViewportOf and Browser.Dom.setViewportOf 每次添加新元素时您都可以转到容器底部。

jumpToBottom : String -> Cmd Msg
jumpToBottom id =
    Dom.getViewportOf id
        |> Task.andThen (\info -> Dom.setViewportOf id 0 info.scene.height)
        |> Task.attempt (\_ -> NoOp)