How do I force Elm to reuse a DOM element? - elm

I want to make a web app using Elm that will contain some video elements, with a dynamic layout that can change depending on the width and height of the browser. When I tried that, it was clear that Elm was generating new elements for the videos, but that doesn't work because the video elements have state that needs to be preserved.
I wrote a demonstration of the problem with counters instead of videos for simplicity. I tried to fix the problem with Html.lazy and Keyed.node but it persisted.
The code here can also be cloned from https://github.com/ijt/elm-dom-elt-reuse.
src/Main.elm:
port module Main exposing (..)
import Browser
import Html exposing (..)
import Html.Attributes as Attribute exposing (id, style)
import Html.Events exposing (onClick)
main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
type alias Model =
{ layout : Layout }
type Layout
= Row
| Column
init : () -> ( Model, Cmd Msg )
init _ =
( { layout = Row }, startCounters () )
type Msg
= ToggleLayout
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
ToggleLayout ->
let
l2 =
case model.layout of
Row ->
Column
Column ->
Row
in
( { model | layout = l2 }, Cmd.none )
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
view : Model -> Html Msg
view model =
div []
[ button [ onClick ToggleLayout ] [ text "Toggle Layout" ]
, counters model
]
counters : Model -> Html Msg
counters model =
case model.layout of
Column ->
div []
[ div [] [ counter1 ]
, div [] [ counter2 ]
]
Row ->
div [] [ counter1, spacer, counter2 ]
spacer : Html Msg
spacer =
text " "
counter1 : Html Msg
counter1 =
span [ id "counter1" ]
[ text "0" ]
counter2 : Html Msg
counter2 =
span [ id "counter2" ]
[ text "0" ]
port startCounters : () -> Cmd msg
static/index.html:
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Main</title>
<style>body { padding: 0; margin: 0; }</style>
</head>
<body>
<pre id="elm"></pre>
<script src="Main.js"></script>
<script>
window.app = Elm.Main.init( { node: document.getElementById("elm") } );
window.app.ports.startCounters.subscribe(function() {
let c1 = document.getElementById("counter1");
let c2 = document.getElementById("counter2");
function increment(e) {
let n = parseInt(e.innerText);
e.innerText = n + 1;
}
requestAnimationFrame(function() {
setInterval(function() {
increment(c1);
increment(c2);
}, 100)
})
});
</script>
</body>
</html>
Makefile:
static/Main.js: src/Main.elm
elm make src/Main.elm --output=static/Main.js

Keeping the elements always at the same depth in the DOM tree does the trick.
Here is the new code:
counters : Model -> Html Msg
counters model =
let
d =
case model.layout of
Column ->
"column"
Row ->
"row"
in
div
[ style "flex-direction" d
, style "display" "flex"
]
[ counter1
, counter2
]
counter1 : Html Msg
counter1 =
span [ id "counter1", style "padding" "8px" ]
[ text "0" ]
counter2 : Html Msg
counter2 =
span [ id "counter2", style "padding" "8px" ]
[ text "0" ]
port startCounters : () -> Cmd msg
Thanks to jessta on the Elm Slack channel for this idea.

Related

Setting model value from input?

