Does anyone have examples of more advanced SPA architecture than the ones in the guide?

I’ve used (read: stolen) the architecture from elm-spa-example and Elm packages repository for the apps I write at work and it’s worked really well, however I’m trying to introduce some new features that require cross-page data that I’m struggling to understand how to manage. I’d really appreciate some examples or advice on how to structure some global data.

What I’d like to accomplish is fetching some global data that’s shared amongst pages. I see that the Elm packages site manages this by having the individual pages request the data. When the data comes in, they modify the Session with the cached data. A drawback of this approach is that the messages always get routed back to the requesting page. Navigating around quickly means those requests get lost. Without getting into gory details, the app involves fetching long-running jobs, so this is a real risk.

Right now, I follow nested TEA with my “main” Model being a custom type with a case for every page. I have a Session module and type with a small amount of global data and have an exit function that pulls the session out so I can pass it to other pages, but that’s just pulled in from flags - I currently don’t update it anywhere.

I’ve read about global actions and translator, but what I’m missing from that is good architecture around this part:

We might need to send a message back to the module that returned the action. E.g. Open a dialog with selections. In this case our actions might need a message associated with them e.g. Action Msg. We will need a Actions.map to just like Html.map

How do I best keep track of these requests and make sure that the requesting module gets notified of its completion? I can’t just “bind” to the data in the session, I may need to pull in a copy once it’s loaded to do the whole “local modification” thing. Here’s a simplified view of the models:

-- Session.elm
type alias Job =
  { id : Int
    -- ...
  }

type alias JobResult =
  { id : Int
    -- ...
  }

type Data = Data
  { jobs : Dict Int Job
  , results : Dict Int JobResult
  } 

-- Main.elm
type Model
  = Home Home.Model -- Lists jobs
  | Job Job.Model -- Lists job results
  | JobResult JobResult.Model -- Shows result details

-- Page/Job.elm
type alias LocalResult =
  { job : Job
  , checked : Bool
  -- ...
  }

type alias Model =
  { session : Session.Data
  , results : WebData (List LocalResult)
  }

I’d like to support something like

  • Submit a job. Navigate to the results. See results pull in live.
  • Arriving directly at a job result page via URL. Fetch other results in the background.
  • Navigate to a result page. It’s not loaded yet, but the request is already running, so I don’t need to fire a new one, and when complete will populate the page’s model with the results.

Perhaps I’m trying to over-optimize a user experience here, but in any case, it’s a useful thought experiment about state management for a more complex app.

I wish I could share the full code, but unfortunately, it’s company code. I know it’s difficult to think about without actually seeing it, but any advice is always appreciated :slight_smile:.

There a couple of options I can think of immediately:

  1. You could add another type to your Model’s pages as so:

-- Main.elm
type Model
  = Home Home.Model CustomGlobalType -- Lists jobs
  | Job Job.Model CustomGlobalType -- Lists job results
  | JobResult JobResult.Model CustomGlobalType -- Shows result details

type alias CustomGlobalType =
  { ... whatever fields you need to track ... }


You will need to pass it from page to page when the route changes.

  1. You could use a flat model instead:
type alias Model =
  { homePage : Home.Model
  , JobPage : Job.Model
  , ...
  }

The benefit of this for you from what I can tell is that your pages will still receive their data if your users change page before the request has completed. This pattern would also allow you to create a dedicated send/receive module that sits as a field on your main model and routes data to wherever it needs to go - you’ll probably need to send messages up to it from your pages to make the requests, but may simplify the management of the data.

Alternatively, if one page needs data from another, you can easily extract it from one page and pass it to another when the route changes and the data is needed.

A few quick thoughts, HTH

Have you considered to use something like elm-pages ( https://elm-pages.com/ ) or Elm Land ( https://elm.land/ ) or use it as an inspiration?

Elm Pages would be my go to. I know @RyanNHG has been working on a new release that should make this even better.

Last I looked at elm-pages, I decided against it specifically because there was no way to share data across pages. Has this changed?

Here is how I share data across pages:

module Top 

type alias Model =
   { shared : ...
   ---
   , page1 : ...
   , page2 : ...
   }
module Page1

type alias Page1 a =
    { a |
      shared : ...
    , page1 : ...
   }

That is, use extensible records and a flat model design. Expose just the model slice you need. Top, Page1, FetchData… all modules working on a slice can read and update the fields they need to and the results of that are visible to the others.

Richard covers this in this talk, which I still think is the best overall guide to scaling Elm: https://www.youtube.com/watch?v=DoA4Txr4GUs

I meant Elm Land :person_facepalming: don’t know why I typed Elm Pages.

Thanks all! I think the Elm land inspiration is where I’m going to go.