Reputation: 61666
Can we render a Blazor component as an independent DOM fragment, or somehow else consume it as a standard Web Component within a vanilla HTML/JS page?
This might be a naive question from the Blazor architectural standpoints. I am not a Blazor expert by far, but I think it can be a useful technique for incremental "brownfield" modernization of legacy web applications. I'm surprised this doesn't appear to be officially supported.
To illustrate, consider this simple web component example, which renders a custom element <date-info>
:
// define a custom web component
customElements.define("date-info", class DateInfo extends HTMLElement {
constructor() {
super();
// create an "open" (vs "closed") shadow DOM,
// i.e., accessible to the outside JavaScript
this.attachShadow({ mode: "open" });
}
async connectedCallback() {
console.log(`${this.constructor.name}.${this.connectedCallback.name} called`);
// get the content from the server,
// this could be a Blazor component markup
try {
const response = await fetch("https://worldtimeapi.org/api/ip");
const data = await response.json();
const content = new Date(data.utc_datetime).toString();
this.shadowRoot.innerHTML = `<span>${content}</span>`;
}
catch(e) {
console.error(e);
const info = document.createTextNode(e.message);
this.shadowRoot.appendChild(info);
}
}
});
<!-- use the web component -->
<p>Current time: <date-info/></p>
Now, instead of fetching https://worldtimeapi.org/api/ip
, I'd like to fetch and render a detached markup for a Blazor/Server component, e.g.:
@* this is a Blazor component *@
<p>@(DateTime.Now)</p>
Moreover, I'd expect this markup to remain functional and dynamic, i.e., the client-side DOM events and the server-side updates for this Blazor component to further propagate both ways, through the life cycle of the wrapping web component.
It's surely possible to make it a Blazor @page
and load it into an iframe
, but I'm rather looking to render it as a part of the outer page's DOM.
So far, I've come across this:
Apparently, that wasn't one of Blazor design goals back in 2018: https://github.com/dotnet/aspnetcore/issues/16033.
Later Steve Sanderson created an (unofficial) library to test Blazor components in isolation, which in theory can be used to get a standalone Blazor component markup: https://stackoverflow.com/a/60457390/1768303.
Is it the best approach to tackle this problem, so far?
Upvotes: 7
Views: 6295
Reputation: 96
MS has addressed this limitation, but the solution requires .Net 6.
This was done by the man himself, Steve Sanderson.
UPDATE 11/8/24: The BlazorCustomElements project was added in .NET 7 (and subsequently deleted from that AspLabs repo). Here is the current microsoft doc for how to configure them in .NET 8.
Upvotes: 8
Reputation: 2800
In the meantime you can mix the old cshtml with razor components.
I use this approach to maintain the same graphic layout between the two systems.
An example, the following file is _Layout.cshtml used by Identity.
I've used various Blazor components via static rendering:
@using Microsoft.AspNetCore.Hosting
@using Microsoft.AspNetCore.Mvc.ViewEngines
@inject IWebHostEnvironment Environment
@inject ICompositeViewEngine Engine
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using Project.Server.Shared
<!DOCTYPE html>
<html>
<head>
<component type="typeof(MainLayoutHead)" render-mode="Static" />
</head>
<body>
<app>
<div class="container main">
<component type="typeof(MainLayoutTopImages)" render-mode="Static" />
<div class="row navmenu-row">
<div class="col-md-12 bg-dark navmenu-col">
<component type="typeof(NavMenu)" render-mode="Static" />
</div>
</div>
<div class="row content pt-4 pb-0 mt-0">
<div class="col-md-12">
<div class="row">
<div class="col-md-12">
@*Required for GDPR.*@
<partial name="_CookieConsentPartial" />
</div>
</div>
<div class="row body-row">
<div class="col-md-12 body-col">
@RenderBody()
</div>
</div>
</div>
</div>
<component type="typeof(MainLayoutFooter)" render-mode="Static" />
</div>
</app>
<script src="~/Identity/lib/jquery/dist/jquery.min.js"></script>
<script src="~/Identity/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/Identity/js/site.js" asp-append-version="true"></script>
@RenderSection("Scripts", required: false)
</body>
</html>
The MainLayoutHead
, MainLayoutFooter
and NavMenu
are regular Blazor components.
Upvotes: 3
Reputation: 30016
Not sure if this helps but you can definitely do it from a server side page (I'll delete this answer if it doesn't). Here's a test page that renders all three standard "pages" inside a cshtml page with server side content. You need to actually forget the "page" concept in Blazor. EVERYTHING is a Component. Pages are just components with a Page
custom attribute.
The problem with this setup is that as soon as you refresh the page you restart the three components and you lose any scoped data.
@page "/test"
@namespace StackOverflow.Answers.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>StackOverflow.Answers</title>
<base href="~/" />
<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link href="css/site.css" rel="stylesheet" />
<link href="StackOverflow.Answers.styles.css" rel="stylesheet" />
</head>
<body>
<div class="m-2 p-s bg-light">
<h3>A Normal Razor Page</h3>
<p>
Lots of server side rendered junk
</p>
<component type="typeof(StackOverflow.Answers.Shared.SurveyPrompt)" render-mode="ServerPrerendered" />
</div>
<div class="m-2 p-s bg-info">
<h3>A Normal Header</h3>
<p>
Lots More server side rendered junk
</p>
<component type="typeof(StackOverflow.Answers.Pages.Counter)" render-mode="ServerPrerendered" />
</div>
<div class="m-2 p-s bg-light">
<h3>A Normal Header</h3>
<p>
Lots More server side rendered junk
</p>
<component type="typeof(StackOverflow.Answers.Pages.Counter)" render-mode="ServerPrerendered" />
</div>
<div class="m-2 p-s bg-light">
<h3>Yet Another Normal Header</h3>
<p>
Lots More server side rendered junk
</p>
<component type="typeof(StackOverflow.Answers.Pages.FetchData)" render-mode="ServerPrerendered" />
</div>
<div class="m-2 p-s bg-light">
<h3>Yet Another Normal Header</h3>
<p>
Lots More server side rendered junk
</p>
<component type="typeof(StackOverflow.Answers.Pages.FetchData)" render-mode="ServerPrerendered" />
</div>
<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.server.js"></script>
</body>
</html>
Upvotes: 2