SPA NavBars and Highlighting, Livewire v3 Style!

A browser window is open. The page has a navbar containing links to Home, About, and Contact. The page's content contain a section with "SPA NAVBAR with Livewire v3" on the left, with Livewire's alien mascot on the right.
Image by Annie Ruygt

In just a few steps, deploy your Laravel application on Fly.io, youโ€™ll be up and running in minutes!

Livewire’s new wire:navigate directive is brilliant. It delivers the combined forces of SEO-friendly rendering, and the smoothness of an SPA powered user experience.

Today, we’ll create an SPA powered NavBar to provide seamless page navigation across nav links, and along the way, answer a very important question: how do we highlight the active link in an SPA powered nav bar?

Let’s get to it!

Let’s say, we have a Laravel Livewire app. And in it, three full page components:

/* routes/web.php */
# Bind routes to Livewire components to create Full Page goodness
Route::get('/', \App\Livewire\HomePage::class );
Route::get('/about', \App\Livewire\AboutPage::class );
Route::get('/contact', \App\Livewire\ContactPage::class );

All three of these components share the same layout file which contains a Livewire component called “nav-bar” attached on top of the page:

<!-- resources/views/app.blade.php -->
<html>
<head>...</head>
<body>
+   <livewire:nav-bar />
    {{ $slot }}
</body>
</html>

Thanks to this nav-bar, users get a “global” space for navigating among the three pages:

<!-- resources\views\livewire\nav-bar.blade.php -->
<nav id="dir-nav">
  <a href="/">Home</a>
  <a href="about">About</a>
  <a href="contact">Contact</a>
</nav

This nav-bar is well and good, but it still has room for improvement. See, moving between its links still take full-page reloads. Full page reloads disrupt the flow of user interaction— users have to wait for an entire new page to finish loading before new content can be displayed. It’s not the worst experience, but well, we can still definitely improve it.

Enter wire:navigate

Livewire v3 just recently (officially) released! With it comes the revolutionary wire:navigate directive. It allows an “SPA” experience by loading the requested page in the background, and updating parts of the current page to reflect new content.

Simply attach the directive to any link:

  <a href="..." wire:navigate>...</a>

And behold—A seamless navigation experience for our users right at the fingertips of a single directive:

The nav bar is attached to the top of the page. It provides nav-links to all three components.

—we get to decide when to do full page reloads OR background content loading.

—on top of that, Livewire renders initial component content with the page: this means each of our pages’ initial content is available for friendly SEO processing!

“Adaptable” attach on the go, SEO friendly SPA—that’s wire:navigate—Livewire’s SPA on a silver platter ๐Ÿ˜Ž

Fly.io โค๏ธ Laravel

Fly your servers close to your users—and marvel at the speed of close proximity. Deploy globally on Fly in minutes!

Deploy your Laravel app!  

Alright, now we have an SPA powered experience for our nav-bar. This fact leads us to a very important question:

How do we highlight the active link in an SPA powered navbar? When a user clicks on a link and visits a page, we want to highlight that recently clicked link as “active”. Clicking on a nav link underlines the link. To go about this highlighting, we’ll have to make changes in our nav-bar: we highlight the <a> tag that matches the current page’s identifier $page:

<!-- resources/views/livewire/nav-bar.blade.php -->

<!-- Highlight a link if page's identifier matches its value -->
<a href="..." wire:navigate 
    @if($page=='...') class="underline" @endif
>...</a>

Now the question is, how exactly do we get the value that identifies the current $page that’s active? Let’s look at three ways:

1. New Page Title

One value that can identify the active link is the current page’s title. Livewire conveniently gives an easy way to set a full component’s title through its [#Title] attribute.

Simply include a [#Title] attribute in every full-page components’ render function:

/* App\Livewire\HomePage.php */
#[Title('Home Page')]
public function render(){...}

/* App\Livewire\AboutPage.php */
#[Title('About Page')]
public function render(){...}

/* App\Livewire\ContactPage.php */
#[Title('Contact Page')]
public function render(){...}

And we’ll automatically receive a page’s title in the layout file through a $title attribute. We can then pass $title to the nav-bar component, and fill in our $page value:

<!-- resources/views/app.blade.php -->
<html>
<head><title>{{ $title ?? 'Page Title' }}</title></head>
<body>
+   <livewire:nav-bar :page="$title"/>

2. New Page URL

A second way to get the active link is through the page’s URL. From the NavBar component, get the page’s URL through url()->current(). Then parse the route from this URL string, and assign it to $page.

We set this assignment inside the render() method so that the URL is refreshed every time the nav-bar is rendered, which happens every time we navigate to a new page:

/* App\Livewire\NavBar.php */
public function render(){
  // $page comes from parsing the route from the url
  $this->page = $this->getPage( url()->current() );
  return view('livewire.nav-bar');
}

public function getPage( $url ){ 
  // Get page from url
  $url = parse_url($url);
  if( isset($url['path']) )
      return trim( $url['path'] ,'/' );
  else    
      return '/';
}

3. window.location, and Alpine

Is there a way we can update $page and highlight our nav-link from the browser side of things? window.location.pathname can identify the current page’s route from the frontend, we’ll use this to update the value of $page client side.

To update $page, we might try to use vanilla JavaScript. The official docs page advise running JavaScript inside its hook livewire:navigated to run the scripts on page visit.

At the time of writing however, the caller of this hook is called before the new page’s url is pushed into window.history. This is why the route we receive inside a listener for livewire:navigated gives the previous route instead of the new one:

/* This is not going to work for our use case: */
document.addEventListener('livewire:navigated',()=>{
    /* This will log the old route, not the current one */
    console.log('New url:',window.location.pathname);
});

Instead of relying on plain JavaScript which we need to place in the right point of code, we can instead use Livewire v3’s built-in AlpineJS to directly update the value of $page.

See, Alpine provides us a “shortcut” syntax for doing JavaScript things, furthermore, it allows us to easily do our intended action at just the right point of time:

<!-- resources/views/livewire/nav-bar.blade.php -->
<nav
  x-init="$wire.set('page',window.location.pathname)"
>

x-init is a way to hook into the initialization phase of an Alpine element( which Livewire components are! ). Since Alpine is initialized after transition to the new page, calling window.location during x-init gives us the updated, new route!

What did we Accomplish?

Along our nav-bar journey, we learned about the use of the magically passed [#Title] attribute. We also learned that updates done in the render method are reflected even on wire:navigate-ed pages. Finally, we learned a much quicker way to do updates client-side after “wire:navigated” by relying on Alpine’s directives and update syntax.

And here we are, six minutes long: we now have an SPA powered( SEO friendly ), “active link” highlighting, NavBar—Livewire v3, wire:navigate style!