Skip to content

Mgbbs/hidden for checkbox render mode #13014

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Aug 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 24 additions & 21 deletions src/Mvc/Mvc.TagHelpers/src/InputTagHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -309,29 +309,32 @@ private TagBuilder GenerateCheckBox(
"checkbox"));
}

// hiddenForCheckboxTag always rendered after the returned element
var hiddenForCheckboxTag = Generator.GenerateHiddenForCheckbox(ViewContext, modelExplorer, For.Name);
if (hiddenForCheckboxTag != null)
{
var renderingMode =
output.TagMode == TagMode.SelfClosing ? TagRenderMode.SelfClosing : TagRenderMode.StartTag;
hiddenForCheckboxTag.TagRenderMode = renderingMode;
if (!hiddenForCheckboxTag.Attributes.ContainsKey("name") &&
!string.IsNullOrEmpty(Name))
if (ViewContext.CheckBoxHiddenInputRenderMode != CheckBoxHiddenInputRenderMode.None)
{
// hiddenForCheckboxTag always rendered after the returned element
var hiddenForCheckboxTag = Generator.GenerateHiddenForCheckbox(ViewContext, modelExplorer, For.Name);
if (hiddenForCheckboxTag != null)
{
// The checkbox and hidden elements should have the same name attribute value. Attributes will
// match if both are present because both have a generated value. Reach here in the special case
// where user provided a non-empty fallback name.
hiddenForCheckboxTag.MergeAttribute("name", Name);
}
var renderingMode =
output.TagMode == TagMode.SelfClosing ? TagRenderMode.SelfClosing : TagRenderMode.StartTag;
hiddenForCheckboxTag.TagRenderMode = renderingMode;
if (!hiddenForCheckboxTag.Attributes.ContainsKey("name") &&
!string.IsNullOrEmpty(Name))
{
// The checkbox and hidden elements should have the same name attribute value. Attributes will
// match if both are present because both have a generated value. Reach here in the special case
// where user provided a non-empty fallback name.
hiddenForCheckboxTag.MergeAttribute("name", Name);
}

if (ViewContext.FormContext.CanRenderAtEndOfForm)
{
ViewContext.FormContext.EndOfFormContent.Add(hiddenForCheckboxTag);
}
else
{
output.PostElement.AppendHtml(hiddenForCheckboxTag);
if (ViewContext.CheckBoxHiddenInputRenderMode == CheckBoxHiddenInputRenderMode.EndOfForm && ViewContext.FormContext.CanRenderAtEndOfForm)
{
ViewContext.FormContext.EndOfFormContent.Add(hiddenForCheckboxTag);
}
else
{
output.PostElement.AppendHtml(hiddenForCheckboxTag);
}
}
}

Expand Down
237 changes: 237 additions & 0 deletions src/Mvc/Mvc.TagHelpers/test/InputTagHelperTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -838,6 +838,243 @@ public void Process_GeneratesFormattedOutput_ForDateTime(string specifiedType, s
Assert.Equal(expectedTagName, output.TagName);
}

[Fact]
public async Task ProcessAsync_GenerateCheckBox_WithHiddenInputRenderModeNone()
{
var propertyName = "-expression-";
var expectedTagName = "input";
var inputTypeName = "checkbox";
var expectedAttributes = new TagHelperAttributeList
{
{ "name", propertyName },
{ "type", inputTypeName },
{ "value", "true" },
};

var metadataProvider = new EmptyModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
var model = false;
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);

viewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.None;

var tagHelper = new InputTagHelper(htmlGenerator)
{
For = modelExpression,
InputTypeName = inputTypeName,
Name = propertyName,
ViewContext = viewContext,
};

var attributes = new TagHelperAttributeList
{
{ "name", propertyName },
{ "type", inputTypeName },
};

var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
var output = new TagHelperOutput(
expectedTagName,
new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
{
TagMode = TagMode.SelfClosing,
};

// Act
await tagHelper.ProcessAsync(context, output);

// Assert
Assert.Equal(expectedAttributes, output.Attributes);
Assert.False(output.IsContentModified);
Assert.Equal(expectedTagName, output.TagName);

Assert.False(viewContext.FormContext.HasEndOfFormContent);
Assert.True(string.IsNullOrEmpty(HtmlContentUtilities.HtmlContentToString(output.PostElement)));
}

[Fact]
public async Task ProcessAsync_GenerateCheckBox_WithHiddenInputRenderModeInline()
{
var propertyName = "-expression-";
var expectedTagName = "input";
var expectedPostElementContent = $"<input name=\"HtmlEncode[[{propertyName}]]\" " +
"type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />";
var inputTypeName = "checkbox";
var expectedAttributes = new TagHelperAttributeList
{
{ "name", propertyName },
{ "type", inputTypeName },
{ "value", "true" },
};

var metadataProvider = new EmptyModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
var model = false;
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);

viewContext.FormContext.CanRenderAtEndOfForm = true;
viewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.Inline;

var tagHelper = new InputTagHelper(htmlGenerator)
{
For = modelExpression,
InputTypeName = inputTypeName,
Name = propertyName,
ViewContext = viewContext,
};

var attributes = new TagHelperAttributeList
{
{ "name", propertyName },
{ "type", inputTypeName },
};

var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
var output = new TagHelperOutput(
expectedTagName,
new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
{
TagMode = TagMode.SelfClosing,
};

// Act
await tagHelper.ProcessAsync(context, output);

// Assert
Assert.Equal(expectedAttributes, output.Attributes);
Assert.False(output.IsContentModified);
Assert.Equal(expectedTagName, output.TagName);

Assert.False(viewContext.FormContext.HasEndOfFormContent);
Assert.Equal(expectedPostElementContent, HtmlContentUtilities.HtmlContentToString(output.PostElement));
}

[Fact]
public async Task ProcessAsync_GenerateCheckBox_WithHiddenInputRenderModeEndOfForm()
{
var propertyName = "-expression-";
var expectedTagName = "input";
var expectedEndOfFormContent = $"<input name=\"HtmlEncode[[{propertyName}]]\" " +
"type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />";
var inputTypeName = "checkbox";
var expectedAttributes = new TagHelperAttributeList
{
{ "name", propertyName },
{ "type", inputTypeName },
{ "value", "true" },
};

var metadataProvider = new EmptyModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
var model = false;
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);

viewContext.FormContext.CanRenderAtEndOfForm = true;
viewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.EndOfForm;

var tagHelper = new InputTagHelper(htmlGenerator)
{
For = modelExpression,
InputTypeName = inputTypeName,
Name = propertyName,
ViewContext = viewContext,
};

var attributes = new TagHelperAttributeList
{
{ "name", propertyName },
{ "type", inputTypeName },
};

var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
var output = new TagHelperOutput(
expectedTagName,
new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
{
TagMode = TagMode.SelfClosing,
};

// Act
await tagHelper.ProcessAsync(context, output);

// Assert
Assert.Equal(expectedAttributes, output.Attributes);
Assert.False(output.IsContentModified);
Assert.Equal(expectedTagName, output.TagName);

Assert.Equal(expectedEndOfFormContent, string.Join("", viewContext.FormContext.EndOfFormContent.Select(html => HtmlContentUtilities.HtmlContentToString(html))));
Assert.True(string.IsNullOrEmpty(HtmlContentUtilities.HtmlContentToString(output.PostElement)));
}

[Fact]
public async Task ProcessAsync_GenerateCheckBox_WithHiddenInputRenderModeEndOfForm_AndCanRenderAtEndOfFormNotSet()
{
var propertyName = "-expression-";
var expectedTagName = "input";
var expectedPostElementContent = $"<input name=\"HtmlEncode[[{propertyName}]]\" " +
"type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />";
var inputTypeName = "checkbox";
var expectedAttributes = new TagHelperAttributeList
{
{ "name", propertyName },
{ "type", inputTypeName },
{ "value", "true" },
};

var metadataProvider = new EmptyModelMetadataProvider();
var htmlGenerator = new TestableHtmlGenerator(metadataProvider);
var model = false;
var modelExplorer = metadataProvider.GetModelExplorerForType(typeof(bool), model);
var modelExpression = new ModelExpression(name: string.Empty, modelExplorer: modelExplorer);
var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider);

viewContext.FormContext.CanRenderAtEndOfForm = false;
viewContext.CheckBoxHiddenInputRenderMode = CheckBoxHiddenInputRenderMode.EndOfForm;

var tagHelper = new InputTagHelper(htmlGenerator)
{
For = modelExpression,
InputTypeName = inputTypeName,
Name = propertyName,
ViewContext = viewContext,
};

var attributes = new TagHelperAttributeList
{
{ "name", propertyName },
{ "type", inputTypeName },
};

var context = new TagHelperContext(attributes, new Dictionary<object, object>(), "test");
var output = new TagHelperOutput(
expectedTagName,
new TagHelperAttributeList(),
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
{
TagMode = TagMode.SelfClosing,
};

// Act
await tagHelper.ProcessAsync(context, output);

// Assert
Assert.Equal(expectedAttributes, output.Attributes);
Assert.False(output.IsContentModified);
Assert.Equal(expectedTagName, output.TagName);

Assert.False(viewContext.FormContext.HasEndOfFormContent);
Assert.Equal(expectedPostElementContent, HtmlContentUtilities.HtmlContentToString(output.PostElement));
}

[Fact]
public async Task ProcessAsync_CallsGenerateCheckBox_WithExpectedParameters()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,12 @@ public static void TryAddModelException<TModel>(this Microsoft.AspNetCore.Mvc.Mo
}
namespace Microsoft.AspNetCore.Mvc.Rendering
{
public enum CheckBoxHiddenInputRenderMode
{
None = 0,
Inline = 1,
EndOfForm = 2,
}
public enum FormMethod
{
Get = 0,
Expand Down Expand Up @@ -678,6 +684,7 @@ public partial class ViewContext : Microsoft.AspNetCore.Mvc.ActionContext
public ViewContext() { }
public ViewContext(Microsoft.AspNetCore.Mvc.ActionContext actionContext, Microsoft.AspNetCore.Mvc.ViewEngines.IView view, Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary viewData, Microsoft.AspNetCore.Mvc.ViewFeatures.ITempDataDictionary tempData, System.IO.TextWriter writer, Microsoft.AspNetCore.Mvc.ViewFeatures.HtmlHelperOptions htmlHelperOptions) { }
public ViewContext(Microsoft.AspNetCore.Mvc.Rendering.ViewContext viewContext, Microsoft.AspNetCore.Mvc.ViewEngines.IView view, Microsoft.AspNetCore.Mvc.ViewFeatures.ViewDataDictionary viewData, System.IO.TextWriter writer) { }
public Microsoft.AspNetCore.Mvc.Rendering.CheckBoxHiddenInputRenderMode CheckBoxHiddenInputRenderMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool ClientValidationEnabled { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string ExecutingFilePath { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public virtual Microsoft.AspNetCore.Mvc.ViewFeatures.FormContext FormContext { get { throw null; } set { } }
Expand Down Expand Up @@ -1062,6 +1069,7 @@ public void EndForm() { }
public partial class HtmlHelperOptions
{
public HtmlHelperOptions() { }
public Microsoft.AspNetCore.Mvc.Rendering.CheckBoxHiddenInputRenderMode CheckBoxHiddenInputRenderMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool ClientValidationEnabled { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public Microsoft.AspNetCore.Mvc.Rendering.Html5DateRenderingMode Html5DateRenderingMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string IdAttributeDotReplacement { get { throw null; } set { } }
Expand Down
14 changes: 12 additions & 2 deletions src/Mvc/Mvc.ViewFeatures/src/HtmlHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -721,8 +721,18 @@ protected virtual IHtmlContent GenerateCheckBox(
isChecked,
htmlAttributes);

if (checkbox == null)
{
return HtmlString.Empty;
}

if (ViewContext.CheckBoxHiddenInputRenderMode == CheckBoxHiddenInputRenderMode.None)
{
return checkbox;
}

var hiddenForCheckbox = _htmlGenerator.GenerateHiddenForCheckbox(ViewContext, modelExplorer, expression);
if (checkbox == null || hiddenForCheckbox == null)
if (hiddenForCheckbox == null)
{
return HtmlString.Empty;
}
Expand All @@ -736,7 +746,7 @@ protected virtual IHtmlContent GenerateCheckBox(
hiddenForCheckbox.MergeAttribute("name", name);
}

if (ViewContext.FormContext.CanRenderAtEndOfForm)
if (ViewContext.CheckBoxHiddenInputRenderMode == CheckBoxHiddenInputRenderMode.EndOfForm && ViewContext.FormContext.CanRenderAtEndOfForm)
{
ViewContext.FormContext.EndOfFormContent.Add(hiddenForCheckbox);
return checkbox;
Expand Down
5 changes: 5 additions & 0 deletions src/Mvc/Mvc.ViewFeatures/src/HtmlHelperOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,10 @@ public string IdAttributeDotReplacement
/// <see cref="IHtmlHelper.ValidationSummary"/> and other overloads.
/// </summary>
public string ValidationSummaryMessageElement { get; set; } = "span";

/// <summary>
/// Gets or sets the way hidden inputs are rendered for checkbox tag helpers and html helpers.
/// </summary>
public CheckBoxHiddenInputRenderMode CheckBoxHiddenInputRenderMode { get; set; } = CheckBoxHiddenInputRenderMode.EndOfForm;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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.

namespace Microsoft.AspNetCore.Mvc.Rendering
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest this should be in the same namespace as HtmlHelperOptions: Microsoft.AspNetCore.Mvc.ViewFeatures but defer to @pranavkm here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick response @dougbu. I initially put it in the .Rendering namespace to follow suit with Html5DateRenderingMode and TagRenderMode, but will also defer to @pranavkm.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems OK to me. We'll do an API review in 5.0 and we can revisit this at that point.

{
/// <summary>
/// Controls the rendering of hidden input fields when using CheckBox tag helpers or html helpers.
/// </summary>
public enum CheckBoxHiddenInputRenderMode
{
/// <summary>
/// Hidden input fields will not be automatically rendered. If checkbox is not checked, no value will be posted.
/// </summary>
None = 0,

/// <summary>
/// Hidden input fields will be rendered inline with each checkbox. Use this for legacy ASP.NET MVC behavior.
/// </summary>
Inline = 1,

/// <summary>
/// Hidden input fields will be rendered for each checkbox at the bottom of the form element. This is the preferred render method and default MVC behavior.
/// If <see cref="Microsoft.AspNetCore.Mvc.ViewFeatures.FormContext.CanRenderAtEndOfForm"/> is <c>false</c>, will fall back on <see cref="Inline"/>.
/// </summary>
EndOfForm = 2
}
}
Loading