I'm building my first server side blazor app. I'm using bootstrap css for styling. I'm not using bootstrap scripts. I have a page with fixed nav bar at top. I need to imitate bootstrap scroll spy. To do this with C# I need to monitor window.onscroll
to manipulate the scroll position and apply required css classes to nav-item
Here is my HTML:
_Host.cshtml:
@page "/"
@namespace Web.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
Layout = null;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Learn Blazor</title>
<base href="~/" />
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/animate/animate.css">
<link rel="stylesheet" href="~/css/font-awesome/css/all.min.css" />
<link rel="stylesheet" href="~/css/style.css">
</head>
<body data-spy="scroll" data-target=".site-nav" data-offset="55">
<app>
<component type="typeof(App)" render-mode="ServerPrerendered" />
</app>
<script src="_framework/blazor.server.js"></script>
<script src="~/js//script.js"></script>
</body>
</html>
Index.razor:
@page "/"
<HomeSection></HomeSection>
<ColumnSection></ColumnSection>
<MediaSection></MediaSection>
Home Component:
<header id="page-hero" class="site-header">
<TopNavMenu></TopNavMenu>
<section class="layout-hero d-flex align-items-center text-light text-center">
<div class="container">
<div class="row justify-content-center">
<div class="col-11 col-sm-10 col-md-8 animated fadeInUp">
<h3 class="page-section-title">Learn Blazor</h3>
<p class="page-section-text">
My First Blazor App
</p>
</div>
</div>
</div>
</section>
</header>
TopNavMenu Component:
@inherits TopNavMenuBase
<nav class="site-nav family-sans text-uppercase navbar navbar-expand-md navbar-dark fixed-top" @ref="headerNav">
<div class="container-fluid">
<NavLink class="navbar-brand" href="#page-hero" @onclick="NavigateToElementAsync">
<i class="fas fa-microphone-alt"></i> The Prodcast
</NavLink>
<button type="button" class="navbar-toggler @NavToggleCssClass" @onclick="ToggleNavMenu" aria-controls="#myTogglerNav"
aria-label="Toggle Navigation" aria-expanded="@($"{!collapseNavMenu}".ToLowerInvariant())">
<span class="navbar-toggler-icon"></span>
</button>
<section class="navbar-collapse collapse @NavMenuCssClass" id="myTogglerNav">
<div class="navbar-nav ml-auto">
<NavLink class="nav-item nav-link" href="#page-hero" @onclick="NavigateToElementAsync">home</NavLink>
<NavLink class="nav-item nav-link" href="#page-multicolumn" @onclick="NavigateToElementAsync">columns</NavLink>
<NavLink class="nav-item nav-link" href="#page-media" @onclick="NavigateToElementAsync">media</NavLink>
</div>
</section>
</div>
</nav>
Here is my ComponentBase:
public class TopNavMenuBase : ComponentBase
{
protected ElementReference headerNav;
protected bool collapseNavMenu = true;
protected string NavToggleCssClass => collapseNavMenu ? "collapsed" : null;
protected string NavMenuCssClass => !collapseNavMenu ? "show" : null;
[Inject]
public IJSRuntime JSRuntime { get; set; }
[Inject]
public NavigationManager NavigationManager { get; set; }
protected void ToggleNavMenu() => collapseNavMenu = !collapseNavMenu;
protected override async Task OnAfterRenderAsync(bool firstRender) => await NavigateToElementAsync();
protected async Task NavigateToElementAsync()
{
var fragment = new Uri(NavigationManager.Uri).Fragment;
var elementId = !string.IsNullOrEmpty(fragment) && fragment.StartsWith("#") ? fragment.Substring(1) : fragment;
await JSRuntime.InvokeAsync<bool>("scrollToElementId", elementId, headerNav);
}
}
And finally my script:
Edit 1: Updated complete working window.onscroll
to make scroll spy using pure javascript
function scrollToElementId(target, headerNav) {
var topoffset = 55;
var element = document.getElementById(target);
if (element) {
const bodyRect = document.body.getBoundingClientRect().top;
const elementRect = element.getBoundingClientRect().top;
const elementPosition = elementRect - bodyRect;
const offsetPosition = elementPosition - topoffset;
window.scrollTo({
top: offsetPosition,
behavior: 'smooth'
});
headerNav.classList.toggle('inbody', target !== 'page-hero');
return true;
}
document.querySelector('a.nav-link[href="#page-hero"]').classList.toggle('active', target === '');
return false;
}
var sections = {};
document.querySelectorAll(".page-section,.site-header").forEach(section => sections[section.id] = section.offsetTop);
window.onscroll = function () {
document.querySelector('header nav').classList.toggle('inbody', document.documentElement.scrollTop > 380);
document.querySelector('#page-media .layout-animation').style['visibility'] = 'hidden';
var scrollPosition = document.documentElement.scrollTop || document.body.scrollTop;
Object.keys(sections).forEach(key => {
if (sections[key] <= scrollPosition) {
document.querySelectorAll('a.nav-item').forEach(a => a.classList.remove('active'));
document.querySelector('a[href="#' + key + '"]').classList.add('active');
if (key === 'page-media') {
document.querySelector('#page-media .layout-animation').classList.add('animated', 'fadeInRight');
}
}
});
};
The above code setup works. But I feel like capturing window.onscroll
using javascript is not the ideal solution. With server side Blazor, I would like to do this with C#. But how to capture this window.onscroll
with @onscroll
? Where do I need to hook this @onscroll
. The body
tag is in _Host.cshtml
and TopNavMenu
component is nested inside Home
component which is a rendered from index.razor
file.
Can anyone assist me on how to make this work properly with Blazor server side? Please correct me if I'm wrong.
I finally made this to work using IntersectionObserver
in javascript.
I replaced window.onscroll
function as it is nasty in performance with highlightMenu()
function
function highlightMenu() {
const sections = document.querySelectorAll('.page-section,.site-header');
const config = {
rootMargin: '-55px 0px -85%'
};
let observer = new IntersectionObserver(function
(entries, self) {
entries.forEach(entry => {
if (entry.isIntersecting) {
intersectionHandler(entry);
}
});
}, config);
sections.forEach(section => observer.observe(section));
}
function intersectionHandler(entry) {
const id = entry.target.id;
document.querySelector('header nav').classList.toggle('inbody', id !== 'page-hero');
if (id === 'page-media') {
document.querySelector('#page-media .layout-animation').classList.add('animated', 'fadeInRight');
}
const currentlyActive = document.querySelector('nav a.nav-item.active');
const shouldBeActive = document.querySelector('nav a.nav-item[href="#' + id + '"]');
if (currentlyActive) {
currentlyActive.classList.remove('active');
}
if (shouldBeActive) {
shouldBeActive.classList.add('active');
}
}
and then I called this function inside OnAfterRenderAsync
from Index.Razor
,
@code{
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JSRuntime.InvokeVoidAsync("highlightMenu");
}
}
}
it worked like a bootstrap scroll spy. Hope this helps someone.