Skip to content

Commit

Permalink
Mgbbs/hidden for checkbox render mode (#13014)
Browse files Browse the repository at this point in the history
* Added CheckBoxHiddenInputRenderMode to HtmlHelperOptions, ViewContext, and html/tag helpers

Fixes #12833
  • Loading branch information
mgbbs authored and pranavkm committed Aug 26, 2019
1 parent a881804 commit 90e89e9
Show file tree
Hide file tree
Showing 8 changed files with 407 additions and 23 deletions.
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 @@ protected string GetInputType(ModelExplorer modelExplorer, out string inputTypeH
"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 partial class ModelStateDictionaryExtensions
}
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 partial class HtmlHelper : Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper
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 MvcForm CreateForm()
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 MvcForm CreateForm()
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
{
/// <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
}
}

1 comment on commit 90e89e9

@rhires
Copy link

@rhires rhires commented on 90e89e9 Oct 13, 2021

Choose a reason for hiding this comment

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

@ Mgbbs thank you for doing this! Do you have insight on how to implement it? I haven't been able to find any documentation regarding this feature, and I'm not able to make sense out of what you wrote (totally on me, btw) to understand how to implement it. Thanks!

Please sign in to comment.