The last word on Sage 9 Bootstrap 4 Navwalkers

Can someone try replacing the contents of [theme-directory]/vendor/mwdelaney/sage-bootstrap4-navwalker/bootstrap4-navwalker.php with this:

<?php
namespace App;
/**
 * Bootstrap 4 "Navbar" component navwalker drop-in functionality for Sage 9
 * Version: 1.0
 * Author: Michael W. Delaney
 *
 */

/**
 * Class Name: wp_bootstrap4_navwalker
 * GitHub URI: https://github.com/twittem/wp-bootstrap-navwalker
 * Description: A custom WordPress nav walker class to implement the Bootstrap 3 navigation style in a custom theme using the WordPress built in menu manager.
 * Version: 2.0.4
 * Author: Edward McIntyre - @twittem
 * License: GPL-2.0+
 * License URI: http://www.gnu.org/licenses/gpl-2.0.txt
 */

if (class_exists('\Walker_Nav_Menu')) {

  class wp_bootstrap4_navwalker extends \Walker_Nav_Menu {
      /**
     * @see Walker::start_lvl()
     * @since 3.0.0
     *
     * @param string $output Passed by reference. Used to append additional content.
     * @param int $depth Depth of page. Used for padding.
     */
    public function start_lvl( &$output, $depth = 0, $args = array() ) {
      $indent = str_repeat( "\t", $depth );
      $output .= "\n$indent<div role=\"menu\" class=\" dropdown-menu\">\n";
    }
    /**
     * Ends the list of after the elements are added.
     *
     * @see Walker::end_lvl()
     *
     * @since 3.0.0
     *
     * @param string $output Passed by reference. Used to append additional content.
     * @param int    $depth  Depth of menu item. Used for padding.
     * @param array  $args   An array of arguments. @see wp_nav_menu()
     */
    public function end_lvl( &$output, $depth = 0, $args = array() ) {
      $indent = str_repeat("\t", $depth);
      $output .= "$indent</div>\n";
    }
    /**
     * Start the element output.
     *
     * @see Walker::start_el()
     *
     * @since 3.0.0
     *
     * @param string $output Passed by reference. Used to append additional content.
     * @param object $item   Menu item data object.
     * @param int    $depth  Depth of menu item. Used for padding.
     * @param array  $args   An array of arguments. @see wp_nav_menu()
     * @param int    $id     Current item ID.
     */
    public function end_el( &$output, $item, $depth = 0, $args = array() ) {
      if($depth === 1){
        if(strcasecmp( $item->attr_title, 'divider' ) == 0 || strcasecmp( $item->title, 'divider') == 0) {
          $output .= '</div>';
        }else if ($depth === 1 && (strcasecmp( $item->attr_title, 'header') == 0 && $depth === 1)) {
          $output .= '</h6>';
        }
      }else{
        $output .= '</li>';
      }
    }
    /**
     * @see Walker::start_el()
     * @since 3.0.0
     *
     * @param string $output Passed by reference. Used to append additional content.
     * @param object $item Menu item data object.
     * @param int $depth Depth of menu item. Used for padding.
     * @param int $current_page Menu item ID.
     * @param object $args
     */
    public function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
      $indent = ( $depth ) ? str_repeat( "\t", $depth ) : '';
      /**
       * Dividers, Headers or Disabled
       * =============================
       * Determine whether the item is a Divider, Header, Disabled or regular
       * menu item. To prevent errors we use the strcasecmp() function to so a
       * comparison that is not case sensitive. The strcasecmp() function returns
       * a 0 if the strings are equal.
       */
      //( strcasecmp($item->attr_title, 'disabled' ) == 0 )

      if($depth === 1 && (strcasecmp( $item->attr_title, 'divider' ) == 0 || strcasecmp( $item->title, 'divider') == 0)) {
        $output .= $indent . '<div class="dropdown-divider">';
      }else if ((strcasecmp( $item->attr_title, 'header') == 0 && $depth === 1) && $depth === 1){
        $output .= $indent . '<h6 class="dropdown-header">' . esc_attr( $item->title );
      }else{
        $class_names = $value = '';
        $classes = empty( $item->classes ) ? array() : (array) $item->classes;

        $atts = array();
        $atts['title']  = ! empty( $item->title )	? $item->title	: '';
        $atts['target'] = ! empty( $item->target )	? $item->target	: '';
        $atts['rel']    = ! empty( $item->xfn )		? $item->xfn	: '';
        $atts['href'] = ! empty( $item->url ) ? $item->url : '';
        $id = apply_filters( 'nav_menu_item_id', 'menu-item-'. $item->ID, $item, $args );

        if ( in_array( 'current-menu-item', $classes ) )
          $classes[] = ' active';
        if($depth === 0){
          $classes[] = 'nav-item';
          $classes[] = 'nav-item-' . $item->ID;
          $atts['class']			= 'nav-link';
          if ( $args->has_children ){
            $classes[] = ' dropdown';
            $atts['href']   		= '#';
            $atts['data-toggle']	= 'dropdown';
            $atts['class']			= 'dropdown-toggle nav-link';
            $atts['role']	= 'button';
            $atts['aria-haspopup']	= 'true';
          }
          $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args ) );
          $class_names = $class_names ? ' class="' . esc_attr( $class_names ) . '"' : '';
          $id = $id ? ' id="' . esc_attr( $id ) . '"' : '';
          $output .= $indent . '<li' . $id . $value . $class_names .'>';
        }else{
          $classes[] = 'dropdown-item';
          $class_names = join( ' ', apply_filters( 'nav_menu_css_class', array_filter( $classes ), $item, $args ) );
          $atts['class'] = $class_names;
          $atts['id'] = $id;
        }

        $atts = apply_filters( 'nav_menu_link_attributes', $atts, $item, $args );
        $attributes = '';
        foreach ( $atts as $attr => $value ) {
          if ( ! empty( $value ) ) {
            $value = ( 'href' === $attr ) ? esc_url( $value ) : esc_attr( $value );
            $attributes .= ' ' . $attr . '="' . $value . '"';
          }
        }
        $item_output = $args->before;
        $item_output .= '<a'. $attributes .'>';

        /*
        * Icons
        * ===========
        * Since the the menu item is NOT a Divider or Header we check the see
        * if there is a value in the attr_title property. If the attr_title
        * property is NOT null we apply it as the class name for the icon
        */
        if ( ! empty( $item->attr_title ) ){
          $item_output .= '<span class="' . esc_attr( $item->attr_title ) . '"></span>&nbsp;';
        }
        $item_output .= $args->link_before . apply_filters( 'the_title', $item->title, $item->ID ) . $args->link_after;
        $item_output .= '</a>';
        $item_output .= $args->after;
        $output .= apply_filters( 'walker_nav_menu_start_el', $item_output, $item, $depth, $args );
      }
    }
    /**
     * Traverse elements to create list from elements.
     *
     * Display one element if the element doesn't have any children otherwise,
     * display the element and its children. Will only traverse up to the max
     * depth and no ignore elements under that depth.
     *
     * This method shouldn't be called directly, use the walk() method instead.
     *
     * @see Walker::start_el()
     * @since 2.5.0
     *
     * @param object $element Data object
     * @param array $children_elements List of elements to continue traversing.
     * @param int $max_depth Max depth to traverse.
     * @param int $depth Depth of current element.
     * @param array $args
     * @param string $output Passed by reference. Used to append additional content.
     * @return null Null on failure with no changes to parameters.
     */
    public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output ) {
      if ( ! $element )
        return;
      $id_field = $this->db_fields['id'];
      // Display this element.
      if ( is_object( $args[0] ) )
        $args[0]->has_children = ! empty( $children_elements[ $element->$id_field ] );
      parent::display_element( $element, $children_elements, $max_depth, $depth, $args, $output );
    }
    /**
     * Menu Fallback
     * =============
     * If this function is assigned to the wp_nav_menu's fallback_cb variable
     * and a manu has not been assigned to the theme location in the WordPress
     * menu manager the function with display nothing to a non-logged in user,
     * and will add a link to the WordPress menu manager if logged in as an admin.
     *
     * @param array $args passed from the wp_nav_menu function.
     *
     */
    public static function fallback( $args ) {
      if ( current_user_can( 'manage_options' ) ) {
        extract( $args );
        $fb_output = null;
        if ( $container ) {
          $fb_output = '<' . $container;
          if ( $container_id )
            $fb_output .= ' id="' . $container_id . '"';
          if ( $container_class )
            $fb_output .= ' class="' . $container_class . '"';
          $fb_output .= '>';
        }
        $fb_output .= '<ul';
        if ( $menu_id )
          $fb_output .= ' id="' . $menu_id . '"';
        if ( $menu_class )
          $fb_output .= ' class="' . $menu_class . '"';
        $fb_output .= '>';
        $fb_output .= '<li><a href="' . admin_url( 'nav-menus.php' ) . '">Add a menu</a></li>';
        $fb_output .= '</ul>';
        if ( $container )
          $fb_output .= '</' . $container . '>';
        echo $fb_output;
      }
    }
  }
}

