Dom not re-rendering on Model change (Elm) - 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.

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

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
}

how to play audio in elm

I am getting some data from my api.
{
"message":"",
"data":[
{
"title":"Test Rock Song",
"artist":"Music Artist Test",
"audioUrl":"https://xyz/radio/03+-+HS3+-+Wajah+Tum+Ho+%5BDJMaza.Info%5D.mp3",
"stateName":"California",
"cityName":"Los Angles",
"businessId":32
},
{
"title":"R&B S1",
"artist":"Music Artist Test",
"audioUrl":"https://xyz/radio/1463469171175_Ba_khuda_tumhi.mp3",
"stateName":"California",
"cityName":"Los Angles",
"businessId":32
},
{
"title":"R&B S2",
"artist":"Music Artist Test",
"audioUrl":"https://xyz/radio/1465890934054_01+-+HS3+-+Tumhe+Apna+Banane+Ka+%5BDJMaza.Info%5D.mp3",
"stateName":"California",
"cityName":"Los Angles",
"businessId":32
}
],
"count":3
}
So i have iterate that data in list, each have one mp3 url. I want when i click on link then particular mp3 will play.
Please any one help me to implement this feature.
If you are looking for the simplest of solutions, I would recommend rendering <audio> tags with the src attribute set to the audio URL. This can give you standard audio controls. Here is a full example along with some JSON decoders:
import Html exposing (..)
import Html.Attributes exposing (..)
import Json.Decode as Json
main =
case Json.decodeString myDecoder exampleJsonInput of
Err err -> text err
Ok data -> div [] <| List.map toAudioBlock data
toAudioBlock entry =
div []
[ div [] [ strong [] [ text entry.title ] ]
, div [] [ text "By: ", text entry.artist ]
, div [] (List.map text ["From: ", entry.cityName, ", ", entry.stateName])
, audio
[ src entry.audioUrl
, controls True
] []
]
type alias Entry =
{ title : String
, artist : String
, audioUrl : String
, stateName : String
, cityName : String
, businessId : Int
}
entryDecoder : Json.Decoder Entry
entryDecoder =
Json.map6
Entry
(Json.field "title" Json.string)
(Json.field "artist" Json.string)
(Json.field "audioUrl" Json.string)
(Json.field "stateName" Json.string)
(Json.field "cityName" Json.string)
(Json.field "businessId" Json.int)
myDecoder : Json.Decoder (List Entry)
myDecoder =
Json.at ["data"] <| Json.list entryDecoder
exampleJsonInput =
"""
{
"message":"",
"data":[
{
"title":"Test Rock Song",
"artist":"Music Artist Test",
"audioUrl":"https://archive.org/download/SuperMarioBros.ThemeMusic/SuperMarioBros.mp3",
"stateName":"California",
"cityName":"Los Angles",
"businessId":32
},
{
"title":"R&B S1",
"artist":"Music Artist Test",
"audioUrl":"http://216.227.134.162/ost/super-mario-bros.-1-3-anthology/pnfhmccstp/1-02-underworld.mp3",
"stateName":"California",
"cityName":"Los Angles",
"businessId":32
},
{
"title":"R&B S2",
"artist":"Music Artist Test",
"audioUrl":"https://ia800504.us.archive.org/33/items/TetrisThemeMusic/Tetris.mp3",
"stateName":"California",
"cityName":"Los Angles",
"businessId":32
}
],
"count":3
}
"""
You can paste this into http://elm-lang.org/try. I have substituted your audio examples with actual mp3s from the internet.
If instead you are looking for a complete audio library port to Elm, there is currently no version compatible with Elm 0.17.
I had the same problem, the other solutions in the thread are also good, but I think that I have a simpler example. Elm does not have a pure solution for sounds,
therefore you need use ports and some javascript, if you want to use more than the
pure html audio component.
You can find the whole example project under Github: Elm sound by example
Or here the pure elm and html/javascript code.
(Main.elm)
port module Main exposing (..)
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = \_ -> init
, view = view
, update = update
, subscriptions = \_ -> Sub.none
}
-- PORTS
port playMusic : String -> Cmd msg
port stopMusic : String -> Cmd msg
port setSource : String -> Cmd msg
-- MODEL
type alias Model =
{ song : String
}
init : ( Model, Cmd Msg )
init =
( { song = "" }
, Cmd.none
)
-- UPDATE
type Msg
= Play
| Stop
| Set String
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Play ->
( model
, playMusic "play"
)
Stop ->
( model
, stopMusic "stop"
)
Set message ->
( { model | song = message }
, setSource message
)
-- VIEW
view : Model -> Html Msg
view model =
div []
[ h1 [ id "song-header" ] [ text ("Current song: " ++ model.song) ]
, div [ Html.Attributes.class "song-holder" ]
[ button [ onClick (Set "song1.mp3") ] [ Html.text "Song1" ]
, button [ onClick (Set "song2.mp3") ] [ Html.text "Song2" ]
, button [ onClick (Set "song3.wav") ] [ Html.text "Song3" ]
]
, div [ Html.Attributes.class "audio-holder" ]
[ button [ onClick Play ] [ Html.text "Play" ]
, button [ onClick Stop ] [ Html.text "Stop" ]
]
, audio [ src "./assets/sounds/song2.mp3", id "audio-player", controls True ] []
]
(javascript in index.html)
document.getElementById("audio-player").volume = 0.1;
app.ports.playMusic.subscribe(function() {
document.getElementById("audio-player").play();
});
app.ports.stopMusic.subscribe(function() {
document.getElementById("audio-player").pause();
});
app.ports.setSource.subscribe(function(source) {
document.getElementById("audio-player").setAttribute("src", "assets/sounds/" + source);
});

ELM - List to table rows

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