/blazor.webcomponents

A simple library that allows Blazor components to be rendered as real standards-based Web Components using custom elements, shadow DOM, and HTML templates.

Primary LanguageC#GNU Affero General Public License v3.0AGPL-3.0

BlazorWebComponents

A simple library that allows Blazor components to be rendered as real standards-based Web Components using custom elements, shadow DOM, and HTML templates.

Still in development, but mostly tested and functional for Blazor WebAssembly projects.

TL;DR

  1. Follow the installation and setup sections.

  2. Modify the call to AddBlazorWebComponents to the following:

    builder.Services.AddBlazorWebComponents(r => r.RegisterAll(Assembly.GetExecutingAssembly())});
  3. Add a new Razor Component to your project:

    MyComponent.razor

    @inherits CustomElementBase
    <p class="shadow">Shadow: @Value</p>
    <p class="light">Light: <Slot Name="value" For="Value">missing value</Slot></p>

    MyComponent.razor.cs

    namespace My.Namespace;
    
    [CustomElement("my-component")]
    public class MyComponent : WebComponent
    {
        [Parameter]
        [EditorRequired]
        public string Value { get; set; } = default!;
    }

    MyComponent.razor.css

    .shadow { background: lightgray; }
    .light { background: lightyellow; }
  4. Add the component to the main page.

    <MyComponent Value="Hello, world!" />

That's it! You've got a full standards-based web component from Blazor!

Rendered output

<my-component>
    #shadowroot (open)
      <style>
        .shadow { background: lightgray; }
        .light { background: lightyellow; }
      </style>
      <p class="shadow">Shadow: Hello, world!</p>
      <p class="light">Light: <slot name="value">missing value</slot></p>

    <span slot="value">Hello, world!</span>
</my-component>

Installation

dotnet add package Ostomachion.Blazor.WebComponents

Setup

First, follow the installation and setup instructions for Ostomachion.Blazor.ShadowDom. (This will be included automatically in a future release.)

WebAssembly

In wwwroot/index.html, add the following script:

<script src="_content/Ostomachion.Blazor.WebComponents/blazor-web-components.js"></script>

In Program.cs, add the following lines:

builder.RootComponents.Add<CustomElementRegistrarComponent>("head::after");
builder.Services.AddBlazorWebComponents();

Server

Note: Blazor Web Components has not yet been thoroughly tested with Blazor server.

In Pages/_Host.html, add the following script:

<script src="_content/Ostomachion.Blazor.WebComponents/blazor-web-components.js"></script>

In Program.cs, add the following lines:

builder.Services.AddBlazorWebComponents();

In Pages/_Host.cs add the following line to the end of the head element:

<component type="typeof(CustomElementRegistrarComponent)" render-mode="ServerPrerendered" />

MAUI

Note: Blazor Web Components has not yet been thoroughly tested with MAUI/Blazor WebView.

In wwwroot/index.html, add the following script:

<script src="_content/Ostomachion.Blazor.WebComponents/blazor-web-components.js"></script>

In MauiProgram.cs add the following line:

builder.Services.AddBlazorWebComponents();

In MainPage.xaml, in the BlazorWebView.RootComponents element, add the following line:

<RootComponent Selector="head::after" ComponentType="{x:Type Ostomachion.BlazorWebComponents.CustomElementRegistrarComponent}" />

Custom Elements

Creating a Custom Element Component

Note: Custom elements must be registered before they can be rendered on a page.

Important: Please read and understand the notes on technical limitations.

Any component class that inherits Ostomachion.WebComponents.CustomElementBase will be rendered inside a custom element.

By default, custom elements are rendered as autonomous custom elements with an identifier generated from the component's class and namespace. (Example)

The default identifier can be specified using a CustomElementAtrribute. (Example)

A customized built-in element can be created using a CustomElementAttribute. (Example)

If a reference to the generated custom element is stored in the Host property of the component.

Attributes on the generated custom element can be set using the HostAttributes property of the component. (Example)

Registering Custom Elements

Before a custom element component can be rendered on a page, it must be registered by passing an action to the call to AddBlazorWebComponents. (Example)

To avoid identifier collisions and to allow more customization, an identifier can be registered with the component which will override any default identifier defined by the component itself. (Example)

For convenience, all custom elements in an assembly can be registered at once using their default identifiers by calling RegisterAll. Any custom elements that have already been registered will be skipped by RegisterAll. (Example)

Web Components

Web Components are extensions of custom elements with many extra features. Everything in the Custom Elements section applies to web components including registration.

In addition to being wrapped in a custom element, web components are also rendered in a shadow DOM and make use of templates and slots.

Creating a Web Component

Any component class that inherits Ostomachion.WebComponents.WebComponentBase will be rendered inside a shadow DOM attached to custom element. (Example)

By default, an open shadow root is attached to the host element. The shadow root mode can be specified by overriding the ShadowRootMode property on the component. (Example)

Any CSS file associated with the class will be automatically encapsulated in the shadow root. (Example)

Slots

By default, the content of a web component is rendered in the shadow DOM and generally encapsulated from CSS and JavaScript outside the component.

TODO

Notes on CSS

Since the host element name of a custom element component can vary, CSS selectors become fragile. As a workaround, this library adds custom namespaced elements to custom elements. The attribute name is equal to the class name in lowercase with a namespace equal to the namespace of the class in lowercase. This not only provides a unique name for each component, it more closely matches the source Razor file. (Example)