And see if it fixes the issue and still works?

Hey @MWDelaney that did help a bit - I did need to switch the namespace to:

namespace App\Controllers;
1 Like

Hi @MWDelaney, I get this error with your code that you asked to pasted (above):

Fatal error: Uncaught Error: Class 'App\wp_bootstrap4_navwalker' not found in /srv/www/website.com/current/web/app/themes/website/app/controllers/app.php on line

Before I got that the NavWalker wasn’t found. I used the method with the Controller, tried to add namespace in many ways, always get a error. I use Trellis Bedrock and Sage with Soil plugin.

Thank you

Oh my mistake, seems to be fine,

I was running composer require from site/ (Bedrock), it must be run from theme folder as it was write in the readme, also it require <?php namespace App; ?> in the header.blade.php file

Thanks a lot for this

This fixes is for me when I run composer test manually, but my CI test fails because it pulls the class which does not have that class_exists line. Any chance that could get updated, please?

I’m sorry I don’t understand. It “pulls the class” how? Do you mean that it installs it from Composer?

Yes. I can modify the existing class locally, but I don’t want to commit anything in /vendor. Can you update the class that gets downloaded from Composer?

Absolutely. I just wanted to make sure the fix was good before committing. I’ll get it updated later tonight.

Awesome. Thanks so much!

