Create a New Record in a List of data in Elm - 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.

Related

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

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.

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 ] []