Laminar 103: A small reactive routing example

In my first Laminar tutorial I showed how to set up a Scala/sbt project to use Laminar to render “static” HTML code. Then in Laminar 102: A reactive “Hello, world” example, I showed how to create a small, reactive Laminar example, that sends signals from one widget to another using observables and observers.

In my experience, the next thing you’ll need to know about Laminar is routing, i.e., how to transition from one page to another in a single-page app (SPA). Therefore, in this tutorial I’ll show how to implement that for very small applications.

This solution can work for very small apps, and it’s important because it shows how routing works under the hood. But when you want to build a larger SPA you’ll want to use a Laminar routing library. I provide links for two such libraries below.

Github project

As with my first two tutorials, I created a Github project to go along with this tutorial, and you can find it here:

Clone that project and run it on your own system so you can see how this code works, and experiment with it as desired.

A quick look at routing

Because Laminar is based on observables and observers, as you can imagine, routing also depends on these. As you’ll see in the Scala code below, a little router looks like this:

val rootElement = 
    div(
        cls := "outer-div",
        // this is where we change the content that’s inside
        // the outer div
        child <-- currentPageVar.signal.map {
            case "home" => home
            case "login" => login
        }
    )

In this code rootElement consists of an outer div, and then when the currentPageVar signal is triggered, the content inside that root div is changed with either the (a) home page content or (b) login content.

The full source code

Given that little introduction, here’s the complete Scala/Scala.js/Laminar source code for this tutorial. I’ve added comments to the code to make it easier to understand:

package alvin

import org.scalajs.dom
import com.raquo.laminar.api.L._

object Laminar103Routing extends App {

    val initialPage = "home"
    val currentPageVar = Var(initialPage)

    // i show home’s type so you can see it more easily
    val home: HtmlElement = div(
        h1("home"),
        button("login", 
            onClick --> (_ => currentPageVar.set("login")
    )))

    // login has the same type as home, but a great thing about
    // Scala is that you don’t have to declare the type if you
    // don’t want to
    val login = div(
        h1("login"),
        button("home",
            onClick --> (_ => currentPageVar.set("home")
    )))
    
    // note that in more complicated examples, instead of using
    // val fields for home and login, you’ll use def methods or
    // objects.

    // here, rootElement consists of an outer div. then we change
    // the content inside that div based on the currentPageVar
    // signal: when the signal is "home", we show the home page,
    // and when the signal is "login", we show the login page.
    val rootElement = 
        div(
            cls := "outer-div",
            // this is where we change the content that’s inside
            // the outer div
            child <-- currentPageVar.signal.map {
                case "home" => home
                case "login" => login
            }
        )

    val appContainer = dom.document.querySelector("#root")
    render(appContainer, rootElement)
}

When you compile your code and open the index.html file — as I discussed in the first tutorial — you’ll initially see the “home” text in an <h1> tag, and a button with the text “login.” When you click that button, the browser content switches to a “login” <h1> tag with a “home” button.

A key for me here was understanding that the content is being changed out inside the outer div tag in the rootElement.

Where to go next (routing libraries)

Again, while this is a relatively simple example, it shows how routing works in a SPA. Routing libraries take this as a basic starting point and add much more functionality to it.

When you get to the point of needing a routing library, Laminar currently has two libraries available for such needs:

I ended up using Waypoint in my SPA, in large part because the project’s author was very responsive on the Laminar Gitter page. (I tried both libraries, had questions about both, wrote my question about Waypoint, and the author responded. Nothing too technical here.)

The Waypoint approach takes a little getting used to, but once you understand it, it’s not too difficult, and it also handles things like variable parameters in URLs (like when your URI has variable values in it, like /foo/1/bar/2).

Finally, if you’re interested in a much long Laminar tutorial — which also covers Waypoint — see this Building the frontend article by Anton Sviridov, who was also very helpful on Gitter.

Previous Laminar tutorials

If you skipped over the two previous tutorials and are now interested in them, you can find them here:

Gratitude

I forgot to mention it in previous posts, but many thanks to Nikita Gazarov (Twitter, Github) for creating Laminar, Airstream, Waypoint, and scala-dom-types, and also for helping me on the Laminar Gitter channel. I hope these three tutorials have given you a taste of what’s possible with Laminar.

The end?

For now this is the end of my Laminar tutorials. I can say from my own use that Laminar looks like a very interesting library for creating SPAs. Once you grok the observable/observer approach, I found it to be a matter of just writing the code I needed for my little SPA.