Hey,
I just pushed a new release with the above fix in it. Should be all set. Let me know if you run into anymore trouble!

Hello There,

Sorry if im being a bit thick here but ive spent ages getting a test site up with trellis and vagrant. everything is working now.

However as sage is a stripped down version i dont have a bootstap nav! so i have followed steps above and indeed its pulled the git repo into the vendor directory. I have included the markup in header-blade.php BUT its still showing an unstyled nav with no responsive nav being shown if i make the browser smaller.

Am i missing something here??? i have ran yarn build a few times. I assumed that the default bootstrap css gets included when you do yarn build


Loved the old sage, go into lib add my ref to new nav include it in header and bobs your uncle. Sage 9 is a much steeper learning curve and i m pulling my hair out again :frowning:

Can any one shed any light please


Can you show us the code you’re using? It’s difficult to troubleshoot without seeing it.

sure. here is what i have in my header-blade.php:

    <header class="banner">
  <div class="container">
    <a class="brand" href="{{ home_url('/') }}">{{ get_bloginfo('name', 'display') }}</a>
    <nav class="nav-primary">
      @if (has_nav_menu('primary_navigation'))
  {!! wp_nav_menu(['theme_location' => 'primary_navigation', 'walker' => new \App\wp_bootstrap4_navwalker(),'container'       => 'div',
	'container_class' => 'collapse navbar-collapse',
	'container_id'    => 'primary-nav', 'menu_class' => 'nav navbar-nav']) !!}
@endif
    </nav>
  </div>
</header>

Ah, I see the confusion.

This is a nav item walker only, it doesn’t produce the rest of the Bootstrap navbar HTML for you; you’ll need to add the necessary containers and classes around the markup produced by this walker.

Compare the output of your template to the example markup in Bootstrap’s documentation and add whichever parts you’re missing:

@MWDelaney thanks for the link you sent. Delay in reply was due to me testing it out just in case i got it wrong


Well i have double checked and i am fairly sure im doing it right!

So I have made sure that the code exists i.e. the markup in the html. I used it via the nav walker in header-blade.php as initially indicated. Here it is:

  <header class="banner">
  <div class="container">
    <a class="brand" href="{{ home_url('/') }}">{{ get_bloginfo('name', 'display') }}</a>
    <nav class="nav-primary">
	<button class="navbar-toggler" 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>
      @if (has_nav_menu('primary_navigation'))
  {!! wp_nav_menu(['theme_location' => 'primary_navigation', 'walker' => new \App\wp_bootstrap4_navwalker(),'container'       => 'div',
	'container_class' => 'collapse navbar-collapse',
	'container_id'    => 'primary-nav', 'menu_class' => 'nav navbar-nav']) !!}
@endif
    </nav>
  </div>
</header>

You can see that i have specified the container id/class and it also includes the toggle button for responsive.

Her is what a screen shot looks like of the normal navbar with the above walker implemented:

and this is what it looks like if a make the browser smaller:

as you can see no navbar is shown. The styling is not present. To confirm the output of the html on that page is:

<html lang="en-US"><head>
  <meta charset="utf-8">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <title>example.com – Just another WordPress site</title>
