Routing in WebPlatform

Routing is one of the important elements in any web library, and it is also one of the things that the Alusus language handles in a distinctive way. Routing in WebPlatform provides the following features:

  • Automatically loading the page that matches the current URL.
  • Navigating between pages without reloading.
  • Updating the address bar when navigating between pages.
  • Responding to the browser’s back button without reloading the site.
  • Applying transition animations when navigating between pages and allowing the user to create their own custom animations.
  • Providing two navigation modes: a replacement mode where one page replaces another, and a stacking mode where a page is loaded on top of the current one with the intent of dismissing it later to return to the previous page.

The last point is what we would like to focus on in this article, as WebPlatform supports these features in a unique way that I have not seen in any other web library.

The Switcher and the Stack

WebPlatform supports routing operations by providing two fundamental components dedicated to this purpose: Switcher and Stack. The Switcher allows switching between pages by removing the current page from memory and creating a different one, while the Stack allows the user to load pages on top of each other to enable returning to previous pages later without losing their data and state.

The user can make these two components fill the entire screen or just part of it. Multiple instances of each can be created and combined within an application in any desired configuration, allowing great flexibility in designing any site map. For example, a user can create a Stack with a Switcher inside it, where the Switcher is responsible for switching between the site’s pages, while the Stack is used to display popup windows or temporary pages that appear on top of the site (i.e., on top of the Switcher) regardless of which page the user is currently on.

The user can also do the reverse: create a Switcher and embed a Stack within one of its views. In this case, the Stack is used to display a temporary window that appears on top of that specific page only, not the entire site — meaning that navigating to another page removes the current page along with its associated popup.

Multiple Switchers or Stacks can also be created as desired and in any configuration. For example, a main Switcher can be created for the site’s primary pages, and then within one of those pages a secondary Switcher can be embedded to handle navigation between sub-pages within that main page.

Routing

The two base classes Switcher and Stack are only responsible for providing the view-switching operation, which is the fundamental mechanism on which routing is built — but they do not provide true routing that binds a URL to the requested page. That responsibility falls on the RoutingSwitcher and RoutingStack classes, which rely on the former two to provide view transitions and link them to the browser’s address bar.

In both classes, the user needs to specify a set of routes along with a closure for each route that creates the desired view for that route. Both classes use regular expressions to determine which view to display, with a difference in the algorithm used as described below.

Before diving into details, it should be noted that all of this assumes you place the entire site under a single UI endpoint — meaning that instead of relying on multiple functions annotated with @uiEndpoint, each dedicated to a specific path, you instead use a single function that covers all the paths handled by the two routing components mentioned above. This is done by using a wildcard * in the endpoint path. For example:

@uiEndpoint["/*"]
@title["WebPlatform Example"]
func main {
}

