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.

Reply via email to