Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ It has the ability to be a single section or multiple for easy customization.

![Demo](mobile_menu.PNG)

# What's new

* Built for .NET 9 Razor class libraries so it can be referenced from the latest Blazor Server, WebAssembly, or Auto render mode apps.
* Event callbacks and lifecycle updates have been hardened to support interactive hand-off when using Blazor's Auto render mode.

# Getting Setup

You can install the package via the NuGet package manager just search for TailBlazor.NavBar. You can also install via powershell using the following command.
Expand Down Expand Up @@ -39,7 +44,7 @@ Simply open up a component and add your content.
<NavBarContent>
<LeftNavBarItem LogoInformation="new NavLogoInformation()" Items="new List<NavItem>()"></LeftNavBarItem>
</NavBarContent>
<MobileNavBarContent Items="new List<NavItem()>"></NavBarItem>
<MobileNavBarContent Items="new List<NavItem>()"></MobileNavBarContent>
</NavBar>
```

Expand All @@ -49,11 +54,11 @@ If you would like to add custom content, instead of using the default list of Na
<NavBarContent>
<LeftNavBarItem>
...
</LeftItem>
</LeftNavBarItem>
<CenterNavBarItem>
...
</CenterItem>
<RightNavBarItems>
</CenterNavBarItem>
<RightNavBarItem>
...
</RightNavBarItem>
</NavBarContent>
Expand All @@ -79,10 +84,10 @@ This allows you to open and close the mobile menu section, you will need a refer

```
<NavBar>
<NavBarContent MobileMenuButtonOnClick="() => MobileNavBarContentRef.HamburgerClicked()">
<NavBarContent MobileMenuButtonOnClick="@(() => MobileNavBarContentRef.HamburgerClicked())">
<LeftNavBarItem LogoInformation="new NavLogoInformation()" Items="new List<NavItem>()"></LeftNavBarItem>
</NavBarContent>
<MobileNavBarContent @ref="MobileNavBarContentRef" Items="new List<NavItem()>"></NavBarItem>
<MobileNavBarContent @ref="MobileNavBarContentRef" Items="new List<NavItem>()"></MobileNavBarContent>
</NavBar>

@code {
Expand All @@ -94,6 +99,10 @@ This allows you to open and close the mobile menu section, you will need a refer

We set the default `background-color` for the navbar as `bg-blue-500`, from [TailWindCss(Background Color)](https://tailwindcss.com/docs/background-color). If you would like to change your default background color, just add a class of `.color-primary` to your css file, or just add the `Class` parameter to the `<NavBar>` component.

### Handling nav item clicks in interactive modes

`NavItem` exposes an `OnClick` `EventCallback` so you can wire up actions that work in Blazor Server, WebAssembly, or Auto render modes without manually constructing anchor tags. You can continue to use the legacy `Action` property and it will be wrapped automatically.

### Changing primary colors when using the `Items` parameter in the components.
If you would like to add a different active class for the nav items, then just add `.color-primary-active` to your css file, or just input your class into the `ActiveItemClass` in the `NavItem` model. Lastly, if you would like to change the primary hover color for the nav items, then add `colorPrimaryHover` into your `tailwind.config.js` file. The default background-color for an active class or on hover is `bg-blue-700`.

Expand Down Expand Up @@ -124,7 +133,7 @@ Parameter | Type | Default Value | Description
`MobileMenuButtonPosition` | `MobileMenuButtonPosition [Enum]` | `MobileMenuButtonPosition.Left` | The mobile menu button position, either left or right.
`MobileMenuButtonContent` | `RenderFragment` | `N/A` | The mobile menu button content, in case you would like to change the buttons that are used for the hamburger menu.
`ChildContent` | `RenderFragment` | `N/A` | The children content for the navbar. You can use `<LeftNavBarItem>`, `<CenterNavBarItem>` or `<RightNavBarContent>`. Otherwise you can input your own custom code here.
`MobileMenuButtonOnClick` | `Action` | `N/A` | An action to open the mobile menu section. <br/></br/> **Note:** You will need to have ` <MobileNavBarContent>` component for this to work.
`MobileMenuButtonOnClick` | `EventCallback` | `N/A` | Invoked when the default mobile menu button toggles. <br/></br/> **Note:** You will need to have `<MobileNavBarContent>` component for this to work.

### ```<LeftNavBarItem>```
---
Expand Down
18 changes: 14 additions & 4 deletions src/TailBlazor.NavBar/CenterNavBarItem.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<div class="@(string.IsNullOrEmpty(Class) ? "hidden flex-1 items-center justify-center sm:flex sm:justify-center" : Class)">
@implements IDisposable

<div class="@(string.IsNullOrEmpty(Class) ? "hidden flex-1 items-center justify-center sm:flex sm:justify-center" : Class)">
@ChildContent
</div>

Expand Down Expand Up @@ -34,7 +36,7 @@

#region injection services
[Inject]
NavigationManager _navManager { get; set; }
NavigationManager _navManager { get; set; } = default!;
#endregion

#region variables
Expand Down Expand Up @@ -103,7 +105,7 @@
ChildContent += item;
}

StateHasChanged();
_ = InvokeAsync(StateHasChanged);
}

