Add unmatched attributes support to forms (#10712)

* Add unmatched attributes support to forms

* Add capture unmatched support to validation message and summary

* ref assemblies.... more like rip assemblies am I right?
This commit is contained in:
Ryan Nowak 2019-05-31 14:50:33 -07:00 committed by GitHub
parent ecacf90c7f
commit 993d943aec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 110 additions and 47 deletions

View File

@ -115,6 +115,8 @@ namespace Microsoft.AspNetCore.Components.Forms
public partial class EditForm : Microsoft.AspNetCore.Components.ComponentBase
{
public EditForm() { }
[Parameter(CaptureUnmatchedValues = true)]
public System.Collections.Generic.IReadOnlyDictionary<string, object> AdditionalAttributes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; }}
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.Forms.EditContext> ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; }}
[Microsoft.AspNetCore.Components.ParameterAttribute]
@ -134,6 +136,8 @@ namespace Microsoft.AspNetCore.Components.Forms
public abstract partial class InputBase<T> : Microsoft.AspNetCore.Components.ComponentBase
{
protected InputBase() { }
[Parameter(CaptureUnmatchedValues = true)]
public System.Collections.Generic.IReadOnlyDictionary<string, object> AdditionalAttributes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; }}
[Microsoft.AspNetCore.Components.ParameterAttribute]
public string Class { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; }}
protected string CssClass { get { throw null; } }
@ -207,6 +211,8 @@ namespace Microsoft.AspNetCore.Components.Forms
public partial class ValidationMessage<T> : Microsoft.AspNetCore.Components.ComponentBase, System.IDisposable
{
public ValidationMessage() { }
[Parameter(CaptureUnmatchedValues = true)]
public System.Collections.Generic.IReadOnlyDictionary<string, object> AdditionalAttributes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; }}
[Microsoft.AspNetCore.Components.ParameterAttribute]
public System.Linq.Expressions.Expression<System.Func<T>> For { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; }}
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { }
@ -217,6 +223,8 @@ namespace Microsoft.AspNetCore.Components.Forms
public partial class ValidationSummary : Microsoft.AspNetCore.Components.ComponentBase, System.IDisposable
{
public ValidationSummary() { }
[Parameter(CaptureUnmatchedValues = true)]
public System.Collections.Generic.IReadOnlyDictionary<string, object> AdditionalAttributes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; }}
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { }
protected override void OnParametersSet() { }
void System.IDisposable.Dispose() { }

View File

@ -2,6 +2,7 @@
// 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 Microsoft.AspNetCore.Components.RenderTree;
@ -24,6 +25,11 @@ namespace Microsoft.AspNetCore.Components.Forms
_handleSubmitDelegate = HandleSubmitAsync;
}
/// <summary>
/// Gets or sets a collection of additional attributes that will be applied to the created <c>form</c> element.
/// </summary>
[Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary<string, object> AdditionalAttributes { get; private set; }
/// <summary>
/// Supplies the edit context explicitly. If using this parameter, do not
/// also supply <see cref="Model"/>, since the model value will be taken
@ -99,11 +105,12 @@ namespace Microsoft.AspNetCore.Components.Forms
builder.OpenRegion(_fixedEditContext.GetHashCode());
builder.OpenElement(0, "form");
builder.AddAttribute(1, "onsubmit", _handleSubmitDelegate);
builder.OpenComponent<CascadingValue<EditContext>>(2);
builder.AddAttribute(3, "IsFixed", true);
builder.AddAttribute(4, "Value", _fixedEditContext);
builder.AddAttribute(5, RenderTreeBuilder.ChildContent, ChildContent?.Invoke(_fixedEditContext));
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "onsubmit", _handleSubmitDelegate);
builder.OpenComponent<CascadingValue<EditContext>>(3);
builder.AddAttribute(4, "IsFixed", true);
builder.AddAttribute(5, "Value", _fixedEditContext);
builder.AddAttribute(6, RenderTreeBuilder.ChildContent, ChildContent?.Invoke(_fixedEditContext));
builder.CloseComponent();
builder.CloseElement();

View File

@ -21,6 +21,11 @@ namespace Microsoft.AspNetCore.Components.Forms
[CascadingParameter] EditContext CascadedEditContext { get; set; }
/// <summary>
/// Gets or sets a collection of additional attributes that will be applied to the created element.
/// </summary>
[Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary<string, object> AdditionalAttributes { get; private set; }
/// <summary>
/// Gets a value for the component's 'id' attribute.
/// </summary>

View File

@ -24,11 +24,12 @@ namespace Microsoft.AspNetCore.Components.Forms
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "input");
builder.AddAttribute(1, "type", "checkbox");
builder.AddAttribute(2, "id", Id);
builder.AddAttribute(3, "class", CssClass);
builder.AddAttribute(4, "checked", BindMethods.GetValue(CurrentValue));
builder.AddAttribute(5, "onchange", BindMethods.SetValueHandler(__value => CurrentValue = __value, CurrentValue));
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "type", "checkbox");
builder.AddAttribute(3, "id", Id);
builder.AddAttribute(4, "class", CssClass);
builder.AddAttribute(5, "checked", BindMethods.GetValue(CurrentValue));
builder.AddAttribute(6, "onchange", BindMethods.SetValueHandler(__value => CurrentValue = __value, CurrentValue));
builder.CloseElement();
}

View File

@ -23,11 +23,12 @@ namespace Microsoft.AspNetCore.Components.Forms
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "input");
builder.AddAttribute(1, "type", "date");
builder.AddAttribute(2, "id", Id);
builder.AddAttribute(3, "class", CssClass);
builder.AddAttribute(4, "value", BindMethods.GetValue(CurrentValueAsString));
builder.AddAttribute(5, "onchange", BindMethods.SetValueHandler(__value => CurrentValueAsString = __value, CurrentValueAsString));
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "type", "date");
builder.AddAttribute(3, "id", Id);
builder.AddAttribute(4, "class", CssClass);
builder.AddAttribute(5, "value", BindMethods.GetValue(CurrentValueAsString));
builder.AddAttribute(6, "onchange", BindMethods.SetValueHandler(__value => CurrentValueAsString = __value, CurrentValueAsString));
builder.CloseElement();
}

View File

@ -62,12 +62,13 @@ namespace Microsoft.AspNetCore.Components.Forms
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "input");
builder.AddAttribute(1, "type", "number");
builder.AddAttribute(2, "step", _stepAttributeValue);
builder.AddAttribute(3, "id", Id);
builder.AddAttribute(4, "class", CssClass);
builder.AddAttribute(5, "value", BindMethods.GetValue(CurrentValueAsString));
builder.AddAttribute(6, "onchange", BindMethods.SetValueHandler(__value => CurrentValueAsString = __value, CurrentValueAsString));
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "type", "number");
builder.AddAttribute(3, "step", _stepAttributeValue);
builder.AddAttribute(4, "id", Id);
builder.AddAttribute(5, "class", CssClass);
builder.AddAttribute(6, "value", BindMethods.GetValue(CurrentValueAsString));
builder.AddAttribute(7, "onchange", BindMethods.SetValueHandler(__value => CurrentValueAsString = __value, CurrentValueAsString));
builder.CloseElement();
}

View File

@ -20,11 +20,12 @@ namespace Microsoft.AspNetCore.Components.Forms
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "select");
builder.AddAttribute(1, "id", Id);
builder.AddAttribute(2, "class", CssClass);
builder.AddAttribute(3, "value", BindMethods.GetValue(CurrentValueAsString));
builder.AddAttribute(4, "onchange", BindMethods.SetValueHandler(__value => CurrentValueAsString = __value, CurrentValueAsString));
builder.AddContent(5, ChildContent);
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "id", Id);
builder.AddAttribute(3, "class", CssClass);
builder.AddAttribute(4, "value", BindMethods.GetValue(CurrentValueAsString));
builder.AddAttribute(5, "onchange", BindMethods.SetValueHandler(__value => CurrentValueAsString = __value, CurrentValueAsString));
builder.AddContent(6, ChildContent);
builder.CloseElement();
}

View File

@ -25,10 +25,11 @@ namespace Microsoft.AspNetCore.Components.Forms
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "input");
builder.AddAttribute(1, "id", Id);
builder.AddAttribute(2, "class", CssClass);
builder.AddAttribute(3, "value", BindMethods.GetValue(CurrentValue));
builder.AddAttribute(4, "onchange", BindMethods.SetValueHandler(__value => CurrentValue = __value, CurrentValue));
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "id", Id);
builder.AddAttribute(3, "class", CssClass);
builder.AddAttribute(4, "value", BindMethods.GetValue(CurrentValue));
builder.AddAttribute(5, "onchange", BindMethods.SetValueHandler(__value => CurrentValue = __value, CurrentValue));
builder.CloseElement();
}

