-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature(#86): Add Blazor Component Rendering Service
- Loading branch information
1 parent
f1802a6
commit bf00590
Showing
35 changed files
with
1,589 additions
and
40 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
// Copyright (c) Fusonic GmbH. All rights reserved. | ||
// Licensed under the MIT License. See LICENSE file in the project root for license information. | ||
|
||
using System.Globalization; | ||
using System.Reflection; | ||
using Microsoft.AspNetCore.Components; | ||
using Microsoft.AspNetCore.Components.Web; | ||
|
||
namespace Fusonic.Extensions.AspNetCore.Blazor; | ||
|
||
public class BlazorRenderingService(HtmlRenderer htmlRenderer) : IBlazorRenderingService | ||
{ | ||
public async Task<string> RenderComponent(IComponentModel model, CultureInfo culture) | ||
{ | ||
var modelType = model.GetType(); | ||
var componentType = model.ComponentType; | ||
|
||
var modelProperty = componentType.GetProperties(BindingFlags.Public | BindingFlags.Instance) | ||
.FirstOrDefault(p => p.PropertyType == modelType) | ||
?? throw new ArgumentNullException($"The Component {componentType.Name} is missing a property of type {modelType.Name}."); | ||
|
||
return await RenderComponent(componentType, culture, new() { [modelProperty.Name] = model }, beforeRender: null); | ||
} | ||
|
||
public Task<string> RenderComponent<T>(CultureInfo culture, Dictionary<string, object?>? dictionary = null) where T : IComponent | ||
=> RenderComponentBase(typeof(T), culture, dictionary is null ? ParameterView.Empty : ParameterView.FromDictionary(dictionary), beforeRender: null); | ||
|
||
public Task<string> RenderComponent(Type componentType, CultureInfo culture, Dictionary<string, object?>? dictionary = null, Action? beforeRender = null) | ||
=> RenderComponentBase(componentType, culture, dictionary is null ? ParameterView.Empty : ParameterView.FromDictionary(dictionary), beforeRender); | ||
|
||
private async Task<string> RenderComponentBase(Type componentType, CultureInfo culture, ParameterView parameters, Action? beforeRender) | ||
=> await htmlRenderer.Dispatcher.InvokeAsync(async () => | ||
{ | ||
var currentCulture = (CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture); | ||
try | ||
{ | ||
// The razor renderer takes the culture from the current thread culture. | ||
CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = culture; | ||
|
||
beforeRender?.Invoke(); | ||
|
||
var output = await htmlRenderer.RenderComponentAsync(componentType, parameters); | ||
return output.ToHtmlString(); | ||
} | ||
finally | ||
{ | ||
(CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture) = currentCulture; | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
// Copyright (c) Fusonic GmbH. All rights reserved. | ||
// Licensed under the MIT License. See LICENSE file in the project root for license information. | ||
|
||
using System.Globalization; | ||
using Microsoft.AspNetCore.Components; | ||
|
||
namespace Fusonic.Extensions.AspNetCore.Blazor; | ||
|
||
public interface IBlazorRenderingService | ||
{ | ||
Task<string> RenderComponent(IComponentModel model, CultureInfo culture); | ||
Task<string> RenderComponent<T>(CultureInfo culture, Dictionary<string, object?>? dictionary = null) where T : IComponent; | ||
Task<string> RenderComponent(Type componentType, CultureInfo culture, Dictionary<string, object?>? dictionary = null, Action? beforeRender = null); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
using Microsoft.AspNetCore.Components; | ||
|
||
public interface IComponentModel<T> : IComponentModel where T : IComponent | ||
{ | ||
Type IComponentModel.ComponentType => typeof(T); | ||
} | ||
|
||
public interface IComponentModel | ||
{ | ||
Type ComponentType { get; } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
// Copyright (c) Fusonic GmbH. All rights reserved. | ||
// Licensed under the MIT License. See LICENSE file in the project root for license information. | ||
|
||
using System.Globalization; | ||
using System.Text.RegularExpressions; | ||
using System.Web; | ||
using FluentAssertions; | ||
using Fusonic.Extensions.AspNetCore.Blazor; | ||
using Xunit; | ||
|
||
namespace Fusonic.Extensions.AspNetCore.Tests.Blazor; | ||
|
||
public partial class BlazorRenderingServiceTests : TestBase | ||
{ | ||
private readonly BlazorRenderingService blazorRenderingService; | ||
|
||
public BlazorRenderingServiceTests(TestFixture fixture) : base(fixture) | ||
{ | ||
blazorRenderingService = GetInstance<BlazorRenderingService>(); | ||
} | ||
|
||
[Fact] | ||
public async Task Succeeds() | ||
{ | ||
// Arrange | ||
var component = new TestComponent.ComponentModel("Hello World!"); | ||
|
||
// Act | ||
var html = await blazorRenderingService.RenderComponent(component, CultureInfo.CurrentCulture); | ||
|
||
// Assert | ||
html.Should().Be("<body>Hello World!</body>"); | ||
} | ||
|
||
[Theory] | ||
[InlineData("de-CH")] | ||
[InlineData("de-AT")] | ||
[InlineData("fr-CH")] | ||
[InlineData("it-CH")] | ||
[InlineData("en-CH")] | ||
public async Task Rendering_RespectsCulture(string cultureName) | ||
{ | ||
// Arrange | ||
var cultureInfo = new CultureInfo(cultureName); | ||
|
||
var viewModel = new TestCultureComponent.ComponentModel(new(2019, 1, 9), 12345678.89); | ||
|
||
var expectedDate = AsString($"{viewModel.Date:d}"); | ||
var expectedMonth = AsString($"{viewModel.Date:MMMM}"); | ||
var expectedNumber = AsString($"{viewModel.Number:n}"); | ||
|
||
// Act | ||
var body = await blazorRenderingService.RenderComponent(viewModel, cultureInfo); | ||
|
||
// Assert | ||
var regex = GetHtmlParagraphContentsRegex(); | ||
var matches = regex.Matches(body); | ||
|
||
Match(0).Should().Be(expectedDate); | ||
Match(1).Should().Be(expectedMonth); | ||
Match(2).Should().Be(expectedNumber); | ||
|
||
string Match(int idx) => HttpUtility.HtmlDecode(matches[idx].Groups[1].Value); | ||
string AsString(IFormattable f) => f.ToString(null, cultureInfo); | ||
} | ||
|
||
[GeneratedRegex("<p>(.*?)<\\/p>")] | ||
private static partial Regex GetHtmlParagraphContentsRegex(); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
@using Fusonic.Extensions.AspNetCore.Razor | ||
|
||
<body>@Model.Name</body> | ||
|
||
@code { | ||
[Parameter] | ||
public required ComponentModel Model { get; init; } | ||
|
||
public record ComponentModel(string Name) : IComponentModel<TestComponent>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
@using Fusonic.Extensions.AspNetCore.Razor | ||
|
||
<html> | ||
<head> | ||
<meta charset="UTF-8" /> | ||
</head> | ||
<body> | ||
<p>@($"{Model.Date:d}")</p> | ||
<p>@($"{Model.Date:MMMM}")</p> | ||
<p>@($"{Model.Number:N}")</p> | ||
</body> | ||
</html> | ||
|
||
@code { | ||
[Parameter] | ||
public required ComponentModel Model { get; init; } | ||
|
||
public record ComponentModel(DateTime Date, double Number) : IComponentModel<TestCultureComponent>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
// Copyright (c) Fusonic GmbH. All rights reserved. | ||
// Licensed under the MIT License. See LICENSE file in the project root for license information. | ||
|
||
using System.Globalization; | ||
using System.Reflection; | ||
using Fusonic.Extensions.AspNetCore.Blazor; | ||
using Microsoft.Extensions.Localization; | ||
|
||
namespace Fusonic.Extensions.Email; | ||
|
||
public class BlazorEmailRenderingService(IBlazorRenderingService blazorRenderingService, IStringLocalizerFactory stringLocalizerFactory) : IEmailRenderingService | ||
{ | ||
private const string DefaultSubjectKey = "Subject"; | ||
|
||
public bool Supports(object model) => model is IComponentModel; | ||
|
||
/// <inheritdoc /> | ||
public async Task<(string Subject, string Body)> RenderAsync( | ||
object model, | ||
CultureInfo culture, | ||
string? subjectKey, | ||
object[]? subjectFormatParameters = null) | ||
{ | ||
var modelType = model.GetType(); | ||
|
||
if (model is not IComponentModel componentModel) | ||
throw new ArgumentException($"The type {modelType.Name} does not implement {nameof(IComponentModel)}.", nameof(model)); | ||
|
||
var componentType = componentModel.ComponentType; | ||
var modelProperty = componentType.GetProperties(BindingFlags.Public | BindingFlags.Instance) | ||
.FirstOrDefault(p => p.PropertyType == modelType) | ||
?? throw new ArgumentNullException(nameof(model), $"The Component {componentType.Name} is missing a property of type {modelType.Name}."); | ||
|
||
subjectKey ??= (componentModel as IProvideEmailSubject)?.SubjectKey ?? DefaultSubjectKey; | ||
var subject = subjectKey; | ||
|
||
var body = await RenderAsync( | ||
componentType, | ||
modelProperty.Name, | ||
componentModel, | ||
culture, | ||
beforeRender: SetSubject); | ||
|
||
return (subject, body); | ||
|
||
void SetSubject() | ||
{ | ||
var subjectLocalization = stringLocalizerFactory.Create(componentType).GetString(subjectKey, subjectFormatParameters ?? []); | ||
subject = subjectLocalization.ResourceNotFound ? subjectKey : subjectLocalization.Value; | ||
} | ||
} | ||
|
||
private async Task<string> RenderAsync(Type componentType, string modelPropertyName, IComponentModel model, CultureInfo culture, Action beforeRender) | ||
{ | ||
var content = await blazorRenderingService.RenderComponent(componentType, culture, new() { [modelPropertyName] = model }, beforeRender); | ||
content = CssInliner.Inline(content); | ||
return content; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.