Elm: Conditional preventDefault (with contentEditable) - elm

I'm trying to make a content editable tag that uses enter to update the model.
My code is below, and here is a version that you can play around with on Ellie.
The on "blur" attribute works and updates the model when you click away. But I want to get the same 'update' functionality when an enter is pressed.
view : Model -> Html Msg
view model =
let
attrs =
[ contenteditable True
--, on "blur" (Json.map UpdateTitle targetTextContent)
, onInput2 UpdateTitle
, onEnter EnterPressed
, id "title"
, class "title"
]
in
div []
[ h1 attrs [ text model.existing ]
, text "Click above to start editing. Blur to save the value. The aim is to capture an <enter> and interpret that as a blur, i.e. to save the value and blur the field"
, p [] [ text <| "(" ++ model.existing ++ ")" ]
]
targetTextContent : Json.Decoder String
targetTextContent =
Json.at [ "target", "textContent" ] Json.string
onInput2 : (String -> msg) -> Attribute msg
onInput2 msgCreator =
on "input" (Json.map msgCreator targetTextContent)
onEnter : (Bool -> msg) -> Attribute msg
onEnter enterMsg =
onWithOptions "keydown"
{ stopPropagation = False
, preventDefault = False
}
(keyCode
|> Json.andThen
(\ch ->
let
_ =
Debug.log "on Enter" ch
in
Json.succeed (enterMsg <| ch == 13)
)
)
This code seems to be updating the model ok, but the DOM is getting messed up. For example if I enter enter after "blast" I see this
I tried switching to Html.Keyed and using "keydown" but it did not make any difference or just created different issues.

Solved! The key point is the filter function that uses Json.Decode.fail so that only <enter> is subject to preventDefault. See https://github.com/elm-lang/virtual-dom/issues/18#issuecomment-273403774 for the idea.
view : Model -> Html Msg
view model =
let
attrs =
[ contenteditable True
, on "blur" (Json.map UpdateTitle targetTextContent)
, onEnter EnterPressed
, id "title"
, class "title"
]
in
div []
[ h1 attrs [ text model.existing ]
, text "Click above to start editing. Blur to save the value. The aim is to capture an <enter> and interpret that as a blur, i.e. to save the value and blur the field"
, p [] [ text <| "(" ++ model.existing ++ ")" ]
]
targetTextContent : Json.Decoder String
targetTextContent =
Json.at [ "target", "textContent" ] Json.string
onEnter : msg -> Attribute msg
onEnter msg =
let
options =
{ defaultOptions | preventDefault = True }
filterKey code =
if code == 13 then
Json.succeed msg
else
Json.fail "ignored input"
decoder =
Html.Events.keyCode
|> Json.andThen filterKey
in
onWithOptions "keydown" options decoder

Related

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
}
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
}

Passing multiple complex parent actions to deeply nested child views

Disclaimer: I realized this was a maybe stupid question after I finished writing it. Please don't spend too much time reading it. I am very new to Elm, functional programming, and not a UI buff.
I have a view in Elm that returns Html Msg and takes in a model. Using the simple increment demo as en example, I have this typical setup:
module Main exposing (..)
import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
-- MAIN
main =
Browser.sandbox { init = init, update = update, view = view }
-- MODEL
type alias Model = Int
init : Model
init =
0
-- UPDATE
type Msg
= Increment
| Decrement
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
model + 1
Decrement ->
model - 1
-- VIEW
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (String.fromInt model) ]
, button [ onClick Increment ] [ text "+" ]
]
I have a button component that's quite complex which I would like to extract into a separate function. I'm able to do this with normal Html, i.e.
-- VIEW
some_html : Html msg
some_html =
text "FOO"
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (String.fromInt model) ]
, button [ onClick Increment ] [ text "+" ]
, some_html
]
I can also pass the Msg type I've defined and have the "sub-function" call the action:
-- VIEW
make_button : Msg -> Html Msg
make_button msg =
button [ onClick msg ] [ text "-" ]
view : Model -> Html Msg
view model =
div []
[ make_button Decrement
, div [] [ text (String.fromInt model) ]
, button [ onClick Increment ] [ text "+" ]
]
My problem and question is:
I would like to be able to have my make_button function be able to handle multiple actions. One way I have found that works is to pass all possible actions and then a key, i.e.
-- VIEW
make_button : Msg -> Msg -> String -> Html Msg
make_button decr incr which =
if which == "Decrement" then
button [ onClick decr ] [ text "-" ]
else button [ onClick incr ] [ text "+" ]
view : Model -> Html Msg
view model =
div []
[ make_button Decrement Increment "Decrement"
, div [] [ text (String.fromInt model) ]
, make_button Decrement Increment "Increment" -- doesn't matter here.
]
But this becomes cumbersome when the number of actions is large (in my use case I have 20 or so actions).
Should I create a dictionary of sorts? Is there a way this is done? Is this a bad thing to do? Please give me grief.
I am imaging scenarios where many nested child components might want to have the ability to call any Action of the parent component on the fly without this being hard-coded, which is why I decided to still ask the question.
Thanks.
You're definitely over thinking things! The way you would do this is
-- camel case is the convention in Elm ;)
makeButton : Msg -> Html Msg
makeButton msg =
button
[ onClick msg ]
[ text <|
-- an if statement would also work in this case
case msg of
Increment ->
"+"
Decrement ->
"-"
]
view : Model -> Html Msg
view model =
div []
[ makeButton Decrement
, div [] [ text (String.fromInt model) ]
, makeButton Increment
]

Elm - How Do I Detect Current Focus

How do you get the current focus in Elm? I know how to set focus with Elm, but I can't find any functionality to detect what currently has focus.
The elm-lang/dom package allows setting focus on an element given an ID but it does not allow you to fetch the currently focused element. It hints that you can use document.activeElement for this. To do that, you'll have to use ports.
Here is a contrived example. Let's say you have a Model that contains the currently selected id and a list of all ids of some textboxes we'll soon create.
type alias Model =
{ selected : Maybe String
, ids : List String
}
The Msgs we will use will be able to inquire about the focus as well as use the Dom library to set focus:
type Msg
= NoOp
| FetchFocused
| FocusedFetched (Maybe String)
| Focus (Maybe String)
For that, we will need two ports:
port focusedFetched : (Maybe String -> msg) -> Sub msg
port fetchFocused : () -> Cmd msg
The javascript calling these ports will report on the current document.activeElement:
var app = Elm.Main.fullscreen()
app.ports.fetchFocused.subscribe(function() {
var id = document.activeElement ? document.activeElement.id : null;
app.ports.focusedFetched.send(id);
});
The view displays the currently selected id, provides a list of buttons that will set the focus on one of the numbered textboxes below.
view : Model -> Html Msg
view model =
div []
[ div [] [ text ("Currently selected: " ++ toString model.selected) ]
, div [] (List.map viewButton model.ids)
, div [] (List.map viewInput model.ids)
]
viewButton : String -> Html Msg
viewButton id =
button [ onClick (Focus (Just id)) ] [ text id ]
viewInput : String -> Html Msg
viewInput idstr =
div [] [ input [ id idstr, placeholder idstr, onFocus FetchFocused ] [] ]
The update function ties it all together:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
NoOp ->
model ! []
FetchFocused ->
model ! [ fetchFocused () ]
FocusedFetched selected ->
{ model | selected = selected } ! []
Focus (Just selected) ->
model ! [ Task.attempt (always NoOp) (Dom.focus selected), fetchFocused () ]
Focus Nothing ->
{ model | selected = Nothing } ! [ fetchFocused () ]
Here is a working example on ellie-app.com.

Elm: clear form on submit

I have a simple form with one field. I would like to clear the field on form submit. I am clearing my model in my update function, but text remains in the text input.
type alias Model =
{ currentSpelling : String }
type Msg
= MorePlease
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
MorePlease ->
( log "cleared spelling: " { model | currentSpelling = "" }
, fetchWord model.currentSpelling )
view : Model -> Html Msg
view model =
div []
[ Html.form [ onSubmit MorePlease ]
[ input [ type' "text"
, placeholder "Search for your word here"
, onInput NewSpelling
, attribute "autofocus" ""
] []
, text model.currentSpelling
, input [ type' "submit" ] [ text "submit!" ]
]
]
The text displaying model.currentSpelling clears out when I empty it with the update function, but the text input box remains populated. Any idea how to clear it?
fetchWord makes an HTTP call, but it's omitted here.
add value model.currentSpelling into Attributes of the
input element. That's how you can control the string
inside of input element in html.

Conditionally toggle `preventDefault` on form submissions in Elm

Is there a way to conditionally toggle preventDefault on form submissions?
I've tried using onWithOptions, below, but it seems only the first setting is used, even though I can see it change using Debug.log, when there is a name on the model.
, onWithOptions
"submit"
(if model.name == "" then
(Debug.log "prevent" { preventDefault = True, stopPropagation = True })
else
(Debug.log "allow" { preventDefault = False, stopPropagation = False })
)
(Json.succeed FormSubmit)
Updated with findings from answer
As stated by #Tosh, hiding the button resolves the issue:
, button
[ onPreventClickHtml FormSubmit
, Html.Attributes.hidden (model.name == "" |> not) ]
[ text " prevent" ]
, button
[ onClick FormSubmit
, Html.Attributes.hidden (model.name == "") ]
[ text "allow" ]
Where onPreventClickHtml is:
onPreventClickHtml : Msg -> Attribute Msg
onPreventClickHtml msg =
onWithOptions
"click"
{ preventDefault = True, stopPropagation = True }
(Json.succeed msg)
Conditionally displaying the button does not work:
, (if model.name == "" then
button
[ onPreventClickHtml FormSubmit ]
[ text " prevent" ]
else
button
[ type' "submit", onClick FormSubmit ]
[ text "allow" ]
)
I'm using elm-mdl and so found implementing the solution more challenging because from my experience creating a custom onclick requires types which are not exposed by elm-mdl, and so need to be duplicated.
, if model.name == "" then
Material.Options.nop
else
Material.Options.css "visibility" "hidden"
, onPreventClick FormSubmit
, if model.name == "" then
Material.Options.css "visibility" "hidden"
else
Material.Options.nop
, Button.onClick FormSubmit
onPreventClick : Msg -> Property Msg
onPreventClick message =
Material.Options.set
(\options -> { options | onClick = Just (onPreventClickHtml message) })
-- Duplicated from elm-mdl
type alias Property m =
Material.Options.Property (Config m) m
-- Duplicated from elm-mdl
type alias Config m =
{ ripple : Bool
, onClick : Maybe (Attribute m)
, disabled : Bool
, type' : Maybe String
}
Another approach, which worked for my scenario, was changing the button type:
, if model.name == "" then
Button.type' "reset"
else
Button.type' "submit"
One simple workaround suggestion until someone can show us how to solve it.
How about create two buttons (with different options as you've shown) and depending on the condition show only one of them?
You can use Html.Attributes.hide for that.