ELM - List to table rows - elm

In the examples, when mapping a list to html I always see something like
ul []
List.map toHtmlFunction myList
But what if the list is only a partial part of the child html elements like
...
table []
[
thead []
[
th [][text "Product"],
th [][text "Amount"]
],
List.map toTableRow myList,
tr []
[
td [][text "Total"],
td [][text toString(model.total)]
]
]
toTableRow: MyListItem -> Html Msg
toTableRow myListItem =
tr []
[
td[][text myListItem.label],
td[][text toString(myListItem.price)]
]
With this code I'm getting
The 1st element has this type:
VirtualDom.Node a
But the 2nd is:
List (Html Msg)

The problem is that thead and tr are of type Html a, while List.map returns a List (Html a), and they can't be combined just by using commas.
You could have a look a the functions for putting list together in the List package. For example you could do something like
table []
List.concat [
[ thead []
[ th [][text "Product"]
, th [][text "Amount"]
]
],
List.map toTableRow myList,
[ tr []
[ td [][text "Total"]
, td [][text toString(model.total)]
]
]
]

IMO the cleanest solution lies in #wintvelt's first suggestion: table [] ([ myHeader ] ++ List.map ...). For new users of elm, this seems the most intuitive. (BTW, I am a new user.)
Essentially, here, the takeaway realization is that the elm compiler doesn't group table [] [] ++ [] as table [] ([] ++ []) (for example). Instead, elm groups it as (table [] []) ++ []. This makes sense, if you think about it.
Thus, elm's evaluation of table [] [] ++ [] produces, at first, something of type Html msg (in Elm 0.18). Thereafter, the ++ function balks when it tries to combine that Html msg with a List.
(Naturally, also, if you try to append a List in the wrong way to some of your Html attributes, by coding table [] ++ [] [], you'll get a similar error message.)
Here's a fleshed out solution, tested with elm-make 0.18 (elm Platform 0.18.0):
module Main exposing (main)
import Html exposing (..)
main : Program Never Model Msg
main =
Html.program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
-- MODEL
type alias Model =
{ messages : List String }
init : ( Model, Cmd Msg )
init =
( Model [], Cmd.none )
-- UPDATE
type Msg
= None
update : Msg -> Model -> ( Model, Cmd Msg )
update msg { messages } =
case msg of
None ->
( Model messages, Cmd.none )
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[]
-- VIEW
type alias MyListItem =
{ label : String
, price : Float
}
total : Float
total =
5.0
myList : List MyListItem
myList =
[ { label = "labelA", price = 2 }
, { label = "labelB", price = 3 }
]
toTableRow : MyListItem -> Html Msg
toTableRow myListItem =
tr []
[ td [] [ text myListItem.label ]
, td [] [ text (toString myListItem.price) ]
]
view : Model -> Html Msg
view model =
table
[]
([ thead []
[ th [] [ text "Product" ]
, th [] [ text "Amount" ]
]
]
++ List.map toTableRow myList
++ [ tr
[]
[ td [] [ text "Total" ]
, td [] [ text (toString total) ]
]
]
)

Related

Elm onInput character one behind

What am I doing wrong that is causing the value reported by onInput to be a character behind?
For example, type "mil" in the text field to filter to the mileposts row. Then delete it back to nothing and you'll see its still filtering mileposts (also see the browser console to see that value is still "m" even thought the text field is visibly "")
module Main exposing (main)
import Browser
import Html exposing (Html, a, button, div, input, li, span, text, ul)
import Html.Attributes exposing (checked, class, classList, placeholder, style, type_, value)
import Html.Events exposing (custom, onBlur, onClick, onFocus, onInput)
import Json.Decode as Json
type alias Layer =
{ name : String
, description : String
, selected : Bool
}
main : Program () Model Msg
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = \_ -> Sub.none
}
type alias Model =
{ open : Bool
, layers : List Layer
, filtered : List Layer
, searchText : String
, highlightedIndex : Int
}
init : () -> ( Model, Cmd Msg )
init _ =
let
layers =
[ { name = "Parcels", description = "Show parcel lines", selected = False }
, { name = "Mileposts", description = "Show Mile post markers", selected = False }
]
in
( { open = False, layers = layers, filtered = layers, searchText = "", highlightedIndex = 0 }, Cmd.none )
type Msg
= Open
| Close
| Change String
| Up
| Down
| Toggle
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
let
lastIndex =
List.length model.filtered - 1
in
case msg of
Open ->
( { model | open = True }, Cmd.none )
Close ->
( { model | open = False }, Cmd.none )
Change value ->
let
filtered =
model.layers
|> List.filter
(\{ name } ->
let
_ =
Debug.log "name" name
_ =
Debug.log "searchText" model.searchText
in
String.contains (String.toLower model.searchText) (String.toLower name) |> Debug.log "contains"
)
in
( { model | searchText = value, filtered = filtered }, Cmd.none )
Up ->
if model.highlightedIndex == 0 then
( { model | highlightedIndex = lastIndex }, Cmd.none )
else
( { model | highlightedIndex = model.highlightedIndex - 1 }, Cmd.none )
Down ->
if model.highlightedIndex == lastIndex then
( { model | highlightedIndex = 0 }, Cmd.none )
else
( { model | highlightedIndex = model.highlightedIndex + 1 }, Cmd.none )
Toggle ->
let
highlightedLayer =
model.filtered
|> List.indexedMap Tuple.pair
|> List.filterMap
(\( idx, layer ) ->
if idx == model.highlightedIndex then
Just layer
else
Nothing
)
updatedFiltered =
model.filtered
|> List.indexedMap
(\idx layer ->
if idx == model.highlightedIndex then
{ layer | selected = not layer.selected }
else
layer
)
updatedLayers =
model.layers
|> List.map
(\layer ->
if [ layer ] == highlightedLayer then
{ layer | selected = not layer.selected }
else
layer
)
in
( { model | filtered = updatedFiltered, layers = updatedLayers }, Cmd.none )
view model =
div []
[ span [ class "mapboxgl-ctrl-geocoder--icon mapboxgl-ctrl-geocoder--icon-search" ]
[ span [ class "ds-badge ds-badge--red ds-badge--circle", style "margin-top" "-2px" ] [ text "3" ]
]
, input
[ type_ "text"
, class "mapboxgl-ctrl-geocoder--input"
, placeholder "Search layers"
, value model.searchText
, onFocus Open
, style "padding-left" "45px"
, onInput Change
--, onKey [(38, Up), (40, Down), (13, Toggle)]
]
[]
, if model.open then
div [ class "suggestions-wrapper" ]
[ ul [ class "suggestions", style "display" "block" ]
(model.filtered
|> List.indexedMap
(\idx { name, description, selected } ->
li [ classList [ ( "active", model.highlightedIndex == idx ) ] ]
[ a []
[ div [ class "mapboxgl-ctrl-geocoder--suggestion flex flex-column" ]
[ div [] [ input [ type_ "checkbox", style "margin-top" "5px", checked selected ] [] ]
, div [ class "ml-1" ]
[ div [ class "mapboxgl-ctrl-geocoder--suggestion-title" ] [ text name ]
, div [ class "mapboxgl-ctrl-geocoder--suggestion-address" ] [ text description ]
]
]
]
]
)
)
]
else
text ""
]
onKey : List ( Int, Msg ) -> Html.Attribute Msg
onKey codes =
let
isEnterKey keyCode =
case codes |> List.filter (\( code, _ ) -> code == keyCode) of
[ ( _, msg ) ] ->
Json.succeed
{ message = msg
, stopPropagation = True
, preventDefault = True
}
_ ->
Json.fail "silent failure :)"
in
custom "keydown" <|
Json.andThen isEnterKey Html.Events.keyCode
options =
{ stopPropagation = True
, preventDefault = True
}
https://ellie-app.com/fcgHCF2z5sza1
The problem is here, when handling the Change message from onInput:
Change value ->
let
filtered =
model.layers
|> List.filter
(\{ name } ->
let
_ =
Debug.log "name" name
_ =
Debug.log "searchText" model.searchText
in
String.contains (String.toLower model.searchText) (String.toLower name) |> Debug.log "contains"
)
in
( { model | searchText = value, filtered = filtered }, Cmd.none )
You're using model.searchText to filter the list, binding the result to filtered, then updating model with the new searchText and filtered list. model.searchText still has the previous value when you're filtering. Use value instead when filtering, then it works as expected.