⚠️ Notes on Technical Limitations ⚠️

Be aware of the following limitations:

  • A component that inherits either CustomElementBase or WebComponentBase MUST declare the base class in the .cs file (and the .razor file if one exists).
  • A CustomElementAttribute MUST be applied to the class in the .cs file and not in the .razor file.
  • Any properties with a SlotAttribute MUST be defined in the .cs file and not in the .razor file.

Adding <UseRazorSourceGenerator>false</UseRazorSourceGenerator> to the .csproj should in theory fix these limitations, but this is not a priority to support. Please report any issue if you need this feature.

Explanation: Some of the functionality of this library is implemented as a C# source generator. Unfortunately, there is currently no great way to get source generators to work will with Razor files. This means that the Blazor Web Component source generator will not work properly if you add certain features to the .razor side of a component rather than the .cs side.

Examples

Basic Custom Element

Test.razor

@inherits CustomElementBase
<p>Hello, world!</p>

Example.razor.cs

namespace Example;

public class Test : CustomElementBase { }

Rendered output

<example-customelementbase>
  <p>Hello, world!</p>
</example-customelementbase>

Custom Default Identifier

The default identifier can be changed by adding a CustomElementAttribute to the class.

Test.razor

@inherits CustomElementBase
<p>Hello, world!</p>

Example.razor.cs

namespace Example;

[CustomElement("docs-test")]
public class Test : CustomElementBase { }

Rendered output

<docs-test>
  <p>Hello, world!</p>
</docs-test>

Customized Built-In Element

A customized built-in element can be created by adding a CustomElementAttribute to the class.

Test.razor

@inherits CustomElementBase
<p>Hello, world!</p>

Example.razor.cs

namespace Example;

[CustomElement("docs-test", Extends = "div")]
public class Test : CustomElementBase { }

Rendered output

<div is="docs-test">
  <p>Hello, world!</p>
</div>

Adding Host Attributes

Attributes can be set on the generated custom element using the HostAttributes property.

Test.razor

@inherits CustomElementBase
@HostAttributes["id"] = "host"
<p>Hello, world!</p>

Example.razor.cs

namespace Example;

[CustomElement("docs-test")]
public class Test : CustomElementBase { }

Rendered output

<docs-test id="host">
  <p>Hello, world!</p>
</docs-test>

Basic Registration

A custom element must be registered before it can be rendered on a page.

Test.razor

@inherits CustomElementBase
<p>Hello, world!</p>

Example.razor.cs

namespace Example;

[CustomElement("docs-test")]
public class Test : CustomElementBase { }

Program.cs / MauiProgram.cs

...
builder.Services.AddBlazorWebComponents(r =>
{
    r.Register<Test>();
});
...

Rendered output

<docs-test>
  <p>Hello, world!</p>
</docs-test>

Identifier Registration

The default identifier of a custom element can be overridden at registration.

Test.razor

@inherits CustomElementBase
<p>Hello, world!</p>

Example.razor.cs

namespace Example;

[CustomElement("docs-test")]
public class Test : CustomElementBase { }

Program.cs / MauiProgram.cs

...
builder.Services.AddBlazorWebComponents(r =>
{
    r.Register<Test>("my-element");
});
...

Rendered output

<my-element>
  <p>Hello, world!</p>
</my-element>

Register All Custom Elements

The RegisterAll method will register all custom elements in a given assembly that have not already been registered.

Test.razor

@inherits CustomElementBase
<p>Hello, world!</p>

Example.razor.cs

namespace Example;

[CustomElement("docs-test")]
public class Test : CustomElementBase { }

Program.cs / MauiProgram.cs

...
builder.Services.AddBlazorWebComponents(r =>
{
    r.RegisterAll(Assembly.GetExecutingAssembly());
});
...

Rendered output

<docs-test>
  <p>Hello, world!</p>
</docs-test>

Basic WebComponent

Test.razor

@inherits WebComponentBase
<p>Hello, world!</p>

Example.razor.cs

namespace Example;

[custom-element("docs-test")]
public class Test : WebComponentBase { }

Rendered output

<docs-test>
  #shadow-root (open)
    <p>Hello, world!</p>

</docs-test>

Shadow Root Mode

Test.razor

@inherits WebComponentBase
<p>Hello, world!</p>

Example.razor.cs

namespace Example;

[custom-element("docs-test")]
public class Test : WebComponentBase
{
    public override ShadowRootMode ShadowRootMode => ShadowRootMode.Closed;
}

Rendered output

<docs-test>
  #shadow-root (closed)
    <p>Hello, world!</p>

</docs-test>

Web Component Styling

Test.razor

@inherits WebComponentBase
<p>Hello, world!</p>

Example.razor.cs

namespace Example;

[custom-element("docs-test")]
public class Test : WebComponentBase
{
    public override ShadowRootMode ShadowRootMode => ShadowRootMode.Closed;
}

Example.razor.css

p {
    color: red;
}

Rendered output

<docs-test>
  #shadow-root (open)
    <style>
        p {
            color: red;
        }
    </style>
    <p>Hello, world!</p>

</docs-test>

CSS Selectors

Test.razor

@inherits WebComponentBase
Hello, world!

Example.razor.cs

namespace Example;

[custom-element("docs-test")]
public class Test : WebComponentBase { }

Index.razor

<Test />

Index.razor.css**

@namespace ce 'example';

[ce|test] {
    border: 1px solid black;
}

Special thanks to Poor Egg Productions for the icon!