When Elm has a lot of DOM manipulation to do, there is some lag before the results show up. I'm trying to figure out how to show a placeholder div that says "Loading..." while Elm is doing its thing.
To demonstrate the lag, I've modified one of the Elm examples to render an increasingly huge number of text elements upon a button click:
import Html exposing (beginnerProgram, div, button, text)
import Html.Events exposing (onClick)
main =
beginnerProgram { model = 0, view = view, update = update }
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (toString model) ]
, button [ onClick Increment ] [ text "+" ]
, div [] (List.repeat (2 ^ model) (text ". ")) -- this is my addition
]
type Msg = Increment | Decrement
update msg model =
case msg of
Increment ->
model + 1
Decrement ->
model - 1
When running the example, clicking the '+' button will show a '.' characters in powers of 2. When the number is sufficiently high (around 16 on my machine), there is a multi-second delay after clicking before the '.' characters are shown.
What is a good way to show a 'loading...' element (in a 'div', say) before rendering the '.' elements?
You'll need to use a regular Html.program and return a Cmd from the Increment/Decrement update handlers that will pause to let the DOM render the "Loading" and then reenter the update:
import Html exposing (program, div, button, text)
import Html.Events exposing (onClick)
import Process
import Task
main =
program { init = (Model 0 False, Cmd.none), view = view, update = update, subscriptions = \_ -> Sub.none }
type alias Model =
{ count: Int
, loading: Bool
}
view model =
let
dotsDisplay =
if model.loading then
div [] [text "Loading..."]
else
div [] (List.repeat (2 ^ model.count) (text ". "))
in
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (toString model.count) ]
, button [ onClick Increment ] [ text "+" ]
, dotsDisplay
]
type Msg = Increment | Decrement | FinishLoading
finishLoadingCmd =
Task.map2 (\_ b -> b) (Process.sleep 10) (Task.succeed FinishLoading)
|> Task.perform identity
update msg model =
case msg of
Increment ->
{model | count = model.count + 1, loading = True} ! [finishLoadingCmd]
Decrement ->
{model | count = model.count - 1, loading = True} ! [finishLoadingCmd]
FinishLoading ->
{model | loading = False} ! []
It's still going to lock up the browser while it renders all those nodes though, so you probably still want to look for a way to not be rendering 100k+ DOM elements...
Related
I have this kind of input:
inputName player =
input
[ type_ "text"
, onInput (Msgs.ChangeName player)
, value player
]
which creates Msgs.ChangeName for every single character added to input.
I would prefer to update model after user leaves input but onBlur doesn't have any payload about input:
inputName player =
input
[ type_ "text"
, onBlur (Msgs.ChangeName player)
, value player
]
Above code doesn't compile ending with error:
The 1st entry has this type:
Html (String -> Msg)
But the 2nd is:
Html (Msg)
Hint: It looks like a function needs 1 more argument.
You can create a variation on the "blur" handler which pulls out target.value like this:
import Html.Events exposing (on, targetValue)
import Json.Decode as Json
onBlurWithTargetValue : (String -> msg) -> Attribute msg
onBlurWithTargetValue tagger =
on "blur" (Json.map tagger targetValue)
I can't find an example anywhere online that answers the question: how does a parent component respond to different actions coming out of a child module?
Consider a simple chat message input with a submit button:
// child component: text input w/ a submit button
type Action
= InputChanged String
| MessageSent String
view : Signal.Address Action -> Model -> Html
view addr model =
div []
[ input
[ type' "text"
, value model.content
, on "input" targetValue (\val -> Signal.message addr (InputChanged val))
]
[]
, button
[ type' "submit"
, onClick addr (MessageSent model.content)
]
[ text "Send" ]
]
How does the parent component holding onto this input box respond to the two actions that might come out of that input box? A traditional "just passing through" looks like this:
// parent component, holds onto a list of posts and the child component
-- update
type Action
= MessageBoxAction MessageBox.Action
update : Action -> Model -> Model
update act model =
case act of
MessageBoxAction msg ->
{ model |
currentMessage = MessageBox.update msg model.currentMessage
}
-- view
view : Signal.Address Action -> Model -> Html
view addr model =
div []
[ MessageBox.view (Signal.forwardTo addr MessageBoxAction) model.currentMessage ]
What I want to be able to do is capture a message coming out of that child component and respond to it beyond the normal "just passing through". Something like this:
case act of
MessageBoxSubmit msg ->
let updatedMessage = MessageBox.update msg model.currentMessage
newPost = Posts.update msg model.posts
in
{ model |
posts = model.posts :: [ newPost ]
, currentMessage = updatedMessage
}
But I have no idea how to do this, particularly because when forwarding the address to a child it's not like you have the opportunity to provide more than one address...
MessageBox.view (Signal.forwardTo addr MessageBoxAction) model.currentMessage
There are two main routes to do this.
You can change the signature of MessageBox update to return a parent action that you provide to MessageBox init.
init : (String -> parentAction) -> Model
init onSend =
{ onSend = onSend
, content = ""
}
update : Action -> Model -> (Model, Maybe parentAction)
update action model =
case action of
MessageSent msg ->
let
model' = ...
in
(model', Just (model.onSend msg))
InputChanged str ->
let
model' = ...
in
(model', Nothing)
and in the parent module you do:
init =
{ posts = []
, currentMessage = MessageBox.init HandleSent
}
update : Action -> Model -> Model
update act model =
case act of
MessageBoxAction msg ->
let
(currentMessage', send) = MessageBox.update msg model.currentMessage
model' = {model | currentMessage = currentMessage'}
in
case send of
Nothing -> model'
Just act -> update act model' -- you recursively call the update function with the new action that you received from the MessageBox.update
HandleSent str -> { model | posts = str::model.posts }
You can provide a decoder for the action in the MessageBox module.
in MessageBox module
sentMessage action =
case action of
MessageSent msg -> Just msg
_ -> Nothing
in parent
update : Action -> Model -> Model
update act model =
case act of
MessageBoxAction msg ->
let
currentMessage' = MessageBox.update msg model.currentMessage
model' = {model | currentMessage = currentMessage'}
in
case MessageBox.sentMessage msg of
Nothing -> model'
Just str -> update (HandleSent str) model'
HandleSent str -> { model | posts = str::model.posts }
Using elm-html, how can I check whether the ctrl key is pressed at the time of the click?
Is "control key down" some state I'd need to maintain elsewhere, perhaps using the Keyboard module? If so, how would that fit into elm-html?
I've adapted the code below from one of the well-known elm examples:
import Keyboard
import Html exposing (..)
import Html.Attributes exposing (style)
import Html.Events exposing (onClick)
import Signal exposing(Signal, Mailbox)
type alias Model =
{ count: Int
, ctrl : Bool
}
initialModel : Model
initialModel = { count = 0, ctrl = False}
type Action = Increment | Decrement | NoOp
update : Action -> Model -> Model
update action model =
case action of
Increment ->
{ model | count = model.count + 1 }
Decrement ->
{ model | count = model.count - 1 }
NoOp ->
model
view : Signal.Address Action -> Model -> Html
view address model =
div []
[ button [ onClick address Decrement ] [ text "-" ]
, div [] [ text (toString model) ]
, button [ onClick address Increment ] [ text "+" ]
]
actions : Mailbox Action
actions =
Signal.mailbox NoOp
model : Signal Model
model =
Signal.foldp update initialModel actions.signal -- Keyboard.ctrl ?
main =
Signal.map (view actions.address) model
How can I update the value of the model's "ctrl" field?
You first need an Action capable of setting the value of whether CTRL is pressed:
type Action = Increment | Decrement | SetCtrl Bool | NoOp
Your case statement in the update function needs to handle that new action:
SetCtrl bool -> { model | ctrl = bool }
And now you need a port that can map the Keyboard.ctrl boolean to a task which sends a signal with the new Action:
port ctrlToAction : Signal (Task.Task Effects.Never ())
port ctrlToAction =
Signal.map (Signal.send actions.address << SetCtrl) Keyboard.ctrl
Got a hopefully simple problem. When I receive action A in my update function, I'd like to return a task that does some stuff, then results in action B, which is received by the update function again. Its my understanding that whatever effects are returned from Update will be executed by startapp, but nothing seems to be happening. Here's a whittled down example:
import StartApp exposing (start)
import Effects
import Task
import Html exposing (..)
import Html.Events exposing (..)
main =
(start
{ init = init
, update = update
, view = view
, inputs = []
}).html
type Action
= Click
| Dummy
type alias Model =
{ clicks: Int
}
init : (Model, Effects.Effects Action)
init = (Model 0, Effects.none)
update : Action -> Model -> (Model, Effects.Effects Action)
update action model =
case action of
Click ->
-- the click is received.
(model, Effects.task (Task.succeed Dummy))
Dummy ->
-- this never happens!!
({ model | clicks = model.clicks + 1}, Effects.none)
view : Signal.Address Action -> Model -> Html
view address model =
let start = button [ onClick address Click ] [ text (toString model.clicks) ]
in
div [] ([start])
StartApp requires a port for tasks in addition to main. Change your main function and add the tasks port like this and you'll be all set:
app =
start
{ init = init
, update = update
, view = view
, inputs = []
}
main =
app.html
port tasks : Signal (Task.Task Effects.Never ())
port tasks =
app.tasks
Is there a way to retrieve the text that's currently selected in a text area in Seaside?
You can do it with jQuery/Javascript.
When do you want to retrieve the selected text ? What will trigger the retrieval (a user click ? Regular polling ?)
Sorry, pasted it in the wrong place, here's the solution:
MyComponent >> script
^ 'function selectedText(input){
var startPos = input.selectionStart;
var endPos = input.selectionEnd;
var doc = document.selection;
if(doc && doc.createRange().text.length != 0){
document.getElementById(''selectedText'').value = (doc.createRange().text);
} else if (!doc && input.value.substring(startPos,endPos).length != 0){
document.getElementById(''selectedText'').value = (input.value.substring(startPos,endPos))
}
}'
MyComponent >> renderContentOn: html
(html form)
with: [
(html hiddenInput)
id: 'selectedText';
callback: [ :value | selection := value ].
(html textArea)
callback: [ :value | theTextAreaText := value];
id: 'myTextArea'
with: theTextAreaText.
(html submitButton)
onClick: 'selectedText(myTextArea); submitForm(this)';
with: 'Work your magic, J.S.' ].