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]
|
||||
protected TValue CurrentValue { 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 internal Microsoft.AspNetCore.Components.Forms.FieldIdentifier FieldIdentifier { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
|
||||
[Microsoft.AspNetCore.Components.ParameterAttribute]
|
||||
|
|
|
|||
|
|
@ -50,6 +50,12 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
/// </summary>
|
||||
[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>
|
||||
/// Gets the associated <see cref="Forms.EditContext"/>.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
}
|
||||
else
|
||||
{
|
||||
validationErrorMessage = string.Format(ParsingErrorMessage, FieldIdentifier.FieldName);
|
||||
validationErrorMessage = string.Format(ParsingErrorMessage, DisplayName ?? FieldIdentifier.FieldName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
else
|
||||
{
|
||||
result = default;
|
||||
validationErrorMessage = $"The {input.FieldIdentifier.FieldName} field is not valid.";
|
||||
validationErrorMessage = $"The {input.DisplayName ?? input.FieldIdentifier.FieldName} field is not valid.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
}
|
||||
else
|
||||
{
|
||||
validationErrorMessage = string.Format(ParsingErrorMessage, FieldIdentifier.FieldName);
|
||||
validationErrorMessage = string.Format(ParsingErrorMessage, DisplayName ?? FieldIdentifier.FieldName);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.AspNetCore.Components.Test.Helpers;
|
||||
using Xunit;
|
||||
|
||||
|
|
@ -35,7 +32,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
// Arrange
|
||||
var model = new TestModel();
|
||||
var rootComponent = new TestInputHostComponent<string, TestInputComponent<string>> { EditContext = new EditContext(model), ValueExpression = () => model.StringProperty };
|
||||
await RenderAndGetTestInputComponentAsync(rootComponent);
|
||||
await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||
|
||||
// Act/Assert
|
||||
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) };
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
|
|
@ -68,7 +65,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
};
|
||||
|
||||
// Act
|
||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
||||
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("some value", inputComponent.CurrentValue);
|
||||
|
|
@ -87,7 +84,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
};
|
||||
|
||||
// Act
|
||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
||||
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||
|
||||
// Assert
|
||||
Assert.Same(rootComponent.EditContext, inputComponent.EditContext);
|
||||
|
|
@ -106,7 +103,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
};
|
||||
|
||||
// Act
|
||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
||||
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(FieldIdentifier.Create(() => model.StringProperty), inputComponent.FieldIdentifier);
|
||||
|
|
@ -123,7 +120,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
Value = "initial value",
|
||||
ValueExpression = () => model.StringProperty
|
||||
};
|
||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
||||
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||
Assert.Equal("initial value", inputComponent.CurrentValue);
|
||||
|
||||
// Act
|
||||
|
|
@ -146,7 +143,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
ValueChanged = val => valueChangedCallLog.Add(val),
|
||||
ValueExpression = () => model.StringProperty
|
||||
};
|
||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
||||
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||
Assert.Empty(valueChangedCallLog);
|
||||
|
||||
// Act
|
||||
|
|
@ -169,7 +166,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
ValueChanged = val => valueChangedCallLog.Add(val),
|
||||
ValueExpression = () => model.StringProperty
|
||||
};
|
||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
||||
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||
Assert.Empty(valueChangedCallLog);
|
||||
|
||||
// Act
|
||||
|
|
@ -190,7 +187,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
Value = "initial value",
|
||||
ValueExpression = () => model.StringProperty
|
||||
};
|
||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
||||
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||
Assert.False(rootComponent.EditContext.IsModified(() => model.StringProperty));
|
||||
|
||||
// Act
|
||||
|
|
@ -213,7 +210,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
|
||||
|
||||
// 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
|
||||
|
||||
// Act/Assert: Modify the field
|
||||
|
|
@ -251,7 +248,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
var fieldIdentifier = FieldIdentifier.Create(() => model.StringProperty);
|
||||
|
||||
// Act/Assert
|
||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
||||
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||
Assert.Equal("my-class other-class valid", inputComponent.CssClass);
|
||||
|
||||
// Act/Assert: Retains custom class when changing field class
|
||||
|
|
@ -270,7 +267,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
Value = new DateTime(1915, 3, 2),
|
||||
ValueExpression = () => model.DateProperty
|
||||
};
|
||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
||||
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||
|
||||
// Act/Assert
|
||||
Assert.Equal("1915/03/02", inputComponent.CurrentValueAsString);
|
||||
|
|
@ -289,7 +286,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
ValueExpression = () => model.DateProperty
|
||||
};
|
||||
var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty);
|
||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
||||
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||
var numValidationStateChanges = 0;
|
||||
rootComponent.EditContext.OnValidationStateChanged += (sender, eventArgs) => { numValidationStateChanges++; };
|
||||
|
||||
|
|
@ -319,7 +316,7 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
ValueExpression = () => model.DateProperty
|
||||
};
|
||||
var fieldIdentifier = FieldIdentifier.Create(() => model.DateProperty);
|
||||
var inputComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
||||
var inputComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||
var numValidationStateChanges = 0;
|
||||
rootComponent.EditContext.OnValidationStateChanged += (sender, eventArgs) => { numValidationStateChanges++; };
|
||||
|
||||
|
|
@ -470,21 +467,6 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
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
|
||||
{
|
||||
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)
|
||||
=> 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.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using Microsoft.AspNetCore.Components.RenderTree;
|
||||
using Microsoft.AspNetCore.Components.Test.Helpers;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Components.Forms
|
||||
|
|
@ -19,12 +15,12 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
{
|
||||
// Arrange
|
||||
var model = new TestModel();
|
||||
var rootComponent = new TestInputSelectHostComponent<TestEnum>
|
||||
var rootComponent = new TestInputHostComponent<TestEnum, TestInputSelect<TestEnum>>
|
||||
{
|
||||
EditContext = new EditContext(model),
|
||||
ValueExpression = () => model.NotNullableEnum
|
||||
};
|
||||
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
||||
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||
|
||||
// Act
|
||||
inputSelectComponent.CurrentValueAsString = "Two";
|
||||
|
|
@ -38,12 +34,12 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
{
|
||||
// Arrange
|
||||
var model = new TestModel();
|
||||
var rootComponent = new TestInputSelectHostComponent<TestEnum>
|
||||
var rootComponent = new TestInputHostComponent<TestEnum, TestInputSelect<TestEnum>>
|
||||
{
|
||||
EditContext = new EditContext(model),
|
||||
ValueExpression = () => model.NotNullableEnum
|
||||
};
|
||||
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
||||
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||
|
||||
// Act
|
||||
inputSelectComponent.CurrentValueAsString = "";
|
||||
|
|
@ -57,12 +53,12 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
{
|
||||
// Arrange
|
||||
var model = new TestModel();
|
||||
var rootComponent = new TestInputSelectHostComponent<TestEnum?>
|
||||
var rootComponent = new TestInputHostComponent<TestEnum?, TestInputSelect<TestEnum?>>
|
||||
{
|
||||
EditContext = new EditContext(model),
|
||||
ValueExpression = () => model.NullableEnum
|
||||
};
|
||||
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
||||
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||
|
||||
// Act
|
||||
inputSelectComponent.CurrentValueAsString = "Two";
|
||||
|
|
@ -76,12 +72,12 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
{
|
||||
// Arrange
|
||||
var model = new TestModel();
|
||||
var rootComponent = new TestInputSelectHostComponent<TestEnum?>
|
||||
var rootComponent = new TestInputHostComponent<TestEnum?, TestInputSelect<TestEnum?>>
|
||||
{
|
||||
EditContext = new EditContext(model),
|
||||
ValueExpression = () => model.NullableEnum
|
||||
};
|
||||
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
||||
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||
|
||||
// Act
|
||||
inputSelectComponent.CurrentValueAsString = "";
|
||||
|
|
@ -96,12 +92,12 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
{
|
||||
// Arrange
|
||||
var model = new TestModel();
|
||||
var rootComponent = new TestInputSelectHostComponent<Guid>
|
||||
var rootComponent = new TestInputHostComponent<Guid, TestInputSelect<Guid>>
|
||||
{
|
||||
EditContext = new EditContext(model),
|
||||
ValueExpression = () => model.NotNullableGuid
|
||||
};
|
||||
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
||||
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||
|
||||
// Act
|
||||
var guid = Guid.NewGuid();
|
||||
|
|
@ -117,12 +113,12 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
{
|
||||
// Arrange
|
||||
var model = new TestModel();
|
||||
var rootComponent = new TestInputSelectHostComponent<Guid?>
|
||||
var rootComponent = new TestInputHostComponent<Guid?, TestInputSelect<Guid?>>
|
||||
{
|
||||
EditContext = new EditContext(model),
|
||||
ValueExpression = () => model.NullableGuid
|
||||
};
|
||||
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
||||
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||
|
||||
// Act
|
||||
var guid = Guid.NewGuid();
|
||||
|
|
@ -138,12 +134,12 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
{
|
||||
// Arrange
|
||||
var model = new TestModel();
|
||||
var rootComponent = new TestInputSelectHostComponent<int>
|
||||
var rootComponent = new TestInputHostComponent<int, TestInputSelect<int>>
|
||||
{
|
||||
EditContext = new EditContext(model),
|
||||
ValueExpression = () => model.NotNullableInt
|
||||
};
|
||||
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
||||
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||
|
||||
// Act
|
||||
inputSelectComponent.CurrentValueAsString = "42";
|
||||
|
|
@ -158,12 +154,12 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
{
|
||||
// Arrange
|
||||
var model = new TestModel();
|
||||
var rootComponent = new TestInputSelectHostComponent<int?>
|
||||
var rootComponent = new TestInputHostComponent<int?, TestInputSelect<int?>>
|
||||
{
|
||||
EditContext = new EditContext(model),
|
||||
ValueExpression = () => model.NullableInt
|
||||
};
|
||||
var inputSelectComponent = await RenderAndGetTestInputComponentAsync(rootComponent);
|
||||
var inputSelectComponent = await InputRenderer.RenderAndGetComponent(rootComponent);
|
||||
|
||||
// Act
|
||||
inputSelectComponent.CurrentValueAsString = "42";
|
||||
|
|
@ -172,19 +168,30 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
Assert.Equal(42, inputSelectComponent.CurrentValue);
|
||||
}
|
||||
|
||||
private static TestInputSelect<TValue> FindInputSelectComponent<TValue>(CapturedBatch batch)
|
||||
=> batch.ReferenceFrames
|
||||
.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)
|
||||
[Fact]
|
||||
public async Task ValidationErrorUsesDisplayAttributeName()
|
||||
{
|
||||
var testRenderer = new TestRenderer();
|
||||
var componentId = testRenderer.AssignRootComponentId(hostComponent);
|
||||
await testRenderer.RenderRootComponentAsync(componentId);
|
||||
return FindInputSelectComponent<TValue>(testRenderer.Batches.Single());
|
||||
// Arrange
|
||||
var model = new TestModel();
|
||||
var rootComponent = new TestInputHostComponent<int, TestInputSelect<int>>
|
||||
{
|
||||
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
|
||||
|
|
@ -218,25 +225,13 @@ namespace Microsoft.AspNetCore.Components.Forms
|
|||
get => base.CurrentValueAsString;
|
||||
set => base.CurrentValueAsString = value;
|
||||
}
|
||||
}
|
||||
|
||||
class TestInputSelectHostComponent<TValue> : AutoRenderComponent
|
||||
{
|
||||
public EditContext EditContext { get; set; }
|
||||
|
||||
public Expression<Func<TValue>> ValueExpression { get; set; }
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
public async Task SetCurrentValueAsStringAsync(string value)
|
||||
{
|
||||
builder.OpenComponent<CascadingValue<EditContext>>(0);
|
||||
builder.AddAttribute(1, "Value", EditContext);
|
||||
builder.AddAttribute(2, "ChildContent", new RenderFragment(childBuilder =>
|
||||
{
|
||||
childBuilder.OpenComponent<TestInputSelect<TValue>>(0);
|
||||
childBuilder.AddAttribute(0, "ValueExpression", ValueExpression);
|
||||
childBuilder.CloseComponent();
|
||||
}));
|
||||
builder.CloseComponent();
|
||||
// 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,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