Conditionally toggle `preventDefault` on form submissions in Elm - 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.

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
}

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: Conditional preventDefault (with contentEditable)

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

Create a New Record in a List of data in Elm

I finished loading resources from an API in Elm, everything is fine... except for one litte problem : I don't know how to update or create a new record without persisting it.
I have a type Msg (I striped some code for this demo)
type Msg
= NoOp
| FetchSucceed (List User)
| FetchError Http.Error
| UpdateTitle String
| ...
update msg model =
case model of
NoOp ->
( model, Cmd.none )
FetchSucceed newModel =
( { model | users = newModel, isLoading = False }, Cmd.none )
FetchError _ =
( { model | isLoading = False }, Cmd.none )
UpdateTitle newTitle =
-- I don't know what to put here, the previous messages
-- have a list, and I Just want to add ONE model
view model =
div []
[ List.map displayRow model.users
, formCreateUser {title = "", username = "", email = ""}
]
formCreateUser user =
div []
[ input [ onInput UpdateTitle, placeholder "Title" ] []
, button [ onClick SaveUser ] [ text "Save" ]
]
I would love to be able to add a new model from this form (formCreateUser), but I keep getting this error :
The 3rd element has this type:
VirtualDom.Node Msg
But the 4th is:
Html Link -> Html (String -> Msg)
edit2: Add some context
If I understand your example snippets, you have a page that shows the list of existing user, and you want to have a "quick add" form that lets you create another user given only a title. I'll give a quick example of how to achieve this which should hopefully shed some light on the problems you've run into.
I'm assuming your User and Model look like this at present:
type alias Model =
{ users : List User
, isLoading : Bool
}
type alias User =
{ title : String
, username : String
, email : String
}
Since you have that quick add form, I don't think you want to append the new user until they hit Submit. With that notion in mind, let's update Model to store the pending new user title:
type alias Model =
{ users : List User
, isLoading : Bool
, newUserTitle : Maybe String
}
Now we can change your view function accordingly. Since we want to display the typed title in the textbox, let's change formCreateUser to this:
formCreateUser model =
div []
[ input [ onInput UpdateTitle, placeholder "Title", value (Maybe.withDefault "" model.newUserTitle) ] []
, button [ onClick SaveUser ] [ text "Save" ]
]
That means the calling code in view needs updating too:
view model =
div []
[ div [] (List.map displayRow model.users)
, formCreateUser model
]
Now we need to handle the UpdateTitle Msg to set the contents as they are typed:
UpdateTitle newTitle ->
( { model | newUserTitle = Just newTitle }, Cmd.none )
And now we can also handle the submit button. This is where you would create the new user and append it to the list of existing users:
SaveUser ->
case model.newUserTitle of
Nothing -> (model, Cmd.none)
Just title ->
( { model
| newUserTitle = Nothing
, users = model.users ++ [{ title = title, username = "", email = "" }]
}, Cmd.none)
If you wanted SaveUser to submit it to your API endpoint, you'd also return an appropriate Cmd, but that seems outside the scope of your question.
While this all isn't an ideal way to handle your situation, hopefully this explanation gives you more understanding of the building blocks needed for this type of thing. I've posted the full gist here which can be pasted and run in elm-lang.org/try.

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.