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.
-
Follow the installation and setup sections.
-
Modify the call to
AddBlazorWebComponents
to the following:builder.Services.AddBlazorWebComponents(r => r.RegisterAll(Assembly.GetExecutingAssembly())});
-
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; }
-
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>
dotnet add package Ostomachion.Blazor.WebComponents
First, follow the installation and setup instructions for Ostomachion.Blazor.ShadowDom. (This will be included automatically in a future release.)
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();
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" />
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}" />
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)
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 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.
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)
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
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)
Be aware of the following limitations:
- A component that inherits either
CustomElementBase
orWebComponentBase
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.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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!