In the example above, we tell WebPlatform to route every path starting with / to the same function, effectively moving all routing from the server to the browser. Of course, you can restrict the scope to a smaller path, such as using a specific prefix followed by * — for instance, /ui/* — to tell WebPlatform that any path beginning with /ui/ should be routed to that function.

It is worth noting that WebPlatform uses priorities when matching paths: it gives priority to back-end endpoint paths, then resource paths, before UI endpoint paths. Therefore, setting the UI endpoint path to /* will not prevent access to an API endpoint like /api/login, for example.

RoutingSwitcher

The algorithm used by RoutingSwitcher is very straightforward: it iterates through the defined routes in sequence, matching each route’s regular expression against the full current URL. If a match is found, that view is displayed and iteration stops. If no match is found, it continues to the next definition, and so on.

RoutingSwitcher().{
    route("^/one$") = closure(RoutePayload): SrdRef[Widget] {
        return View1();
    };
    route("^/two$") = closure(RoutePayload): SrdRef[Widget] {
        return View2();
    };
    route("^/three$") = closure(RoutePayload): SrdRef[Widget] {
        return View3();
    };
}

But what if we want to embed a RoutingSwitcher inside another one — for instance, if View3 contains another Switcher for navigating between inner pages? Suppose the sub-paths are:

  • /three/subview1
  • /three/subview2

In this case, we need to account for these paths in the regular expressions of both the outer and inner Switchers. We change the outer Switcher’s route from ^/three$ to ^/three — i.e., by removing the $ — to allow View3 to be created when sub-paths match. Otherwise, the inner Switcher would never be instantiated and the inner pages could never be shown. Likewise, the inner Switcher’s routes must account for the presence of the three segment in the URL, either by writing the full URL or by writing the sub-path without using the ^ anchor.

RoutingStack

The routing Stack works differently. Instead of iterating through all views looking for the one matching view, it does the opposite: it iterates through all views looking for the first non-matching view. It keeps creating views one on top of another until it reaches a view whose path does not match, at which point it stops. Therefore, the user must ensure that each view defines a path that matches all the views before it, otherwise it will never be displayed. For example:

RoutingStack().{
    route(String("^/one")) = closure(RoutePayload): SrdRef[Widget] {
        return View1();
    };
    route(String("^/one/edit")) = closure(RoutePayload): SrdRef[Widget] {
        return Editor();
    };
};

If the current path is /one, only View1 is displayed. If the path is /one/edit, then View1 is displayed with Editor stacked on top of it.

What if you have more than one sub-view that you want to display on top of the current view — say /one/edit and /one/share? To handle this, you can make the sub-view a Switcher that decides which of the two views to load. For example:

RoutingStack().{
    route(String("^/one")) = closure(RoutePayload): SrdRef[Widget] {
        return View1();
    };
    route(String("^/one/(edit|share)")) = closure(RoutePayload): SrdRef[Widget] {
        return RoutingSwitcher().{
            route("^/one/edit$") = closure(RoutePayload): SrdRef[Widget] {
                return Editor();
            };
            route("^/one/share$") = closure(RoutePayload): SrdRef[Widget] {
                return Share();
            };
        };
    };
};

Navigation Without Reloading

Relying on traditional hyperlinks allows navigation between pages, but clicking a traditional hyperlink causes the page to reload. To avoid reloading, we use the pushLocation function from the Window class. For example:

Window.instance.pushLocation("/one/edit");

This updates the address bar in the browser and then navigates to the requested view. WebPlatform will take care of displaying the new view with the appropriate transition animations. Pressing the browser’s back button afterward will return to the previous page with transition animations, without reloading the page.

This does not mean hyperlinks should be avoided entirely — they serve an important purpose: informing search engines about the pages that can be navigated to. If the page is private and not meant for search engine indexing, hyperlinks can be omitted. But if it is a public page that we want search engines to index, it is better to use hyperlinks alongside pushLocation, as in this example:

Hyperlink().{
    url = String("/about");
    setChild(Text(String("About")));
    onClick.defaultPrevented = 1;
    onClick.connect(closure (ref[Widget], ref[Int]) {
        Window.instance.pushLocation("/about");
    });
};

In the example above, we use a hyperlink but override its click behavior to use pushLocation instead of relying on the default behavior that reloads the page. This way we get the best of both worlds: smooth navigation with animations and no reloading, plus links that search engines can read.

Animations

The Switcher and Stack both support transition animations when moving from one view to another, and they allow the user to specify these animations — either by defining a custom animation style or by choosing from a set of predefined effects. Animations are set using the setTransition function available in both RoutingSwitcher and RoutingStack. It accepts two arguments: the first specifies the animation when transitioning to a higher view (according to the order in the view list), and the second specifies the animation for the reverse direction. For example, to set a transition on the Switcher:

setTransition(
    createSlideSwitcherTransition(0.5, 0, 0),
    createSlideSwitcherTransition(0.5, 0, 1)
);

The function above uses a slide effect, meaning the new view slides in from and out to the edge of the screen. The first argument specifies the duration of the slide in seconds, the second specifies whether the slide is vertical (1) or horizontal (0), and the third specifies the direction of movement — entering or exiting.

The following functions are available for creating Switcher animations:

  • createSlideSwitcherTransition
  • createFadeSwitcherTransition

And the following for Stack animations:

  • createSlideStackTransition
  • createFadeStackTransition
  • createFadeWithScaleStackTransition

Creating your own custom animations is also straightforward. You can look at the definitions of these functions in the WebPlatform library to learn how to create your own animations.

Full Example

The following WebPlatform example demonstrates routing using these two components: