27

I'm learning Bootstrap and can't get the selected item into an "active" state. The active state remains on the default item. The newly selected/clicked item changes to active briefly, but reverts back. I've read all the posts and still can't get this code to work. I'm using MVC5 and JQuery 2.1.

EDIT: If I change the li's hrefs to href="#", then the active class gets applied properly. What's happening when a new view gets loaded? I think Sebastian's response is close, but gets messy with Areas.

Markup

<div class="navbar-wrapper">
    <div class="container">
        <div class="navbar navbar-inverse navbar-fixed-top">

            <div class="navbar-header">
                <a class="navbar-toggle" data-toggle="collapse" data-target=".nav-collapse">
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </a>
                <a class="navbar-brand" href="#">Test</a>
            </div>
            <div class="btn-group pull-right">
                <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
                    <i class="icon-user"></i>Login
          <span class="caret"></span>
                </a>
                <ul class="dropdown-menu">
                    <li><a href="#">Profile</a></li>
                    <li class="divider"></li>
                    <li><a href="#">Sign Out</a></li>
                </ul>
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li class="active"><a href="~/Home">Home</a></li>
                    <li><a href="~/Home/About">About</a></li>
                    <li><a href="~/Student">Students Sample</a></li>
                    <li class="dropdown">
                        <a href="#" class="dropdown-toggle" data-toggle="dropdown">Dropdown <b class="caret"></b></a>
                        <ul class="dropdown-menu">
                            <li><a href="~/Admin/Home/Index"">Admin</a></li>
                            <li><a href="#">Another action</a></li>
                            <li><a href="#">Something else here</a></li>
                            <li class="divider"></li>
                            <li><a href="#">Separated link</a></li>
                            <li><a href="#">One more separated link</a></li>
                        </ul>
                    </li>
                </ul>
            </div>

        </div>
    </div>
    <!-- /container -->
</div>
<!-- /navbar wrapper -->

Script

<script type="text/javascript">

    $(function () {
        $('.navbar-nav li').click(function () {
            $(this).addClass('active').siblings().removeClass('active');
        });
    });

</script>

EDIT: Here's what I ended up doing with the help of the posted answers and some research.

public static string MakeActive(this UrlHelper urlHelper,string action, string controller, string area = "")
        {
            string result = "active";
            string requestContextRoute;
            string passedInRoute;

            // Get the route values from the request           
            var sb = new StringBuilder().Append(urlHelper.RequestContext.RouteData.DataTokens["area"]);
            sb.Append("/");
            sb.Append(urlHelper.RequestContext.RouteData.Values["controller"].ToString());
            sb.Append("/");
            sb.Append(urlHelper.RequestContext.RouteData.Values["action"].ToString());
            requestContextRoute = sb.ToString();

            if (string.IsNullOrWhiteSpace(area))
            {
                passedInRoute = "/" + controller + "/" + action;
            }
            else
            {
                passedInRoute = area + "/" + controller + "/" + action;
            }

            //  Are the 2 routes the same?
            if (!requestContextRoute.Equals(passedInRoute, StringComparison.OrdinalIgnoreCase))
            {
                result = null;
            }

            return result;
        }
Big Daddy
  • 5,160
  • 5
  • 46
  • 76
  • Did you find a definitive solution for this BigDaddy? I have always wanted to call someone that... Esp. Hans Passant! – MoonKnight Apr 17 '14 at 16:32
  • @Killercam...Take a look at my edit. It shows what I came up with. It's an expansion of the accepted answer. – Big Daddy Apr 17 '14 at 17:17

10 Answers10

28

You have to check in your controller or view which menu item is active based on the current url:

I have an extension method similar to this:

public static string MakeActiveClass(this UrlHelper urlHelper, string controller)
{
    string result = "active";

    string controllerName = urlHelper.RequestContext.RouteData.Values["controller"].ToString();

    if (!controllerName.Equals(controller, StringComparison.OrdinalIgnoreCase))
    {
        result = null;
    }

    return result;
}

You can use it in your view like this:

<!-- Make the list item active when the current controller is equal to "blog" -->
<li class="@Url.MakeActive("blog")">
   <a href="@Url.Action("index", "blog")">....</a>
</li>
SebastianStehle
  • 2,409
  • 1
  • 22
  • 32
24

The JavaScript isn't working because the page is getting reloaded after it runs. So it correctly sets the active item and then the page loads because the browser is following the link. Personally, I would remove the JavaScript you have because it serves no purpose. To do this client side (instead of the server side code you have), you need JavaScript to set the active item when the new page loads. Something like:

$(document).ready(function() {
    $('ul.nav.navbar-nav').find('a[href="' + location.pathname + '"]')
        .closest('li').addClass('active');
});

I recommend adding an id or other class to your navbar so you can be sure you have selected the correct one.

Jeff Walker Code Ranger
  • 4,634
  • 1
  • 43
  • 62
9

Simplest thing to do is send a ViewBag parameter from your controllers like following;

public ActionResult About()
    {            
        ViewBag.Current = "About";
        return View();
    }


    public ActionResult Contact()
    {            
        ViewBag.Current = "Contact";
        return View();
    }

In the cshtml page do the following;

           <ul class="nav navbar-nav">                    
                <li class="@(ViewBag.Current == "About" ? "active" : "")">@Html.ActionLink("About", "About", "Home")</li>
                <li class="@(ViewBag.Current == "Contact" ? "active" : "")">@Html.ActionLink("Contact", "Contact", "Home")</li>
            </ul>

