5

Starting from those three declarations :

type SharedMsg
   = SharedAction

type Page1Msg
   = Page1Action

type Page2Msg
   = Page2Action

I there a way to obtain an equivalent of the following one? Like a way to "merge" union types ?

type Msg
   = SharedAction
   | Page1Action
   | Page2Action

=============================

Context : I am splitting an Elm application into one module per page with their own folders.

Some actions will be shared, and some actions will be page-specific.

If I was to use the Html.map method, I feel that I would have to re-write every shared action that a page uses in its own PageMsg message type:

type Page1Msg
   = Page1Action
   | SharedAction

type Msg
   = Page1Msg Page1Msg
   | Page2Msg Page2Msg

view : Model -> Html Msg
view =
   Html.map Page1Msg (Page1View.view model)

Hence my thinking of using a unique Msg type for all pages, but preserving modularity by writing page-specific messages in their own folders, and then somehow defining a unique Msg type by merging them.

AlexHv
  • 1,694
  • 14
  • 21
  • 1
    Are these pages in a Single Page App? What sorts of shared messages do you have, and where do they arise? Back when the Elm community was first trying to figure these questions out, I wrote a piece on Medium (https://medium.com/@alex.lew/the-translator-pattern-a-model-for-child-to-parent-communication-in-elm-f4bfaa1d3f98) about my solution to the problem of Page1 and Page2 both being able to trigger some shared message in the "parent" update function. But the community's thinking has evolved since then, and knowing the specifics of your case might inspire a better solution! – Alex Lew May 11 '17 at 15:52
  • 1
    (I should mention that there is no way to merge union types as you're trying to do, without creating new tags/constructors.) – Alex Lew May 11 '17 at 15:54
  • 1. Thanks for the answer about merging types and 2. Wow super smart translator pattern, I'm going to dig in that direction! The shared messages I was thinking of are of the same nature of those you mention: display a global app loading spinner, load cross-pages shared data ... – AlexHv May 12 '17 at 07:40

3 Answers3

7

@z5h's answer is almost correct, but the type constructors have to have different names.

You can't merge the types the way you'd like to.

As for the idiomatic way: You would name the split types just Msg, not Page1Msg. So, for example:

Page1.elm:

module Page1 exposing (Msg)

type Msg
  = Foo

Page2.elm:

module Page2 exposing (Msg)

type Msg
  = Bar

Shared.elm:

module Shared exposing (Msg)

type Msg
  = Baz

Main.elm:

module Main exposing (..)

import Shared
import Page1
import Page2

type Msg
  = SomethingCustom
  | SharedMsg Shared.Msg
  | Page1Msg Page1.Msg
  | Page2Msg Page2.Msg

By the way, remember that if you split the modules into Page1.View, Page1.Types, etc., then as long as the exposed functions don't overlap, you can import different modules under the same name, ie:

import Page1.Types as Page1
import Page1.State as Page1
import Page1.View as Page1
import Page1.Decoders as Page1
Martin Janiczek
  • 2,996
  • 3
  • 24
  • 32
  • The tricky thing is this case is to write a view function for Page1. Do you Html.map the output? Using what function? Maybe the view function can take in the SharedMsg and Page1Msg constructors as arguments? – Alex Lew May 13 '17 at 17:50
  • `Page1.view : Page1.Model -> Html Page1.Msg`, usage from Main: `Main.view mainModel = Html.map Page1Msg (Page1.view mainModel.page1Model)` – Martin Janiczek May 13 '17 at 21:12
  • 1
    But then how does `Page1.view` generate a Shared message? – Alex Lew May 14 '17 at 14:05
  • The same as `Main` would: `Page1.Msg = Foo | SharedMsg Shared.Msg, Page1.view = Html.map SharedMsg (Shared.view page1Model.sharedModel)`. Note there is no name clashing if you don't import `Page1.Msg` constructors unqualified into `Main`. But still it would probably be more clear to give the `Page1.SharedMsg` constructor a different name, to avoid confusion with `Main.SharedMsg`. – Martin Janiczek May 15 '17 at 07:01
  • Thanks for your answer, I could have the principle of your solution to work, as shown in this (not entirely modularized) gist : https://gist.github.com/Alexandre-Herve/dda1572341101d32e7f4ce2a48711fce – AlexHv May 15 '17 at 09:11
  • Thanks for your response -- what still confuses me is that after you then Html.map Page1Msg over the Page1Msg view, you wind up with double-tagged messages like `Page1Msg (SharedMsg ...)`, which, even though it's a shared message, will not be routed to Main's handler for that message. If the shared message is supposed to affect the global model, and not just Page1's model, what do you do? – Alex Lew May 16 '17 at 11:05
  • 1
    @AlexLew The way I see it: The `Shared.Model` should reside inside `Main.Model`, be given to `Page1.view` as an extra argument (`Page1.view : Page1.Model -> Shared.Model -> Html Page1.Msg`), and when you get `SharedMsg` in `Page1.update`, give that to `Main.update` - that means: `Page1.update : Page1.Msg -> Page1.Model -> (Page1.Model, Cmd Page1.Msg, Maybe SharedMsg)`. You would then respond in `Main.update` to that `Maybe SharedMsg` element of the tuple. – Martin Janiczek May 16 '17 at 12:11
  • @AlexLew Or, tl;dr: ///// Shared model is as high in the hierarchy of models as needs to be (with Main model considered the highest), and being passed around in `view` arguments. ///// Child `update` functions can return extra data to parents until it bubbles to the `update` function that has access to the shared model and can use it. ///// – Martin Janiczek May 16 '17 at 12:22
2

Do not forget that you are absolutely not obliged to follow the update-view definitions exactly as in the basic examples. In your case, you could adapt the update function to your needs

how about in parent:

update message model = 
    let 
        sharedMsgs = 
            { msg1 = Msg1 
            , msg2 = Msg2 
            }

    in case message of
        Page1Msg msg ->
            let (m, c) =
                update sharedMsgs msg model.page1
            in case c of 
                Nothing ->
                    m 
                Just c ->
                    update c m

where the update function in page1 has signature

update : SharedMessages msg -> Msg -> Page1Model -> (Page1Model, Maybe msg)
Simon H
  • 20,332
  • 14
  • 71
  • 128
0

The problem is, you've said:

type SharedMsg
   = SharedAction

So we know the type of SharedAction is a SharedMsg.
But then you say:

type Msg
   = SharedAction
   | Page1Action
   | Page2Action

So now a contradiction, in that SharedAction is a Msg.

The easy way to work around this is to say:

type Msg
   = Msg SharedMsg
   | Msg Page1Msg
   | Msg Page2Msg

I.e. Msg is a constructor who's instances are of type Msg, which can have the following values.

Mark Bolusmjak
  • 23,606
  • 10
  • 74
  • 129
  • 2
    The last code example won't work. The constructors have to have different names, ie. `type A = A1 Foo | A2 Bar | A3 Baz` instead of `type A = A Foo | A Bar | A Baz` – Martin Janiczek May 12 '17 at 20:47