/// <summary>
Expand All @@ -115,7 +117,15 @@
void OnLocationChanged(object sender, LocationChangedEventArgs e)
{
relativePath = _navManager.ToBaseRelativePath(_navManager.Uri);
StateHasChanged();
_ = InvokeAsync(StateHasChanged);
}

void IDisposable.Dispose()
{
if (_navManager is not null)
{
_navManager.LocationChanged -= OnLocationChanged;
}
}
#endregion
}
13 changes: 11 additions & 2 deletions src/TailBlazor.NavBar/Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ namespace TailBlazor.NavBar
{
public class Helpers
{
static readonly EventCallbackFactory CallbackFactory = new();

/// <summary>
/// Build the nav items, which are a tags
/// Based on the NavItem return it will add the appropriate attributes/content
Expand Down Expand Up @@ -41,8 +43,15 @@ public static RenderFragment BuildNavItemFragment(NavItem navItem, string relati
if (!string.IsNullOrEmpty(navItem.Href))
i.AddAttribute(3, "href", navItem.Href);

if (navItem.Action != null)
i.AddAttribute(4, "onclick", navItem.Action);
if (navItem.OnClick.HasDelegate)
{
i.AddAttribute(4, "onclick", navItem.OnClick);
}
else if (navItem.Action != null)
{
var receiver = navItem.Action.Target ?? navItem;
i.AddAttribute(4, "onclick", CallbackFactory.Create(receiver, navItem.Action));
}

if (navItem.PreventDefaultClick)
i.AddEventPreventDefaultAttribute(5, "onclick", true);
Expand Down
18 changes: 14 additions & 4 deletions src/TailBlazor.NavBar/LeftNavBarItem.razor
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<div class="@(string.IsNullOrEmpty(Class) ? "flex-1 flex items-center justify-center sm:justify-start" : Class)">
@implements IDisposable

<div class="@(string.IsNullOrEmpty(Class) ? "flex-1 flex items-center justify-center sm:justify-start" : Class)">
@ChildContent
</div>

Expand Down Expand Up @@ -34,7 +36,7 @@

#region injection services
[Inject]
NavigationManager _navManager { get; set; }
NavigationManager _navManager { get; set; } = default!;
#endregion

#region variables
Expand Down Expand Up @@ -102,7 +104,7 @@
ChildContent += item;
}

StateHasChanged();
_ = InvokeAsync(StateHasChanged);
}

/// <summary>
Expand All @@ -114,7 +116,15 @@
void OnLocationChanged(object sender, LocationChangedEventArgs e)
{
relativePath = _navManager.ToBaseRelativePath(_navManager.Uri);
StateHasChanged();
_ = InvokeAsync(StateHasChanged);
}

void IDisposable.Dispose()
{
if (_navManager is not null)
{
_navManager.LocationChanged -= OnLocationChanged;
}
}
#endregion
}
35 changes: 20 additions & 15 deletions src/TailBlazor.NavBar/MobileNavBarContent.razor
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
/// <summary>
/// Container class
/// </summary>
[Parameter]
public string Class { get; set; }

/// <summary>
Expand All @@ -32,10 +33,10 @@

#region injection services
[Inject]
NavigationManager _navManager { get; set; }
NavigationManager _navManager { get; set; } = default!;

[Inject]
IResizeListener _listener { get; set; }
IResizeListener _listener { get; set; } = default!;
#endregion

#region variables
Expand All @@ -45,8 +46,6 @@
// Boolean Variables
bool mobileMenuOpen = false;

// Model Variables
BrowserWindowSize browser = new BrowserWindowSize();
#endregion

#region intializers
Expand All @@ -57,10 +56,10 @@
// hook to OnLocationChanged() so we can change the active class for the element
_navManager.LocationChanged += OnLocationChanged;

if(Items != null) {
if (Items != null) {
foreach (var navItem in Items)
{
ChildContent = Helpers.BuildNavItemFragment(navItem, relativePath);
ChildContent += Helpers.BuildNavItemFragment(navItem, relativePath);
}
}
}
Expand All @@ -69,7 +68,7 @@
{
if(firstRender)
{
_listener.OnResized += WindowResized;
_listener?.OnResized += WindowResized;
}
}

Expand All @@ -93,7 +92,7 @@
public void HamburgerClicked()
{
mobileMenuOpen = !mobileMenuOpen;
StateHasChanged();
_ = InvokeAsync(StateHasChanged);
}
#endregion

