Elm-spa: a tool for building single page apps

Hey everyone! :wave:

I’m Ryan and I’ve been spending the last couple weeks (or months or something) designing a way to make building single page applications nicer.

I’ve reached out to a few members of the Elm community for feedback on this new API and it has already been super helpful!

your time is important

Instead of asking folks to checkout my README or download the package, I’ve uploaded two short videos instead! ( and I even made a powerpoint :man_shrugging: )

Hopefully it’s a fun way to have more people feel welcome in the conversation!

Part 1: the intro

Part 2: the demo

I was really inspired by @rtfeldman 's elm-spa-example and his talks about using the simplest page for the job (instead of elm-fork-knife :fork_and_knife:), so hopefully the project aligns with those values!

I’d love to hear what you all think, and am looking forward to having a conversation about what you love/despise about this package.

Huge shoutout to @ianmackenzie and @dillonkearns for their help and guidance on the API design so far!

Here’s the repo: https://github.com/ryannhg/elm-spa/

Have a great weekend,
Ryan

31 Likes

This is awesome! Just started to port some small project to elm-spa and it works very well so far. This saves so much time! Thank you!!!

4 Likes

Glad to hear you like it!

I’m still in the design phase, so I would not do anything mission critical, but it’s great to hear your experience with a small project is already a positive one :grin:

1 Like

This is the area where Elm apps feel a bit slow development wise, I applaud your efforts for having a crack at this and I think it will be great for the community to discuss and iterate on.

8 Likes

Thanks for the encouragement! :slight_smile:

I started testing and loved the friendly help message of the cli without params, nice first impression. With the code generation and cli commands to add content I felt like using Rails 10 years ago, very pleasant! The problem is, it took me some minutes to realize I needed to run elm-spa build! About the router, how is possible to pass parameters to a route, instead of static routes? Is there a elmslack room for talking about it? And I think it could be even better if integrated with some tool by default like elm-live using elm-spa run (Edit: realized the package.json commands). It’s early to say, but if polished and battle tested, this framework certainly can be a killer app for Elm for fast prototyping!

3 Likes

Great work! Not only a timesaver in the CLI, but I love how the library cleans things up, providing consistency in the SPA architecture like TEA did for the main loop.

I’m indebted to elm-spa-example for getting me going on one of my early larger projects, but I was learning as I went and ended up with some unnecessarily complex architecture (trying to share and reuse components… yes, I wish I’d seen that talk earlier!)

Now whenever I go back to add or change even something simple I’m horrified at how many files I have to touch and how much logic I have to retrace when things go wrong. This would really help cut down on some of that complexity.

Can’t wait to see how this progresses.

3 Likes

This looks really exciting. :+1: I think I’m at a good point to test this with a project that I’m working on just now. It’s starting to feel a bit unwieldy and needs refactoring, so I’ll have a go at doing it the elm-spa way and let you know how I get on.

Good job trying to address this general problem, scaling an Elm app is tricky, mainly because there’s no obvious “right” way to do it. This personally lead to a kind of existential dread when I was first learning the language.

1 Like

Hi Ryan! I’ve tried the generator out, it’s great :clap: The dev experience is really good with the dev command, with live reload and debug turned on by default.

Thank you for making it!

One element I was looking in the docs was a simple example of how to override certain routes. It didn’t take me too long to figure out a way, but I’m not quite sure it’s the right one:

module Main exposing (main)

import Application
import Application.Route as AppRoute
import Generated.Pages as Pages
import Generated.Route as Route
import Global


main =
    Application.create
        { ui = Application.usingHtml
        , global =
            { init = Global.init
            , update = Global.update
            , subscriptions = Global.subscriptions
            }
        , routing =
            { routes = routes
            , toPath = toPath
            , notFound = Route.NotFound ()
            }
        , page = Pages.page
        }

-- Overriding routes
routes =
    List.append Route.routes [ AppRoute.path "new-page-with-a-special-url" Route.NewPage ]

-- Overriding paths
toPath route =
    case route of
        Route.NewPage _ ->
            "/new-page-with-a-special-url"

        _ ->
            Route.toPath route

For toPath, I’m able to prevent the generated behaviour to occur (seeing as it’s a case expression), while keeping the other generated paths. For routes though, it doesn’t really seem possible (here, /new-page will still display the given page). So either I let /new-page display the page, or I rewrite all paths for all routes here.

Is there another (better) way to go about it?

1 Like

@GabeAtWork – this is really interesting, I didn’t think about overriding certain routes!

I’m impressed with the workaround you found :sunglasses:, I didn’t realize you could just modify the generated stuff. I’ll try to think if there might be a nicer API for doing something like this…

Two questions:

  1. Would it make more sense to have the server handle the redirect:
    https://github.com/ryannhg/elm-spa/blob/master/netlify.toml

  2. Would this work for the routes issue?

