7

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:

noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 1
    I'm not a big fun of _mixing_ technologies on the same frontend project. Too many bad experience in the past. A lot of things are _under control_ from Blazor, specially on Server side where Signalr is used to update the UI. I don't remember the library you cite but I think it should works as a prerender HTML integration, leaving the management of the component _outside_ the Blazor circuits. Look at https://chrissainty.com/using-blazor-components-in-an-existing-mvc-application/ for an example of integration in existing MVC project. – Nicola Biada Aug 23 '21 at 09:06
  • Thanks @NicolaBiada. I'm not a fan of that kind of mixing either, but some project have very specific constraints, so this could be a way of making incremental changes. The article you linked is useful. Maybe, using `Html.RenderComponentAsync` inside a partial ASP.NET MVC view could do it. – noseratio Aug 23 '21 at 09:38
  • 2
    Yes, I use the same approach to mix blazor components and MVC view page in the identity server part, that is still in cshtml format. – Nicola Biada Aug 23 '21 at 10:12
  • @NicolaBiada `.cshtml` should be fine, as long as I can `fetch` the functional markup. I'll give it a try. – noseratio Aug 23 '21 at 10:15
  • @NicolaBiada I'd be really interested in seeing how you've done that - I raised this issue specifically to cover the requirement that I'd like to use Blazor components in the scaffolded identity pages: https://github.com/dotnet/aspnetcore/issues/35551 Do you have any example code I can look at? It would be very cool to be able to use (for example) MudBlazor components on my login page. – Webreaper Aug 23 '21 at 10:55
  • 1
    @Webreaper, on Twitter Florian Rappl [mentioned](https://twitter.com/FlorianRappl/status/1429756127537938434?s=20) his project Piral is capable of doing that. As I take it: https://github.com/smapiot/Piral.Blazor – noseratio Aug 23 '21 at 11:11
  • 2
    Added answer with some code. Same code added to https://github.com/dotnet/aspnetcore/issues/35551 – Nicola Biada Aug 23 '21 at 19:20
  • Now a part of .NET 6: [Render Blazor components from JavaScript](https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-6-rc-1/#render-blazor-components-from-javascript) – noseratio Sep 25 '21 at 12:45
  • Also: https://visualstudiomagazine.com/articles/2021/09/16/aspnet-core-updates.aspx – noseratio Sep 30 '21 at 02:49

3 Answers3

8

MS has addressed this limitation, but the solution requires .Net 6.

https://github.com/aspnet/AspLabs/tree/main/src/BlazorCustomElements

This was done by the man himself, Steve Sanderson.

3

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.

Nicola Biada
  • 2,325
  • 1
  • 8
  • 22
  • This is great. When you say "static rendering", does that mean the components aren't interactive (i.e., you're just rendering styled static components, with no dynamic behaviour)? Or are you referring to something else? (my MVC is rusty, so there may be another meaning I'm not aware of here). – Webreaper Aug 23 '21 at 21:15
  • 2
    Static render means that the HTML will be rendered as is, without a js or wasm mutation in second phase. – Nicola Biada Aug 23 '21 at 22:17
2

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>
MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31