Expand All @@ -107,29 +106,35 @@
void OnLocationChanged(object sender, LocationChangedEventArgs e)
{
relativePath = _navManager.ToBaseRelativePath(_navManager.Uri);
StateHasChanged();
_ = InvokeAsync(StateHasChanged);
}

void IDisposable.Dispose()
{
// Always use IDisposable in your component to unsubscribe from the event.
// Be a good citizen and leave things how you found them.
// Be a good citizen and leave things how you found them.
// This way event handlers aren't called when nobody is listening.
_listener.OnResized -= WindowResized;
if (_listener is not null)
{
_listener.OnResized -= WindowResized;
}

if (_navManager is not null)
{
_navManager.LocationChanged -= OnLocationChanged;
}
}

async void WindowResized(object _, BrowserWindowSize window)
{
browser = window;

// Check a media query to see if it was matched. We can do this at any time, but it's best to check on each resize
if(!await _listener.MatchMedia(Breakpoints.SmallDown))
if(_listener is not null && !await _listener.MatchMedia(Breakpoints.SmallDown))
{
mobileMenuOpen = false;
}

// We're outside of the component's lifecycle, be sure to let it know it has to re-render.
StateHasChanged();
await InvokeAsync(StateHasChanged);
}
#endregion
}
35 changes: 22 additions & 13 deletions src/TailBlazor.NavBar/NavBarContent.razor
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
}
else
{
<button type="button" class="@(string.IsNullOrEmpty(MobileMenuButtonClass) ? "inline-flex items-center justify-center p-2 rounded-md text-white hover:text-white hover:bg-blue-700 hover:bg-colorPrimaryHover focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white" : MobileMenuButtonClass)" aria-controls="mobile-menu" aria-expanded="false" @onclick="@(() => { mobileMenuButtonToggle = !mobileMenuButtonToggle; MobileMenuButtonOnClick?.Invoke(); })">
<button type="button" class="@(string.IsNullOrEmpty(MobileMenuButtonClass) ? "inline-flex items-center justify-center p-2 rounded-md text-white hover:text-white hover:bg-blue-700 hover:bg-colorPrimaryHover focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white" : MobileMenuButtonClass)" aria-controls="mobile-menu" aria-expanded="false" @onclick="HandleMobileMenuToggleAsync">
<span class="sr-only">Open main menu</span>
<svg class="@(mobileMenuButtonToggle ? "hidden" : "block") h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
Expand Down Expand Up @@ -70,46 +70,55 @@
public RenderFragment ChildContent { get; set; }

/// <summary>
/// An action to open the mobile menu section.
/// An event callback that is invoked when the default mobile menu button is toggled.
/// Note: You will need to have MobileNavBarContent component for this to work.
/// </summary>
[Parameter]
public Action MobileMenuButtonOnClick { get; set; }
public EventCallback MobileMenuButtonOnClick { get; set; }

[Inject]
IResizeListener _listener { get; set; }
IResizeListener _listener { get; set; } = default!;

bool mobileMenuButtonToggle = false;

BrowserWindowSize browser = new BrowserWindowSize();

protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
_listener.OnResized += WindowResized;
_listener?.OnResized += WindowResized;
}
}

void IDisposable.Dispose()
{
// Always use IDisposable in your component to unsubscribe from the event.
// Be a good citizen and leave things how you found them.
// Be a good citizen and leave things how you found them.
// This way event handlers aren't called when nobody is listening.
_listener.OnResized -= WindowResized;
if (_listener is not null)
{
_listener.OnResized -= WindowResized;
}
}

async void WindowResized(object _, BrowserWindowSize window)
async Task HandleMobileMenuToggleAsync()
{
browser = window;
mobileMenuButtonToggle = !mobileMenuButtonToggle;

if (MobileMenuButtonOnClick.HasDelegate)
{
await MobileMenuButtonOnClick.InvokeAsync();
}
}

async void WindowResized(object _, BrowserWindowSize window)
{
// Check a media query to see if it was matched. We can do this at any time, but it's best to check on each resize
if (!await _listener.MatchMedia(Breakpoints.SmallDown))
if (_listener is not null && !await _listener.MatchMedia(Breakpoints.SmallDown))
{
mobileMenuButtonToggle = false;
}

// We're outside of the component's lifecycle, be sure to let it know it has to re-render.
StateHasChanged();
await InvokeAsync(StateHasChanged);
}
}
4 changes: 3 additions & 1 deletion src/TailBlazor.NavBar/NavBarModel.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using System;

namespace TailBlazor.NavBar
Expand Down Expand Up @@ -28,6 +29,7 @@ public class NavItem
public bool PreventDefaultClick { get; set; }
public NavLinkTarget Target { get; set; }
public Action Action { get; set; }
public EventCallback<MouseEventArgs> OnClick { get; set; }
public bool HasIcon { get; set; }
public RenderFragment Icon { get; set; }

Expand Down
Loading