routes =
  List.concat
    [ [ AppRoute.path "/new-page" Route.NotFound` ]
    , Route.routes
    , [ AppRoute.path "new-page-with-a-special-url" Route.NewPage ]
    ]

It would be great to know more about the use-case for "new-page-with-a-special-url" that would make you prefer the overwrite vs. NewPageWithASpecialUrl.elm

( I understand that it’s a theoretical limitation of the design, but I’m hoping we can have a nice example of when it would be used in practice)

Thanks again for sharing this, super helpful– and exactly the kind of questions i was hoping for! :smile:

1 Like

@G4BB3R – glad you liked the cli messages!

I’ll follow up with more documentation on how dynamic routes / parameters work.

Right now I’m working with something like this:

Pages/
  Top.elm
  Docs.elm
  Docs/
    Something.elm
    Dynamic.elm

will generate routes like this:

/               -> Top.elm
/docs           -> Docs.elm
/docs/something -> Docs/Something.elm
/docs/anything  -> Docs/Dynamic.elm
/docs/dynamic   -> Docs/Dynamic.elm
/docs/goes-here -> Docs/Dynamic.elm
/docs/_________ -> Docs/Dynamic.elm

In the current design dynamic parameters are passed in as Flags in the init function of that page:

init : String -> Model
init flags =
  { thatStringFromTheUrl = flags
  }

You can store it in your model from there!

( Note: moving away from names Index.elm and Slug.elm, because i just inherited those from previous work, but Top.elm and Dynamic.elm seem more discoverable / “google-able” for beginners )

I’m looking forward to releasing official documentation. The fact that you didn’t know about the package.json commands is a design problem on my end. So even mentioning that bit was helpful!

Maybe when things are more stable, we can request a channel in the slack! :smile:

Thanks again for taking time to leave your feedback :heart:

2 Likes

Thanks for the reply!

I don’t feel it would, at least from my experience. I tend to only use server-side stuff for very broad config (like redirecting to index.html). I’d rather not have anything from the business-side handled by the server (although I could live with protecting a few “sensitive” urls that way if I had too, I’d just rather not). That’s only my perspective of course.

I think it works (although you have to omit the leading / in paths), though it feels like a workaround. That being said, it probably doesn’t matter too much if it is workaround, seeing as this is probably an edge-case. If you need to override most of the paths, then you’re probably better off defining all the routes by hand.

Here’s a few use cases I can think of:

  • when from a code perspective, it makes a lot of sense to name the page SomeComplicatedOrObscureStuff (stuff like a shipment service’s name and service for a concrete example from my work), but you’d much rather have a /simple-url for your client (like /express-transporter for instance)
  • when you’d like to translate a few front-facing routes while keeping the admin routes auto-generated. For instance I’d have a /produit page that matches the Product.elm page. I want to keep the code in english, but for my customers, using french might make sense, while for the admin urls I don’t care.

Those are a few I can think of. It’s not a big deal for most people of course, so adding an example of this workaround is probably sufficient.

I’ve opened a PR to add such an example: https://github.com/ryannhg/elm-spa/pull/4
It might be better to add it to an /advanced folder seeing as it’s not a common use case, or it can even be documented elsewhere if you’d rather. I’d be happy to write it, let me know!

1 Like

Awesome work! Thanks for sharing this with us. I especially love the send pattern for communicating with the parent component. I used to write horrible hacks for this. Now you showed me it’s an elegant one-liner :smiley:

2 Likes

Very interesting project! I love Elm, but coming from the Angular world, I missed that kind of tool to help building apps.

Just a quick question: what about route parameters (like /users/:id)? I saw in one of the examples there was parameters types, but they where empty so I don’t know exactly how to exploit them.

1 Like

How can one add User authentication via e.g. tokens? I see that in the examples there is already something ready: https://github.com/ryannhg/elm-spa/blob/master/examples/html/src/Api/Users.elm but it is not used anywhere. Or am I missing something?

Thank you for this, Ryan. Your videos are very helpful even for one who prefers reading.

An observation: it appears that only one elm-spa app can exist per host address because the URL’s path must be / for the home page and built onto that for the other pages.

E.g., https://example.com/ & https://example.com/docs work; https://example.com/myElmSpaApp & https://example.com/myElmSpaApp/docs do not–they always bring up page-not-found because elm-spa tries to navigate to /myElmSpaApp/ and /myElmSpaApp/docs, which don’t exist in the generated routes.

Is this just an error in my configuration? I dug through the generation code and the generated code, but I’m too much of an Elm novice to be confident with this observation.

1 Like

Nice work! We are starting a new project now and I am evaluating this approach.

The first impression is great!

2 Likes

Excellent! Surely this is a missing piece in the ecosystem.

2 Likes

Thanks everyone for the kind words!

Hoping to release an update soon, but just wanted to remind folks that the current API is super unstable.

I can’t wait to share a more official release, which addresses a lot of the design issues and questions brought here :slight_smile:

6 Likes

I am very excited about this! There is one thing I am missing: component views placed on a page in the layout view like a navbar or sidebar should be able to create global messages. This is currently not possible.

5 Likes