I am trying to make a To Do List with Elm and I am having problems figuring out how to set value from the input to add it to the list in model:
module ToDoList exposing (..)
import Browser
import Html exposing (Html, div, text, li, button, ul, input)
import Html.Events exposing (onClick)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
type alias Model = List String
type Msg =
NewTask String
init : Model
init = ["Premade Task"]
type alias AddToDo =
{ newTask : String
, status: String }
update : Msg -> Model -> Model
update msg model =
case msg of
NewTask newContent ->
model ++ [newContent]
view : Model -> Html Msg
view model =
div [] [
input [ placeholder "New Task", value model.content, onInput NewTask ] []
, button [ onClick <| (NewTask model.content) ] [text "Add a task"]
, ul [] (List.map viewTodo model)
]
viewTodo todo =
li [] [text todo]
main =
Browser.sandbox { init = init, update = update, view = view }
I've tried making Model a tuple but didn't work out, what is the correct way of implementing this, I just need to take value from the input and add it to the list when I click on the "Add a Task" button.
You need a way to temporary store the todo item being typed before adding to the collection. In the following example I introduced a draft field on the Model. I introduced a new message type DraftTask, too, that gets dispatched as long as the user is typing. When they hit the button, the program appends the draft value to todos list.
module ToDoList exposing (..)
import Browser
import Html exposing (Html, button, div, input, li, text, ul)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick, onInput)
type alias Model =
{ todos : List String
, draft : String
}
type Msg
= NewTask
| DraftTask String
init : Model
init =
{ todos = [ "Premade Task" ]
, draft = ""
}
update : Msg -> Model -> Model
update msg model =
case msg of
NewTask ->
{ model
| todos = model.todos ++ [model.draft]
, draft = ""
}
DraftTask s ->
{ model | draft = s }
view : Model -> Html Msg
view model =
div []
[ input [ placeholder "New Task", value model.draft, onInput DraftTask ] []
, button [ onClick <| NewTask ] [ text "Add a task" ]
, ul [] (List.map viewTodo model.todos)
]
viewTodo todo =
li [] [ text todo ]
main =
Browser.sandbox { init = init, update = update, view = view }

How to update the model in the dynamically generated view?

import Browser
import Html exposing (Html, button, div, text)
import Html.Attributes exposing (disabled)
import Html.Events exposing (onClick)
main =
Browser.sandbox
{ init =
{ count = 0
, node = text ""
, inactive = False
}
, update = update
, view = view
}
type Msg
= Increment
| AddNode
update msg model =
case msg of
Increment ->
Debug.log "count"
{ model
| count = model.count + 1
}
AddNode ->
let
viewDynamic =
div []
[ text (String.fromInt model.count) ]
in
{ model
| node =
div []
[ button [ onClick Increment ] [ text "+" ]
, viewDynamic
]
, inactive = True
}
view model =
let
viewStatic =
button
[ disabled model.inactive
, onClick AddNode
]
[ text "new" ]
in
div []
[ viewStatic
, model.node
]
Above, the initial static view can react to the update of model.inactive, but the subsequent dynamic view fails to update model.count. How to ideally achieve it?
Solved ideally thanks to the hints from #glennsl .
import Browser
import Html exposing (Html, button, div, text)
import Html.Attributes exposing (disabled)
import Html.Events exposing (onClick)
main =
Browser.sandbox
{ init =
{ count = 0
, added = False
, inactive = False
}
, update = update
, view = view
}
type Msg
= Increment
| AddNode
update msg model =
case msg of
Increment ->
{ model | count = model.count + 1 }
AddNode ->
{ model | inactive = True, added = True }
view model =
let
viewStatic =
button
[ disabled model.inactive
, onClick AddNode
]
[ text "new" ]
viewDynamic =
div []
[ text (String.fromInt model.count) ]
in
div []
[ viewStatic
, if model.added then
div []
[ button [ onClick Increment ] [ text "+" ]
, viewDynamic
]
else
text ""
]

view field. It looks like a function needs 1 more argument

I have a problem in the view I would like display the model so i am using the code:
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
main =
beginnerProgram { model = model, update = update, view = view }
type alias Model = {
toto : String
}
model = { toto = "" }
type Msg = UpdateText String
update msg model =
case msg of
UpdateText text ->
{model | toto = text }
view : Model -> Html (String -> Msg)
view model =
div []
[ button [ onClick UpdateText ,value "hello" ] [ text "-" ]
, div [] [ text (toString model.toto) ]
, button [ onClick UpdateText, value "word" ] [ text "+" ]
]
I have this error but i don't understand what is not good in my view :
The argument to function beginnerProgram is causing a mismatch.
11| beginnerProgram { model = model, update = update, view = view }
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Function beginnerProgram is expecting the argument to be:
{ ..., view : { toto : String } -> Html (Msg) }
But it is:
{ ..., view : Model -> Html (String -> Msg) }
Hint: Problem in the view field. It looks like a function needs 1
more argument.
Thank you
UpdateText accepts a single argument and returns Msg value. Your view buttons should look something like this:
button [ onClick (UpdateText "hello") ] [ text "-" ]

I cannot find module "Widget"

I have been following the Elm tutorial on the website and I tried it on a Mac and it worked, but when I ported it to Linux, it gave me the following error:
- I cannot find module 'Widget'.
Module 'Main' is trying to import it.
Potential problems could be:
* Misspelled the module name
* Need to add a source directory or new dependency to elm-package.json
This was the code which was being used:
main.elm
module Main exposing (..)
import Html exposing (Html)
import Html.App
import Widget
-- MODEL
type alias AppModel =
{ widgetModel : Widget.Model
}
initialModel : AppModel
initialModel =
{ widgetModel = Widget.initialModel
}
init : ( AppModel, Cmd Msg )
init =
( initialModel, Cmd.none )
-- MESSAGES
type Msg
= WidgetMsg Widget.Msg
-- VIEW
view : AppModel -> Html Msg
view model =
Html.div []
[ Html.App.map WidgetMsg (Widget.view model.widgetModel)
]
-- UPDATE
update : Msg -> AppModel -> ( AppModel, Cmd Msg )
update message model =
case message of
WidgetMsg subMsg ->
let
( updatedWidgetModel, widgetCmd ) =
Widget.update subMsg model.widgetModel
in
( { model | widgetModel = updatedWidgetModel }, Cmd.map WidgetMsg widgetCmd )
-- SUBSCIPTIONS
subscriptions : AppModel -> Sub Msg
subscriptions model =
Sub.none
-- APP
main : Program Never
main =
Html.App.program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
widget.elm
module Widget exposing (..)
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
-- MODEL
type alias Model =
{ count : Int
}
initialModel : Model
initialModel =
{ count = 0
}
-- MESSAGES
type Msg
= Increase
-- VIEW
view : Model -> Html Msg
view model =
div []
[ div [] [ text (toString model.count) ]
, button [ onClick Increase ] [ text "Click" ]
]
-- UPDATE
update : Msg -> Model -> ( Model, Cmd Msg )
update message model =
case message of
Increase ->
( { model | count = model.count + 1 }, Cmd.none )
Any tips on how to fix this?
Because Linux filesystems are case sensitive, you should name your Elm files with the same case as the module they declare.
So in your case:
Main module should be in "Main.elm".
Widget module should be in "Widget.elm".

Work with elm and select

