Add Client Side Routing in ASP.NET Core
As an ASP.NET Core developer you are familiar with server side routing. If
you ever worked with JavaScript frameworks such as Angular you might be familiar
with their client side routing capabilities. You are probably aware that many
frameworks and libraries offer a client side "router" that enables client side
routing capabilities for your JavaScript code. For real-world professional web
apps it makes sense to pick some robust and feature rich client side routing
mechanism. But for simple purposes and for learning how client side routing
works, it would be good to write some code that implements such a capabilities.
To that end this article illustrates how a simple client side router can be
built using JavaScript.
Take a look at the following HTML page rendered in the browser.
The page consists of three buttons - Hello World, Hello Galaxy, and Hello
Universe. Initially when the page is loaded, a Welcome message is displayed to
the user. Upon clicking on the buttons respective message is displayed.
Additionally, the URL shown in the address bar changes to indicate the button
being clicked. These are client side routes.
The figure also shows client side URL being bookmarked by the end user. If
you hit the bookmarked URL in a new browser session, it will throw an error
because the server doesn't have any idea about the client side routes. You will
also learn to fix this error using ASP.NET Core code.
Let's get going!
To begin, create a new ASP.NET Core MVC web application using Visual Studio.
Right click on the wwwroot folder and add a new HTML page named Index.html
Open the index.html file and add a <script> reference to jQuery library in
the head section.
<script src="/lib/jquery/dist/jquery.js"></script>
We will use jQuery to handle the click events of those three buttons. If you
want you can skip jQuery altogether and use plain JavaScript to accomplish that
task.
Then add a new JavaScript file named SimpleRouter.js in the wwwroot > js
folder. This file will hold our custom routing code and is shown below:
Add a <script> reference to SimpleRouter.js like this:
<script src="/js/SimpleRouter.js"></script>
Now we will define client side routes required by our application. These
routers are defined by an array of route objects and are added to the
SimpleRouter.js as shown below:
const routes = [
{
path: '/',
template: '<h1>Welcome!</h1>'
},
{
path: '/spa/world',
template: '<h1>Hello World!</h1>'
},
{
path: '/spa/galaxy',
template: '<h1>Hello Galaxy!</h1>',
},
{
path: '/spa/universe',
template: '<h1>Hello Universe!</h1>',
},
];
As you can see, the routes array contains four route definitions. Each route
definition consists of a path that holds a client side route under
consideration. For example, /spa/world, /spa/galaxy, and /spa/universe are the
client side routes required by our application. The first path / indicates the
default route.
Each route object also holds an HTML template that will be rendered in the
browser when the client side route is accessed. In this case templates simply
output a message inside an <h1></h1> element.
Next, add a JavaScript class named SimpleRouter as shown below:
class SimpleRouter {
}
The JavaScript class keyword allows you to define classes. I won't go into
too much details of JS classes in this article. You can read more about it
here.
Then write the following code inside the SimpleRouter class.
routes;
constructor(routes) {
this.routes = routes;
this.handleRoute(window.location.pathname);
}
The code declares a field named routes. The routes field is assigned a value
in the constructor of the class. Then the code grabs the current window location
and passes it to handleRoute() method. Initially the pathname will be / but if
you are clicking on a bookmarked URL it would be something like /spa/world (for
example).
Ok. Now add the handleRoute() method inside the SimpleRouter class as shown
below:
handleRoute(path) {
const route = this.findRoute(path);
window.history.pushState({}, '', path);
const templateContainer =
document.getElementById("container");
templateContainer.innerHTML = route.template;
}
The above code calls the findRoute() method to determine which of the route
objects from the routes array match the current route. You will write the
findRoute() method shortly.
Then
comes the important part. The code calls the pushState() method of the history
object to programmatically add a history entry. The
pushState() method is provided by HTML5 and most of the modern browsers
support it.
The pushState() method takes three parameters. The first parameter represents
a state object and we pass an empty object here because we don't intend to store
any particular state information with this entry. The second parameter is the
title of the new entry being added. This string title can be used to identity
the entry. The third parameter is the client side URL and we pass the route path value
in it. After the call to pushState() is made the browser's address bar reflects
the new client side URL
Recollect that a route object contains two properties - path and template.
The path property is used by the findRoute() method (discussed shortly). The
template property is used here. The template property holds the HTML markup to
be rendered in the browser when a particular route is navigated to. This
template is rendered inside a <div> element with ID of container. We will see
this <div> markup in detail later in this article.
The getElementById() method grabs the template container <div> element. Then
we set its innerHTML property to the HTML template markup. This way browser's
address bar shows a client side URL and the page also shows the associated
templated.
The findRoute() method mentioned above is shown below. Add it to the
SimpleRouter class.
findRoute(path) {
const testingFunc = function (routeItem) {
return (routeItem.path === path ? true : false);
};
const route = this.routes.find(testingFunc);
return route;
}
The findRoute() method basically finds a route object that matches a
particular route path. It does so using the
find() method of the routes array. The find() method expects a testing
function that is used to find a particular array element matching certain
criteria.
Therefore, the code declares a testingFunc function. The parameter to testing
function is an individual routes object. Inside, the code checks if the supplied
route path matches with a route item's path. If they match that route object is returned from the findRoute() method.
This completes SimpleRouter.js file.
Now add the following HTML markup inside the <body> of index.html file.
<button id="button1">Hello World!</button>
<button id="button2">Hello Galaxy!</button>
<button id="button3">Hello Universe!</button>
<div id="container"></div>
The above markup consists of three buttons and a <div> element. The three
buttons navigate to a specific route (such as /spa/world, /spa/galaxy, and
/spa/universe). The <div> element acts as the container for rendering the route
template. To distinguish this <div> from other <div> elements we set its ID
element to container. Recollect that this ID is used inside the handleRoute()
method discussed earlier.
To handle click event of the three buttons you can write the following jQuery
<script> block in the <head> section.
$(document).ready(function () {
var router = new SimpleRouter(routes);
$("#button1").click(function () {
router.handleRoute('/spa/world');
});
$("#button2").click(function () {
router.handleRoute('/spa/galaxy');
});
$("#button3").click(function () {
router.handleRoute('/spa/universe');
});
});
As you can see, ready() handler creates a SimpleRouter object. The click
event handlers of the three buttons call the handleRoute() method and pass the
desired route path to it.
This completes the application. If you run this application at this stage it
will try to hit /Home/Index action. But we don't want that to happen. We want
that our Index.html page be loaded. To accomplish this, open the Startup class
and add this line to the Configure() method.
public void Configure(IApplicationBuilder app,
IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseDefaultFiles();
app.UseStaticFiles();
...
...
}
The UseDefaultFiles() ensures that default documents from the wwwroot folder
such as Index.html are loaded if present.
If you run the application you will see the outcome as shown below:
Try clicking on various button and check whether address bar URL changes and
also observe the template displayed in the browser.
So far so good.
Now try to do the following:
- Go in the browser's address bar, manually type a valid client side URL
(such as /spa/world) and hit enter.
- When a client side URL is displayed in the browser bookmark the URL.
Then click on the bookmarked URL.
You will find that any of the above actions don't result in expected outcome.
Why does this happen? That's because any of the above actions treat the URL
to be a server side resource. And on the server there is no resource that
corresponds to /spa/world (or any other client side URL).
To fix this error, you need to trap such client side URLs using ASP.NET
Core's routing mechanism and map them to a valid controller action. So, add the
following code to the Configure() method:
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/
{action=Index}/{id?}");
endpoints.MapControllerRoute(
name: "infoPage",
pattern: "spa/{*infoPage}",
defaults: new
{
controller = "Spa",
action = "ShowIndexPage"
});
});
The above code sets a "catch-all" routes and hands them over to the
ShowIndexPage() action of SpaController. You will add this controller and action
next.
So, add a new MVC controller class named SpaController and aso add
ShowIndexPage() action in it as shown below:
public class SpaController : Controller
{
public IActionResult ShowIndexPage
([FromServices]IWebHostEnvironment env)
{
return PhysicalFile
(env.WebRootPath + "/index.html", "text/html");
}
}
Observe t he ShowIndexPage() action carefully. It receives
IWebHostEnvironment object through DI. Then it calls PhysicalFile() method to
return a PhysicalFileResult object. The PhysicalFile() method accepts two
parameters - physical path of a file to send as a response and file's MIME
content type. In this case we want to return Index.html to the browser.
Now run the application again and try to invoke the bookmarked URL (or
manually type it). This time control will go to ShowIndexPage() action and then
Index.html is returned. Recollect that SimpleRouter's constructor implements
logic to handle these initial URLs.
In this article we developed a very simple yet working client side router.
You will find many professional and feature rich client side routers and
plug-ins that you can use in your applications. Hopefully this example has given
a backgrounder so that you get an idea about the internal working of such client
side routers.
That's it for now! Keep coding!!