Elm: clear form on submit - elm

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.

Related

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: 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.

Extending the Elm tutorial form app to include a numbered input Age

I've been following this tutorial: http://guide.elm-lang.org/architecture/user_input/forms.html
The text there makes sense to me, my question pertains to the exercise it lists at the bottom of the page. It asks that I:
"Add an additional field for age and check that it is a number."
I am having difficulty with this because the onInput function seems to only accept a String input. I find it odd that there is no equivalent for type="number" inputs.
Nevertheless, this is my attempt which does not work:
import Html exposing (..)
import Html.App as Html
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import String exposing (length)
main =
Html.beginnerProgram { model = model, view = view, update = update }
-- MODEL
type alias Model =
{ name : String
, password : String
, passwordAgain : String
, age : Int
}
model : Model
model =
Model "" "" "" 0
-- UPDATE
type Msg
= Name String
| Password String
| PasswordAgain String
| Age Int
update : Msg -> Model -> Model
update msg model =
case msg of
Name name ->
{ model | name = name }
Password password ->
{ model | password = password }
PasswordAgain password ->
{ model | passwordAgain = password }
Age age ->
{ model | age = age }
-- VIEW
view : Model -> Html Msg
view model =
div []
[ input [ type' "text", placeholder "Name", onInput Name ] []
, input [ type' "password", placeholder "Password", onInput Password ] []
, input [ type' "password", placeholder "Re-enter Password", onInput PasswordAgain ] []
, input [ type' "number", placeholder "Age", onInput Age ] []
, viewValidation model
]
viewValidation : Model -> Html msg
viewValidation model =
let
(color, message) =
if model.password /= model.passwordAgain then
("red", "Passwords do not match!")
else if length model.password <= 8 then
("red", "Password must be more than 8 characters!")
else
("green", "OK")
in
div [ style [("color", color)] ] [ text message ]
The error I get is the following:
-- TYPE MISMATCH ----------------------------------------------------- forms.elm
The argument to function `onInput` is causing a mismatch.
58| onInput Age
^^^
Function `onInput` is expecting the argument to be:
String -> a
But it is:
Int -> Msg
Note: I am aware that I could create the Age input as just another text input, but the exercise specifically asked me to check that it is a `number type. I assume this means I should hold it inside the model as an Int.
I am clear about what the error is. I simply want to know the idiomatic way to fix this in Elm. Thanks.
Any user-input from onInput event is a String.
Your Model expects it to be an Int
Use String.toInt to parse the integer value from a string value.
Adjust update function to convert the type to an Int and change the type signature to Age String
Age age ->
case String.toInt age of
Ok val ->
{ model | age = val }
-- Notify the user, or simply ignore the value
Err err ->
model
That way you have an option to notify the user about the error.
In case if Maybe value suits you better, the whole statement can be simplified to:
Age age ->
{ model | age = Result.toMaybe (String.toInt age) }
You'll need the equivalent of onInput but for one that operates on integers. Based on how targetValue is defined, you can do something similar with the addition of Json.Decode.int to parse it as an integer:
onIntInput : (Int -> msg) -> Attribute msg
onIntInput tagger =
Html.Events.on "input" (Json.map tagger (Json.at ["target", "value"] Json.int))
You can then use it as such:
, input [ type' "number", placeholder "Age", onIntInput Age ] []