Add DisplayName to inputs (#24029)
Add a `DisplayName` parameter to `InputBase`, which is used in validation messages instead of `FieldIdentifier.FieldName`. - This works for `InputDate`, `InputNumber` and `InputSelect`. - Extracted some shared code, just like what @StephanZahariev did in his PR. Addresses #11414
This commit is contained in:
parent
bbb5bb7755
commit
5bc2c49ed5
|
|
@ -60,6 +60,8 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
[System.Diagnostics.CodeAnalysis.AllowNullAttribute]
|
[System.Diagnostics.CodeAnalysis.AllowNullAttribute]
|
||||||
protected TValue CurrentValue { get { throw null; } set { } }
|
protected TValue CurrentValue { get { throw null; } set { } }
|
||||||
protected string? CurrentValueAsString { get { throw null; } set { } }
|
protected string? CurrentValueAsString { get { throw null; } set { } }
|
||||||
|
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||||
|
public string? DisplayName { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||||
protected Microsoft.AspNetCore.Components.Forms.EditContext EditContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
protected Microsoft.AspNetCore.Components.Forms.EditContext EditContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||||
protected internal Microsoft.AspNetCore.Components.Forms.FieldIdentifier FieldIdentifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
protected internal Microsoft.AspNetCore.Components.Forms.FieldIdentifier FieldIdentifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,12 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Parameter] public Expression<Func<TValue>>? ValueExpression { get; set; }
|
[Parameter] public Expression<Func<TValue>>? ValueExpression { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the display name for this field.
|
||||||
|
/// <para>This value is used when generating error messages when the input value fails to parse correctly.</para>
|
||||||
|
/// </summary>
|
||||||
|
[Parameter] public string? DisplayName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the associated <see cref="Forms.EditContext"/>.
|
/// Gets the associated <see cref="Forms.EditContext"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -75,7 +75,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
validationErrorMessage = string.Format(ParsingErrorMessage, FieldIdentifier.FieldName);
|
validationErrorMessage = string.Format(ParsingErrorMessage, DisplayName ?? FieldIdentifier.FieldName);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
result = default;
|
result = default;
|
||||||
validationErrorMessage = $"The {input.FieldIdentifier.FieldName} field is not valid.";
|
validationErrorMessage = $"The {input.DisplayName ?? input.FieldIdentifier.FieldName} field is not valid.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
validationErrorMessage = string.Format(ParsingErrorMessage, FieldIdentifier.FieldName);
|
validationErrorMessage = string.Format(ParsingErrorMessage, DisplayName ?? FieldIdentifier.FieldName);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Linq.Expressions;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Components.Rendering;
|
|
||||||
using Microsoft.AspNetCore.Components.RenderTree;
|
|
||||||
using Microsoft.AspNetCore.Components.Test.Helpers;
|
using Microsoft.AspNetCore.Components.Test.Helpers;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
@ -35,7 +32,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
// Arrange
|
// Arrange
|
||||||
var model = new TestModel();
|
var model = new TestModel();
|
||||||
var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>> { EditContext = new EditContext(model), ValueExpression = () => model.StringProperty };
|
var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>> { EditContext = new EditContext(model), ValueExpression = () => model.StringProperty };
|
||||||
await RenderAndGetTestInputComponentAsync(rootComponent);
|
await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
|
|
||||||
// Act/Assert
|
// Act/Assert
|
||||||
rootComponent.EditContext = new EditContext(model);
|
rootComponent.EditContext = new EditContext(model);
|
||||||
|
|
@ -51,7 +48,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>> { EditContext = new EditContext(model) };
|
var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>> { EditContext = new EditContext(model) };
|
||||||
|
|
||||||
// Act/Assert
|
// Act/Assert
|
||||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => RenderAndGetTestInputComponentAsync(rootComponent));
|
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => InputRenderer.RenderAndGetComponent(rootComponent));
|
||||||
Assert.Contains($"{typeof(TestInputComponent<string>)} requires a value for the 'ValueExpression' parameter. Normally this is provided automatically when using 'bind-Value'.", ex.Message);
|
Assert.Contains($"{typeof(TestInputComponent<string>)} requires a value for the 'ValueExpression' parameter. Normally this is provided automatically when using 'bind-Value'.", ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,7 +65,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal("some value", inputComponent.CurrentValue);
|
Assert.Equal("some value", inputComponent.CurrentValue);
|
||||||
|
|
@ -87,7 +84,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Same(rootComponent.EditContext, inputComponent.EditContext);
|
Assert.Same(rootComponent.EditContext, inputComponent.EditContext);
|
||||||
|
|
@ -106,7 +103,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
};
|
};
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
Assert.Equal(FieldIdentifier.Create(() => model.StringProperty), inputComponent.FieldIdentifier);
|
Assert.Equal(FieldIdentifier.Create(() => model.StringProperty), inputComponent.FieldIdentifier);
|
||||||
|
|
@ -123,7 +120,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
Value = "initial value",
|
Value = "initial value",
|
||||||
ValueExpression = () => model.StringProperty
|
ValueExpression = () => model.StringProperty
|
||||||
};
|
};
|
||||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
Assert.Equal("initial value", inputComponent.CurrentValue);
|
Assert.Equal("initial value", inputComponent.CurrentValue);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -146,7 +143,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
ValueChanged = val => valueChangedCallLog.Add(val),
|
ValueChanged = val => valueChangedCallLog.Add(val),
|
||||||
ValueExpression = () => model.StringProperty
|
ValueExpression = () => model.StringProperty
|
||||||
};
|
};
|
||||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
Assert.Empty(valueChangedCallLog);
|
Assert.Empty(valueChangedCallLog);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -169,7 +166,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
ValueChanged = val => valueChangedCallLog.Add(val),
|
ValueChanged = val => valueChangedCallLog.Add(val),
|
||||||
ValueExpression = () => model.StringProperty
|
ValueExpression = () => model.StringProperty
|
||||||
};
|
};
|
||||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
Assert.Empty(valueChangedCallLog);
|
Assert.Empty(valueChangedCallLog);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -190,7 +187,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
Value = "initial value",
|
Value = "initial value",
|
||||||
ValueExpression = () => model.StringProperty
|
ValueExpression = () => model.StringProperty
|
||||||
};
|
};
|
||||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
Assert.False(rootComponent.EditContext.IsModified(() => model.StringProperty));
|
Assert.False(rootComponent.EditContext.IsModified(() => model.StringProperty));
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
@ -213,7 +210,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
|
var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
|
||||||
|
|
||||||
// Act/Assert: Initially, it's valid and unmodified
|
// Act/Assert: Initially, it's valid and unmodified
|
||||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
Assert.Equal("valid", inputComponent.CssClass); // no Class was specified
|
Assert.Equal("valid", inputComponent.CssClass); // no Class was specified
|
||||||
|
|
||||||
// Act/Assert: Modify the field
|
// Act/Assert: Modify the field
|
||||||
|
|
@ -251,7 +248,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
|
var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
|
||||||
|
|
||||||
// Act/Assert
|
// Act/Assert
|
||||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
Assert.Equal("my-class other-class valid", inputComponent.CssClass);
|
Assert.Equal("my-class other-class valid", inputComponent.CssClass);
|
||||||
|
|
||||||
// Act/Assert: Retains custom class when changing field class
|
// Act/Assert: Retains custom class when changing field class
|
||||||
|
|
@ -270,7 +267,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
Value = new DateTime(1915, 3, 2),
|
Value = new DateTime(1915, 3, 2),
|
||||||
ValueExpression = () => model.DateProperty
|
ValueExpression = () => model.DateProperty
|
||||||
};
|
};
|
||||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
|
|
||||||
// Act/Assert
|
// Act/Assert
|
||||||
Assert.Equal("1915/03/02", inputComponent.CurrentValueAsString);
|
Assert.Equal("1915/03/02", inputComponent.CurrentValueAsString);
|
||||||
|
|
@ -289,7 +286,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
ValueExpression = () => model.DateProperty
|
ValueExpression = () => model.DateProperty
|
||||||
};
|
};
|
||||||
var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty);
|
var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty);
|
||||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
var numValidationStateChanges = 0;
|
var numValidationStateChanges = 0;
|
||||||
rootComponent.EditContext.OnValidationStateChanged += (sender, eventArgs) => { numValidationStateChanges++; };
|
rootComponent.EditContext.OnValidationStateChanged += (sender, eventArgs) => { numValidationStateChanges++; };
|
||||||
|
|
||||||
|
|
@ -319,7 +316,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
ValueExpression = () => model.DateProperty
|
ValueExpression = () => model.DateProperty
|
||||||
};
|
};
|
||||||
var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty);
|
var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty);
|
||||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
var numValidationStateChanges = 0;
|
var numValidationStateChanges = 0;
|
||||||
rootComponent.EditContext.OnValidationStateChanged += (sender, eventArgs) => { numValidationStateChanges++; };
|
rootComponent.EditContext.OnValidationStateChanged += (sender, eventArgs) => { numValidationStateChanges++; };
|
||||||
|
|
||||||
|
|
@ -470,21 +467,6 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
Assert.Equal("userSpecifiedValue", component.AdditionalAttributes["aria-invalid"]);
|
Assert.Equal("userSpecifiedValue", component.AdditionalAttributes["aria-invalid"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TComponent FindComponent<TComponent>(CapturedBatch batch)
|
|
||||||
=> batch.ReferenceFrames
|
|
||||||
.Where(f => f.FrameType == RenderTreeFrameType.Component)
|
|
||||||
.Select(f => f.Component)
|
|
||||||
.OfType<TComponent>()
|
|
||||||
.Single();
|
|
||||||
|
|
||||||
private static async Task<TComponent> RenderAndGetTestInputComponentAsync<TValue, TComponent>(TestInputHostComponent<TValue, TComponent> hostComponent) where TComponent : TestInputComponent<TValue>
|
|
||||||
{
|
|
||||||
var testRenderer = new TestRenderer();
|
|
||||||
var componentId = testRenderer.AssignRootComponentId(hostComponent);
|
|
||||||
await testRenderer.RenderRootComponentAsync(componentId);
|
|
||||||
return FindComponent<TComponent>(testRenderer.Batches.Single());
|
|
||||||
}
|
|
||||||
|
|
||||||
class TestModel
|
class TestModel
|
||||||
{
|
{
|
||||||
public string StringProperty { get; set; }
|
public string StringProperty { get; set; }
|
||||||
|
|
@ -530,7 +512,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestDateInputComponent : TestInputComponent<DateTime>
|
private class TestDateInputComponent : TestInputComponent<DateTime>
|
||||||
{
|
{
|
||||||
protected override string FormatValueAsString(DateTime value)
|
protected override string FormatValueAsString(DateTime value)
|
||||||
=> value.ToString("yyyy/MM/dd");
|
=> value.ToString("yyyy/MM/dd");
|
||||||
|
|
@ -549,35 +531,5 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TestInputHostComponent<TValue, TComponent> : AutoRenderComponent where TComponent : TestInputComponent<TValue>
|
|
||||||
{
|
|
||||||
public Dictionary<string, object> AdditionalAttributes { get; set; }
|
|
||||||
|
|
||||||
public EditContext EditContext { get; set; }
|
|
||||||
|
|
||||||
public TValue Value { get; set; }
|
|
||||||
|
|
||||||
public Action<TValue> ValueChanged { get; set; }
|
|
||||||
|
|
||||||
public Expression<Func<TValue>> ValueExpression { get; set; }
|
|
||||||
|
|
||||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
|
||||||
{
|
|
||||||
builder.OpenComponent<CascadingValue<EditContext>>(0);
|
|
||||||
builder.AddAttribute(1, "Value", EditContext);
|
|
||||||
builder.AddAttribute(2, "ChildContent", new RenderFragment(childBuilder =>
|
|
||||||
{
|
|
||||||
childBuilder.OpenComponent<TComponent>(0);
|
|
||||||
childBuilder.AddAttribute(0, "Value", Value);
|
|
||||||
childBuilder.AddAttribute(1, "ValueChanged",
|
|
||||||
EventCallback.Factory.Create(this, ValueChanged));
|
|
||||||
childBuilder.AddAttribute(2, "ValueExpression", ValueExpression);
|
|
||||||
childBuilder.AddMultipleAttributes(3, AdditionalAttributes);
|
|
||||||
childBuilder.CloseComponent();
|
|
||||||
}));
|
|
||||||
builder.CloseComponent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.Forms
|
||||||
|
{
|
||||||
|
public class InputDateTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidationErrorUsesDisplayAttributeName()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new TestModel();
|
||||||
|
var rootComponent = new TestInputHostComponent<DateTime, TestInputDateComponent>
|
||||||
|
{
|
||||||
|
EditContext = new EditContext(model),
|
||||||
|
ValueExpression = () => model.DateProperty,
|
||||||
|
AdditionalAttributes = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "DisplayName", "Date property" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty);
|
||||||
|
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await inputComponent.SetCurrentValueAsStringAsync("invalidDate");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var validationMessages = rootComponent.EditContext.GetValidationMessages(fieldIdentifier);
|
||||||
|
Assert.NotEmpty(validationMessages);
|
||||||
|
Assert.Contains("The Date property field must be a date.", validationMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestModel
|
||||||
|
{
|
||||||
|
public DateTime DateProperty { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestInputDateComponent : InputDate<DateTime>
|
||||||
|
{
|
||||||
|
public async Task SetCurrentValueAsStringAsync(string value)
|
||||||
|
{
|
||||||
|
// This is equivalent to the subclass writing to CurrentValueAsString
|
||||||
|
// (e.g., from @bind), except to simplify the test code there's an InvokeAsync
|
||||||
|
// here. In production code it wouldn't normally be required because @bind
|
||||||
|
// calls run on the sync context anyway.
|
||||||
|
await InvokeAsync(() => { base.CurrentValueAsString = value; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.Forms
|
||||||
|
{
|
||||||
|
public class InputNumberTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task ValidationErrorUsesDisplayAttributeName()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var model = new TestModel();
|
||||||
|
var rootComponent = new TestInputHostComponent<int, TestInputNumberComponent>
|
||||||
|
{
|
||||||
|
EditContext = new EditContext(model),
|
||||||
|
ValueExpression = () => model.SomeNumber,
|
||||||
|
AdditionalAttributes = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "DisplayName", "Some number" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var fieldIdentifier = FieldIdentifier.Create(() => model.SomeNumber);
|
||||||
|
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await inputComponent.SetCurrentValueAsStringAsync("notANumber");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var validationMessages = rootComponent.EditContext.GetValidationMessages(fieldIdentifier);
|
||||||
|
Assert.NotEmpty(validationMessages);
|
||||||
|
Assert.Contains("The Some number field must be a number.", validationMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestModel
|
||||||
|
{
|
||||||
|
public int SomeNumber { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestInputNumberComponent : InputNumber<int>
|
||||||
|
{
|
||||||
|
public async Task SetCurrentValueAsStringAsync(string value)
|
||||||
|
{
|
||||||
|
// This is equivalent to the subclass writing to CurrentValueAsString
|
||||||
|
// (e.g., from @bind), except to simplify the test code there's an InvokeAsync
|
||||||
|
// here. In production code it wouldn't normally be required because @bind
|
||||||
|
// calls run on the sync context anyway.
|
||||||
|
await InvokeAsync(() => { base.CurrentValueAsString = value; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Components.RenderTree;
|
||||||
|
using Microsoft.AspNetCore.Components.Test.Helpers;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.Forms
|
||||||
|
{
|
||||||
|
internal static class InputRenderer
|
||||||
|
{
|
||||||
|
public static async Task<TComponent> RenderAndGetComponent<TValue, TComponent>(TestInputHostComponent<TValue, TComponent> hostComponent)
|
||||||
|
where TComponent : InputBase<TValue>
|
||||||
|
{
|
||||||
|
var testRenderer = new TestRenderer();
|
||||||
|
var componentId = testRenderer.AssignRootComponentId(hostComponent);
|
||||||
|
await testRenderer.RenderRootComponentAsync(componentId);
|
||||||
|
return FindComponent<TComponent>(testRenderer.Batches.Single());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TComponent FindComponent<TComponent>(CapturedBatch batch)
|
||||||
|
=> batch.ReferenceFrames
|
||||||
|
.Where(f => f.FrameType == RenderTreeFrameType.Component)
|
||||||
|
.Select(f => f.Component)
|
||||||
|
.OfType<TComponent>()
|
||||||
|
.Single();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,12 +2,8 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Collections.Generic;
|
||||||
using System.Linq.Expressions;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Components.Rendering;
|
|
||||||
using Microsoft.AspNetCore.Components.RenderTree;
|
|
||||||
using Microsoft.AspNetCore.Components.Test.Helpers;
|
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components.Forms
|
namespace Microsoft.AspNetCore.Components.Forms
|
||||||
|
|
@ -19,12 +15,12 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var model = new TestModel();
|
var model = new TestModel();
|
||||||
var rootComponent = new TestInputSelectHostComponent<TestEnum>
|
var rootComponent = new TestInputHostComponent<TestEnum, TestInputSelect<TestEnum>>
|
||||||
{
|
{
|
||||||
EditContext = new EditContext(model),
|
EditContext = new EditContext(model),
|
||||||
ValueExpression = () => model.NotNullableEnum
|
ValueExpression = () => model.NotNullableEnum
|
||||||
};
|
};
|
||||||
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
inputSelectComponent.CurrentValueAsString = "Two";
|
inputSelectComponent.CurrentValueAsString = "Two";
|
||||||
|
|
@ -38,12 +34,12 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var model = new TestModel();
|
var model = new TestModel();
|
||||||
var rootComponent = new TestInputSelectHostComponent<TestEnum>
|
var rootComponent = new TestInputHostComponent<TestEnum, TestInputSelect<TestEnum>>
|
||||||
{
|
{
|
||||||
EditContext = new EditContext(model),
|
EditContext = new EditContext(model),
|
||||||
ValueExpression = () => model.NotNullableEnum
|
ValueExpression = () => model.NotNullableEnum
|
||||||
};
|
};
|
||||||
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
inputSelectComponent.CurrentValueAsString = "";
|
inputSelectComponent.CurrentValueAsString = "";
|
||||||
|
|
@ -57,12 +53,12 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var model = new TestModel();
|
var model = new TestModel();
|
||||||
var rootComponent = new TestInputSelectHostComponent<TestEnum?>
|
var rootComponent = new TestInputHostComponent<TestEnum?, TestInputSelect<TestEnum?>>
|
||||||
{
|
{
|
||||||
EditContext = new EditContext(model),
|
EditContext = new EditContext(model),
|
||||||
ValueExpression = () => model.NullableEnum
|
ValueExpression = () => model.NullableEnum
|
||||||
};
|
};
|
||||||
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
inputSelectComponent.CurrentValueAsString = "Two";
|
inputSelectComponent.CurrentValueAsString = "Two";
|
||||||
|
|
@ -76,12 +72,12 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var model = new TestModel();
|
var model = new TestModel();
|
||||||
var rootComponent = new TestInputSelectHostComponent<TestEnum?>
|
var rootComponent = new TestInputHostComponent<TestEnum?, TestInputSelect<TestEnum?>>
|
||||||
{
|
{
|
||||||
EditContext = new EditContext(model),
|
EditContext = new EditContext(model),
|
||||||
ValueExpression = () => model.NullableEnum
|
ValueExpression = () => model.NullableEnum
|
||||||
};
|
};
|
||||||
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
inputSelectComponent.CurrentValueAsString = "";
|
inputSelectComponent.CurrentValueAsString = "";
|
||||||
|
|
@ -96,12 +92,12 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var model = new TestModel();
|
var model = new TestModel();
|
||||||
var rootComponent = new TestInputSelectHostComponent<Guid>
|
var rootComponent = new TestInputHostComponent<Guid, TestInputSelect<Guid>>
|
||||||
{
|
{
|
||||||
EditContext = new EditContext(model),
|
EditContext = new EditContext(model),
|
||||||
ValueExpression = () => model.NotNullableGuid
|
ValueExpression = () => model.NotNullableGuid
|
||||||
};
|
};
|
||||||
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var guid = Guid.NewGuid();
|
var guid = Guid.NewGuid();
|
||||||
|
|
@ -117,12 +113,12 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var model = new TestModel();
|
var model = new TestModel();
|
||||||
var rootComponent = new TestInputSelectHostComponent<Guid?>
|
var rootComponent = new TestInputHostComponent<Guid?, TestInputSelect<Guid?>>
|
||||||
{
|
{
|
||||||
EditContext = new EditContext(model),
|
EditContext = new EditContext(model),
|
||||||
ValueExpression = () => model.NullableGuid
|
ValueExpression = () => model.NullableGuid
|
||||||
};
|
};
|
||||||
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
var guid = Guid.NewGuid();
|
var guid = Guid.NewGuid();
|
||||||
|
|
@ -138,12 +134,12 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var model = new TestModel();
|
var model = new TestModel();
|
||||||
var rootComponent = new TestInputSelectHostComponent<int>
|
var rootComponent = new TestInputHostComponent<int, TestInputSelect<int>>
|
||||||
{
|
{
|
||||||
EditContext = new EditContext(model),
|
EditContext = new EditContext(model),
|
||||||
ValueExpression = () => model.NotNullableInt
|
ValueExpression = () => model.NotNullableInt
|
||||||
};
|
};
|
||||||
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
inputSelectComponent.CurrentValueAsString = "42";
|
inputSelectComponent.CurrentValueAsString = "42";
|
||||||
|
|
@ -158,12 +154,12 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var model = new TestModel();
|
var model = new TestModel();
|
||||||
var rootComponent = new TestInputSelectHostComponent<int?>
|
var rootComponent = new TestInputHostComponent<int?, TestInputSelect<int?>>
|
||||||
{
|
{
|
||||||
EditContext = new EditContext(model),
|
EditContext = new EditContext(model),
|
||||||
ValueExpression = () => model.NullableInt
|
ValueExpression = () => model.NullableInt
|
||||||
};
|
};
|
||||||
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
inputSelectComponent.CurrentValueAsString = "42";
|
inputSelectComponent.CurrentValueAsString = "42";
|
||||||
|
|
@ -172,19 +168,30 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
Assert.Equal(42, inputSelectComponent.CurrentValue);
|
Assert.Equal(42, inputSelectComponent.CurrentValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TestInputSelect<TValue> FindInputSelectComponent<TValue>(CapturedBatch batch)
|
[Fact]
|
||||||
=> batch.ReferenceFrames
|
public async Task ValidationErrorUsesDisplayAttributeName()
|
||||||
.Where(f => f.FrameType == RenderTreeFrameType.Component)
|
|
||||||
.Select(f => f.Component)
|
|
||||||
.OfType<TestInputSelect<TValue>>()
|
|
||||||
.Single();
|
|
||||||
|
|
||||||
private static async Task<TestInputSelect<TValue>> RenderAndGetTestInputComponentAsync<TValue>(TestInputSelectHostComponent<TValue> hostComponent)
|
|
||||||
{
|
{
|
||||||
var testRenderer = new TestRenderer();
|
// Arrange
|
||||||
var componentId = testRenderer.AssignRootComponentId(hostComponent);
|
var model = new TestModel();
|
||||||
await testRenderer.RenderRootComponentAsync(componentId);
|
var rootComponent = new TestInputHostComponent<int, TestInputSelect<int>>
|
||||||
return FindInputSelectComponent<TValue>(testRenderer.Batches.Single());
|
{
|
||||||
|
EditContext = new EditContext(model),
|
||||||
|
ValueExpression = () => model.NotNullableInt,
|
||||||
|
AdditionalAttributes = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "DisplayName", "Some number" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var fieldIdentifier = FieldIdentifier.Create(() => model.NotNullableInt);
|
||||||
|
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await inputSelectComponent.SetCurrentValueAsStringAsync("invalidNumber");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var validationMessages = rootComponent.EditContext.GetValidationMessages(fieldIdentifier);
|
||||||
|
Assert.NotEmpty(validationMessages);
|
||||||
|
Assert.Contains("The Some number field is not valid.", validationMessages);
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TestEnum
|
enum TestEnum
|
||||||
|
|
@ -218,25 +225,13 @@ namespace Microsoft.AspNetCore.Components.Forms
|
||||||
get => base.CurrentValueAsString;
|
get => base.CurrentValueAsString;
|
||||||
set => base.CurrentValueAsString = value;
|
set => base.CurrentValueAsString = value;
|
||||||
}
|
}
|
||||||
}
|
public async Task SetCurrentValueAsStringAsync(string value)
|
||||||
|
|
||||||
class TestInputSelectHostComponent<TValue> : AutoRenderComponent
|
|
||||||
{
|
|
||||||
public EditContext EditContext { get; set; }
|
|
||||||
|
|
||||||
public Expression<Func<TValue>> ValueExpression { get; set; }
|
|
||||||
|
|
||||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
|
||||||
{
|
{
|
||||||
builder.OpenComponent<CascadingValue<EditContext>>(0);
|
// This is equivalent to the subclass writing to CurrentValueAsString
|
||||||
builder.AddAttribute(1, "Value", EditContext);
|
// (e.g., from @bind), except to simplify the test code there's an InvokeAsync
|
||||||
builder.AddAttribute(2, "ChildContent", new RenderFragment(childBuilder =>
|
// here. In production code it wouldn't normally be required because @bind
|
||||||
{
|
// calls run on the sync context anyway.
|
||||||
childBuilder.OpenComponent<TestInputSelect<TValue>>(0);
|
await InvokeAsync(() => { base.CurrentValueAsString = value; });
|
||||||
childBuilder.AddAttribute(0, "ValueExpression", ValueExpression);
|
|
||||||
childBuilder.CloseComponent();
|
|
||||||
}));
|
|
||||||
builder.CloseComponent();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using Microsoft.AspNetCore.Components.Rendering;
|
||||||
|
using Microsoft.AspNetCore.Components.Test.Helpers;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.Forms
|
||||||
|
{
|
||||||
|
internal class TestInputHostComponent<TValue, TComponent> : AutoRenderComponent where TComponent : InputBase<TValue>
|
||||||
|
{
|
||||||
|
public Dictionary<string, object> AdditionalAttributes { get; set; }
|
||||||
|
|
||||||
|
public EditContext EditContext { get; set; }
|
||||||
|
|
||||||
|
public TValue Value { get; set; }
|
||||||
|
|
||||||
|
public Action<TValue> ValueChanged { get; set; }
|
||||||
|
|
||||||
|
public Expression<Func<TValue>> ValueExpression { get; set; }
|
||||||
|
|
||||||
|
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||||
|
{
|
||||||
|
builder.OpenComponent<CascadingValue<EditContext>>(0);
|
||||||
|
builder.AddAttribute(1, "Value", EditContext);
|
||||||
|
builder.AddAttribute(2, "ChildContent", new RenderFragment(childBuilder =>
|
||||||
|
{
|
||||||
|
childBuilder.OpenComponent<TComponent>(0);
|
||||||
|
childBuilder.AddAttribute(0, "Value", Value);
|
||||||
|
childBuilder.AddAttribute(1, "ValueChanged",
|
||||||
|
EventCallback.Factory.Create(this, ValueChanged));
|
||||||
|
childBuilder.AddAttribute(2, "ValueExpression", ValueExpression);
|
||||||
|
childBuilder.AddMultipleAttributes(3, AdditionalAttributes);
|
||||||
|
childBuilder.CloseComponent();
|
||||||
|
}));
|
||||||
|
builder.CloseComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue