在外部单击时隐藏组件

Hide a component when clicked outside

处理应该隐藏此组件的单个组件外部的点击的正确方法是什么?

此类组件的示例可能是下拉菜单、日期选择器等。我们通常希望它们在我们点击外部时隐藏起来。但要做到这一点,我们似乎必须执行一些 "impure" 我不确定如何在 FRP 风格中避免的 hack。

我搜索了相关的 React 示例以寻找想法并找到了 this,但它们似乎都依赖于将回调附加到全局对象,然后修改内部组件的状态。

以下示例与您描述的内容类似。

modal 显示地址(发送 'dismiss' 事件)、当前 window 维度和 elm-html Html 组件(这是要重点关注的事情,例如日期选择器或表格)。

我们将点击处理程序附加到周围的元素;给它一个合适的 id 后,我们可以计算出收到的点击是否适用于它或 child,并适当地转发它们。唯一真正聪明的一点是部署 customDecoder 来过滤掉对 child 元素的点击。

在其他地方,在接收到 'dismiss' 事件时,我们的模型状态发生变化,因此我们不再需要调用 modal.

这是一个相当大的代码示例,它使用了相当少的 elm 包,所以请询问是否需要进一步解释

import Styles exposing (..)

import Html exposing (Attribute, Html, button, div, text)
import Html.Attributes as Attr exposing (style)
import Html.Events exposing (on, onWithOptions, Options)
import Json.Decode as J exposing (Decoder, (:=))
import Result
import Signal exposing (Message)


modal : (Signal.Address ()) -> (Int, Int) -> Html -> Html
modal addr size content = 
    let modalId = "modal"
        cancel = targetWithId (\_ -> Signal.message addr ()) "click" modalId
        flexCss = [ ("display", "flex")
                  , ("align-items", "center")
                  , ("justify-content", "center")
                  , ("text-align", "center")
                  ]
    in div (
            cancel :: (Attr.id modalId) :: [style (flexCss ++ absolute ++ dimensions size)]
           ) [content]

targetId : Decoder String
targetId = ("target" := ("id" := J.string))        

isTargetId : String -> Decoder Bool
isTargetId id = J.customDecoder targetId (\eyed -> if eyed == id then     Result.Ok True else Result.Err "nope!") 

targetWithId : (Bool -> Message) -> String -> String -> Attribute
targetWithId msg event id = onWithOptions event stopEverything (isTargetId id) msg

stopEverything = (Options True True)

现有答案在 elm v0.18 中不起作用(Signal 在 0.17 中被删除),所以我想更新它。这个想法是在下拉菜单后面添加一个顶级透明背景。如果您愿意,这具有能够使菜单后面的所有内容变暗的额外效果。

这个示例模型有一个单词列表,任何单词都可能有一个打开的下拉列表(和一些相关信息),所以我映射它们以查看是否有任何一个是打开的,在这种情况下我显示背景div 在其他一切之前:

主视图功能中有背景:

view : Model -> Html Msg
view model =
    div [] <|
        [ viewWords model
        ] ++ backdropForDropdowns model

backdropForDropdowns : Model -> List (Html Msg)
backdropForDropdowns model =
    let
        dropdownIsOpen model_ =
            List.any (isJust << .menuMaybe) model.words
        isJust m =
            case m of
                Just _ -> True
                Nothing -> False
    in
        if dropdownIsOpen model then
            [div [class "backdrop", onClick CloseDropdowns] []]
        else
            []

CloseDropdowns 在应用程序的顶级更新功能中处理:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        CloseDropdowns ->
            let
                newWords = List.map (\word -> { word | menuMaybe = Nothing } ) model.words
            in
                ({model | words = newWords}, Cmd.none)

并使用 scss 设置样式:

.popup {
    z-index: 100;
    position: absolute;
    box-shadow: 0px 2px 3px 2px rgba(0, 0, 0, .2);
}

.backdrop {
    z-index: 50;
    position: absolute;
    background-color: rgba(0, 0, 0, .4);
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
}

这里的聚会有点晚了,但我一直在努力解决完全相同的问题,slack 上的 elm 社区提出了一种检测元素外部点击的好方法(比方说,下拉菜单)。

我们的想法是,您可以通过 BrowserEvents.onMouseDown 将全局侦听器附加到 mousedown,并向其传递一个自定义解码器,该解码器将从事件中解码 target DOM 节点目的。 "decoding DOM node" 我的意思是只解码节点的 idparentNode 属性。 parentNode 将允许递归遍历 DOM 树,并为每个节点检查其 id 是否与下拉列表的 id 相同。

此代码(在 elm 0.19 中)如下所示:

-- the result answers the question: is the node outside of the dropdown?
isOutsideDropdown : String -> Decode.Decoder Bool
isOutsideDropdown dropdownId =
    Decode.oneOf
        [ Decode.field "id" Decode.string
            |> Decode.andThen
                (\id ->
                    if dropdownId == id then
                        -- found match by id
                        Decode.succeed False

                    else
                        -- try next decoder
                        Decode.fail "continue"
                )
        , Decode.lazy 
            (\_ -> isOutsideDropdown dropdownId |> Decode.field "parentNode")

        -- fallback if all previous decoders failed
        , Decode.succeed True
        ]


-- sends message Close if target is outside the dropdown
outsideTarget : String -> Decode.Decoder Msg
outsideTarget dropdownId =
    Decode.field "target" (isOutsideDropdown "dropdown")
        |> Decode.andThen
            (\isOutside ->
                if isOutside then
                    Decode.succeed Close

                else
                    Decode.fail "inside dropdown"
            )


-- subscribes to the global mousedown
subscriptions : Model -> Sub Msg
subscriptions _ =
   Browser.Events.onMouseDown (outsideTarget "dropdown")

代码使用了Json-Decode包,需要通过elm install elm/json安装。

我也写了一个article explaining in details how this works, and have an example of a dropdown on github.