aspnetcore icon indicating copy to clipboard operation
aspnetcore copied to clipboard

Hash routing to named element

Open MovGP0 opened this issue 6 years ago • 40 comments

Describe the bug

I cannot use an html anchor to navigate to a specific html element of a page.

To Reproduce

Steps to reproduce the behavior:

Given there is a Blazor page that looks like the following:

@layout MainLayout
@page "/foo"

<nav>
    <!-- One version I've tried -->
    <a href="#bar">Bar</a>

    <!-- Another version I've tried -->
    <NavLink href="#bar">Bar</NavLink>

    @* ... *@
</nav>

@* ... *@

<section>
    <h2 id="bar">Bar</h2>
     @* ... *@
</section>

When I click the link to Bar, i get redirected to the route http://localhost:5000/foo#bar, but will be at the top of the page. The fragment of the route is not used for selection of a specific HTML element.

Expected behavior

The browser should scroll down to the proper element, as specified by the Element Selector.

In cases where the fragment of the URL is used for other purposes, ie. for OAuth, it should be possible to overwrite the default Blazor router behavior. The behavior should be set on a per-page and per-app level.

Workaround

The proposed workaround does not work, since the browser crashes and is only for illustration.

The current workaround is to write explicit code that interprets the URL. In the example above, we could use a little bit of JavaScript and then call that from our Blazor code:

<!-- in index.html -->
<script>
        window.scrollToElementId = (elementId) => {
            console.info('scrolling to element', elementId);
            var element = document.getElementById(elementId);
            if(!element)
            {
                console.warn('element was not found', elementId);
                return false;
            }
            element.scrollIntoView();
            return true;
        }
</script>
@inject Microsoft.AspNetCore.Components.Services.IUriHelper UriHelper

@functions {
    protected override void OnInit()
    {
        NavigateToElement();
        UriHelper.OnLocationChanged += OnLocationChanges;
    }

    private void OnLocationChanges(object sender, string location) => NavigateToElement();

    private void NavigateToElement()
    {
        var url = UriHelper.GetAbsoluteUri();
        var fragment = new Uri(url).Fragment;

        if(string.IsNullOrEmpty(fragment))
        {
            return;
        }

        var elementId = fragment.StartsWith("#") ? fragment.Substring(1) : fragment;

        if(string.IsNullOrEmpty(elementId))
        {
            return;
        }
    
        ScrollToElementId(elementId);
    }

    private static bool ScrollToElementId(string elementId)
    {
        return JSRuntime.Current.InvokeAsync<bool>("scrollToElementId", elementId).GetAwaiter().GetResult();
    }
}

Additional context

blazor 0.8.0-preview-19104-04

.NET Core SDK (reflecting any global.json):
 Version:   3.0.100-preview-010184
 Commit:    c57bde4593

Runtime Environment:
 OS Name:     Windows
 OS Version:  10.0.17763
 OS Platform: Windows
 RID:         win10-x64
 Base Path:   C:\Program Files\dotnet\sdk\3.0.100-preview-010184\

Host (useful for support):
  Version: 3.0.0-preview-27324-5
  Commit:  63a01b08e5

.NET Core SDKs installed:
  2.1.600-preview-009426 [C:\Program Files\dotnet\sdk]
  3.0.100-preview-010184 [C:\Program Files\dotnet\sdk]

.NET Core runtimes installed:
  Microsoft.AspNetCore.All 2.1.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.All]
  Microsoft.AspNetCore.App 2.1.6 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.AspNetCore.App 3.0.0-preview-19075-0444 [C:\Program Files\dotnet\shared\Microsoft.AspNetCore.App]
  Microsoft.NETCore.App 2.1.6 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.NETCore.App 3.0.0-preview-27324-5 [C:\Program Files\dotnet\shared\Microsoft.NETCore.App]
  Microsoft.WindowsDesktop.App 3.0.0-preview-27325-3 [C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App]

MovGP0 avatar Mar 10 '19 22:03 MovGP0

see also #6175

MovGP0 avatar Mar 10 '19 22:03 MovGP0

This also happens when using the basic functionality of a tabbed panel. For example: paste into a blazor page

<div class="tab-content">
    <div id="home" class="tab-pane fade in active">
        <h3>HOME</h3>
        <p>Some content.</p>
    </div>
    <div id="menu1" class="tab-pane fade">
        <h3>Menu 1</h3>
        <p>Some content in menu 1.</p>
    </div>
</div>

Clicking any of the panels will try to navigate you to a page instead of the named tab element.

Psimatrix avatar May 29 '19 01:05 Psimatrix

I want to note that this is still an issue with Preview 8 (only testet in Server Side Blazor). Consider the following example

@page "/some/page"
<a href="#">#blank</a>
<a href="#example">#exmaple</a>
<h1 id="example">Example</h1>

