Elm spa example header with shared button (on Click) attribute

Hello Folks,

So I am developing an application using the elm-spa-example by rfeldman.
I lately found my self in a situation where I would like to include server generated content on the header which responds to onCLick events .

Kind of how Facebook notifies you whether new messages for you came in.
The problem I think is apparent in the elm-spa-example the main method is the aggregator and every Page has it’s own message type and main has a Custom Msg type with variants for every possible page msg type in the application.

Then in the module that creates the view (Page.view) it passes the view of the particular page that is supposed to be on the screen which is of type Html PageMsgType and the Main method is mapping it to it’s local message type which has a variant for every posible PageMsgType containing the PageMsgType so it can then parse the message when a component is clicked or something and forward it to the appropriate update function of the particular page.

My problem now is that I would like to implement the functionality described above and what I actually was thinking of is create a button with events that are handled in the Main module but somehow it’s not possible (at least as far as I can understand) because what I am actually trying to do is compose a an Documet with more than one Msg types, PageMsgType and MainMsgType .

Have you ever encountered a situation like the described before ? How would you go with this ?

One way could be to render the header in the Main outside the page specific view (without using Html.map).

I think it is quite common for SPAs to render the layout of the app (header or app bar, footer, menu) this way in Main with Main messages, and then render the page only inside its container with Html.map.

Something like:

type Msg
    = HeaderClicked
    | ChangedUrl Url
    | ClickedLink Browser.UrlRequest
    | GotHomeMsg Home.Msg
    ...

view : Model -> Document Msg
view model =
    let
        viewer =
            Session.viewer (toSession model)

        viewPage page toMsg config =
            let
                { title, body } =
                    Page.view viewer page config
            in
            { title = title
            , body = viewHeader :: List.map (Html.map toMsg) body
            }
    in
    ...


viewHeader : Html Msg
viewHeader =
    div []
        [ button
            [ onClick HeaderClicked ]
            [ text "Click me" ]
        ]

and you would remove the banner view from each page.

2 Likes

The way I model this is to have global state and global messages. This global state is updated in the Main and sent down to the view and the update of pages as a form of context.

A very similar pattern is elm-shared-state (formerly elm-taco).

1 Like

Looks like the obvious answer, I was just avoiding to put to much on the Main because of fear it becomes to big and more complex than I can manage. And because of a comment by RFeldman saying
“-- NOTE: Based on discussions around how asset management features
– like code splitting and lazy loading have been shaping up, it’s possible
– that most of this file may become unnecessary in a future release of Elm.
– Avoid putting things in this module unless there is no alternative!”
but I guess there is not apparent alternative on this one.

Thank you I will consider this approach carefully.

This is a good attitude. You usually have only one Main.elm and it pays to take extra care not to make it a magnet for things that would have ended up in other modules with more careful consideration.

The viewHeader function in my example does not have to be literally in the Main.elm module.
It can be in its own module, taking a msg argument:

module Main exposing (main)

import Header

type Msg
    = HeaderClicked
    | GotHomeMsg Home.Msg


view model =
            ...
            in
            { title = title
            , body = Header.view HeaderClicked :: List.map (Html.map toMsg) body
            }
module Header exposing (view)

view : msg -> Html msg
view onClick = 
    div []
    [ button
        [ onClick msg ]
        []
    ]

or if really needed could have its own Msg which would be mapped like the pages ones.

At the end, all messages are Main ones, for example:

type Msg
    = HeaderMsg Header.Msg
    | GotHomeMsg Home.Msg
    ...


view model =
            ...
            in
            { title = title
            , body = Html.map HeaderMsg viewHeader :: List.map (Html.map toMsg) body
            }

I would strongly prefer the first solution (the msg argument(s)) as long as one or two messages for the header are enough.

Lastly:

Try to structure by types, not by code size.
See The life of a file.

I think that comment was written prior to 0.19, when there were ideas about code splitting floating around. That never made it into 0.19, and I think those ideas may now be abandoned. Partly due to dead code elimination in 0.19 reducing asset sizes and making code splitting less of an advantage as a result.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.