I was wondering what would be the preferred way for modifying global state from a child component using TEA.
My use case is having a global loading indicator showing while any json request is in progress.
A google search lead to me this article, but am not 100% this is the best solution. Any other suggestions?
A bit more background:
I have an SPA with various components and a "Loader" component. The loader component is supposed to handle transitions between views.
For example, let's say I have 3 views: (A) Dashboard (B) Accounts (C) Transaction Details.
When the user is on (A) and clicks on a Transaction I want (A) to send notify the Loader that it needs to load the transaction resource and one done update the Browser Location so the the root component can handle the routing.
The Loader should display a loading bar on the top of the page while loading the resource. Once done, it will update the Location bar, so the root component can handle the routing.
In essence what I am trying to avoid is having display the (C) page, before the resource for this is loaded.
A structure like below could hopefully get you going.
More on scaling Elm here (highly recommended)
In the example below you keep all data in your top-level model, and your messages as well.
Such a setup has no parent-child relationships. All views use the same top-level model, and all views produce the same type of messages.
This example navigates to the transactions page as soon as transactions come in.
type alias Model =
{ accounts: List Account
, transactions: Transactions
, currentPage : Page
, message : String
}
type Transactions = Idle | Loading | GotEm (List Transaction)
type Page = DashboardPage | AccountsPage | TransactionsPage
type Msg = ShowDashboard | ShowAccounts | ShowTransactions | GotJSONData
update msg model =
case msg of
ShowTransactions ->
case model.transactions of
GotEm transactions ->
{ model
| currentPage = TransactionsPage
, message = ""
} ! []
Idle ->
{ model
| currentPage = DashboardPage
| transactions = Loading
, message = "Loading transactions"
} ! [ API.getTransactions model GotJSONData ]
Loading ->
model ! []
GotJSONData transactions ->
{ model
| transactions = GotEm transactions
, message = ""
, currentPage = TransactionsPage
}
view model =
case model.currentPage of
DashboardPage ->
DashboardPage.view model
-- DASHBOARD module
view model =
div []
[ ...
, button [ onClick ShowTransactions ] [ text "Show transactions"]
]
Related
Can't quite figure out how to go about sending messages between modules when working with reusable components.
I have an expanding text area that I'd like to use on a number of different sections on a site. The Text Area accepts a portion of HTML that makes up the user actions. Typically handling submit, cancel, upload icons, etc..
Tried to write up a quick example of what I'm talking about without throwing a ton of code on here. So essentially, I'd like to just plug and play peices of HTML that are already attached.
I'm assuming CancelNote is getting fired as a TextArea msg, so it never sees a Cancel Note msg. Not sure how I would use Html.map here (or even if I would).....feel like plug and play method is probably a bad approach, but not sure how else I could achieve decent reusability .
SEPERATE MODULE
update model msg =
case msg of
CancelText ->
( { model | note = (Just "") }
, Cmd.none
)
view: stuff
view stuff =
......
TextArea.view
(button [ Html.Events.onClick CancelText] [])
TEXT AREA MODULE
view : Html.Html msg -> Html msg
view actionHtml =
div [ class "extended_text_area_container" ] [
textarea [] [
]
, actionHtml
]
Messages are just values like any other. You can pass them around directly:
-- SEPERATE MODULE
view: stuff
view stuff =
......
TextArea.view CancelText
-- TEXT AREA MODULE
view : msg -> Html msg
view msg =
div [ class "extended_text_area_container" ]
[ textarea [] []
, button [ onClick msg ] []
]
Edit: If you need to also maintain internal state, just use another message to tell the parent to update the state:
-- Main module
type msg =
...
SetTextAreaState TextArea.state
update model msg =
case msg of
...
SetTextAreaState state ->
{ model | textAreaState = state }
view : Model -> Html msg
TextArea.view SetTextAreaState model.textAreaState
-- TextArea module
type State =
...
type Msg =
Clicked
update : State -> Msg -> State
update state msg =
case msg of
Clicked ->
{ state | clicked = True }
view : (State -> msg) -> State -> Html msg
view toMsg state =
let
updateAndWrap msg =
toMsg (update state msg)
in
div [ class "extended_text_area_container" ]
[ textarea [] []
, button [ onClick (updateAndWrap Clicked) ] []
]
Here, instead of passing a msg to onClick directly in TextArea.view, call a function that updates the state and then wraps it in the msg constructor passed in from the parent, which will produce a message of a type that we don't know anything about.
Also, while I use an internal Msg type and update function similarly to the overall Elm architecture, that is in no way mandatory. It's just a nice way of doing it since it's familiar and scales well.
I have a feature called Editor that I'm trying to plug into my app. Its view returns a the type Html EditorMsg. I plug it in here:
edit : Editor.Types.Model -> Html EditorMsg
edit editorModel =
div [ class "edit" ] [ Editor.view editorModel ]
In the app, I have routing and so edit is called by way of my main view function, plus a couple of functions that manage which route to show:
-- VIEW
view : Model -> Html Msg
view model =
div []
[ RoutingMsg navBar
, EditorMsg (render model)
]
render : Model -> Html EditorMsg
render model =
case model.route of
Nothing ->
li [] [ text "Invalid URL" ]
Just route ->
showRoute route model
showRoute : Route -> Model -> Html EditorMsg
showRoute route model =
case route of
Home ->
home
Editor ->
edit model.editor
My view also contains a navBar as you can see, and that returns Html possibly containing a different type of message, RoutingMsg:
navBar : Html RoutingMsg
navBar =
NavBarState.config
|> NavBarState.items
[ navItem "/" "Home" False
, navItem "/editor" "Edit" False
]
|> NavBar.view
This setup didn't compile because in view I have a list of two different types, one returning Html RoutingMsg and the other Html EditorMsg, so I set up a union type to try to contain that difference:
type Msg
= RoutingMsg (Html RoutingMsg)
| EditorMsg (Html EditorMsg)
This seems to work, but then I run into trouble in my update method, where the matching doesn't quite work. Long and short of it is that it feels as though I've just got the wrong pattern. My goal was to make my Editor somewhat independent, like a module, that can be plugged in different places. But it's hard for me to understand in Elm, how to integrate things.
To illustrate the problem more simply, I created this example on ellie-app that approximates what I tried to do but can't get working: https://ellie-app.com/PKmzsV3PC7a1/1
Is my approach here just incorrect, or if not, how can I get this code to work?
You should use Html.map to map children messages to the top-level Msg
Here's what you've been missing:
view : Model -> Html Msg
view model =
div []
[ Html.map ButtonDecMsg buttonDec
, div [] [ text (toString model) ]
, Html.map ButtonIncMsg buttonInc
]
Also the type annotation definition of child update functions should include the message type:
buttonDecUpdate : ButtonDecMsg -> Model -> Int
buttonDecUpdate msg model =
model - 1
Here is an example of working app: https://ellie-app.com/PM4H2dpFsfa1/0
What is the recommended way to have mdl components affect the layout of the page in a single-page application using elm and elm-mdl?
Can we pass the layout messages, like Model.Layout.ToggleDrawer directly to model.mdl (in the standard setup), and if yes, how?
Or should we maintain a separated record element in the model, like model.layout : Material.Layout.Model, to which we forward these messages? But in this case, how to initialize the view?
Context
I am using elm-mdl version 8.1.0, with elm version 0.18. I am trying to layout the basic architecture for a single-page application with elm, and this library. I have taken inspiration from here and there, as well as this ticket, but I have not seen what I was looking for, or understood it if it was there.
Examples of what I tried
For re-usability, the main model of my application is the only one containing a Material.Model entry:
type alias Model =
{ drawer : MyDrawer
, ...
, mdl : Material.Model
}
From the component MyDrawer, I want to define a button that will send a ToggleSignal. I have a case in my main update method that forwards this signal to the Model.Layout.update method, using the Model.Layout.ToggleSignal. However, the return type of this call is a Material.Layout.Model, which I don't have in my own Model.
If I define add a layout : Material.Layout.Model to my own model, I can forward the calls to this element, but how do I initialize the view? My view so far is this:
view : Model -> Html Msg
view model =
Layout.render Mdl
model
[ Layout.fixedHeader
, ...
According to the signature of Layout.render, since my model contains a layout field, this should be taken into account. The relevant part of my update method is
ToggleDrawer ->
let
( updatedLayout, cmd ) = Material.Layout.update Material.Layout.ToggleDrawer model.layout
( updatedDrawer, _ ) = Drawer.update subMsg model.drawer
in
( { model | drawer = updatedDrawer, layout = updatedLayout }, Cmd.map MdlLayout cmd )
And yet, when clicking on the button, the drawer does not hide.
Thank you in advance for any help about this - the library and the language are such a joy to use already!
The Material.Layout.render function retrieves its model from model.mdl.layout, not from model.layout.
A good way to toggle the drawer is to use toggleDrawer to issue a suitable command in your update function, like so:
ToggleDrawer ->
let
( updatedDrawer, _ ) = Drawer.update subMsg model.drawer
in
( { model | drawer = updatedDrawer, layout = updatedLayout }
, Layout.toggleDrawer Mdl
)
I've only been trying out Elm for a few days and have come across what seems like a common scenario that I can't figure out.
I have a parent component that holds a list of items. To render the list, I have a child component such that the parent calls the view function of the child passing in the address and model. Since the Action types are different, I think the compiler is complaining about that, but I'm really not sure.
Parent Component:
type alias Model = List ToDoItem.Model
type Action
= Remove Int
update : Action -> Model -> Model
update action model =
case action of
Remove id ->
List.filter (\todo -> todo.id /= id) model
view : Signal.Address Action -> Model -> Html
view address model =
let
buildToDos =
List.map (ToDoItem.view address) model
in
div [] [ buildToDos ]
Child Component:
type alias Model =
{ id : Int
, name : String
, description : String
, complete: Bool
}
type alias ID = Int
type Action
= Toggle Bool
update : Action -> Model -> Model
update action model =
case action of
Toggle toggle ->
if toggle == True then
{ model | complete = False }
else
{ model | complete = True }
view : Signal.Address Action -> Model -> Html
view address model =
let
toggleText : Bool -> String
toggleText complete =
case complete of
True -> "Incomplete"
False -> "Complete"
in
div
[ class "wrapper" ]
[ span [] [ text ("[" ++ toString model.id ++ "]") ]
, span [ class "name" ] [ text model.name ]
, div [ class "description" ] [ text model.description ]
, a [ onClick address (Toggle model.complete)] [ text (toggleText model.complete)]
]
Compiler Error:
-- TYPE MISMATCH ----------------------------------------------
The type annotation for `view` does not match its definition.
20│ view : Signal.Address Action -> Model -> Html
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The type annotation is saying:
Address Action -> List ToDoItem.Model -> Html
But I am inferring that the definition has this type:
Address ToDoItem.Action -> List ToDoItem.Model -> Html
-- TYPE MISMATCH ----------------------------------------------
The 2nd argument to function `div` is causing a mismatch.
26│ div [] [ buildToDos ]
^^^^^^^^^^^^^^
Function `div` is expecting the 2nd argument to be:
List VirtualDom.Node
But it is:
List (List Html)
How do I declare either the child component's view function correctly or pass in the parameters to the child's view function correctly from the parent?
In reality, I don't actually want to pass any action to the child component - I just want to render it. The action in the child component is for the onClick there.
Then again, maybe I'm way off because this is day 2 of my Elm life.
Elm Architecture Tutorial covers the child view issue. Take another look at how the example 4 is implemented.
In short, you need to have an Action in the parent that wraps the child action:
type Action = Remove Int | ToDo Int ToDoItem.Action
And you need to forward this action to the appropriate Item in update.
In view you need to create a forwarding address for each of your ToDoItem views.
view : Signal.Address Action -> Model -> Html
view address model =
let
fwd idx = Signal.forwardTo address (ToDo idx)
toDoList =
List.indexedMap (\(idx, m) -> ToDoItem.view (fwd idx) m) model
in
div [] toDoList
Please note that your old buildToDos is already a list and saying [buildToDos] is actually saying List (List Html) which is why you got the second error.
The compiler had already told you the answer. First of all, the
Address Action -> List ToDoItem.Model -> Html
The Action here has to be specified to the children Action. Just fix it like the compiler tell you:
view : Signal.Address TodoItem.Action -> Model -> Html
Second one, because you buildToDos is already a list, you just need to:
div [] buildToDos
Spend some time understanding type annotation and follow compiler carefully then you are should be able to solve this kind of problem yourself.
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