I try to understand how elm works with a custom example.
durationOption duration =
option [value (toString duration) ] [ text (toString duration)]
view : Model -> Html Msg
view model =
Html.div []
[ h2 [] [ text "Month selector"]
, select []
(List.map durationOption [1..12])
]
It's an easy example to work with a select. I would like, each time I change the month value it multiplies to value by 10 for example. According to the documentation there is not events like onChange or onSelect, do I have to create mine with on ?
For future reference for Elm-newbies (like me): with Elm 0.18.0 + elm-lang/html 2.0.0, the onInput event (see code below) works. (Also notice the difference in int range notation (List.range 0 12 instead of [0..12]).
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
main =
Html.beginnerProgram
{ model = model
, view = view
, update = update
}
-- MODEL
type alias Model =
{ duration : Int
}
model : Model
model =
Model 0
-- UPDATE
type Msg
= SetDuration String
update : Msg -> Model -> Model
update msg model =
case msg of
SetDuration s ->
let result =
String.toInt s
in
case result of
Ok v ->
{ model | duration = v }
Err message ->
model
-- VIEW
view : Model -> Html Msg
view model =
div []
[ select [ onInput SetDuration ]
(List.range 0 12 |> List.map intToOption)
, div [] [ text <| "Selected: " ++ (toString model.duration) ]
]
intToOption : Int -> Html Msg
intToOption v =
option [ value (toString v) ] [ text (toString v) ]
UPDATE: onInput works, see another answer below with 0.19 working code: https://stackoverflow.com/a/41516493/540810
Yes, you will need to use on to handle the change event. If you look at the source for other event handlers built into Elm, like onClick, you'll see that they are all built using the on function.
Here's an example that is using targetValueIntParse from elm-community/html-extra for turning the string value from the option into an int.
Updated to Elm-0.18
import Html exposing (..)
import Html.Events exposing (on)
import Html.Attributes exposing (..)
import Json.Decode as Json
import String
import Html.Events.Extra exposing (targetValueIntParse)
main =
beginnerProgram { model = { duration = 1 }, view = view, update = update }
durationOption duration =
option [ value (toString duration) ] [ text (toString duration) ]
view : Model -> Html Msg
view model =
Html.div []
[ h2 [] [ text "Month selector" ]
, select [ on "change" (Json.map SetDuration targetValueIntParse) ]
(List.map durationOption (List.range 1 12))
, div [] [ text <| "Selected: " ++ (toString model.duration) ]
]
type Msg
= SetDuration Int
type alias Model =
{ duration : Int }
update msg model =
case msg of
SetDuration val ->
{ model | duration = val }
You can run this example in browser https://runelm.io/c/ahz
Here is an update for Elm 0.19:
module Main exposing (main)
import Browser
import Html exposing (..)
import Html.Events exposing (on)
import Html.Attributes exposing (..)
import Json.Decode as Json
import String
import Html.Events.Extra exposing (targetValueIntParse)
main =
Browser.sandbox { init = { duration = 1 }, view = view, update = update }
durationOption duration =
option [ value (String.fromInt duration) ] [ text (String.fromInt duration) ]
view : Model -> Html Msg
view model =
Html.div []
[ h2 [] [ text "Month selector" ]
, select [ on "change" (Json.map SetDuration targetValueIntParse) ]
(List.map durationOption (List.range 1 12))
, div [] [ text <| "Selected: " ++ (String.fromInt model.duration) ]
]
type Msg
= SetDuration Int
type alias Model =
{ duration : Int }
update msg model =
case msg of
SetDuration val ->
{ model | duration = val }
An example with onInput handler (you can also check the Ellie):
module Main exposing (main)
import Browser import Html exposing (Html, button, div, text, select, option) import Html.Attributes exposing (value, selected) import Html.Events exposing (onInput) import Dict exposing (Dict)
type alias Model =
{ options : Dict Int (String, Bool)
}
initialModel : Model initialModel =
{ options = Dict.fromList [(0, ("All time", False)), (1, ("One week", True)), (2, ("24h", False))]
}
type Msg
= Select String
update : Msg -> Model -> Model update msg model =
case msg of
Select value ->
case String.toInt value of
Just selectedID ->
let
changeSelection id (label, _) =
if id == selectedID then
(label, True)
else
(label, False)
in
{model | options = Dict.map changeSelection model.options}
Nothing ->
model
view : Model -> Html Msg view model =
let
toOption (id, (label, isSelected)) =
option [value (String.fromInt id), selected isSelected] [text label]
in
div []
[ select [onInput Select] (List.map toOption <| Dict.toList model.options)
, div [] [text "DEBUG"]
, div [] [text <| Debug.toString model.options]
]
main : Program () Model Msg main =
Browser.sandbox
{ init = initialModel
, view = view
, update = update
}
This works with Ellie on Elm 0.19.0: https://ellie-app.com/58wGf2YsR9Ya1
Full code:
import Browser
import Html exposing (..)
import Html.Events exposing (on)
import Html.Attributes exposing (..)
import Json.Decode as Json
import String
import Html.Events.Extra exposing (targetValueIntParse)
main =
Browser.sandbox { init = init, view = view, update = update }
init =
{ duration = 1 }
durationOption duration =
option [ value (String.fromInt duration) ] [ text (String.fromInt duration) ]
view : Model -> Html Msg
view model =
Html.div []
[ h2 [] [ text "Month selector" ]
, select [ on "change" (Json.map SetDuration targetValueIntParse) ]
(List.map durationOption (List.range 1 12))
, div [] [ text <| "Selected: " ++ (String.fromInt model.duration) ]
]
type Msg
= SetDuration Int
type alias Model =
{ duration : Int }
update msg model =
case msg of
SetDuration val ->
{ model | duration = val }