Reset an Elm select dropdown value

I basically have a little select dropdown like this:
viewDropdown : Model -> Html Msg
viewDropdown model =
let
options =
[ ( "", "-- Select --" )
, ( "not_available", "Unavailable" )
, ( "available", "Available" )
]
buildOption ( k, v ) =
option [ value k, selected (k == model.isAvailable) ] [ text v ]
viewOptions =
options
|> List.map
(\( k, v ) ->
buildOption ( k, v )
)
in
div [ class "styled-select" ]
[ select
[ on "change" (Decode.map (UpdateAvailability) targetValue)
]
viewOptions
]
If the user selects "Available", a modal pops up and they are prompted to confirm. If they hit "Cancel", I want the Select dropdown's value to reset to the value of "". This is not the case and although my model reflects the a Nothing val, the dropdown selection option is still on "Available". Any idea on what I can do to reset the DOM state?
While this doesn't answer why your code doesn't work, it seems that if you add the step of confirming the selection, then code will work:
Ellie example, with full code below: https://ellie-app.com/3P5TTM9YqVWa1
module Main exposing (main)
import Browser
import Html exposing (Html, button, div, text, option, select)
import Html.Events exposing (onClick, on, targetValue)
import Html.Attributes exposing (value, selected, class)
import Json.Decode as Decode
import Task
type alias Model =
{ isAvailable : String , showConfirm: Bool }
init : Model
init =
{ isAvailable = "-", showConfirm = False }
type Msg
= UpdateAvailability String
| ConfirmYes
| Reset
update : Msg -> Model -> Model
update msg model =
case msg of
UpdateAvailability v ->
{ model | isAvailable = v, showConfirm = (v == "available") }
ConfirmYes ->
{ model | showConfirm = False }
Reset ->
{ model | isAvailable = "-", showConfirm = False }
viewDropdown : Model -> Html Msg
viewDropdown model =
let
options =
[ ( "-", "-- Select --" )
, ( "not_available", "Unavailable" )
, ( "available", "Available" )
]
buildOption ( k, v ) =
option [ value k, selected (k == model.isAvailable) ] [ text v ]
viewOptions = List.map buildOption options
in
div []
[ select
[ on "change" (Decode.map (UpdateAvailability) targetValue) ]
viewOptions
]
viewConfirm model =
if model.showConfirm then
div []
[ text "Really available?"
, button [ onClick ConfirmYes ] [ text "Yes" ]
, button [ onClick Reset ] [ text "No" ]
]
else
div [] []
view : Model -> Html Msg
view model =
div []
[ viewDropdown model
, viewConfirm model
, button [ onClick Reset ] [ text "Reset" ]
]
main : Program () Model Msg
main =
Browser.sandbox
{ init = init
, view = view
, update = update
}
Turns out, adding a value attribute to the select html will override the option selected state, so this is pretty important if you need your select input to be in sync with model state.
https://ellie-app.com/3NZgYQYKv2Fa1
Are you setting model.isAvailable to "" when hitting the close button? Your code should work as is https://ellie-app.com/3NYRvgwkHWPa1

How Can I Dynamically Create Table Rows From A List Of Records?

My Model type is:
type alias Model =
{ freeSyllables : List FreeSyllable
, freeSyllableInput : String
, usageStartInput : Bool
, usageMidInput : Bool
, usageEndInput : Bool
}
The FreeSyllable type looks like:
type alias FreeSyllable =
{ syllable : String
, usage : Usage
}
The Usage type has three Boolean fields:
type alias Usage =
{ start : Bool
, mid : Bool
, end : Bool
}
I tried to render each item of the model's FreeSyllables-List to a table.
I didn't succeed.
So my question is how I can dynamically render each of the model's "FreeSyllables" into a proper html table with these columns:
syllable (textbox)
start usage (checkbox)
mid usage (checkbox)
end usage (checkbox)
actions (save-Button)
You'll have to attach event handlers (that fire Msg), but here is example view:
view : Model -> Html Msg
view model =
table [] <|
[ tr []
[ th [] [ text "syllable" ]
, th [] [ text "start" ]
, th [] [ text "mid" ]
, th [] [ text "end" ]
, th [] [ text "actions" ]
]
]
++ (List.map viewItem model.freeSyllables)
viewItem : FreeSyllable -> Html Msg
viewItem s =
tr []
[ th [] [ text s.syllable ]
, th [] [ input [ type_ "checkbox", checked s.usage.start ] [] ]
, th [] [ input [ type_ "checkbox", checked s.usage.mid ] [] ]
, th [] [ input [ type_ "checkbox", checked s.usage.end ] [] ]
, th [] [ button [] [ text "save" ] ]
]

Dom not re-rendering on Model change (Elm)

I'm trying a simple Elm CRUD example, but the DOM is not re-rendering when I add a new item to a list of items in the model.
Right now, I'm just adding a static record, and I can confirm that the model is changing via the elm-reactor debugger, but the mapped itemView items is not updating w/ a new div element.
I feel like I might be missing a pretty important part of the Elm architecture/virtual dom. Can anybody point me in right direction?
import Html exposing (Html, text, beginnerProgram, div, input, button)
import Html.Events exposing (onInput, onClick)
import List exposing (map, repeat, append)
main =
Html.beginnerProgram
{ model = model
, view = view
, update = update }
type alias Model =
{ items : List Item
, inputTxt : String }
model : Model
model =
{ items = items
, inputTxt = inputTxt }
type alias Item =
{ id : Int
, txt : String }
item : Item
item =
{ id = 0
, txt = "some text" }
items : List Item
items =
repeat 2 item
inputTxt : String
inputTxt =
""
type Msg
= NoOp
| ChangeTxt String
| AddItem
update : Msg -> Model -> Model
update msg model =
case msg of
NoOp ->
model
ChangeTxt newTxt ->
{ model | inputTxt = newTxt }
AddItem ->
{ model | items = model.items ++ [{ id = 0, txt = "some text" }] }
view : Model -> Html Msg
view model =
div [] [
div [] [ itemsView items ]
, div [] [
input [ onInput ChangeTxt ] []
]
, div [] [
text model.inputTxt
]
, div [] [
button [ onClick AddItem ] [ text "click me!" ]
]
]
itemView : Item -> Html Msg
itemView item =
div [] [
div [] [ text ( toString item.id ) ]
, div [] [ text item.txt ]
]
itemsView : List Item -> Html Msg
itemsView items =
div [] [
div [] (List.map itemView items)
]
The second line of your view function should be
div [] [ itemsView model.items ]
Right now it always renders items defined above, not the items in the model.

mdl-lite dialog not displaying correct information

I'm trying to make a list of buttons, each of which opens up a dialog which displays a different number. For example, the first button says '10' and then when it is clicked a dialog opens which also says '10'. The second says '20', and when it is clicked a dialog is opened that also says '20', etc. However, all of the dialogs say '10' when they are opened.
Here is the code:
module Main exposing (..)
import Html exposing (Html, div, text, p)
import Html.App as App exposing (program)
import Material
import Material.Button as Button
import Material.Scheme as Scheme
import Material.Dialog as Dialog
-- MODEL
type alias Model =
{ buttons : List Int, mdl : Material.Model }
init : ( Model, Cmd Msg )
init =
( { buttons = [ 10, 20, 30, 40, 50, 60, 70 ], mdl = Material.model }, Cmd.none )
-- MESSAGES
type Msg
= Log Int
| Mdl (Material.Msg Msg)
--VIEW
element : Int -> Model -> Html Msg
element int model =
Dialog.view
[]
[ Dialog.title [] [ text "Greetings" ]
, Dialog.content []
[ p [] [ text "What is this insanity?" ]
, p [] [ text (toString int) ]
]
, Dialog.actions []
[ Button.render Mdl
[ 0 ]
model.mdl
[ Dialog.closeOn "click" ]
[ text "Close" ]
]
]
view : Model -> Html Msg
view model =
div []
(List.map (\b -> button b model) model.buttons)
|> Scheme.top
button : Int -> Model -> Html Msg
button int model =
div []
[ Button.render
Mdl
[ 1 ]
model.mdl
[ Button.raised
, Button.ripple
, Button.onClick (Log int)
, Dialog.openOn "click"
]
[ text (toString int) ]
, element int model
]
-- UPDATE
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Log int ->
let
check =
Debug.log "Int" int
in
model ! []
Mdl msg' ->
Material.update msg' model
-- MAIN
main : Program Never
main =
program
{ init = init
, view = view
, update = update
, subscriptions = always Sub.none
}
I read mdl-lite only supports one dialog per application, so the problem must have been calling element multiple times. The solution is to call element once in the view function, and with each button click to update a dialogInt value in the model, and then to display this value in the dialog.
Here's the code:
module Main exposing (..)
import Html exposing (Html, div, text, p)
import Html.App as App exposing (program)
import Material
import Material.Button as Button
import Material.Scheme as Scheme
import Material.Dialog as Dialog
-- MODEL
type alias Model =
{ ints : List Int, dialogInt : Int, mdl : Material.Model }
init : ( Model, Cmd Msg )
init =
( { ints = [ 10, 20, 30, 40, 50, 60, 70 ], dialogInt = 0, mdl = Material.model }, Cmd.none )
-- MESSAGES
type Msg
= Log Int
| UpdateDialogInt Int
| Mdl (Material.Msg Msg)
--VIEW
element : Model -> Html Msg
element model =
-- let
-- check =
-- Debug.log "int" int
-- in
Dialog.view
[]
[ Dialog.title [] [ text "Greetings" ]
, Dialog.content []
[ p [] [ text "What is this insanity?" ]
, p [] [ text (toString model.dialogInt) ]
]
, Dialog.actions []
[ Button.render Mdl
[ 1 ]
model.mdl
[ Dialog.closeOn "click" ]
[ text "Close" ]
]
]
view : Model -> Html Msg
view model =
div []
((element
model
)
:: (List.map (\b -> button b model) model.ints)
)
|> Scheme.top
button : Int -> Model -> Html Msg
button int model =
div []
[ Button.render
Mdl
[ int ]
model.mdl
[ Button.raised
, Button.ripple
, Button.onClick (UpdateDialogInt int)
, Dialog.openOn "click"
]
[ text (toString int) ]
]
-- UPDATE
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Log int ->
let
check =
Debug.log "int" int
in
model ! []
UpdateDialogInt int ->
{ model | dialogInt = int } ! []
Mdl msg' ->
let
check =
Debug.log "msg" msg'
in
Material.update msg' model
-- MAIN
main : Program Never
main =
program
{ init = init
, view = view
, update = update
, subscriptions = always Sub.none
}