diff --git a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.Manual.cs b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.Manual.cs index 357b78e82a..0b9e98dbda 100644 --- a/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.Manual.cs +++ b/src/Components/Components/ref/Microsoft.AspNetCore.Components.netstandard2.0.Manual.cs @@ -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 AdditionalAttributes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; }} [Microsoft.AspNetCore.Components.ParameterAttribute] public Microsoft.AspNetCore.Components.RenderFragment 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 : Microsoft.AspNetCore.Components.ComponentBase { protected InputBase() { } + [Parameter(CaptureUnmatchedValues = true)] + public System.Collections.Generic.IReadOnlyDictionary 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 : Microsoft.AspNetCore.Components.ComponentBase, System.IDisposable { public ValidationMessage() { } + [Parameter(CaptureUnmatchedValues = true)] + public System.Collections.Generic.IReadOnlyDictionary AdditionalAttributes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; }} [Microsoft.AspNetCore.Components.ParameterAttribute] public System.Linq.Expressions.Expression> 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 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() { } diff --git a/src/Components/Components/src/Forms/EditForm.cs b/src/Components/Components/src/Forms/EditForm.cs index 2041144bdb..89a329165b 100644 --- a/src/Components/Components/src/Forms/EditForm.cs +++ b/src/Components/Components/src/Forms/EditForm.cs @@ -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; } + /// + /// Gets or sets a collection of additional attributes that will be applied to the created form element. + /// + [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary AdditionalAttributes { get; private set; } + /// /// Supplies the edit context explicitly. If using this parameter, do not /// also supply , 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>(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>(3); + builder.AddAttribute(4, "IsFixed", true); + builder.AddAttribute(5, "Value", _fixedEditContext); + builder.AddAttribute(6, RenderTreeBuilder.ChildContent, ChildContent?.Invoke(_fixedEditContext)); builder.CloseComponent(); builder.CloseElement(); diff --git a/src/Components/Components/src/Forms/InputComponents/InputBase.cs b/src/Components/Components/src/Forms/InputComponents/InputBase.cs index d63f91b42e..da13ccbdee 100644 --- a/src/Components/Components/src/Forms/InputComponents/InputBase.cs +++ b/src/Components/Components/src/Forms/InputComponents/InputBase.cs @@ -21,6 +21,11 @@ namespace Microsoft.AspNetCore.Components.Forms [CascadingParameter] EditContext CascadedEditContext { get; set; } + /// + /// Gets or sets a collection of additional attributes that will be applied to the created element. + /// + [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary AdditionalAttributes { get; private set; } + /// /// Gets a value for the component's 'id' attribute. /// diff --git a/src/Components/Components/src/Forms/InputComponents/InputCheckbox.cs b/src/Components/Components/src/Forms/InputComponents/InputCheckbox.cs index 61f73f24e0..e25867aa1a 100644 --- a/src/Components/Components/src/Forms/InputComponents/InputCheckbox.cs +++ b/src/Components/Components/src/Forms/InputComponents/InputCheckbox.cs @@ -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(); } diff --git a/src/Components/Components/src/Forms/InputComponents/InputDate.cs b/src/Components/Components/src/Forms/InputComponents/InputDate.cs index 5f794d4813..26e253b3c7 100644 --- a/src/Components/Components/src/Forms/InputComponents/InputDate.cs +++ b/src/Components/Components/src/Forms/InputComponents/InputDate.cs @@ -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(); } diff --git a/src/Components/Components/src/Forms/InputComponents/InputNumber.cs b/src/Components/Components/src/Forms/InputComponents/InputNumber.cs index 72f0fea0f7..9301f33559 100644 --- a/src/Components/Components/src/Forms/InputComponents/InputNumber.cs +++ b/src/Components/Components/src/Forms/InputComponents/InputNumber.cs @@ -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(); } diff --git a/src/Components/Components/src/Forms/InputComponents/InputSelect.cs b/src/Components/Components/src/Forms/InputComponents/InputSelect.cs index cee64cb281..e54a8078c6 100644 --- a/src/Components/Components/src/Forms/InputComponents/InputSelect.cs +++ b/src/Components/Components/src/Forms/InputComponents/InputSelect.cs @@ -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(); } diff --git a/src/Components/Components/src/Forms/InputComponents/InputText.cs b/src/Components/Components/src/Forms/InputComponents/InputText.cs index 24e1f29617..61b4ea13dc 100644 --- a/src/Components/Components/src/Forms/InputComponents/InputText.cs +++ b/src/Components/Components/src/Forms/InputComponents/InputText.cs @@ -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(); } diff --git a/src/Components/Components/src/Forms/InputComponents/InputTextArea.cs b/src/Components/Components/src/Forms/InputComponents/InputTextArea.cs index c52368672f..e170d4ff19 100644 --- a/src/Components/Components/src/Forms/InputComponents/InputTextArea.cs +++ b/src/Components/Components/src/Forms/InputComponents/InputTextArea.cs @@ -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(); } diff --git a/src/Components/Components/src/Forms/ValidationMessage.cs b/src/Components/Components/src/Forms/ValidationMessage.cs index 52b86551dc..ef8ee1fbd8 100644 --- a/src/Components/Components/src/Forms/ValidationMessage.cs +++ b/src/Components/Components/src/Forms/ValidationMessage.cs @@ -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 _validationStateChangedHandler; private FieldIdentifier _fieldIdentifier; + /// + /// Gets or sets a collection of additional attributes that will be applied to the created div element. + /// + [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary AdditionalAttributes { get; private set; } + [CascadingParameter] EditContext CurrentEditContext { get; set; } /// @@ -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(); } } diff --git a/src/Components/Components/src/Forms/ValidationSummary.cs b/src/Components/Components/src/Forms/ValidationSummary.cs index 0159f1be31..1bd46fde66 100644 --- a/src/Components/Components/src/Forms/ValidationSummary.cs +++ b/src/Components/Components/src/Forms/ValidationSummary.cs @@ -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 _validationStateChangedHandler; + /// + /// Gets or sets a collection of additional attributes that will be applied to the created ul element. + /// + [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary AdditionalAttributes { get; private set; } + [CascadingParameter] EditContext CurrentEditContext { get; set; } /// ` @@ -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()); diff --git a/src/Components/test/E2ETest/Tests/FormsTest.cs b/src/Components/test/E2ETest/Tests/FormsTest.cs index 9f1083af20..ec0c5fcddf 100644 --- a/src/Components/test/E2ETest/Tests/FormsTest.cs +++ b/src/Components/test/E2ETest/Tests/FormsTest.cs @@ -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(); + 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); diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/SimpleValidationComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/SimpleValidationComponent.razor index b78ce413b4..c95a75c2a2 100644 --- a/src/Components/test/testassets/BasicTestApp/FormsTest/SimpleValidationComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/FormsTest/SimpleValidationComponent.razor @@ -1,7 +1,7 @@ @using System.ComponentModel.DataAnnotations @using Microsoft.AspNetCore.Components.Forms - +

diff --git a/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor b/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor index 8776f05bf9..39819ee729 100644 --- a/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor +++ b/src/Components/test/testassets/BasicTestApp/FormsTest/TypicalValidationComponent.razor @@ -5,30 +5,30 @@

- Name: + Name:

- Age (years): + Age (years):

Height (optional):

- Description: + Description:

- Renewal date: + Renewal date:

Expiry date (optional):

Ticket class: - + @@ -37,7 +37,7 @@ @person.TicketClass

- Accepts terms: + Accepts terms:

Is evil: