Skip to content

Instantly share code, notes, and snippets.

@smutek
Last active July 30, 2022 06:50
Show Gist options
  • Star 52 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save smutek/cd95c8bc4f1db70ee1eda2740bfbf6fd to your computer and use it in GitHub Desktop.
Save smutek/cd95c8bc4f1db70ee1eda2740bfbf6fd to your computer and use it in GitHub Desktop.
Bootstrap 4 Walker for Sage 9

Credit

This is a frankensteind version of the current Soil nav walker, by the Roots team, and Michael Remoero's Sagextras walker. All credit goes to those good folks. :)

Use

  • Replace the contents of header.blade.php with the attached header.
  • Copy the walker.php file to the /app directory.
  • Add walker.php to the Sage required files array in resources/functions.php - eg. on a stock Sage install the entry would look like:
/**
 * Sage required files
 *
 * The mapped array determines the code library included in your theme.
 * Add or remove files to the array as needed. Supports child theme overrides.
 */
array_map(function ($file) use ($sage_error) {
    $file = "../app/{$file}.php";
    if (!locate_template($file, true, true)) {
        $sage_error(sprintf(__('Error locating <code>%s</code> for inclusion.', 'sage'), $file), 'File not found');
    }
}, ['helpers', 'setup', 'filters', 'admin', 'walker']);

Basic output looks like:

- ul id="menu-main" class="navbar-nav mr-auto"
  -  li class="nav-item menu-item menu-page-1"
    - a class="nav-link" 

Also supports dropdowns.

<nav class="navbar navbar-toggleable-md navbar-light bg-faded">
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse"
data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<a class="navbar-brand" href="{{ home_url('/') }}">{{ get_bloginfo('name', 'display') }}</a>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
@if (has_nav_menu('primary_navigation'))
{!! wp_nav_menu(['theme_location' => 'primary_navigation', 'menu_class' => 'navbar-nav mr-auto']) !!}
@endif
</div>
</nav>
<?php
namespace App;
/**
* Class NavWalker
*
* Bootstrap 4 walker with cleaner markup for wp_nav_menu()
* For use with Sage >= 8.5
*
* Based on Soil NavWalker
* @url https://github.com/roots/soil
*
*
* Walker_Nav_Menu (WordPress default) example output:
* <li id="menu-item-8" class="menu-item menu-item-type-post_type menu-item-object-page menu-item-8"><a href="/">Home</a></li>
*
* NavWalker example output:
* <li class="nav-item menu-item menu-home"><a class="nav-link" href="/">Home</a></li>
*
* @package Roots\Sage\Nav
*/
class NavWalker extends \Walker_Nav_Menu {
/**
* @var bool
*/
private $cpt; // Boolean, is current post a custom post type
/**
* @var false|string
*/
private $archive; // Stores the archive page for current URL
/**
* NavWalker constructor.
*/
public function __construct() {
add_filter( 'nav_menu_css_class', array( $this, 'cssClasses' ), 10, 2 );
add_filter( 'nav_menu_item_id', '__return_null' );
$cpt = get_post_type();
$this->cpt = in_array( $cpt, get_post_types( array( '_builtin' => false ) ) );
$this->archive = get_post_type_archive_link( $cpt );
}
/**
* Check item classes for current or active
*
* @param $classes
*
* @return int
*/
public function checkCurrent( $classes ) {
return preg_match( '/(current[-_])|active/', $classes );
}
// @codingStandardsIgnoreStart
/**
* Add dropdown menu class to dropdown UL
*
* @param string $output
* @param int $depth
* @param array $args
*/
function start_lvl( &$output, $depth = 0, $args = [] ) {
$output .= "\n<ul class=\"dropdown-menu\" aria-labelledby=\"navbarDropdownMenuLink\">\n";
}
/**
* Add required Bootstrap 4 classes to anchor links.
*
* @param string $output
* @param \WP_Post $item
* @param int $depth
* @param array $args
* @param int $id
*/
function start_el( &$output, $item, $depth = 0, $args = [], $id = 0 ) {
$item_html = '';
parent::start_el( $item_html, $item, $depth, $args );
if ( $item->is_subitem ) {
$item_html = str_replace( '<a', '<a class="nav-link dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"', $item_html );
$item_html = str_replace( '</a>', ' <b class="caret"></b></a>', $item_html );
} else {
$item_html = str_replace( '<a', '<a class="nav-link"', $item_html );
}
$item_html = apply_filters( 'wp_nav_menu_item', $item_html );
$output .= $item_html;
}
/**
* Add active classes to active items & sub items
*
* @param object $element
* @param array $children_elements
* @param int $max_depth
* @param int $depth
* @param array $args
* @param string $output
*/
public function display_element( $element, &$children_elements, $max_depth, $depth = 0, $args, &$output ) {
$element->is_subitem = ( ( ! empty( $children_elements[ $element->ID ] ) && ( ( $depth + 1 ) < $max_depth || ( $max_depth === 0 ) ) ) );
if ( $element->is_subitem ) {
foreach ( $children_elements[ $element->ID ] as $child ) {
if ( $child->current_item_parent || $this->url_compare( $this->archive, $child->url ) ) {
$element->classes[] = 'active';
}
}
}
$element->is_active = ( ! empty( $element->url ) && strpos( $this->archive, $element->url ) );
if ( $element->is_active ) {
$element->classes[] = 'active';
}
parent::display_element( $element, $children_elements, $max_depth, $depth, $args, $output );
}
// @codingStandardsIgnoreEnd
/**
* Clean up css classes
*
* @param $classes
* @param $item
*
* @return array
*/
public function cssClasses( $classes, $item ) {
$slug = sanitize_title( $item->title );
// Fix core `active` behavior for custom post types
if ( $this->cpt ) {
$classes = str_replace( 'current_page_parent', '', $classes );
if ( $this->url_compare( $this->archive, $item->url ) ) {
$classes[] = 'active';
}
}
// Remove most core classes
$classes = preg_replace( '/(current(-menu-|[-_]page[-_])(item|parent|ancestor))/', 'active', $classes );
$classes = preg_replace( '/^((menu|page)[-_\w+]+)+/', '', $classes );
// Add `menu-item` class & re-add core `menu-item` class
$classes[] = 'nav-item menu-item';
// Add `dropdown` class & re-add core `menu-item-has-children` class on parent elements
if ( $item->is_subitem ) {
$classes[] = 'dropdown menu-item-has-children';
}
// Add `menu-<slug>` class
$classes[] = 'menu-' . $slug;
$classes = array_unique( $classes );
$classes = array_map( 'trim', $classes );
return array_filter( $classes );
}
/**
* Make a URL relative
*
* Utility function, from soil
* @url https://github.com/roots/soil
*
* @param $input
*
* @return string
*/
public function root_relative_url( $input ) {
if ( is_feed() ) {
return $input;
}
$url = parse_url( $input );
if ( ! isset( $url['host'] ) || ! isset( $url['path'] ) ) {
return $input;
}
$site_url = parse_url( network_home_url() ); // falls back to home_url
if ( ! isset( $url['scheme'] ) ) {
$url['scheme'] = $site_url['scheme'];
}
$hosts_match = $site_url['host'] === $url['host'];
$schemes_match = $site_url['scheme'] === $url['scheme'];
$ports_exist = isset( $site_url['port'] ) && isset( $url['port'] );
$ports_match = ( $ports_exist ) ? $site_url['port'] === $url['port'] : true;
if ( $hosts_match && $schemes_match && $ports_match ) {
return wp_make_link_relative( $input );
}
return $input;
}
/**
* Compare URL against relative URL
*
* Utility function, from Soil
* @url https://github.com/roots/soil
*
* @param $url
* @param $rel
*
* @return bool
*/
public function url_compare( $url, $rel ) {
$url = trailingslashit( $url );
$rel = trailingslashit( $rel );
return ( ( strcasecmp( $url, $rel ) === 0 ) || $this->root_relative_url( $url ) == $rel );
}
}
/**
* Clean up wp_nav_menu_args
*
* Remove the container
* Remove the id="" on nav menu items
*
* @param string $args
*
* @return array
*/
function nav_menu_args( $args = '' ) {
$nav_menu_args = [];
$nav_menu_args['container'] = false;
if ( is_array($args) && !$args['items_wrap'] ) {
$nav_menu_args['items_wrap'] = '<ul class="%2$s">%3$s</ul>';
}
if ( ! $args['walker'] ) {
$nav_menu_args['walker'] = new NavWalker();
}
return array_merge( $args, $nav_menu_args );
}
add_filter( 'wp_nav_menu_args', __NAMESPACE__ . '\\nav_menu_args' );
add_filter( 'nav_menu_item_id', '__return_null' );
@odtcreative
Copy link

The dropdown sub item links need to have the class 'dropdown-item' instead of 'nav-link' to work correctly with the bootstrap 4 CSS, would you be able to suggest how I would go about doing this or alternatively update this walker?

Any help will be appreciated, thank you.

@RiaanWest
Copy link

RiaanWest commented Jul 12, 2017

Thank you, exactly what I needed :)

@scallemang
Copy link

Having the same issue that @odtcreative is. Any help greatly appreciated. Will probably solve with jQuery for now, but would love to know a 'best practice' solution.

@mark-kusters
Copy link

@odtcreative & @scallemang did you got it working eventually?

I recently frankensteind my own version (minor changes because of ACF integration) and also ran into the dropdown not working.
This was, after a couple of hours of debugging, caused by the required dropdown JS not being loaded, a 2 minute fix.

Added bootstrap/js/dist/dropdown to the assets/scripts/vendor.js and voila, it worked like a charm.
Hope this helps!

@davidtmiller
Copy link

Here's the issue that it looks like a few of us are looking to solve. The dropdown is working fine, but the markup is incorrect. Here's the current markup that gets produced when you have a dropdown menu:

<ul class="dropdown-menu show" aria-labelledby="navbarDropdownMenuLink">
    <li class="nav-item menu-item menu-sample-page">
        <a class="nav-link" href="#">Sample Page</a>
    </li>
</ul>

Here is the correct Bootstrap markup that we're trying to achieve within a dropdown.

<div class="dropdown-menu show" aria-labelledby="navbarDropdownMenuLink">
    <a class="dropdown-item" href="#">Sample Page</a>
</div>

I'm wondering what needs to change in the walker.php file in order to make a few things happen.

  • The dropdown menu needs to be changed from a ul to a div
  • Since the dropdown menu is no longer a ul, the li's need to be removed
  • The link a needs the dropdown-item class added to it.

Also - if anyone needs it, my new header.blade.php file looks like this with Bootstrap 4 (non-beta version).

<nav class="navbar navbar-expand-lg navbar-light bg-light">

  <div class="container">

    <a class="navbar-brand" href="{{ home_url('/') }}">{{ get_bloginfo('name', 'display') }}</a>

    <button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse"
            data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false"
            aria-label="Toggle navigation">
      <span class="navbar-toggler-icon"></span>
    </button>

    <div class="collapse navbar-collapse" id="navbarSupportedContent">
      @if (has_nav_menu('primary_navigation'))
        {!! wp_nav_menu(['theme_location' => 'primary_navigation', 'menu_class' => 'navbar-nav ml-auto']) !!}
      @endif
    </div>

  </div>

</nav>

Note: the container is optional.

@CantinaDigital
Copy link

@timosnysder
Copy link

// NOTE: [line 83] change data-toggle="dropdown" to data-hover="dropdown" for making the parent link clickable.

@smutek
Copy link
Author

smutek commented Sep 16, 2018

Oh, man, I had no idea any of these comments were here, sorry about that!

@kevintruby
Copy link

Thank you very much for sharing this!

@kevintruby
Copy link

I had some trouble getting the solution @ginkoQ provided, data-hover instead of data-toggle to work. It allowed the parent nav to behave as a link, but I could no longer trigger the menu. I'm sure there's a simple solution that I'm missing, but in the meantime, I found an alternative in case anyone's interested.

For anyone that wants the top-level nav to function as both a link and a dropdown toggle (i.e. the link part takes you to a given page, and the carot/chevron still controls the menu's appearance) but having issues with the data-hover approach, I've modified the start_el() function, and overridden the parent end_el() method like so:

// @codingStandardsIgnoreStart
/**
 * Close dropdown menu UL with a </div> for the .btn-group introduced in start_el()
 *
 * @param string $output
 * @param int $depth
 * @param array $args
 */
function end_lvl( &$output, $depth = 0, $args = [] ) {
	$output .= "\n</ul></div>\n";
}
/**
 * Add required Bootstrap 4 classes to anchor links.
 *
 * @param string $output
 * @param \WP_Post $item
 * @param int $depth
 * @param array $args
 * @param int $id
 */
function start_el( &$output, $item, $depth = 0, $args = [], $id = 0 ) {
    $item_html = '';
    parent::start_el( $item_html, $item, $depth, $args );
    if ( $item->is_subitem ) {
        $link_btn = '<button type="button" class="btn btn-link">';
        $dropdown_toggle = '<button type="button" class="btn btn-link dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"><b class="caret"></b></button>';
        $item_html = str_replace( '<a', '<div class="btn-group">'.$link_btn.'<a class="nav-link"', $item_html );
        $item_html = str_replace( '</a>', ' </a></button>'.$dropdown_toggle, $item_html );
    } else {
        $item_html = str_replace( '<a', '<a class="nav-link"', $item_html );
    }
    $item_html = apply_filters( 'wp_nav_menu_item', $item_html );
    $output .= $item_html;
}

@chrisvasey
Copy link

Thanks so much for this!
The one friction I always have starting a new project with sage is swapping out the walker. This include just got me up and running in seconds with the bootstrap nav

@smutek
Copy link
Author

smutek commented Jun 29, 2020

Hey, glad to hear it's still helpful! 👍

@makiborabon
Copy link

works like a charm. thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment