tutorial // Apr 08, 2022

How to Build Dynamic Breadcrumbs From a URL Path

How to take the value of url.path in a Joystick component and convert it into a dynamic breadcrumb UI.

How to Build Dynamic Breadcrumbs From a URL Path

Getting Started

For this tutorial, we're going to use CheatCode's full-stack JavaScript framework, Joystick. Joystick brings together a front-end UI framework with a Node.js back-end for building apps.

To begin, we'll want to install Joystick via NPM. Make sure you're using Node.js 16+ before installing to ensure compatibility (give this tutorial a read first if you need to learn how to install Node.js or run multiple versions on your computer):

Terminal

npm i -g @joystick.js/cli

This will install Joystick globally on your computer. Once installed, next, let's create a fresh project:

Terminal

joystick create app

After a few seconds, you will see a message logged out to cd into your new project and run joystick start:

Terminal

cd app && joystick start

After this, your app should be running and we're ready to get started.

Adding nested routes

In order to demonstrate a breadcrumb UI, we're going to need a set of nested routes we can work with. To keep things simple, let's start by opening up the index.server.js file at the root of the project we just created and add a few routes:

Terminal

import node from "@joystick.js/node";
import api from "./api";

node.app({
  api,
  routes: {
    "/": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "/nested": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "/nested/path": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "/nested/path/to": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "/nested/path/to/:thing": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "*": (req, res) => {
      res.render("ui/pages/error/index.js", {
        layout: "ui/layouts/app/index.js",
        props: {
          statusCode: 404,
        },
      });
    },
  },
});

In the app we just created, the index.server.js file is the main "starting point" for our application's server. Inside, we call to the node.app() function from the @joystick.js/node package to start up our server, passing it the API we want it to load and the routes we want available in our app.

What we want to focus on here are the routes, and specifically, all of the routes we've added starting with /nested. Here, we're creating a pseudo-nested URL pattern that we can use to test out our breadcrumb generation code.

For each /nested route, we do the exact same thing: render the index page component (we've just copied and pasted the contents of the / route's callback function for each /nested route). The difference between each is the path itself. Notice that for each route we've added we go an additional level deeper:

  • /nested
  • /nested/path
  • /nested/path/to
  • /nested/path/to/:thing

The end goal being that with this structure, now we have a nested set of routes that we can easily represent as breadcrumbs.

Next, we want to modify the /ui/pages/index/index.js file we're rendering here to build out our breadcrumbs UI.

Adding a Dynamic Breadcrumb Generator

When we created our app with joystick create app earlier, we were also given an example page component at /ui/pages/index/index.js. Now, let's open that up and replace the existing contents with a skeleton component that we can use to build our breadcrumb UI.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  render: () => {
    return `
      <div>
      </div>
    `;
  },
});

export default Index;

With that in place, the first thing we want to do is wire up the actual creation of our breadcrumbs and then focus on rendering them to the page. To do this, we're going to rely on the methods property of a Joystick component.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  methods: {
    getBreadcrumbs: (component) => {
      // We'll build our breadcrumbs array here...
    },
  },
  render: () => {
    return `
      <div>
      </div>
    `;
  },
});

export default Index;

In a Joystick component, the methods property contains an object of miscellaneous methods (another name for functions defined on an object in JavaScript) related to our component. What we want to do now is define a function getBreadcrumbs() which will perform the heavy lifting to convert our URL path into an array of objects which describe each breadcrumb we want to render.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  methods: {
    getBreadcrumbs: (component) => {
      const pathParts = component?.url?.path?.split('/').filter((part) => part?.trim() !== '');
      return pathParts?.map((part, partIndex) => {
        const previousParts = pathParts.slice(0, partIndex);
        return {
          label: part,
          href: previousParts?.length > 0 ? `/${previousParts?.join('/')}/${part}` : `/${part}`,
        };
      }) || [];
    },
  },
  render: () => {
    return `
      <div>
      </div>
    `;
  },
});

export default Index;

We've dumped the whole code here for the sake of clarity, so let's step through it. First, our goal is to be able to call this function getBreadcrumbs and have it return an array of objects to us where each object describes one of our breadcrumbs.

To get there, we need to get the current path our user is looking at. We have two options for this in our app, both equally easy. First, natively in a web browser, we can always get the current path via the window.location.pathname global value (location.pathname for short). Because we're working within a Joystick app, here, we're going to use the url.path value (which is identical to location.pathname) available on our component instance.

You'll notice that when defining a method on a Joystick component, if no arguments are passed to that function when we call it, Joystick will automatically assign the last possible argument to the component instance. For example, if we called methods.getBreadcrumbs('something'), the function signature above would change to getBreadcrumbs: (someValue, component) => { ... }.

Inside of our function, from the component instance, we obtain the current path with component.url.path as a string. In order to get to an array, first, we need to split our path into parts. To do that, we need to use the .split() function available on all strings in JavaScript. To .split(), we can pass a character that we want to split at. Because we're dealing with a path like /nested/path/to/123 we want to split at the / forward slash character. The end result being an array like this:

