-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added fluent ComponentRenderer type - fixes #12
- Loading branch information
1 parent
040e8b2
commit 5ae7103
Showing
7 changed files
with
530 additions
and
12 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
using BlazorTemplater.Library; | ||
using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
using System; | ||
|
||
namespace BlazorTemplater.Tests | ||
{ | ||
/// <summary> | ||
/// | ||
/// </summary> | ||
[TestClass] | ||
public class ComponentRenderer_Tests | ||
{ | ||
[TestMethod] | ||
public void Ctor_Test() | ||
{ | ||
var builder = new ComponentRenderer<Simple>(); | ||
|
||
Assert.IsNotNull(builder); | ||
} | ||
|
||
#region Simple render | ||
|
||
/// <summary> | ||
/// Render a component (no service injection or parameters) | ||
/// </summary> | ||
[TestMethod] | ||
public void Simple_Test() | ||
{ | ||
const string expected = @"<b>Jan 1st is 2021-01-01</b>"; | ||
var actual = new ComponentRenderer<Simple>() | ||
.Render(); | ||
|
||
Console.WriteLine(actual); | ||
Assert.AreEqual(expected, actual); | ||
} | ||
|
||
#endregion Simple render | ||
|
||
#region Parameters | ||
|
||
/// <summary> | ||
/// Test a component with a parameter | ||
/// </summary> | ||
[TestMethod] | ||
public void ComponentBuilder_Parameters_Test() | ||
{ | ||
// expected output | ||
const string expected = "<p>Steve Sanderson is awesome!</p>"; | ||
|
||
var model = new TestModel() | ||
{ | ||
Name = "Steve Sanderson", | ||
Description = "is awesome" | ||
}; | ||
|
||
var html = new ComponentRenderer<Parameters>() | ||
.Set(c => c.Model, model) | ||
.Render(); | ||
|
||
// trim leading space and trailing CRLF from output | ||
var actual = html.Trim(); | ||
|
||
Console.WriteLine(actual); | ||
Assert.AreEqual(expected, actual); | ||
} | ||
|
||
/// <summary> | ||
/// Test a component with a parameter | ||
/// </summary> | ||
[TestMethod] | ||
public void ComponentBuilder_Parameters_TestHtmlEncoding() | ||
{ | ||
// expected output | ||
const string expected = "<p>Safia & Pranav are awesome too!</p>"; | ||
|
||
var templater = new Templater(); | ||
var model = new TestModel() | ||
{ | ||
Name = "Safia & Pranav", // the text here is HTML encoded | ||
Description = "are awesome too" | ||
}; | ||
var html = new ComponentRenderer<Parameters>() | ||
.Set(c => c.Model, model) | ||
.Render(); | ||
|
||
// trim leading space and trailing CRLF from output | ||
var actual = html.Trim(); | ||
|
||
Console.WriteLine(actual); | ||
Assert.AreEqual(expected, actual); | ||
} | ||
|
||
/// <summary> | ||
/// Test a component with a parameter which isn't set | ||
/// </summary> | ||
[TestMethod] | ||
public void ComponentBuilder_Parameters_TestIfModelNotSet() | ||
{ | ||
// expected output | ||
const string expected = "<p>No model!</p>"; | ||
|
||
var html = new ComponentRenderer<Parameters>() | ||
.Render(); | ||
|
||
// trim leading space and trailing CRLF from output | ||
var actual = html.Trim(); | ||
|
||
Console.WriteLine(actual); | ||
Assert.AreEqual(expected, actual); | ||
} | ||
|
||
#endregion Parameters | ||
|
||
#region Errors | ||
|
||
/// <summary> | ||
/// Test rendering model with error (null reference is expected) | ||
/// </summary> | ||
[TestMethod] | ||
public void ComponentRenderer_Error_Test() | ||
{ | ||
var templater = new Templater(); | ||
|
||
// we should get a NullReferenceException thrown as Model parameter is not set | ||
Assert.ThrowsException<NullReferenceException>(() => | ||
{ | ||
_ = new ComponentRenderer<ErrorTest>().Render(); | ||
}); | ||
} | ||
|
||
#endregion Errors | ||
|
||
#region Dependency Injection | ||
|
||
[TestMethod] | ||
public void AddService_Test() | ||
{ | ||
// set up | ||
const int a = 2; | ||
const int b = 3; | ||
const int c = a + b; | ||
string expected = $"<p>If you add {a} and {b} you get {c}</p>"; | ||
|
||
// fluent ComponentBuilder approach | ||
var actual = new ComponentRenderer<ServiceInjection>() | ||
.AddService<ITestService>(new TestService()) | ||
.Set(p => p.A, a) | ||
.Set(p => p.B, b) | ||
.Render(); | ||
|
||
Console.WriteLine(actual); | ||
Assert.AreEqual(expected, actual); | ||
} | ||
|
||
#endregion | ||
|
||
#region Nesting | ||
|
||
/// <summary> | ||
/// Test that a component containing other components render correctly | ||
/// </summary> | ||
[TestMethod] | ||
public void ComponentRenderer_Nested_Test() | ||
{ | ||
// expected output | ||
// the spaces before the <p> come from the Parameters.razor component | ||
// on Windows the string contains \r\n and on unix it's just \n | ||
string expected = $"<b>Jan 1st is 2021-01-01</b>{Environment.NewLine} <p>Dan Roth is cool!</p>"; | ||
|
||
var templater = new Templater(); | ||
var model = new TestModel() | ||
{ | ||
Name = "Dan Roth", | ||
Description = "is cool" | ||
}; | ||
var html = new ComponentRenderer<NestedComponents>() | ||
.Set(c => c.Model, model) | ||
.Render(); | ||
|
||
// trim leading space and trailing CRLF from output | ||
var actual = html.Trim(); | ||
|
||
Console.WriteLine(actual); | ||
Assert.AreEqual(expected, actual); | ||
} | ||
|
||
#endregion | ||
|
||
#region Cascading Values | ||
|
||
[TestMethod] | ||
public void ComponentRenderer_CascadingValues_Test() | ||
{ | ||
const string expected = "<p>The name is Bill</p>"; | ||
var info = new CascadeInfo() { Name = "Bill" }; | ||
|
||
var html = new ComponentRenderer<CascadeParent>() | ||
.Set(c => c.Info, info) | ||
.Render(); | ||
|
||
// trim leading space and trailing CRLF from output | ||
var actual = html.Trim(); | ||
|
||
Assert.AreEqual(expected, actual); | ||
|
||
} | ||
|
||
#endregion | ||
|
||
} | ||
} |
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,127 @@ | ||
using Microsoft.AspNetCore.Components; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq.Expressions; | ||
using System.Reflection; | ||
|
||
namespace BlazorTemplater | ||
{ | ||
/* | ||
* Adapted from ParameterViewBuilder.cs in Egil Hansen's Genzor | ||
* https://github.com/egil/genzor/blob/main/src/genzor/ParameterViewBuilder.cs | ||
* Thanks for the suggestion, Egil! | ||
*/ | ||
|
||
/// <summary> | ||
/// Fluent component renderer | ||
/// </summary> | ||
/// <typeparam name="TComponent">Type of component to render</typeparam> | ||
public class ComponentRenderer<TComponent> where TComponent : IComponent | ||
{ | ||
private const string ChildContent = nameof(ChildContent); | ||
private static readonly Type TComponentType = typeof(TComponent); | ||
|
||
private readonly Dictionary<string, object> parameters = new(StringComparer.Ordinal); | ||
private readonly Templater templater; | ||
|
||
#region Ctor | ||
|
||
/// <summary> | ||
/// Create a new renderer | ||
/// </summary> | ||
public ComponentRenderer() | ||
{ | ||
templater = new Templater(); | ||
} | ||
|
||
#endregion Ctor | ||
|
||
#region Services | ||
|
||
/// <summary> | ||
/// Fluent add-service with contract and implementation | ||
/// </summary> | ||
/// <typeparam name="TContract"></typeparam> | ||
/// <typeparam name="TImplementation"></typeparam> | ||
/// <param name="implementation"></param> | ||
/// <returns></returns> | ||
public ComponentRenderer<TComponent> AddService<TContract, TImplementation>(TImplementation implementation) where TImplementation : TContract | ||
|
||
{ | ||
templater.AddService<TContract, TImplementation>(implementation); | ||
return this; | ||
} | ||
|
||
/// <summary> | ||
/// Fluent add-service with implemention | ||
/// </summary> | ||
/// <typeparam name="TImplementation"></typeparam> | ||
/// <param name="implementation"></param> | ||
/// <returns></returns> | ||
public ComponentRenderer<TComponent> AddService<TImplementation>(TImplementation implementation) | ||
|
||
{ | ||
templater.AddService<TImplementation>(implementation); | ||
return this; | ||
} | ||
|
||
#endregion Services | ||
|
||
#region Set Parameters | ||
|
||
/// <summary> | ||
/// Sets the <paramref name="value"/> to the parameter selected with the <paramref name="parameterSelector"/>. | ||
/// </summary> | ||
/// <typeparam name="TValue">Type of <paramref name="value"/>.</typeparam> | ||
/// <param name="parameterSelector">A lambda function that selects the parameter.</param> | ||
/// <param name="value">The value to pass to <typeparamref name="TComponent"/>.</param> | ||
/// <returns>This <see cref="ComponentRenderer{TComponent}"/> so that additional calls can be chained.</returns> | ||
public ComponentRenderer<TComponent> Set<TValue>(Expression<Func<TComponent, TValue>> parameterSelector, TValue value) | ||
{ | ||
if (value is null) | ||
throw new ArgumentNullException(nameof(value)); | ||
|
||
parameters.Add(GetParameterName(parameterSelector), value); | ||
return this; | ||
} | ||
|
||
private static string GetParameterName<TValue>(Expression<Func<TComponent, TValue>> parameterSelector) | ||
{ | ||
if (parameterSelector is null) | ||
throw new ArgumentNullException(nameof(parameterSelector)); | ||
|
||
if (parameterSelector.Body is not MemberExpression memberExpression || | ||
memberExpression.Member is not PropertyInfo propInfoCandidate) | ||
throw new ArgumentException($"The parameter selector '{parameterSelector}' does not resolve to a public property on the component '{typeof(TComponent)}'.", nameof(parameterSelector)); | ||
|
||
var propertyInfo = propInfoCandidate.DeclaringType != TComponentType | ||
? TComponentType.GetProperty(propInfoCandidate.Name, propInfoCandidate.PropertyType) | ||
: propInfoCandidate; | ||
|
||
var paramAttr = propertyInfo?.GetCustomAttribute<ParameterAttribute>(inherit: true); | ||
|
||
if (propertyInfo is null || paramAttr is null) | ||
throw new ArgumentException($"The parameter selector '{parameterSelector}' does not resolve to a public property on the component '{typeof(TComponent)}' with a [Parameter] or [CascadingParameter] attribute.", nameof(parameterSelector)); | ||
|
||
return propertyInfo.Name; | ||
} | ||
|
||
#endregion Set Parameters | ||
|
||
/// <summary> | ||
/// Builds the <see cref="ParameterView"/> with the parameters added to the builder. | ||
/// </summary> | ||
/// <returns>The created <see cref="ParameterView"/>.</returns> | ||
private ParameterView Build() => ParameterView.FromDictionary(parameters); | ||
|
||
/// <summary> | ||
/// Render the component to HTML | ||
/// </summary> | ||
/// <returns></returns> | ||
public string Render() | ||
{ | ||
// renders the component and returns the markup HTML | ||
return templater.RenderComponent<TComponent>(Build()); | ||
} | ||
} | ||
} |
Oops, something went wrong.