View File

@ -25,10 +25,11 @@ namespace Microsoft.AspNetCore.Components.Forms
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "textarea");
builder.AddAttribute(1, "id", Id);
builder.AddAttribute(2, "class", CssClass);
builder.AddAttribute(3, "value", BindMethods.GetValue(CurrentValue));
builder.AddAttribute(4, "onchange", BindMethods.SetValueHandler(__value => CurrentValue = __value, CurrentValue));
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "id", Id);
builder.AddAttribute(3, "class", CssClass);
builder.AddAttribute(4, "value", BindMethods.GetValue(CurrentValue));
builder.AddAttribute(5, "onchange", BindMethods.SetValueHandler(__value => CurrentValue = __value, CurrentValue));
builder.CloseElement();
}

View File

@ -2,6 +2,7 @@
// 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.RenderTree;
@ -17,6 +18,11 @@ namespace Microsoft.AspNetCore.Components.Forms
private readonly EventHandler<ValidationStateChangedEventArgs> _validationStateChangedHandler;
private FieldIdentifier _fieldIdentifier;
/// <summary>
/// Gets or sets a collection of additional attributes that will be applied to the created <c>div</c> element.
/// </summary>
[Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary<string, object> AdditionalAttributes { get; private set; }
[CascadingParameter] EditContext CurrentEditContext { get; set; }
/// <summary>
@ -67,8 +73,9 @@ namespace Microsoft.AspNetCore.Components.Forms
foreach (var message in CurrentEditContext.GetValidationMessages(_fieldIdentifier))
{
builder.OpenElement(0, "div");
builder.AddAttribute(1, "class", "validation-message");
builder.AddContent(2, message);
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "class", "validation-message");
builder.AddContent(3, message);
builder.CloseElement();
}
}

View File

@ -2,6 +2,7 @@
// 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 Microsoft.AspNetCore.Components.RenderTree;
namespace Microsoft.AspNetCore.Components.Forms
@ -18,6 +19,11 @@ namespace Microsoft.AspNetCore.Components.Forms
private EditContext _previousEditContext;
private readonly EventHandler<ValidationStateChangedEventArgs> _validationStateChangedHandler;
/// <summary>
/// Gets or sets a collection of additional attributes that will be applied to the created <c>ul</c> element.
/// </summary>
[Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary<string, object> AdditionalAttributes { get; private set; }
[CascadingParameter] EditContext CurrentEditContext { get; set; }
/// <summary>`
@ -55,13 +61,14 @@ namespace Microsoft.AspNetCore.Components.Forms
if (messagesEnumerator.MoveNext())
{
builder.OpenElement(0, "ul");
builder.AddAttribute(1, "class", "validation-errors");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "class", "validation-errors");
do
{
builder.OpenElement(2, "li");
builder.AddAttribute(3, "class", "validation-message");
builder.AddContent(4, messagesEnumerator.Current);
builder.OpenElement(3, "li");
builder.AddAttribute(4, "class", "validation-message");
builder.AddContent(5, messagesEnumerator.Current);
builder.CloseElement();
}
while (messagesEnumerator.MoveNext());

View File

@ -12,6 +12,7 @@ using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI;
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
@ -38,11 +39,15 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
public async Task EditFormWorksWithDataAnnotationsValidator()
{
var appElement = MountTestComponent<SimpleValidationComponent>();
var form = appElement.FindElement(By.TagName("form"));
var userNameInput = appElement.FindElement(By.ClassName("user-name")).FindElement(By.TagName("input"));
var acceptsTermsInput = appElement.FindElement(By.ClassName("accepts-terms")).FindElement(By.TagName("input"));
var submitButton = appElement.FindElement(By.TagName("button"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
// The form emits unmatched attributes
Browser.Equal("off", () => form.GetAttribute("autocomplete"));
// Editing a field doesn't trigger validation on its own
userNameInput.SendKeys("Bert\t");
acceptsTermsInput.Click(); // Accept terms
@ -77,6 +82,9 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var nameInput = appElement.FindElement(By.ClassName("name")).FindElement(By.TagName("input"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
// InputText emits unmatched attributes
Browser.Equal("Enter your name", () => nameInput.GetAttribute("placeholder"));
// Validates on edit
Browser.Equal("valid", () => nameInput.GetAttribute("class"));
nameInput.SendKeys("Bert\t");
@ -101,6 +109,9 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var ageInput = appElement.FindElement(By.ClassName("age")).FindElement(By.TagName("input"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
// InputNumber emits unmatched attributes
Browser.Equal("Enter your age", () => ageInput.GetAttribute("placeholder"));
// Validates on edit
Browser.Equal("valid", () => ageInput.GetAttribute("class"));
ageInput.SendKeys("123\t");
@ -154,6 +165,9 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var descriptionInput = appElement.FindElement(By.ClassName("description")).FindElement(By.TagName("textarea"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
// InputTextArea emits unmatched attributes
Browser.Equal("Tell us about yourself", () => descriptionInput.GetAttribute("placeholder"));
// Validates on edit
Browser.Equal("valid", () => descriptionInput.GetAttribute("class"));
descriptionInput.SendKeys("Hello\t");
@ -178,6 +192,9 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var renewalDateInput = appElement.FindElement(By.ClassName("renewal-date")).FindElement(By.TagName("input"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
// InputDate emits unmatched attributes
Browser.Equal("Enter the date", () => renewalDateInput.GetAttribute("placeholder"));
// Validates on edit
Browser.Equal("valid", () => renewalDateInput.GetAttribute("class"));
renewalDateInput.SendKeys("01/01/2000\t");
@ -232,6 +249,9 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var select = ticketClassInput.WrappedElement;
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
// InputSelect emits unmatched attributes
Browser.Equal("4", () => select.GetAttribute("size"));
// Validates on edit
Browser.Equal("valid", () => select.GetAttribute("class"));
ticketClassInput.SelectByText("First class");
@ -251,6 +271,9 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var isEvilInput = appElement.FindElement(By.ClassName("is-evil")).FindElement(By.TagName("input"));
var messagesAccessor = CreateValidationMessagesAccessor(appElement);
// InputCheckbox emits unmatched attributes
Browser.Equal("You have to check this", () => acceptsTermsInput.GetAttribute("title"));
// Correct initial checkedness
Assert.False(acceptsTermsInput.Selected);
Assert.True(isEvilInput.Selected);

View File

@ -1,7 +1,7 @@
@using System.ComponentModel.DataAnnotations
@using Microsoft.AspNetCore.Components.Forms
<EditForm Model="@this" OnValidSubmit="@HandleValidSubmit" OnInvalidSubmit="@HandleInvalidSubmit">
<EditForm Model="@this" OnValidSubmit="@HandleValidSubmit" OnInvalidSubmit="@HandleInvalidSubmit" autocomplete="off">
<DataAnnotationsValidator />
<p class="user-name">

View File

@ -5,30 +5,30 @@
<DataAnnotationsValidator />
<p class="name">
Name: <InputText @bind-Value="@person.Name" />
Name: <InputText @bind-Value="@person.Name" placeholder="Enter your name" />
</p>
<p class="email">
Email: <InputText @bind-Value="@person.Email" />
<ValidationMessage For="@(() => person.Email)" />
</p>
<p class="age">
Age (years): <InputNumber @bind-Value="@person.AgeInYears" />
Age (years): <InputNumber @bind-Value="@person.AgeInYears" placeholder="Enter your age" />
</p>
<p class="height">
Height (optional): <InputNumber @bind-Value="@person.OptionalHeight" />
</p>
<p class="description">
Description: <InputTextArea @bind-Value="@person.Description" />
Description: <InputTextArea @bind-Value="@person.Description" placeholder="Tell us about yourself" />
</p>
<p class="renewal-date">
Renewal date: <InputDate @bind-Value="@person.RenewalDate" />
Renewal date: <InputDate @bind-Value="@person.RenewalDate" placeholder="Enter the date" />
</p>
<p class="expiry-date">
Expiry date (optional): <InputDate @bind-Value="@person.OptionalExpiryDate" />
</p>
<p class="ticket-class">
Ticket class:
<InputSelect @bind-Value="@person.TicketClass">
<InputSelect @bind-Value="@person.TicketClass" size="4">
<option>(select)</option>
<option value="@TicketClass.Economy">Economy class</option>
<option value="@TicketClass.Premium">Premium class</option>
@ -37,7 +37,7 @@
<span id="selected-ticket-class">@person.TicketClass</span>
</p>
<p class="accepts-terms">
Accepts terms: <InputCheckbox @bind-Value="@person.AcceptsTerms" />
Accepts terms: <InputCheckbox @bind-Value="@person.AcceptsTerms" title="You have to check this" />
</p>
<p class="is-evil">
Is evil: <InputCheckbox @bind-Value="@person.IsEvil" />