['', 'nested', 'path', 'to', '123']

This gets us most of the way, but notice that there's an empty string here. That's because when we did a .split('/'), the first slash was counted but because there's nothing preceding it, we just get an empty value.

To handle this, notice that the full line here is:

const pathParts = component?.url?.path?.split('/').filter((part) => part?.trim() !== '');

What this says is "take the url.path value as a string, split it into an array using the / forward slash as a separator, then, filter out any part in the resulting array if trimming all of its whitespace results in an empty string."

The end result? We get a clean array to work with like ['nested', 'path', 'to', '123'] in our pathParts variable.

With this array, we have what we need to build out our breadcrumbs. Next, we need to map over this array. For each iteration, we want to do the work necessary to build our breadcrumb object. Each breadcrumb will have two properties: label which is the rendered name users will see in the breadcrumb chain and href which is the URL the breadcrumb will be linked to.

For the label, our work is easy: we'll just reuse the name of the path part we're currently looping over. href is a bit trickier. Here, we need to make sure that each successive breadcrumb is aware of what came before it so when we click it, we're referencing the proper URL.

To do it, just inside of our map we've added a new variable previousParts which takes our pathParts array and calls the .slice() method on it, saying "give me everything from the first element in the array up to the current part's index." In other words, this will return us a new array with everything that came before the current part.

Down on the object we're returning from our .map() we use a ternary operator to set the value of href depending on the length of the previousParts array. If the length is 0, we're at the start of our path and so there are no previous parts to render. If this is the case, we just return the href as /${part}.

If there are previousParts, we use the .join() method on that array to convert the array back into a string, concatenating the resulting string together with the name of the current part. The end result? For each iteration, we get something like this:

{ label: 'nested', href: '/nested' }
{ label: 'path', href: '/nested/path' }
{ label: 'to', href: '/nested/path/to' }
{ label: '123', href: '/nested/path/to/123' }

That's it for getting our breadcrumbs. Now, let's get them rendered to the page.

/ui/pages/index/index.js

import ui from '@joystick.js/ui';

const Index = ui.component({
  methods: {
    getBreadcrumbs: (component) => { ... },
  },
  css: `
    .breadcrumbs {
      display: flex;
    }

    .breadcrumbs li {
      list-style: none;
    }

    .breadcrumbs li:before {
      content: "/";
      display: inline-flex;
      margin-right: 10px;
    }

    .breadcrumbs li:not(:last-child) {
      margin-right: 10px;
    }
  `,
  render: ({ when, each, methods }) => {
    const breadcrumbs = methods.getBreadcrumbs();

    return `
      <div>
        ${when(breadcrumbs?.length > 0, `
          <ul class="breadcrumbs">
            ${each(breadcrumbs, (breadcrumb) => {
              return `
                <li><a href="${breadcrumb?.href}">${breadcrumb?.label}</a></li>
              `;
            })}
          </ul>
        `)}
      </div>
    `;
  },
});

export default Index;

The part we want to pay attention to is down in the render() function. Here, we've swapped the rendering of our empty <div></div> with our breadcrumbs.

To our render() function, we anticipate that Joystick will pass us an object representing the current component instance. Instead of writing render: (component) => {} here, we use JavaScript destructuring to "pluck off" the specific variables we want from that object. So, instead of writing component.when, component.each, etc., we can just write when, each, and methods (pointing to the same properties using shorthand).

Using the methods property from this, just inside of render(), we make a call to methods.getBreadcrumbs() storing the result (our array of breadcrumb objects) in a variable breadcrumbs. With this array, next, we use the when() render function in Joystick which allows us to conditionally render some HTML when the first value we pass to the function is true.

Here, we want to return a string of HTML which renders a <ul></ul> (representing our list of breadcrumbs). Inside of that <ul></ul> in order to render each breadcrumb, we use the each() render function to say given the array passed as the first argument, for each item in that array, call the function passed as the second argument.

To that function, we expect to receive each item in the array we passed to each, or, one of our breadcrumb objects. Inside of the function, Joystick expects us to return a string of HTML for each iteration of the breadcrumbs array. Because we're inside of a <ul></ul> tag, for each breadcrumb we want to render an <li></li> tag with an <a></a> tag inside of it. From there, we just use plain ol' JavaScript interpolation to pass the value of our href and label from the current breadcrumb object.

That's it! Up top, we've added a css property with some simple styling to clean things up. If we pop open a browser and shift between our nested routes, we should see our breadcrumbs update dynamically.

Wrapping Up

In this tutorial, we learned how to set up some nested routes in a Joystick app. Then, we learned how to create a Joystick component which took the current path, and converted it to an array of breadcrumb objects that we could use for rendering in our UI. Finally, we learned how to conditionally render our breadcrumbs in our UI, using Joystick's when and each render functions.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode