Skip to content

Commit c8d47d3

Browse files
authored
Blazor Web can use BasePath component instead of <base href=""> (#64590)
* Proposal of `BasePath` component. * Unit tests. * Feedback: give the component more coverage. * Feedback: update our template.
1 parent 1b871a0 commit c8d47d3

File tree

12 files changed

+156
-8
lines changed

12 files changed

+156
-8
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Components.Endpoints.BasePath
3+
Microsoft.AspNetCore.Components.Endpoints.BasePath.BasePath() -> void
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Components.Rendering;
5+
6+
namespace Microsoft.AspNetCore.Components.Endpoints;
7+
8+
/// <summary>
9+
/// Renders a &lt;base&gt; element whose <c>href</c> value matches the current request path base.
10+
/// </summary>
11+
public sealed class BasePath : IComponent
12+
{
13+
private RenderHandle _renderHandle;
14+
15+
[Inject]
16+
private NavigationManager NavigationManager { get; set; } = default!;
17+
18+
void IComponent.Attach(RenderHandle renderHandle)
19+
{
20+
_renderHandle = renderHandle;
21+
}
22+
23+
Task IComponent.SetParametersAsync(ParameterView parameters)
24+
{
25+
_renderHandle.Render(Render);
26+
return Task.CompletedTask;
27+
}
28+
29+
private void Render(RenderTreeBuilder builder)
30+
{
31+
builder.OpenElement(0, "base");
32+
builder.AddAttribute(1, "href", ComputeHref());
33+
builder.CloseElement();
34+
}
35+
36+
private string ComputeHref()
37+
{
38+
var baseUri = NavigationManager.BaseUri;
39+
if (Uri.TryCreate(baseUri, UriKind.Absolute, out var absoluteUri))
40+
{
41+
return absoluteUri.AbsolutePath;
42+
}
43+
44+
return "/";
45+
}
46+
}

src/Components/Endpoints/test/Microsoft.AspNetCore.Components.Endpoints.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
</PropertyGroup>
77

88
<ItemGroup>
9-
<Compile Include="..\..\Shared\test\AutoRenderComponent.cs" Link="TestComponents\AutoRenderComponent.cs" />
109
<Compile Include="$(ComponentsSharedSourceRoot)src\WebRootComponentParameters.cs" Link="Shared\WebRootComponentParameters.cs" />
10+
<Compile Include="$(ComponentsSharedSourceRoot)test\**\*.cs" LinkBase="Shared" />
1111
</ItemGroup>
1212

1313
<ItemGroup>
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Components.RenderTree;
5+
using Microsoft.AspNetCore.Components.Test.Helpers;
6+
7+
#nullable enable
8+
9+
namespace Microsoft.AspNetCore.Components.Endpoints;
10+
11+
public class BasePathTest
12+
{
13+
[Fact]
14+
public void PreservesCasingFromNavigationManagerBaseUri()
15+
{
16+
_ = CreateServices(out var renderer, "https://example.com/Dashboard/");
17+
var componentId = RenderBasePath(renderer);
18+
19+
Assert.Equal("/Dashboard/", GetHref(renderer, componentId));
20+
}
21+
22+
[Theory]
23+
[InlineData("https://example.com/a/b/", "/a/b/")]
24+
[InlineData("https://example.com/a/b", "/a/")]
25+
public void RendersBaseUriPathExactly(string baseUri, string expected)
26+
{
27+
_ = CreateServices(out var renderer, baseUri);
28+
29+
var componentId = RenderBasePath(renderer);
30+
31+
Assert.Equal(expected, GetHref(renderer, componentId));
32+
}
33+
34+
private static TestServiceProvider CreateServices(out TestRenderer renderer, string baseUri = "https://example.com/app/")
35+
{
36+
var services = new TestServiceProvider();
37+
var uri = baseUri.EndsWith('/') ? baseUri + "dashboard" : baseUri + "/dashboard";
38+
var navigationManager = new TestNavigationManager(baseUri, uri);
39+
services.AddService<NavigationManager>(navigationManager);
40+
services.AddService<IServiceProvider>(services);
41+
42+
renderer = new TestRenderer(services);
43+
return services;
44+
}
45+
46+
private static int RenderBasePath(TestRenderer renderer)
47+
{
48+
var component = (BasePath)renderer.InstantiateComponent<BasePath>();
49+
var componentId = renderer.AssignRootComponentId(component);
50+
renderer.RenderRootComponent(componentId);
51+
return componentId;
52+
}
53+
54+
private static string? GetHref(TestRenderer renderer, int componentId)
55+
{
56+
var frames = renderer.GetCurrentRenderTreeFrames(componentId);
57+
for (var i = 0; i < frames.Count; i++)
58+
{
59+
ref readonly var frame = ref frames.Array[i];
60+
if (frame.FrameType == RenderTreeFrameType.Element && frame.ElementName == "base")
61+
{
62+
for (var j = i + 1; j < frames.Count; j++)
63+
{
64+
ref readonly var attribute = ref frames.Array[j];
65+
if (attribute.FrameType == RenderTreeFrameType.Attribute && attribute.AttributeName == "href")
66+
{
67+
return attribute.AttributeValue?.ToString();
68+
}
69+
70+
if (attribute.FrameType != RenderTreeFrameType.Attribute)
71+
{
72+
break;
73+
}
74+
}
75+
}
76+
}
77+
78+
return null;
79+
}
80+
81+
private sealed class TestNavigationManager : NavigationManager
82+
{
83+
public TestNavigationManager(string baseUri, string uri)
84+
{
85+
Initialize(baseUri, uri);
86+
}
87+
88+
protected override void NavigateToCore(string uri, bool forceLoad)
89+
{
90+
throw new NotImplementedException();
91+
}
92+
}
93+
}

src/Components/Samples/BlazorUnitedApp/App.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<head>
44
<meta charset="utf-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6-
<base href="/" />
6+
<BasePath />
77
<link rel="stylesheet" href="@Assets["css/bootstrap/bootstrap.min.css"]" />
88
<link rel="stylesheet" href="@Assets["css/bootstrap-icons/bootstrap-icons.min.css"]" />
99
<link rel="stylesheet" href="@Assets["css/site.css"]" />

src/Components/Samples/BlazorUnitedApp/_Imports.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
@using Microsoft.AspNetCore.Components.Forms
44
@using Microsoft.AspNetCore.Components.Routing
55
@using Microsoft.AspNetCore.Components.Web
6+
@using Microsoft.AspNetCore.Components.Endpoints
67
@using Microsoft.AspNetCore.Components.Web.Virtualization
78
@using Microsoft.JSInterop
89
@using BlazorUnitedApp

src/Components/test/testassets/Components.TestServer/RazorComponents/App.razor

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
@using TestContentPackage.NotFound
55
@using Components.TestServer.RazorComponents
66
@using Microsoft.AspNetCore.Components
7+
@using Microsoft.AspNetCore.Components.Endpoints
78
@using Microsoft.AspNetCore.Components.Routing
89
@using Microsoft.AspNetCore.Components.Web
910
@using System.Threading.Tasks
@@ -99,7 +100,7 @@
99100
<html lang="en">
100101
<head>
101102
<meta charset="utf-8" />
102-
<base href="/subdir/" />
103+
<BasePath />
103104
<HeadOutlet />
104105
</head>
105106
<body>

src/Components/test/testassets/Components.TestServer/RazorComponents/NamedFormContextNoFormContextApp.razor

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
@using Components.TestServer.RazorComponents.Pages.Forms
2+
@using Microsoft.AspNetCore.Components.Endpoints
23

34
<!DOCTYPE html>
45
<html lang="en">
56
<head>
67
<meta charset="utf-8" />
7-
<base href="/subdir/" />
8+
<BasePath />
89
<HeadOutlet />
910
</head>
1011
<body>

src/Components/test/testassets/Components.TestServer/RazorComponents/RemoteAuthenticationApp.razor

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
<!DOCTYPE html>
1+
@using Microsoft.AspNetCore.Components.Endpoints
2+
3+
<!DOCTYPE html>
24
<html lang="en">
35

46
<head>
57
<meta charset="utf-8" />
6-
<base href="/subdir/" />
8+
<BasePath />
79

810
<HeadOutlet @rendermode="new InteractiveWebAssemblyRenderMode(prerender: false)" />
911
</head>

src/Components/test/testassets/Components.TestServer/RazorComponents/Root.razor

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
@using Components.TestServer.RazorComponents.Pages.Forms
2+
@using Microsoft.AspNetCore.Components.Endpoints
23
@using Microsoft.AspNetCore.Components.Web
34

45
<!DOCTYPE html>
56
<html lang="en">
67
<head>
78
<meta charset="utf-8" />
8-
<base href="/subdir/" />
9+
<BasePath />
910
<link rel="stylesheet" href="@Assets["Components.TestServer.styles.css"]" />
1011
<HeadOutlet />
1112
</head>

0 commit comments

Comments
 (0)