How to debounce elm signals? - elm

Im using start-app to shape my application. Html.Events supports creating custom events with custom Signal.message. But how to send that message is abstracted behind the Html library. Also there is a library called which implements debouncing (http://package.elm-lang.org/packages/Apanatshka/elm-signal-extra/5.7.0/Signal-Time#settledAfter).
SearchBar.elm:
module PhotosApp.SearchBar (view) where
import Html exposing (input, div, button, text, Html)
import Html.Events exposing (on, onClick, targetValue)
import Html.Attributes exposing (value, type')
import Effects exposing (Effects, Never)
import PhotosApp.Actions as Actions
onTextChange : Signal.Address a -> (String -> a) -> Html.Attribute
onTextChange address contentToValue =
on "input" targetValue (\str -> Signal.message address (contentToValue str))
view : Signal.Address Actions.Action -> String -> Html
view address model =
div []
[ input [ type' "text", value model, onTextChange address Actions.Search] []
, button [ onClick address Actions.Clear ] [text "Clear"]
]

You can set up debouncing from an HTML attribute by going through a proxy mailbox before tying into #Apanatshka's settledAfter function.
(Note that since you're using StartApp, we don't have direct access to the main Address and therefore I've had to compromise a few of these functions by ignoring the address passed into view. You could get something more generalized by not using StartApp, but this should hopefully get you started)
Since event attributes cannot create tasks directly, we have to use an intermediate proxy mailbox. We can set that up like this:
debounceProxy : Signal.Mailbox Action
debounceProxy =
Signal.mailbox NoOp
Note that the above function expects a NoOp Action, common in Elm architecture, which just means the update function makes no changes to the model.
Now we need to set up another Signal which listens to the proxy mailbox, then passes the signal through the settledAfter function. We can define this signal like this:
debounce : Signal Action
debounce =
settledAfter (500 * Time.millisecond) debounceProxy.signal
We can now change the onTextChange function to point at the proxy mailbox. Notice that I've taken out the first Address parameter since it's being ignored (see my earlier comment about conforming to StartApp):
onTextChange : (String -> Action) -> Html.Attribute
onTextChange contentToValue =
on "input" targetValue (\str -> Signal.message debounceProxy.address (contentToValue str))
Lastly, you have to tie in the debounce signal into StartApp's inputs parameter, which means your call to start will look something like this:
app = StartApp.start
{ init = init
, update = update
, view = view
, inputs = [ debounce ]
}
I've pasted the full working example at a gist here.

Related

Incorrect View Functon Return Type in elm

I am making a program that changes the rendered text on the screen to be whatever the user inputs in a text box. I think I have the model and the update part of the elm architecture correct, but I really don't understand the view portion.
I'm just having trouble wrapping my head around the square bracket view functions.
Anyway, I am getting this error.
This div call produces:
Html #(Model -> Model)#
But the type annotation on view says it should be:
Html #Msg#Elm
But I am not sure how to change my view function to return Html Msg and I am kinda confused between the difference between that and a string.
Thank you everyone!
Here is my code ...
module Main exposing (..)
import Browser
import Html exposing (Html, div, text, input, Attribute)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
main =
Browser.sandbox { init = init, update = update, view = view }
type alias Model = String
init : Model
init = "Hello, World!"
type alias Msg = String
update : Msg -> Model -> Model
update msg model =
msg
view : Model -> Html Msg
view model =
div []
[ input [ placeholder "Input new string", value model, onInput update ] []
, div [] [ text model ]
]
You're passing the update function as an argument to onInput. Your probably meant to pass it a Msg, which the runtime will then pass to the update function.
Since your Msg type is an alias for String, you can use onInput identity

Elm and VSCode: Formatter messes up spacing

So I started learning Elm today. I use VSCode as my editor.
I followed the docs for the set up and installed elm as well as el-format via npm install -g elm elm-format. I also installed the VSCode Elm extension.
Next, in my settings.json I set:
"[elm]": {
"editor.formatOnSave": true
},
Then I went on with the tutorial. In it the code is formatted like this:
import Browser
import Html exposing (Html, Attribute, div, input, text)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
-- MAIN
main =
Browser.sandbox { init = init, update = update, view = view }
-- MODEL
type alias Model =
{ content : String
}
init : Model
init =
{ content = "" }
-- UPDATE
type Msg
= Change String
update : Msg -> Model -> Model
update msg model =
case msg of
Change newContent ->
{ model | content = newContent }
-- VIEW
view : Model -> Html Msg
view model =
div []
[ input [ placeholder "Text to reverse", value model.content, onInput Change ] []
, div [] [ text (String.reverse model.content) ]
]
But when I hit safe, it formats the code like this:
module Main exposing (Model, Msg(..), init, main, update, view)
import Browser
import Html exposing (Attribute, Html, div, input, text)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
-- MAIN
main =
Browser.sandbox { init = init, update = update, view = view }
-- MODEL
type alias Model =
{ content : String
}
init : Model
init =
{ content = "" }
-- UPDATE
type Msg
= Change String
update : Msg -> Model -> Model
update msg model =
case msg of
Change newContent ->
{ model | content = newContent }
-- VIEW
view : Model -> Html Msg
view model =
div []
[ input [ placeholder "Text to reverse", value model.content, onInput Change ] []
, div [] [ text (String.reverse model.content) ]
]
So it adds extra lines, and extra module Main exposing ... and doubles the number of spaces. I tried setting spaces to 2 again using the footer in VSCode, but that didn't help.
My questions are:
Is it okay that saving adds the extra module Main ...?
Is having 2 spaces best practice / community standard, or 4?
If it is 2 (like it is in the tutorial's default code) how can I get my formatter to respect that?
If it is not, why has the tutorial the non-standard indentation?
First of all, this question is both too broad and primarily opinion-based and will probably be closed because of that. It would have been more suited for the forums I think.
That said, I'm going to try to answer it as best as I can anyway, since it's here:
Yes? Most modules won't be very useful without exposing something and it's good practice to be explicit about what's being exposed.
elm-format is the community standard, so 4 it is.
You can't. This is by design. It has also been discussed to death in various fora. Here's one issue discussing it
You'd have to ask Evan about that. It might be related to formatting for the web, or just Evan being lazy.

Elm no data except isTrusted in JSON events

I wrote a simple program based on time example, to test what data are in events. It decodes JSON to value then encodes it back to JSON, then show it in SVG text element. And the only thing I get is {"isTrusted":true}.
Why that happens? How do I get another data of event? I'm using Firefox 49 and online compiler:
import Html exposing (Html)
import Svg exposing (..)
import Svg.Attributes exposing (..)
import Svg.Events exposing(on)
import Json.Decode as Json
import Json.Encode exposing (encode)
main =
Html.program
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
-- MODEL
type alias Model = String
init : (Model, Cmd Msg)
init =
("No event", Cmd.none)
-- UPDATE
type Msg
= Event String
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Event event ->
(event, Cmd.none)
subscriptions model = Sub.none
stringifyEvent: Json.Encode.Value -> Msg
stringifyEvent x =
Event (encode 2 x)
-- VIEW
view : Model -> Svg Msg
view model =
svg [ viewBox "0 0 300 300", width "300px", on "mousedown" (Json.map stringifyEvent Json.value) ] [
text_ [x "0", y "30"] [text model]
]
When I try in console
svgElement.addEventListener('click', function(e) {console.log(e)})
It works with all the attributes.
I do not know a way to achieve your goal.
But I can give you an answer why it does the way you described.
If you look at the source code, you'll find that Elm runtime uses
JSON.stringify() for converting Value to String.
And guess what...
svgElement.addEventListener('click', function(e) {console.log(JSON.stringify(e))})
will give you {"isTrusted":true} when you click...

SPA Dynamic Routing and Page Rendering

I am new to Elm, probably have an incorrect understanding of the architecture and this may be an XY question (but I wouldn't know yet...)
Anyways, I am trying to create a url-routed SPA in Elm. I am using evancz/start-app to generate individual dynamic pages using the elm-achitecture-tutorial example 1 as a starting point for each 'page':
Frontend.Pages.Home.display : Signal Html
Frontend.Pages.Home.display = StartApp.start
{ model = 0
, update = update
, view = view
}
type alias Model = Int
type Action = Increment | Decrement
update : Action -> Model -> Model
update action model =
case action of
Increment -> model + 1
Decrement -> model - 1
view : Signal.Address Action -> Model -> Html
view address model =
div []
[ button [ onClick address Decrement ] [ text "-" ]
, div [ countStyle ] [ text (toString model) ]
, button [ onClick address Increment ] [ text "+" ]
]
I am using TheSeamau5/elm-router to
route : String -> Signal Html
route = Router.match
[ "/" :-> Frontend.Pages.Home.display
] Frontend.Pages.Errors.FourOhFour.display
I am using TheSeamau5/elm-history to gather the latest url:
main : Signal Html.Html
main = Frontend.Routes.route History.path
Clearly this is the error; I am passing a Signal String into a method that takes a String. The problem is that if I use the following:
main = Signal.map Frontend.Routes.route History.path
which I would expect Signal.map Frontend.Routes.route History.path to be of type Signal Html, instead the compiler complains of a conflict:
The type annotation for `main` does not match its definition.
7| main : Signal Html.Html
^^^^^^^^^^^^^^^^ As I infer the type of values flowing through your program, I see a conflict between these two types:
Html.Html
Signal Html.Html
What's going on here? It seems like Signal.Map is "unpacking" the Signal Html. Is this the case?
It seems my core question, at any rate, is: How can I present a single-page-app, routed to separate reactive 'pages' based on a Signal String while not reimplementing Router?
Thanks in advance.
I believe you are ending up with a final result of Signal (Signal Html) and main is expecting Signal Html. I believe the error you are receiving is specifically for the type it is expecting for Signal a. It is saying, I was expecting a to be Html but in fact I found it to be Signal Html.
Here are the types we have:
Signal.map : (a -> result) -> Signal a -> Signal result
Frontend.Routes.route : String -> Signal Html
Historypath : Signal String
Then we have Signal.map Frontend.Routes.route which has the type Signal String -> Signal (Signal Html).
We then apply History.path giving us the final result Signal (Signal Html)
If you change route to return Html rather than Signal Html you should be good to go.

Elm Architecture and tasks

UPDATE : This has now been covered in the Elm Architecture documentation.
--
I don't understand how you tie the Elm Architecture and Tasks.
-- Action is an enumeration of possible actions
type Action = ..
-- Model is a Model that evolves in time
model : Signal Model
-- View turns a model into Html, potentially sending actions to an address
view : Address Action -> Model -> Html
-- Update turns a model and an action into another model
update : Action -> Model -> Model
-- Main waits for new values of the model to be emitted, and pass then to the view action
main : Signal Html
main =
Signal.map (view actions.address) model
I'm trying to model this:
when a user click on a button, a "DoSomethingAction" is emitted
this action should start a Task, handled by some port
when the task is done, it should emit another Action ("SomethingDoneAction"), with the result
Is that the right way to go?
Which functions should I change (update, main)?
I understand this is what is alluded to here, but the explanation is not too clear and I don't understand what needs to be changed.
So any complete example / or more explanation would be welcome.
EDIT :
A more complete version (that "kinda" works) is available here : http://share-elm.com/sprout/55bf3c3ce4b06aacf0e8ba17
I'm a few steps from having it working, it seems, since the Task is not always run when I click on the button (but hopefully I'm getting somewhere.)
While it is very possible to perform Tasks when a button is pressed, it is often not needed. What you probably want is to send a message to the Action signal, then with that update the Model, then with that the view may change. This is standard Elm architecture.
If you really want to perform Tasks, you could do the following:
type Action = NoOp | ButtonClicked | SetText String
type alias Model =
{ tasks : Task Http.Error String
, text : String
}
init : Model
init =
{ task = Task.succeed "Fetching..."
, text = ""
}
-- We can use, for example, the Http.get task
update : Action -> Model -> Model
update action model =
case action of
ButtonClicked ->
{ model | task <-
Http.getString "http://example.org/some-text.txt"}
SetText t ->
{ model | text <- t }
_ -> model
view : Address Action -> Model -> Html
view address model =
div []
[ div
[ class "btn btn-default"
, onClick address ButtonClicked
]
[ text "Click me!" ]
, h3 "Here is what was fetched:"
, p [] [ text model.text ]
]
-- now possibly in another file
actions : Signal.Mailbox Action
actions : Signal.mailbox NoOp
model : Signal Model
model = Signal.foldp init update actions.signal
-- This is what actually performs the Tasks
-- The Elm task example also details how to send
port taskPort : Signal (Task Http.Error String)
port taskPort =
((.task) <~ model) `andThen`
(Signal.send actions.address << SetText)
main : Signal Html
main = view actions.address <~ model
Note that you can use Signal.dropRepeats if you want to perform the task only when the task changes.
The way to do it is to have the update of the model mapped to a response signal that receive the results of what you want your model to react to.
The view sends the tasks to a query/requests mailbox.
The port would be a map on the query signal that executes the tasks andThen sends the results to the response mailbox.
If you want to see this in action take a look at this file (I've highlighted the relevant parts)
https://github.com/pdamoc/elmChallenges/blob/master/challenge4.elm#L72-L94
Please be advised that what you see there is a little bit more complicated than what you need because it also implements a mechanism to execute the tasks only when the user stops typing.
This is answered in the doc :
https://github.com/evancz/elm-architecture-tutorial#user-content-example-5-random-gif-viewer