Courtesy to @Damith in here

Community
  • 1
  • 1
Mahib
  • 3,977
  • 5
  • 53
  • 62
6

Simply you can do this in any view

<ul class="nav navbar-nav">
    @Html.NavigationLink("Link1", "Index", "Home")
    @Html.NavigationLink("Link2", "Index", "Home")
    @Html.NavigationLink("Link3", "Index", "Home")
    @Html.NavigationLink("Links with Parameter", "myAction", "MyController", new{ id=999}, new { @class= " icon-next" })
</ul>

After you add this method to a new class or existing HtmlExtensions class

public static class HtmlExtensions
{
    public static MvcHtmlString NavigationLink(this HtmlHelper html, string linkText, string action, string controller, object routeValues=null, object css=null)
    {
        TagBuilder aTag = new TagBuilder("a");
        TagBuilder liTag = new TagBuilder("li");
        var htmlAttributes = HtmlHelper.AnonymousObjectToHtmlAttributes(css);
        string url = (routeValues == null)?
            (new UrlHelper(html.ViewContext.RequestContext)).Action(action, controller)
            :(new UrlHelper(html.ViewContext.RequestContext)).Action(action, controller, routeValues);

        aTag.MergeAttribute("href", url);
        aTag.InnerHtml = linkText;
        aTag.MergeAttributes(htmlAttributes);

        if (action.ToLower() == html.ViewContext.RouteData.Values["action"].ToString().ToLower() && controller.ToLower() == html.ViewContext.RouteData.Values["controller"].ToString().ToLower())
            liTag.MergeAttribute("class","active");

        liTag.InnerHtml = aTag.ToString(TagRenderMode.Normal);
        return new MvcHtmlString(liTag.ToString(TagRenderMode.Normal));
    }
}
Moji
  • 5,720
  • 2
  • 38
  • 39
1

I believe you have the selection backward. You're adding the class, then removing it from the siblings, and I think doing the remove second is causing the issue. Can you try reversing this to be:

<script type="text/javascript">

    $(function () {
        $('.navbar-nav li').click(function () {
            $(this).siblings().removeClass('active');
            $(this).addClass('active');
        });
    });

</script>
tlbignerd
  • 1,104
  • 9
  • 21
0

Your javascript function should work fine... The issue is that your links route to a controller and reload the entire page. In order to avoid this behavior you could render your body content as a partial view, that way the navbar elements do not reload. You shouldn't have to write a function to handle dom events - that is what javascript is for.

To see what I mean, change your code:

<li><a href="~/Home/About">About</a></li>
<li><a href="~/Student">Students Sample</a></li>

to:

 <li><a href="">About</a></li>
 <li><a href="">Students Sample</a></li>
jmyns
  • 127
  • 10
0

Just add this JQuery coded :

<script>
        $(document).ready(function () {
            $('body').find('a[href="' + location.pathname + '"]')
                .addClass('active');
        });
</script>
Ali Mahmoodi
  • 858
  • 10
  • 14
0

With this simplified version of the code from here, you can use a tag helper to mark the anchor 'active' if the controller & action match (or just the controller if no action is supplied).

The nav:

  <li class="nav-item px-2">
       <a class="nav-link" asp-active-route asp-controller="Home" asp-action="Index">Home</a>
  </li>
  <li class="nav-item px-2">
       <a class="nav-link" asp-active-route asp-controller="Car" asp-action="Add">Add Car</a>
   </li>

The tag helper class

[HtmlTargetElement(Attributes = "asp-active-route")]
public class ActiveRouteTagHelper : TagHelper
{
    [HtmlAttributeName("asp-controller")] public string Controller { get; set; }

    [HtmlAttributeName("asp-action")] public string Action { get; set; }

    [HtmlAttributeNotBound] [ViewContext] public ViewContext ViewContext { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        if (IsActive())
        {
            output.AddClass("active", HtmlEncoder.Default);
        }

        output.Attributes.RemoveAll("asp-active-route");

        base.Process(context, output);
    }

    private bool IsActive()
    {
        var currentController = ViewContext.RouteData.Values["Controller"].ToString();
        var currentAction = ViewContext.RouteData.Values["Action"].ToString();
        var active = false;

        if (!string.IsNullOrWhiteSpace(Controller) && !string.IsNullOrWhiteSpace(currentController))
        {
            active = Controller.Equals(currentController, StringComparison.CurrentCultureIgnoreCase);

            if (active && !string.IsNullOrWhiteSpace(Action) && !string.IsNullOrWhiteSpace(currentAction))
            {
                active = Action.Equals(currentAction, StringComparison.CurrentCultureIgnoreCase);
            }
        }

        return active;
    }
}

Don't forget to add the tag helper to your _ViewImports.cshtml

 @addTagHelper *, My.Awesome.Web
  • Future improvement would be to also check areas and possibly wrap the entire nav in a control/helper so you only have to compare the routes once and flag the item
James White
  • 2,062
  • 2
  • 24
  • 36
0

You can use Bootstrap's nav-pills to change the color of active menu item.

Bootstrap's nav-pills to change the color of active menu item

Shangwu
  • 1,440
  • 12
  • 12
0

Here is a simple solution that works - store class name in TempData:

In each action of the controller add one line:

// add "active" class to nav-item TempData["Home"] = "active";

In Layout view nav item:

<a class="nav-link @TempData["Home"]">Home</a>

Thus only one nav-item will get class "active"