Here's one way to achieve the all-http-errors-should-go-to-root
functionality described by OP. I'm not sure I love it, but thought I'd
throw it in.
A general idea of what's going on: every component, rather than just
returning a `Cmd Msg`, produces a `Cmd (Wrapped Msg)`, where a `Wrapped
Msg` can either be a message-for-the-root-component or a
message-for-the-component-itself. When the parent uses `Cmd.map` on these
wrapped messages, it lets root messages through unchanged, but tags other
messages as being for-the-child.
Here's the code. First, create a separate module called, e.g.,
`RootMessage`, that looks like this:
module RootMessage exposing (..)
type RootMessage =
LoginFailed Http.Error
type Wrapped msg = Internal msg | ForRoot RootMessage
tag : (childMsg -> parentMsg) -> Wrapped childMsg -> Wrapped parentMsg
tag tagger msg =
case msg of
Internal m -> Internal (tagger m)
ForRoot r -> ForRoot r
Then, in each *non-root* module, you do something like the following. Here,
we have a Child module that includes a Grandchild component.
module Child exposing (..)
import RootMessage exposing (..)
type Msg = GrandchildMsg Grandchild.Msg
| APIResponse String
| ButtonPressed
update : Msg -> Model -> (Model, Cmd (Wrapped Msg))
update msg model =
case msg of
GrandchildMsg subMsg ->
let
(newModel, cmd) = Grandchild.update subMsg model.grandchild
in
{ model | grandchild = newModel } ! [ Cmd.map (tag GrandchildMsg)
cmd ]
ButtonPressed ->
model ! [ Task.perform (ForRoot << LoginFailed) (Internal <<
APIResponse) Http.get... ]
The trick here is in `Cmd.map (tag GrandchildMsg) cmd`. The `tag` function,
defined in the RootMessage module, takes in a normal tag, like
GrandchildMsg, but returns a "tagger" that only applies the `GrandchildMsg`
tag to non-root messages. It leaves the root messages unaltered.
Finally, in the root, you can handle these generated root messages:
module Root exposing (..)
import RootMessage exposing (..)
import Child exposing (..)
type Msg = Something | AnotherThing | ChildMsg Child.Msg
update : Wrapped Msg -> Model -> (Model, Cmd (Wrapped Msg))
update msg model =
case msg of
ForRoot LoginFailed err -> ...
Internal Something -> ...
Internal AnotherThing -> ...
Internal ChildMsg subMsg ->
let
(newModel, cmd) = Child.update subMsg model.child
in
{ model | child = newModel } ! [ Cmd.map (tag ChildMsg) cmd ]
Sorry for the terseness -- happy to elaborate if something is unclear!
-Alex
On Thursday, July 7, 2016 at 11:30:08 AM UTC-4, Erik Lott wrote:
>
> Mark:
>
> That's lead me to think about but not yet write a command alternative that
>> could also handle tasks that didn't need to be routed back to the
>> originator and that could be used to send messages to the top-level (or
>> elsewhere).
>
>
> I think you've nailed it. The suggested ELM architecture doesn't easily
> allow for application specific communication from the lower nested
> components to the upper components. There are times when in the scope of an
> overall architecture, you'd like to return some type of application wide
> recognized event/message back up the update stack, and have any interested
> ancestor component react to it... This is essentially how Commands
> function. Commands are a globally recognized type, that are returned up the
> stack, and although they may carry a local message type with them, the
> command itself is never returned or reused. The Command reaches the
> application root, and is processed by the core language.
>
> Like you said: I think what we need (or at least what I need) is our own
> Application specific type to return up the stack instead of Cmd. An "Event"
> union type possibly? And CmdEvt tag could wrap the native elm Cmd, and be
> unwrapped at the root.
>
> type Event
> = CmdEvt Cmd
> | MsgEvt
> | Unauthorized
> | LoggedIn User
> | ConfirmPopup CancelMsg OkMsg
>
>
>
> On Tuesday, July 5, 2016 at 1:30:51 PM UTC-4, Mark Hamburg wrote:
>>
>> The first option feels repugnant from an encapsulation standpoint. I've
>> built the second option and it works but it increases the amount of
>> boilerplate in hierarchical systems because we now have three results to
>> deal with in update functions. That's lead me to think about but not yet
>> write a command alternative that could also handle tasks that didn't need
>> to be routed back to the originator and that could be used to send messages
>> to the top-level (or elsewhere). That said, once one gets into replacing
>> Cmd, the API request model makes a lot of sense.
>>
>> Mark
>>
>> On Jul 5, 2016, at 5:46 AM, Erik Lott <[email protected]> wrote:
>>
>> My app has several layers of nested components. Various components
>> throughout the tree will need to interact with our API via http requests.
>> If any API request returns a 401 - Not Authorized error, or a Timeout
>> Error, the error needs to bubble up to the root component where is can be
>> handled appropriately.
>>
>> What is the most idiomatic way of dealing with this?
>>
>> *1. Parent should pattern match against important child messages*:
>> Reference
>> <https://groups.google.com/d/msg/elm-discuss/QPqrJd4C78Y/_TLLg81SAQAJ>
>> This could work, but would be unreasonable in this case. The root
>> component would need to match against every failing api http request made
>> by every child, grandchild, great-grandchild, etc, component in the tree.
>> If a single pattern is missed, the app would be in an error state, so this
>> is prone to mistakes.
>>
>> *2. Nested Components return additional info from the "update" function*:
>> Reference
>> <http://stackoverflow.com/questions/37328203/elm-0-17-how-to-subscribe-to-sibling-nested-component-changes>
>> Each component returns an additional value from its update function like
>> this:
>> update : Msg -> Model -> (Model, Cmd Msg, SomeInfo)
>>
>> The parent component could inspect the returned "SomeInfo" value from its
>> direct children, and act on that information if necessary. In my case, any
>> nested component that makes http requests to our API would be responsible
>> for returning a APINotAuthorized and APITimeout value to its parent, and
>> its parent would do the same, until the error has bubbled up to the root
>> component.
>>
>>
>> Option 2 is simple and robust, and can be used to pass messages of any
>> type, for any situation... but I'm wondering if I'm missing an obvious 3rd
>> solution?
>>
>> --
>> You received this message because you are subscribed to the Google Groups
>> "Elm Discuss" group.
>> To unsubscribe from this group and stop receiving emails from it, send an
>> email to [email protected].
>> For more options, visit https://groups.google.com/d/optout.
>>
>>
--
You received this message because you are subscribed to the Google Groups "Elm
Discuss" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
For more options, visit https://groups.google.com/d/optout.