<meta name="robots" content="noindex,follow">
<link rel="dns-prefetch" href="//fonts.googleapis.com">
<link rel="dns-prefetch" href="//code.jquery.com">
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto:400,700|Ubuntu:400,700">
<link rel="stylesheet" href="/app/themes/Example/dist/styles/main.css">
<link rel="canonical" href="http://example.test/">
</head>
  <body class="home page app-data index-data singular-data page-data page-2-data page-sample-page-data front-page-data">
        <header class="banner">
  <div class="container">
    <a class="brand" href="http://example.test/">example.com</a>
    <nav class="nav-primary">
	<button class="navbar-toggler" 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>
        <ul id="menu-menu-1" class="nav navbar-nav"><li class="menu-item menu-item-type-custom menu-item-object-custom current-menu-item current_page_item menu-item-home  active nav-item nav-item-5"><a title="Home" href="http://exmaple.test/" class="nav-link">Home</a></li><li class="menu-item menu-item-type-post_type menu-item-object-page menu-item-home current-menu-item page_item page-item-2 current_page_item  active nav-item nav-item-6"><a title="Sample Page" href="http://example.test/" class="nav-link">Sample Page</a></li></ul>
    </nav>
  </div>
</header>    <div class="wrap container" role="document">
      <div class="content">
        <main class="main">
                 <div class="page-header">
  <h1>Sample Page</h1>
</div>
    <p>This is an example page. It’s different from a blog post because it will stay in one place and will show up in your site navigation (in most themes). Most people start with an About page that introduces them to potential site visitors. It might say something like this:</p>
<blockquote><p>Hi there! I’m a bike messenger by day, aspiring actor by night, and this is my website. I live in Los Angeles, have a great dog named Jack, and I like piña coladas. (And gettin’ caught in the rain.)</p></blockquote>
<p>
or something like this:</p>
<blockquote><p>The XYZ Doohickey Company was founded in 1971, and has been providing quality doohickeys to the public ever since. Located in Gotham City, XYZ employs over 2,000 people and does all kinds of awesome things for the Gotham community.</p></blockquote>
<p>As a new WordPress user, you should go to <a href="http://example.test/wp/wp-admin/">your dashboard</a> to delete this page and create new pages for your content. Have fun!</p>

          </main>
              </div>
    </div>
        <footer class="content-info">
  <div class="container">
      </div>
</footer>
    <script src="https://code.jquery.com/jquery-1.12.4.min.js"></script>
<script>(window.jQuery && jQuery.noConflict()) || document.write('<script src="/wp/wp-includes/js/jquery/jquery.js"><\/script>')</script>
<script src="/app/themes/Example/dist/scripts/main.js"></script>
  

</body></html>

not sure what i am doing wrong here. I havent added any scss yet so its all the autoload scss that getting called. It looks correct i.e. main.css is populated with what looks like the bootstrap 4.1 code

really stumped. I assumed that it was just a case of getting the nav walker file via composer command and then adding the correct nav html and nav responsive html and the it should look and work like the example given in the bootstrap link you provided. Not the case :frowning:

Aha, im getting somewhere now @MWDelaney didn’t help that i was testing during the night. After some rest and doing another project and then coming back to it i seem to have made some progress!

So i now understand your comment about including the html bootstrap markup. I was expecting the html markup to be injected via the code that added to the navwalker caller:

@if (has_nav_menu('primary_navigation'))
  {!! wp_nav_menu(['theme_location' => 'primary_navigation', 'walker' => new \App\wp_bootstrap4_navwalker(),'container'       => 'div',
	'container_class' => 'collapse navbar-collapse',
	'container_id'    => 'navbarSupportedContent primary-nav',
	'menu_class' => 'nav navbar-nav']) !!}
@endif

I can see now that was were i was going wrong. Can you confirm that me adding those additional lines actually don’t work (i used to do that with other wordpress navs and it works) doesn’t seem to be the case with latest sage version.

I have had to manually add in the html elements surrounding the navwalker call and it all works as expected now.

I also noticed that if i used your bootstrap navwalker then clean menus no longer work. The soil plugin doesn’t do anything to the menu. That’s a bummer. If i revert back to the original version (and as long as i use the manual html elements included) then its a clean menu with no junk. Is that a bug in your navwalker?

1 Like

Soil “cleans up” the menu by using its own Walker. Since you’re using @MWDelaney’s Walker, Soil’s is never used. That’s not a bug. If you feel like his plugin can be improved, I’m sure he’d be happy to review a pull request.

1 Like

ah, thats such a shame. its so nice and clean to have clean html. It would be nice if @MWDelaney could review this and get a clean nav walker working like the soil one. Sorry no idea on how to do a pull request at moment as i dont use github (yea major downside i know I should be using it!).

That’s not a feature I’m planning to add at this point.

No time like the present to learn! Git is an invaluable tool for development, and collaboration is the foundation of open software like Sage. Here’s a good guide to help you learn how pull requests work: How To Create a Pull Request on GitHub | DigitalOcean

2 Likes