Skip to content

Commit be4c603

Browse files
[release/7.0-preview6] Custom HTML elements rendering Blazor components (#42383)
- backport of #42314
1 parent 47f5d8f commit be4c603

34 files changed

+3571
-9
lines changed

AspNetCore.sln

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1748,10 +1748,16 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestInfrastructure", "TestI
17481748
EndProject
17491749
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Templates.Mvc.Tests", "src\ProjectTemplates\test\Templates.Mvc.Tests\Templates.Mvc.Tests.csproj", "{AA7445F5-BD28-400C-8507-E2E0D3CF7D7E}"
17501750
EndProject
1751-
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Templates.Blazor.Tests", "src\ProjectTemplates\test\Templates.Blazor.Tests\Templates.Blazor.Tests.csproj", "{281BF9DB-7B8A-446B-9611-10A60903F125}"
1751+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Templates.Blazor.Server.Tests", "src\ProjectTemplates\test\Templates.Blazor.Server.Tests\Templates.Blazor.Server.Tests.csproj", "{281BF9DB-7B8A-446B-9611-10A60903F125}"
17521752
EndProject
17531753
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "stress", "stress", "{A5946454-4788-4871-8F23-A9471D55F115}"
17541754
EndProject
1755+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Templates.Blazor.WebAssembly.Tests", "src\ProjectTemplates\test\Templates.Blazor.WebAssembly.Tests\Templates.Blazor.WebAssembly.Tests.csproj", "{7CA0A9AF-9088-471C-B0B6-EBF43F21D3B9}"
1756+
EndProject
1757+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.CustomElements", "src\Components\CustomElements\src\Microsoft.AspNetCore.Components.CustomElements.csproj", "{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD}"
1758+
EndProject
1759+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CustomElements", "CustomElements", "{0BB58FB6-8B66-4C6D-BA8A-DF3AFAF9AB8F}"
1760+
EndProject
17551761
Global
17561762
GlobalSection(SolutionConfigurationPlatforms) = preSolution
17571763
Debug|Any CPU = Debug|Any CPU
@@ -10481,6 +10487,38 @@ Global
1048110487
{281BF9DB-7B8A-446B-9611-10A60903F125}.Release|x64.Build.0 = Release|Any CPU
1048210488
{281BF9DB-7B8A-446B-9611-10A60903F125}.Release|x86.ActiveCfg = Release|Any CPU
1048310489
{281BF9DB-7B8A-446B-9611-10A60903F125}.Release|x86.Build.0 = Release|Any CPU
10490+
{7CA0A9AF-9088-471C-B0B6-EBF43F21D3B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
10491+
{7CA0A9AF-9088-471C-B0B6-EBF43F21D3B9}.Debug|Any CPU.Build.0 = Debug|Any CPU
10492+
{7CA0A9AF-9088-471C-B0B6-EBF43F21D3B9}.Debug|arm64.ActiveCfg = Debug|Any CPU
10493+
{7CA0A9AF-9088-471C-B0B6-EBF43F21D3B9}.Debug|arm64.Build.0 = Debug|Any CPU
10494+
{7CA0A9AF-9088-471C-B0B6-EBF43F21D3B9}.Debug|x64.ActiveCfg = Debug|Any CPU
10495+
{7CA0A9AF-9088-471C-B0B6-EBF43F21D3B9}.Debug|x64.Build.0 = Debug|Any CPU
10496+
{7CA0A9AF-9088-471C-B0B6-EBF43F21D3B9}.Debug|x86.ActiveCfg = Debug|Any CPU
10497+
{7CA0A9AF-9088-471C-B0B6-EBF43F21D3B9}.Debug|x86.Build.0 = Debug|Any CPU
10498+
{7CA0A9AF-9088-471C-B0B6-EBF43F21D3B9}.Release|Any CPU.ActiveCfg = Release|Any CPU
10499+
{7CA0A9AF-9088-471C-B0B6-EBF43F21D3B9}.Release|Any CPU.Build.0 = Release|Any CPU
10500+
{7CA0A9AF-9088-471C-B0B6-EBF43F21D3B9}.Release|arm64.ActiveCfg = Release|Any CPU
10501+
{7CA0A9AF-9088-471C-B0B6-EBF43F21D3B9}.Release|arm64.Build.0 = Release|Any CPU
10502+
{7CA0A9AF-9088-471C-B0B6-EBF43F21D3B9}.Release|x64.ActiveCfg = Release|Any CPU
10503+
{7CA0A9AF-9088-471C-B0B6-EBF43F21D3B9}.Release|x64.Build.0 = Release|Any CPU
10504+
{7CA0A9AF-9088-471C-B0B6-EBF43F21D3B9}.Release|x86.ActiveCfg = Release|Any CPU
10505+
{7CA0A9AF-9088-471C-B0B6-EBF43F21D3B9}.Release|x86.Build.0 = Release|Any CPU
10506+
{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
10507+
{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
10508+
{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD}.Debug|arm64.ActiveCfg = Debug|Any CPU
10509+
{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD}.Debug|arm64.Build.0 = Debug|Any CPU
10510+
{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD}.Debug|x64.ActiveCfg = Debug|Any CPU
10511+
{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD}.Debug|x64.Build.0 = Debug|Any CPU
10512+
{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD}.Debug|x86.ActiveCfg = Debug|Any CPU
10513+
{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD}.Debug|x86.Build.0 = Debug|Any CPU
10514+
{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
10515+
{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD}.Release|Any CPU.Build.0 = Release|Any CPU
10516+
{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD}.Release|arm64.ActiveCfg = Release|Any CPU
10517+
{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD}.Release|arm64.Build.0 = Release|Any CPU
10518+
{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD}.Release|x64.ActiveCfg = Release|Any CPU
10519+
{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD}.Release|x64.Build.0 = Release|Any CPU
10520+
{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD}.Release|x86.ActiveCfg = Release|Any CPU
10521+
{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD}.Release|x86.Build.0 = Release|Any CPU
1048410522
EndGlobalSection
1048510523
GlobalSection(SolutionProperties) = preSolution
1048610524
HideSolutionNode = FALSE
@@ -11346,6 +11384,9 @@ Global
1134611384
{AA7445F5-BD28-400C-8507-E2E0D3CF7D7E} = {08D53E58-4AAE-40C4-8497-63EC8664F304}
1134711385
{281BF9DB-7B8A-446B-9611-10A60903F125} = {08D53E58-4AAE-40C4-8497-63EC8664F304}
1134811386
{A5946454-4788-4871-8F23-A9471D55F115} = {4FDDC525-4E60-4CAF-83A3-261C5B43721F}
11387+
{7CA0A9AF-9088-471C-B0B6-EBF43F21D3B9} = {08D53E58-4AAE-40C4-8497-63EC8664F304}
11388+
{76C3E22D-092B-4E8A-81F0-DCF071BFF4CD} = {0BB58FB6-8B66-4C6D-BA8A-DF3AFAF9AB8F}
11389+
{0BB58FB6-8B66-4C6D-BA8A-DF3AFAF9AB8F} = {60D51C98-2CC0-40DF-B338-44154EFEE2FF}
1134911390
EndGlobalSection
1135011391
GlobalSection(ExtensibilityGlobals) = postSolution
1135111392
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

eng/ProjectReferences.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@
140140
<ProjectReferenceProvider Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" ProjectPath="$(RepoRoot)src\SignalR\server\StackExchangeRedis\src\Microsoft.AspNetCore.SignalR.StackExchangeRedis.csproj" />
141141
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.Authorization" ProjectPath="$(RepoRoot)src\Components\Authorization\src\Microsoft.AspNetCore.Components.Authorization.csproj" />
142142
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Components" ProjectPath="$(RepoRoot)src\Components\Components\src\Microsoft.AspNetCore.Components.csproj" />
143+
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.CustomElements" ProjectPath="$(RepoRoot)src\Components\CustomElements\src\Microsoft.AspNetCore.Components.CustomElements.csproj" />
143144
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.Forms" ProjectPath="$(RepoRoot)src\Components\Forms\src\Microsoft.AspNetCore.Components.Forms.csproj" />
144145
<ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.Server" ProjectPath="$(RepoRoot)src\Components\Server\src\Microsoft.AspNetCore.Components.Server.csproj" />
145146
<ProjectReferenceProvider Include="Microsoft.Authentication.WebAssembly.Msal" ProjectPath="$(RepoRoot)src\Components\WebAssembly\Authentication.Msal\src\Microsoft.Authentication.WebAssembly.Msal.csproj" />

eng/TrimmableProjects.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
<TrimmableProject Include="Microsoft.AspNetCore.WebSockets" />
8080
<TrimmableProject Include="Microsoft.AspNetCore.Components.Authorization" />
8181
<TrimmableProject Include="Microsoft.AspNetCore.Components" />
82+
<TrimmableProject Include="Microsoft.AspNetCore.Components.CustomElements" />
8283
<TrimmableProject Include="Microsoft.AspNetCore.Components.Forms" />
8384
<TrimmableProject Include="Microsoft.Authentication.WebAssembly.Msal" />
8485
<TrimmableProject Include="Microsoft.JSInterop.WebAssembly" />

src/Components/Components.slnf

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"src\\Components\\Components\\perf\\Microsoft.AspNetCore.Components.Performance.csproj",
1212
"src\\Components\\Components\\src\\Microsoft.AspNetCore.Components.csproj",
1313
"src\\Components\\Components\\test\\Microsoft.AspNetCore.Components.Tests.csproj",
14+
"src\\Components\\CustomElements\\src\\Microsoft.AspNetCore.Components.CustomElements.csproj",
1415
"src\\Components\\Forms\\src\\Microsoft.AspNetCore.Components.Forms.csproj",
1516
"src\\Components\\Forms\\test\\Microsoft.AspNetCore.Components.Forms.Tests.csproj",
1617
"src\\Components\\Samples\\BlazorServerApp\\BlazorServerApp.csproj",
@@ -140,4 +141,4 @@
140141
"src\\WebEncoders\\src\\Microsoft.Extensions.WebEncoders.csproj"
141142
]
142143
}
143-
}
144+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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 System.Diagnostics.CodeAnalysis;
5+
6+
namespace Microsoft.AspNetCore.Components.Web;
7+
8+
/// <summary>
9+
/// Extension methods for registering custom elements from an <see cref="IJSComponentConfiguration"/>.
10+
/// </summary>
11+
public static class CustomElementsJSComponentConfigurationExtensions
12+
{
13+
/// <summary>
14+
/// Marks the specified component type as allowed for use as a custom element.
15+
/// </summary>
16+
/// <typeparam name="TComponent">The component type.</typeparam>
17+
/// <param name="configuration">The <see cref="IJSComponentConfiguration"/>.</param>
18+
/// <param name="identifier">A unique name for the custom element. This must conform to custom element naming rules, so it must contain a dash character.</param>
19+
public static void RegisterCustomElement<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TComponent>(this IJSComponentConfiguration configuration, string identifier) where TComponent : IComponent
20+
=> configuration.RegisterForJavaScript<TComponent>(identifier, "registerBlazorCustomElement");
21+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Razor">
2+
3+
<Import Project="Sdk.props" Sdk="Yarn.MSBuild" Condition=" '$(DotNetBuildFromSource)' != 'true'" />
4+
5+
<PropertyGroup>
6+
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
7+
<Description>Provides a mechanism for using Blazor components as custom HTML elements.</Description>
8+
<IsTrimmable>true</IsTrimmable>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<Reference Include="Microsoft.AspNetCore.Components.Web" />
13+
</ItemGroup>
14+
15+
<PropertyGroup>
16+
<YarnWorkingDir>$(MSBuildThisFileDirectory)js\</YarnWorkingDir>
17+
<ResolveStaticWebAssetsInputsDependsOn>
18+
CompileJs;
19+
IncludeCompileJsOutput;
20+
$(ResolveStaticWebAssetsInputsDependsOn)
21+
</ResolveStaticWebAssetsInputsDependsOn>
22+
</PropertyGroup>
23+
24+
<ItemGroup>
25+
<YarnInputs Include="$(YarnWorkingDir)**" Exclude="$(YarnWorkingDir)node_modules\**;$(YarnWorkingDir)*.d.ts;$(YarnWorkingDir)dist\**" />
26+
<YarnOutputs Include="$(YarnWorkingDir)dist\$(Configuration)\BlazorCustomElements.js" />
27+
28+
<Content Remove="$(YarnWorkingDir)**" />
29+
<None Include="$(YarnWorkingDir)*" Exclude="$(YarnWorkingDir)node_modules\**" />
30+
31+
<UpToDateCheckInput Include="@(YarnInputs)" Set="StaticWebassets" />
32+
<UpToDateCheckInput Include="@(YarnOutputs)" Set="StaticWebassets" />
33+
</ItemGroup>
34+
35+
<Target Name="_CreateJsHash" BeforeTargets="CompileJs" Condition="'$(BuildNodeJS)' != 'false' AND '$(DesignTimeBuild)' != 'true'">
36+
37+
<PropertyGroup>
38+
<JsCompilationCacheFile>$(IntermediateOutputPath)js.cache</JsCompilationCacheFile>
39+
</PropertyGroup>
40+
41+
<Hash ItemsToHash="@(YarnInputs)">
42+
<Output TaskParameter="HashResult" PropertyName="_YarnInputsHash" />
43+
</Hash>
44+
45+
<WriteLinesToFile Lines="$(_YarnInputsHash)" File="$(JsCompilationCacheFile)" Overwrite="True" WriteOnlyWhenDifferent="True" />
46+
47+
<ItemGroup>
48+
<FileWrites Include="$(JsCompilationCacheFile)" />
49+
</ItemGroup>
50+
51+
</Target>
52+
53+
<Target Name="CompileJs" Condition="'$(BuildNodeJS)' != 'false' AND '$(DesignTimeBuild)' != 'true'" Inputs="$(JsCompilationCacheFile)" Outputs="@(YarnOutputs)">
54+
<Yarn Command="install --mutex network --frozen-lockfile" WorkingDirectory="$(YarnWorkingDir)" IgnoreStandardErrorWarningFormat="$(IgnoreYarnWarnings)" />
55+
<Yarn Command="run build:production" WorkingDirectory="$(YarnWorkingDir)" Condition="'$(Configuration)' == 'Release'" IgnoreStandardErrorWarningFormat="$(IgnoreYarnWarnings)" />
56+
<Yarn Command="run build:debug" WorkingDirectory="$(YarnWorkingDir)" Condition="'$(Configuration)' == 'Debug'" IgnoreStandardErrorWarningFormat="$(IgnoreYarnWarnings)" />
57+
58+
<Message Importance="high" Text="@(_JsBuildOutput->'Emitted %(FullPath)')" />
59+
60+
</Target>
61+
62+
<Target Name="IncludeCompileJsOutput">
63+
<ItemGroup>
64+
<_JsBuildOutput Include="$(YarnWorkingDir)dist\$(Configuration)\**" Exclude="$(YarnWorkingDir)dist\.gitignore" />
65+
</ItemGroup>
66+
67+
<DefineStaticWebAssets Condition="'@(_JsBuildOutput)' != ''"
68+
SourceType="Computed"
69+
SourceId="$(PackageId)"
70+
ContentRoot="$(YarnWorkingDir)dist\$(Configuration)\"
71+
BasePath="_content\$(PackageId)"
72+
CandidateAssets="@(_JsBuildOutput)"
73+
RelativePathFilter="**.js"
74+
>
75+
<Output TaskParameter="Assets" ItemName="StaticWebAsset" />
76+
</DefineStaticWebAssets>
77+
78+
<ItemGroup>
79+
<_JsBuildOutput Include="$(YarnWorkingDir)dist\$(Configuration)\**" Exclude="$(YarnWorkingDir)dist\.gitignore" />
80+
<FileWrites Include="$(_JsBuildOutput)" />
81+
</ItemGroup>
82+
</Target>
83+
84+
<Import Project="Sdk.targets" Sdk="Yarn.MSBuild" Condition=" '$(DotNetBuildFromSource)' != 'true'" />
85+
86+
</Project>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
#nullable enable
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#nullable enable
2+
Microsoft.AspNetCore.Components.Web.CustomElementsJSComponentConfigurationExtensions
3+
static Microsoft.AspNetCore.Components.Web.CustomElementsJSComponentConfigurationExtensions.RegisterCustomElement<TComponent>(this Microsoft.AspNetCore.Components.Web.IJSComponentConfiguration! configuration, string! identifier) -> void
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
declare const Blazor: any;
2+
3+
// This function is called by the framework because RegisterAsCustomElement sets it as the initializer function
4+
(window as any).registerBlazorCustomElement = function defaultRegisterCustomElement(elementName: string, parameters: JSComponentParameter[]): void {
5+
customElements.define(elementName, class ConfiguredBlazorCustomElement extends BlazorCustomElement {
6+
static get observedAttributes() {
7+
return BlazorCustomElement.getObservedAttributes(parameters);
8+
}
9+
10+
constructor() {
11+
super(parameters);
12+
}
13+
});
14+
}
15+
16+
export class BlazorCustomElement extends HTMLElement {
17+
private _attributeMappings: { [attributeName: string]: JSComponentParameter };
18+
private _parameterValues: { [dotNetName: string]: any } = {};
19+
private _addRootComponentPromise: Promise<any>;
20+
private _hasPendingSetParameters = true; // The constructor will call setParameters, so it starts true
21+
private _isDisposed = false;
22+
private _disposalTimeoutHandle: any;
23+
24+
public renderIntoElement = this;
25+
26+
// Subclasses will need to call this if they want to retain the built-in behavior for knowing which
27+
// attribute names to observe, since they have to return it from a static function
28+
static getObservedAttributes(parameters: JSComponentParameter[]): string[] {
29+
return parameters.map(p => dasherize(p.name));
30+
}
31+
32+
constructor(parameters: JSComponentParameter[]) {
33+
super();
34+
35+
// Keep track of how we'll map the attributes to parameters
36+
this._attributeMappings = {};
37+
parameters.forEach(parameter => {
38+
const attributeName = dasherize(parameter.name);
39+
this._attributeMappings[attributeName] = parameter;
40+
});
41+
42+
// Defer until end of execution cycle so that (1) we know the heap is unlocked, and (2) the initial parameter
43+
// values will be populated from the initial attributes before we send them to .NET
44+
this._addRootComponentPromise = Promise.resolve().then(() => {
45+
this._hasPendingSetParameters = false;
46+
return Blazor.rootComponents.add(this.renderIntoElement, this.localName, this._parameterValues);
47+
});
48+
49+
// Also allow assignment of parameters via properties. This is the only way to set complex-typed values.
50+
for (const [attributeName, parameterInfo] of Object.entries(this._attributeMappings)) {
51+
const dotNetName = parameterInfo.name;
52+
Object.defineProperty(this, camelCase(dotNetName), {
53+
get: () => this._parameterValues[dotNetName],
54+
set: newValue => {
55+
if (this.hasAttribute(attributeName)) {
56+
// It's nice to keep the DOM in sync with the properties. This set a string representation
57+
// of the value, but this will get overwritten with the original typed value before we send it to .NET
58+
this.setAttribute(attributeName, newValue);
59+
}
60+
61+
this._parameterValues[dotNetName] = newValue;
62+
this._supplyUpdatedParameters();
63+
}
64+
});
65+
}
66+
}
67+
68+
connectedCallback() {
69+
if (this._isDisposed) {
70+
throw new Error(`Cannot connect component ${this.localName} to the document after it has been disposed.`);
71+
}
72+
73+
clearTimeout(this._disposalTimeoutHandle);
74+
}
75+
76+
disconnectedCallback() {
77+
this._disposalTimeoutHandle = setTimeout(async () => {
78+
this._isDisposed = true;
79+
const rootComponent = await this._addRootComponentPromise;
80+
rootComponent.dispose();
81+
}, 1000);
82+
}
83+
84+
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
85+
const parameterInfo = this._attributeMappings[name];
86+
if (parameterInfo) {
87+
this._parameterValues[parameterInfo.name] = BlazorCustomElement.parseAttributeValue(newValue, parameterInfo.type, parameterInfo.name);
88+
this._supplyUpdatedParameters();
89+
}
90+
}
91+
92+
private async _supplyUpdatedParameters() {
93+
if (!this._hasPendingSetParameters) {
94+
this._hasPendingSetParameters = true;
95+
96+
// Continuation from here will always be async, so at the earliest it will be at
97+
// the end of the current JS execution cycle
98+
const rootComponent = await this._addRootComponentPromise;
99+
if (!this._isDisposed) {
100+
const setParametersPromise = rootComponent.setParameters(this._parameterValues);
101+
this._hasPendingSetParameters = false; // We just snapshotted _parameterValues, so we need to start allowing new calls in case it changes further
102+
await setParametersPromise;
103+
}
104+
}
105+
}
106+
107+
static parseAttributeValue(attributeValue: string, type: JSComponentParameterType, parameterName: string): any {
108+
switch (type) {
109+
case 'string':
110+
return attributeValue;
111+
case 'boolean':
112+
switch (attributeValue) {
113+
case 'true':
114+
case 'True':
115+
return true;
116+
case 'false':
117+
case 'False':
118+
return false;
119+
default:
120+
throw new Error(`Invalid boolean value '${attributeValue}' for parameter '${parameterName}'`);
121+
}
122+
case 'number':
123+
const number = Number(attributeValue);
124+
if (Number.isNaN(number)) {
125+
throw new Error(`Invalid number value '${attributeValue}' for parameter '${parameterName}'`);
126+
} else {
127+
return number;
128+
}
129+
case 'boolean?':
130+
return attributeValue ? BlazorCustomElement.parseAttributeValue(attributeValue, 'boolean', parameterName) : null;
131+
case 'number?':
132+
return attributeValue ? BlazorCustomElement.parseAttributeValue(attributeValue, 'number', parameterName) : null;
133+
case 'object':
134+
throw new Error(`The parameter '${parameterName}' accepts a complex-typed object so it cannot be set using an attribute. Try setting it as a element property instead.`);
135+
default:
136+
throw new Error(`Unknown type '${type}' for parameter '${parameterName}'`);
137+
}
138+
}
139+
}
140+
141+
function dasherize(value: string): string {
142+
return camelCase(value).replace(/([A-Z])/g, "-$1").toLowerCase();
143+
}
144+
145+
function camelCase(value: string): string {
146+
return value[0].toLowerCase() + value.substring(1);
147+
}
148+
149+
interface JSComponentParameter {
150+
name: string;
151+
type: JSComponentParameterType;
152+
}
153+
154+
// JSON-primitive types, plus for those whose .NET equivalent isn't nullable, a '?' to indicate nullability
155+
// This allows custom element authors to coerce attribute strings into the appropriate type
156+
type JSComponentParameterType = 'string' | 'boolean' | 'boolean?' | 'number' | 'number?' | 'object';
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"presets": [ [ "@babel/preset-env", { "targets": { "node": true } } ] ]
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
**/*.js
2+
**/*.js.map
3+
**/*.txt

0 commit comments

Comments
 (0)