EverydayDeveloper
EverydayDeveloper

Reputation: 1150

Modal in Elm without framework

I am new to ELM and I want to create a modal without the use of any libraries such as Bootstrap or ELM-UI. I found this simple example online which is also using JSON Decode. Is there a possibility to have the modal work simply without any framework/library and JSON Decode? How can I modify the code to simply get a working modal?

module Main exposing (main)

import Browser
import Html exposing (Html, Attribute, button, div, span, text)
import Html.Events exposing (onClick, on)
import Html.Attributes exposing (class, style)
import Json.Decode as Decode


type alias Model =
    { isVisible : Bool, count : Int }


initialModel : Model
initialModel =
    { isVisible = False, count = 0 }


type Msg
    = Show
    | Hide
    | Increment
    | Decrement


update : Msg -> Model -> Model
update msg model =
    case msg of
        Show ->
            { model | isVisible = True }

        Hide ->
            { model | isVisible = False }
            
        Increment ->
            { model | count = model.count + 1 }
            
        Decrement ->
            { model | count = model.count - 1 }


view : Model -> Html Msg
view model =
    div []
        [ button [ onClick Show ] [ text "Show!" ]
        , if model.isVisible then
            div
                ([ class dialogContainerClass
                 , on "click" (containerClickDecoder Hide)
                 ]
                    ++ dialogContainerStyle
                )
                [ div dialogContentStyle
                    [ span [] [ text "Click anywhere outside this dialog to close it!" ]
                    , span [] [ text "Clicking on anything inside of this dialog works as normal." ]
                    , div []
                        [ button [ onClick Decrement ] [ text "-" ]
                        , text (String.fromInt model.count)
                        , button [ onClick Increment ] [ text "+" ]
                        ]
                    ]
                ]
          else
            div [] []
        ]


dialogContainerClass : String
dialogContainerClass = "dialog-container-class"


containerClickDecoder : msg -> Decode.Decoder msg
containerClickDecoder closeMsg =
    Decode.at [ "target", "className" ] Decode.string
        |> Decode.andThen
            (\c ->
                if String.contains dialogContainerClass c then
                    Decode.succeed closeMsg

                else
                    Decode.fail "ignoring"
            )



dialogContainerStyle : List (Attribute msg)
dialogContainerStyle =
    [ style "position" "absolute"
    , style "top" "0"
    , style "bottom" "0"
    , style "right" "0"
    , style "left" "0"
    , style "display" "flex"
    , style "align-items" "center"
    , style "justify-content" "center"
    , style "background-color" "rgba(33, 43, 54, 0.4)"
    ]
    
    
dialogContentStyle : List (Attribute msg)
dialogContentStyle =
    [ style "border-style" "solid"
    , style "border-radius" "3px"
    , style "border-color" "white"
    , style "background-color" "white"
    , style "height" "120px"
    , style "width" "440px"
    , style "display" "flex"
    , style "flex-direction" "column"
    , style "align-items" "center"
    , style "justify-content" "center"
    ]


main : Program () Model Msg
main =
    Browser.sandbox
        { init = initialModel
        , view = view
        , update = update
        }

Upvotes: 1

Views: 787

Answers (1)

Igid
Igid

Reputation: 515

If I understand your question correctly, the problem you're trying to solve is clicking outside the modal to close it. Decoding the event object to get information about the DOM is a bit of a hack in Elm – I think you're right to try to avoid it, unless necessary. One way to achieve the same thing is to add a click event handler with stop propagation to your modal contents – this stops the click event from firing on the container when it originates from within the modal.

I've put your example code in an Ellie and made some small changes: https://ellie-app.com/b9gDPHgtz2ca1

This solution uses Html.Events.stopPropagationOn, which is like on but does a call to event.stopPropagation(). This function does require you to supply a decoder, so I'm afraid you can't get away from importing Json.Decode, but we are using the simplest possible decoder – Decode.succeed – and only to satisfy the parameters of the function.

I've added a NoOp variant to Msg, as there is nothing to do when the modal is clicked; simply attaching this event handler stops the Hide event from firing when we don't want it to.

Code

module Main exposing (main)

import Browser
import Html exposing (Attribute, Html, button, div, span, text)
import Html.Attributes exposing (class, style)
import Html.Events exposing (on, onClick)
import Json.Decode as Decode


type alias Model =
    { isVisible : Bool, count : Int }


initialModel : Model
initialModel =
    { isVisible = False, count = 0 }


type Msg
    = Show
    | Hide
    | Increment
    | Decrement
    | NoOp


update : Msg -> Model -> Model
update msg model =
    case msg of
        Show ->
            { model | isVisible = True }

        Hide ->
            { model | isVisible = False }

        Increment ->
            { model | count = model.count + 1 }

        Decrement ->
            { model | count = model.count - 1 }

        NoOp ->
            model


view : Model -> Html Msg
view model =
    div []
        [ button [ onClick Show ] [ text "Show!" ]
        , if model.isVisible then
            div
                (onClick Hide
                    :: dialogContainerStyle
                )
                [ div
                    (onClickStopPropagation NoOp
                        :: dialogContentStyle
                    )
                    [ span [] [ text "Click anywhere outside this dialog to close it!" ]
                    , span [] [ text "Clicking on anything inside of this dialog works as normal." ]
                    , div []
                        [ button [ onClick Decrement ] [ text "-" ]
                        , text (String.fromInt model.count)
                        , button [ onClick Increment ] [ text "+" ]
                        ]
                    ]
                ]

          else
            div [] []
        ]


onClickStopPropagation : msg -> Html.Attribute msg
onClickStopPropagation msg =
    Html.Events.stopPropagationOn "click" <| Decode.succeed ( msg, True )


dialogContainerStyle : List (Attribute msg)
dialogContainerStyle =
    [ style "position" "absolute"
    , style "top" "0"
    , style "bottom" "0"
    , style "right" "0"
    , style "left" "0"
    , style "display" "flex"
    , style "align-items" "center"
    , style "justify-content" "center"
    , style "background-color" "rgba(33, 43, 54, 0.4)"
    ]


dialogContentStyle : List (Attribute msg)
dialogContentStyle =
    [ style "border-style" "solid"
    , style "border-radius" "3px"
    , style "border-color" "white"
    , style "background-color" "white"
    , style "height" "120px"
    , style "width" "440px"
    , style "display" "flex"
    , style "flex-direction" "column"
    , style "align-items" "center"
    , style "justify-content" "center"
    ]


main : Program () Model Msg
main =
    Browser.sandbox
        { init = initialModel
        , view = view
        , update = update
        }

Upvotes: 5

Related Questions