【问题标题】:Hide a component when clicked outside在外部单击时隐藏组件
【发布时间】:2015-10-17 10:23:41
【问题描述】:

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

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

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

【问题讨论】:

  • 你不能尝试一些类似于你找到的 React 的东西吗?全局点击处理程序发送一个在组件父级的update 函数中处理的Action(或组件本身,如果您认为它是知道何时隐藏的任​​务的一部分),并评估是否需要某些东西被隐藏,然后反映在你的Model
  • 感谢您的建议,我会尝试做类似的事情,如果它可行,我将使用我的大多数解决方案(我只需要先了解更多关于 Native js 模块的信息)。我对这条路径的主要关注(假设您的意思是在安装/卸载事件上附加处理程序,例如在 React 解决方案上)是,如果我们无论如何都要打破强加的限制,这可能会破坏 FRP 的目的。但我意识到,如果只在诸如此类的极少数情况下使用它可能没问题。
  • 我不知道合适的 in-Elm 解决方案,所以我建议您现在复制 React 解决方案来解决您的问题。但是请在邮件列表上讨论这个问题。应该有一个不需要这些黑客的适当解决方案;)
  • 好的,稍后会在邮件列表中发帖(似乎更活跃)

标签: elm


【解决方案1】:

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

这个想法是,您可以通过BrowserEvents.onMouseDown 将全局侦听器附加到mousedown,并将自定义解码器传递给它,该解码器将从事件对象中解码target DOM 节点。 “解码 DOM 节点”是指仅解码节点的 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 详细解释了它是如何工作的,并在github 上有一个下拉示例。

【讨论】:

    【解决方案2】:

    现有答案在 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;
    }
    

    【讨论】:

      【解决方案3】:

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

      modal 会显示一个地址(用于向其发送 'dismiss' 事件)、当前窗口尺寸和一个 elm-html Html 组件(这是要关注的东西,如日期选择器或形式)。

      我们为周围的元素附加一个点击处理程序;给它一个适当的ID后,我们可以确定收到的点击是否适用于它或孩子,并适当地转发它们。唯一真正聪明的一点是部署customDecoder 来过滤子元素上的点击。

      在其他地方,在接收到“解雇”事件时,我们的模型状态会发生变化,因此我们不再需要调用 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)
      

      【讨论】:

      • 哇,非常感谢,这很有帮助!也有点类似于我到目前为止所尝试的。 target 的解码部分很有趣。这可能超出了问题的范围,但只是想指出,在我看来,不可避免地要使用 JS 原生调用来计算相对于文档的单击元素位置和生成的模态尺寸(至少我是这样做的,绕过类型安全检查并调用内部使用target.getBoundingClientRect()的js原生函数,以便可以将模态放置在目标附近)。
      • 你也许可以用 package.elm-lang.org/packages/TheSeamau5/elm-html-decoder/1.0.1/… 的位来替换你的本地调用(注意:不是我的)
      猜你喜欢
      • 2016-11-24
      • 2012-07-17
      • 2011-09-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-04-18
      • 1970-01-01
      相关资源
      最近更新 更多