Clicking either of the two links takes the browser to the url /# or /#example, which is clearly not the expected behaviour. The expected behaviour is to stay on the same page, e.g. /some/page# or /some/page#example, but have the relevant "hash" section appended/replaced in the url.

egil avatar Aug 16 '19 10:08 egil

Here is a temporary workaround that seems to work for internal clicks, but not deep linking

<a href="#Bar" onclick="event.stopPropagation();">Bar</a>

SQL-MisterMagoo avatar Aug 30 '19 10:08 SQL-MisterMagoo

Here is a temporary workaround that seems to work for internal clicks, but not deep linking

<a href="#Bar" onclick="event.stopPropagation();">Bar</a>

@SQL-MisterMagoo the problem with that workaround is that it also stops any other @onclick=... attached events from firing (they are listened to at the document level).

I do not know what the implications are, but it looks like a fix for this bug could be adding a check around this line in NavigationManager.ts#L47, that checks if the anchor's href starts with a #, and if it does, don't trigger the navigation logic.

egil avatar Aug 30 '19 10:08 egil

@egil Yes, it does, but you can easily use @onmouseup or another event if you need Blazor code to fire as well as the internal browser scroll.

See this example https://blazorfiddle.com/s/jtufkw80

this is a workaround - of course the real fix is needed.

SQL-MisterMagoo avatar Aug 30 '19 10:08 SQL-MisterMagoo

I haven't tested this but I believe it should work: https://community.devexpress.com/blogs/aspnet/archive/2019/08/28/blazor-components-free-anchor-navigation-tool.aspx

Andrzej-W avatar Aug 31 '19 00:08 Andrzej-W

This is still a problem in the release version

TheGrandUser avatar Oct 27 '19 16:10 TheGrandUser

I was just reading the source and realised if you simply include target="_top" in your anchor, it will avoid the navigation interception and just work - not found a workaround for deep linking yet though.

This line of code in NavigationManager returns directly out of the click handler before preventDefault can be called - if there is a target and it doesn't equal "_self".

https://github.com/aspnet/AspNetCore/blob/32a2cc594363672ccfe7644a649f77a8bfc9c4a8/src/Components/Web.JS/src/Services/NavigationManager.ts#L59

Example:

<a id="bottom" href="#top" target="_top">Jump to top</a>

SQL-MisterMagoo avatar Nov 28 '19 00:11 SQL-MisterMagoo

I just tried that in my project and it's still redirecting to the root url, not scrolling to the element.

TheGrandUser avatar Nov 30 '19 23:11 TheGrandUser

I just tried that in my project and it's still redirecting to the root url, not scrolling to the element.

Can you share the link?

SQL-MisterMagoo avatar Nov 30 '19 23:11 SQL-MisterMagoo

It's <a href="#k28" target="_top">#28</a>

And the element it refers to on that page is

<h3 id="k28">...</h3>

It's as straight forward as it can be

TheGrandUser avatar Nov 30 '19 23:11 TheGrandUser

@TheGrandUser is this server side Blazor or web assembly?

SQL-MisterMagoo avatar Dec 01 '19 00:12 SQL-MisterMagoo

This is server side, using .net core 3.1 preview 3 in the VS preview

TheGrandUser avatar Dec 01 '19 00:12 TheGrandUser

@TheGrandUser if the page is not the root of your site you will need to include the page route in the anchor href because Blazor uses root relative link resolution. Have you tried that? href=mypage#k28

SQL-MisterMagoo avatar Dec 01 '19 00:12 SQL-MisterMagoo

Ok, that works

TheGrandUser avatar Dec 01 '19 01:12 TheGrandUser

Updated work-around:

    window.scrollToElementId = (elementId) => {
        console.info('scrolling to element', elementId);
        var element = document.getElementById(elementId);
        if (!element) {
            console.warn('element was not found', elementId);
            return false;
        }
        element.scrollIntoView();
        return true;
    }

    async protected sealed override Task OnAfterRenderAsync(bool firstRender)
    {
        await NavigateToElementAsync();
    }

    async Task NavigateToElementAsync()
    {
        var fragment = new Uri(Navigation.Uri).Fragment;

        if (string.IsNullOrEmpty(fragment))
            return;

        var elementId = fragment.StartsWith("#") ? fragment.Substring(1) : fragment;

        if (string.IsNullOrEmpty(elementId))
            return;

        await JSRuntime.InvokeAsync<bool>("scrollToElementId", elementId);
    }

Grauenwolf avatar Dec 14 '19 06:12 Grauenwolf

@TheGrandUser if the page is not the root of your site you will need to include the page route in the anchor href because Blazor uses root relative link resolution. Have you tried that? href=mypage#k28

This worked for me...thank you so much

