Do we need to manage the state of radio buttons in Elm? - elm

I am referring to this example in http://elm-lang.org/examples/radio-buttons. I don't see anywhere whereby the state of the buttons is being managed.
In my own little Elm project I need to do something like
label []
[ input
[ type_ "radio"
, checked (model.choosenSize == size)
, onClick (SetSize size)
] []
, text (sizeToString size)
]
Without managing the checked attribute, all the radio buttons will remain checked after you click on it.
So what is the magic in the example?

The example you are referring is very simple. It doesn't explicitly manage the state of the buttons. Instead, their state is managed by the browser. In a real application, of course, you would better manage it explicitly. Something like:
view : Model -> Html Msg
view model =
div []
[ fieldset []
[ radio "Small" (model.fontSize == Small) (SwitchTo Small)
, radio "Medium" (model.fontSize == Medium) (SwitchTo Medium)
, radio "Large" (model.fontSize == Large) (SwitchTo Large)
]
, Markdown.toHtml [ sizeToStyle model.fontSize ] model.content
]
radio : String -> Bool -> msg -> Html msg
radio value isChecked msg =
label
[ style [("padding", "20px")]
]
[ input [ type_ "radio", checked isChecked, name "font-size", onClick msg ] []
, text value
]
(I added a Bool argument to radio)

Related

elm-ui center elements in wrapped row

I'm using Elm with mdgriffiths/elm-ui, and I've really been enjoying it. Right now, I'm trying to create a centered, wrapped row of elements:
I can get it to this point:
using this code:
button : String -> String -> Element Msg
button label url =
link
[ height (px 150)
, width (px 150)
, Border.width 1
, Background.color (rgb255 255 255 255)
]
{ url = url
, label =
Element.paragraph
[ Font.center ]
[ textEl [] label ]
}
row : Element Msg
row =
Element.wrappedRow
[ Element.spacing 25
, Element.centerX
, Element.centerY
, width (fill |> Element.maximum 600)
, Font.center
]
[ button "A" "/a"
, button "B" "/b"
, button "C" "/c"
, button "D" "/d"
]
But when I try adding Element.centerX to my buttons like this
link
[ Element.centerX
, ...
]
I get this instead:
I've also tried Font.center without success, and I don't know what else I can try.
I'm not sure if I'm missing something I should be using, or if the whole thing needs re-arranging, or if I just need to use the built-in CSS stuff.
Update:
Link to an Ellie with the issues I'm seeing.
https://ellie-app.com/7NpM6SPfhLHa1
This Github issue is useful for this problem. I'm afraid you will have to use some CSS (unless I'm missing something). I've found this before with elm-ui; every now and then it can't quite do what you want and you need a bit of CSS.
This works for me (taken from the post by AlienKevin in the link above). You need to set "marginLeft" and "marginRight" to "auto".
module Main exposing (main)
import Element as E
import Element.Border
import Html.Attributes
box : String -> E.Element msg
box label =
E.paragraph
[ E.width <| E.px 200
, Element.Border.width 1
, E.htmlAttribute (Html.Attributes.style "marginLeft" "auto")
, E.htmlAttribute (Html.Attributes.style "marginRight" "auto")
]
[ E.text label ]
row : E.Element msg
row =
E.wrappedRow []
[ box "A"
, box "B"
, box "C"
]
main =
E.layout [] row
(See here for an Ellie.)
You can also do the following:
Define the following elm-ui classes. I usually setup a UI.elm module for this
centerWrap : Attribute msg
centerWrap =
Html.Attributes.class "centerWrap"
|> htmlAttribute
dontCenterWrap : Attribute msg
dontCenterWrap =
Html.Attributes.class "dontCenterWrap"
|> htmlAttribute
Add the following to your css. Basically says center elements if has centerWrap class but not dontCenterWrap class.
:not(.dontCenterWrap).centerWrap>div.wrp {
justify-content: center !important;
}
Apply the attribute
wrappedRow [ width fill, UI.centerWrap, spaceEvenly ] [...]
Assuming you created a custom element that centerWraps and wanted to disable that you could use UI.dontCenterWrap
centerWrappedRow attr children =
wrappedRow (UI.centerWrap :: attr) children
-- somewhere else
...
centerWrappedRow [UI.dontCenterWrap] [..]
...

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-mdl: How to push a record from tab X into the model of tab Y and update tab Y view?

I'm using the demo codebase of elm-mdl as a starting point for my elm project. I have a situation where I need to click a button on one tab (e.g. "tab X") and mutate the state of a different tab (e.g. "tab Y").
Every way I've wired it up so far does not work. This seems like an odd case because the parent of all tabs (e.g. Layout) in the demo codebase is "Demo". It seems in my case that the dependency graph become convoluted because the effect would reach across "Demo" children.
How can this be done? I'm running 0.18.0.
https://github.com/debois/elm-mdl/tree/v8/demo
I've done an example playing around with what I've understand from your question.
I think your issue have to be that you are not passing the model through your tabs , so they are not rendering data related to the current state, instead you should be taking data from the initial model which always keeps inmutable.
Starting from the example code you should have this:
view : Model -> Html Msg
view model =
Tabs.render Mdl
[ 0 ]
model.mdl
[ Tabs.ripple
, Tabs.onSelectTab SelectTab
, Tabs.activeTab model.tab
]
[ Tabs.label
[ Options.center ]
[ Icon.i "info_outline"
, Options.span [ css "width" "4px" ] []
, text "About tabs"
]
, Tabs.label
[ Options.center ]
[ Icon.i "code"
, Options.span [ css "width" "4px" ] []
, text "Example"
]
]
[ case model.tab of
0 ->
tab0 model
_ ->
defaultTab model
]
My working example is here

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.