ramkiranhota avatar Jul 03 '20 04:07 ramkiranhota

@TheGrandUser if the page is not the root of your site you will need to include the page route in the anchor href because Blazor uses root relative link resolution. Have you tried that? href=mypage#k28

This worked for me...thank you so much

Be careful with this.... if you use a component that uses anchors with this approach, and that component is used in different routes, then you'll have a problem since the URL is not static anymore. Regards

chrdlx avatar Jul 05 '20 13:07 chrdlx

Well obviously you don't hard code URLs...

On Sun, 5 Jul 2020, 14:49 chrdlx, [email protected] wrote:

@TheGrandUser https://github.com/TheGrandUser if the page is not the root of your site you will need to include the page route in the anchor href because Blazor uses root relative link resolution. Have you tried that? href=mypage#k28

This worked for me...thank you so much

Be careful with this.... if you use a component that uses anchors with this approach, and that component is used in different routes, then you'll have a problem since the URL is not static anymore. Regards

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/dotnet/aspnetcore/issues/8393#issuecomment-653891259, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACWQFPWXTLOTCXSTS5KUZ2LR2CAG7ANCNFSM4G46F53A .

SQL-MisterMagoo avatar Jul 05 '20 14:07 SQL-MisterMagoo

Updated work-around:

    window.scrollToElementId = (elementId) => {
        // Omitted for brevity
    }

    async protected sealed override Task OnAfterRenderAsync(bool firstRender)
    {
        await NavigateToElementAsync();
    }

    async Task NavigateToElementAsync()
    {
        // Omitted for brevity
    }

@Grauenwolf Where is that supposed to go? Does the javascript go into its own static file and the C# go into the razor component/layout we need to call it from?

mark-at-tusksoft avatar Jul 23 '20 17:07 mark-at-tusksoft

The JavaScript can go directly in index.html

Grauenwolf avatar Jul 24 '20 01:07 Grauenwolf

Thanks for contacting us. We're moving this issue to the Next sprint planning milestone for future evaluation / consideration. We will evaluate the request when we are planning the work for the next milestone. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

ghost avatar Oct 09 '20 18:10 ghost

This will be very important feature when it comes to component design , hopefully it could be there very soon

ahmed007boss avatar Oct 21 '20 16:10 ahmed007boss

Update for Workaround:

index.html

    <script>
        window.scrollToElementId = (elementId) => {
            console.info('scrolling to element', elementId);
            var element = document.getElementById(elementId);
            if (!element) {
                console.warn('element was not found', elementId);
                return false;
            }
            element.scrollIntoView();
            return true;
        }
    </script>

MainLayout.razor (can also be in another base class, when not every page should have this)

@inherits LayoutComponentBase
@inject NavigationManager Navigation
@inject IJSRuntime JSRuntime

@Body

@functions {
    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        Navigation.LocationChanged += OnLocationChanges;
    }
    
    protected void OnLocationChanges(object sender, LocationChangedEventArgs args) => NavigateToElement();

    protected void NavigateToElement()
    {
        NavigateToElementAsync().Wait();
    }

    async protected override Task OnAfterRenderAsync(bool firstRender)
    {
        await NavigateToElementAsync();
    }

    protected async Task NavigateToElementAsync()
    {
        var fragment = new Uri(Navigation.Uri).Fragment;

        if (string.IsNullOrEmpty(fragment))
            return;

        var elementId = fragment.StartsWith("#") ? fragment.Substring(1) : fragment;

        if (string.IsNullOrEmpty(elementId))
            return;

        await JSRuntime.InvokeAsync<bool>("scrollToElementId", elementId);
    }
}

MovGP0 avatar Oct 25 '20 23:10 MovGP0

Moving this to 6.0-poreview2 to spend 1 days and see what can be done here. If turns out this is a large effort, we'll push it out to the future.

mkArtakMSFT avatar Jan 26 '21 18:01 mkArtakMSFT

Thanks for contacting us. We're moving this issue to the Next sprint planning milestone for future evaluation / consideration. We will evaluate the request when we are planning the work for the next milestone. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

ghost avatar Feb 19 '21 17:02 ghost

@mkArtakMSFT Possible to commit to this for 6.0?

mrlife avatar Apr 07 '21 16:04 mrlife

@mkArtakMSFT Over two years have passed since the OP reported this issue.

You mentioned spending a day in January on the issue. Did it turn out to be "a large issue", and if so can you say where in the future a fix will be available ?

jonrmorgan99 avatar May 07 '21 08:05 jonrmorgan99

We've moved this issue to the Backlog milestone. This means that it is not going to be worked on for the coming release. We will reassess the backlog following the current release and consider this item at that time. To learn more about our issue management process and to have better expectation regarding different types of issues you can read our Triage Process.

ghost avatar Aug 03 '21 18:08 ghost