Refactor GenerateCheckBox (#6229)

Addresses #5981 and #5983
This commit is contained in:
Jass Bagga 2017-05-05 11:53:53 -07:00 committed by GitHub
parent a80594d706
commit 87bb1d0ff5
4 changed files with 48 additions and 76 deletions

View File

@ -194,8 +194,8 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
break; break;
case "checkbox": case "checkbox":
GenerateCheckBox(modelExplorer, output); tagBuilder = GenerateCheckBox(modelExplorer, output);
return; break;
case "password": case "password":
tagBuilder = Generator.GeneratePassword( tagBuilder = Generator.GeneratePassword(
@ -252,7 +252,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
return inputTypeHint; return inputTypeHint;
} }
private void GenerateCheckBox(ModelExplorer modelExplorer, TagHelperOutput output) private TagBuilder GenerateCheckBox(ModelExplorer modelExplorer, TagHelperOutput output)
{ {
if (modelExplorer.ModelType == typeof(string)) if (modelExplorer.ModelType == typeof(string))
{ {
@ -280,53 +280,30 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
"checkbox")); "checkbox"));
} }
// Prepare to move attributes from current element to <input type="checkbox"/> generated just below. // hiddenForCheckboxTag always rendered after the returned element
var htmlAttributes = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase); var hiddenForCheckboxTag = Generator.GenerateHiddenForCheckbox(ViewContext, modelExplorer, For.Name);
if (hiddenForCheckboxTag != null)
// Perf: Avoid allocating enumerator
// Construct attributes correctly (first attribute wins).
for (var i = 0; i < output.Attributes.Count; i++)
{ {
var attribute = output.Attributes[i]; var renderingMode =
if (!htmlAttributes.ContainsKey(attribute.Name)) output.TagMode == TagMode.SelfClosing ? TagRenderMode.SelfClosing : TagRenderMode.StartTag;
hiddenForCheckboxTag.TagRenderMode = renderingMode;
if (ViewContext.FormContext.CanRenderAtEndOfForm)
{ {
htmlAttributes.Add(attribute.Name, attribute.Value); ViewContext.FormContext.EndOfFormContent.Add(hiddenForCheckboxTag);
}
else
{
output.PostElement.AppendHtml(hiddenForCheckboxTag);
} }
} }
var checkBoxTag = Generator.GenerateCheckBox( return Generator.GenerateCheckBox(
ViewContext, ViewContext,
modelExplorer, modelExplorer,
For.Name, For.Name,
isChecked: null, isChecked: null,
htmlAttributes: htmlAttributes); htmlAttributes: null);
if (checkBoxTag != null)
{
// Do not generate current element's attributes or tags. Instead put both <input type="checkbox"/> and
// <input type="hidden"/> into the output's Content.
output.Attributes.Clear();
output.TagName = null;
var renderingMode =
output.TagMode == TagMode.SelfClosing ? TagRenderMode.SelfClosing : TagRenderMode.StartTag;
checkBoxTag.TagRenderMode = renderingMode;
output.Content.AppendHtml(checkBoxTag);
var hiddenForCheckboxTag = Generator.GenerateHiddenForCheckbox(ViewContext, modelExplorer, For.Name);
if (hiddenForCheckboxTag != null)
{
hiddenForCheckboxTag.TagRenderMode = renderingMode;
if (ViewContext.FormContext.CanRenderAtEndOfForm)
{
ViewContext.FormContext.EndOfFormContent.Add(hiddenForCheckboxTag);
}
else
{
output.Content.AppendHtml(hiddenForCheckboxTag);
}
}
}
} }
private TagBuilder GenerateRadio(ModelExplorer modelExplorer) private TagBuilder GenerateRadio(ModelExplorer modelExplorer)

View File

@ -65,7 +65,7 @@
</div> </div>
<div> <div>
<label class="order" for="HtmlEncode[[NeedSpecialHandle]]">HtmlEncode[[NeedSpecialHandle]]</label> <label class="order" for="HtmlEncode[[NeedSpecialHandle]]">HtmlEncode[[NeedSpecialHandle]]</label>
<input checked="HtmlEncode[[checked]]" data-val="HtmlEncode[[true]]" data-val-required="HtmlEncode[[The NeedSpecialHandle field is required.]]" id="HtmlEncode[[NeedSpecialHandle]]" name="HtmlEncode[[NeedSpecialHandle]]" type="HtmlEncode[[checkbox]]" value="HtmlEncode[[true]]" /> <input type="HtmlEncode[[checkbox]]" checked="HtmlEncode[[checked]]" data-val="HtmlEncode[[true]]" data-val-required="HtmlEncode[[The NeedSpecialHandle field is required.]]" id="HtmlEncode[[NeedSpecialHandle]]" name="HtmlEncode[[NeedSpecialHandle]]" value="HtmlEncode[[true]]" />
</div> </div>
<div> <div>
<label class="order" for="HtmlEncode[[PaymentMethod]]">HtmlEncode[[PaymentMethod]]</label> <label class="order" for="HtmlEncode[[PaymentMethod]]">HtmlEncode[[PaymentMethod]]</label>

View File

@ -65,7 +65,7 @@
</div> </div>
<div> <div>
<label class="order" for="NeedSpecialHandle">NeedSpecialHandle</label> <label class="order" for="NeedSpecialHandle">NeedSpecialHandle</label>
<input checked="checked" data-val="true" data-val-required="The NeedSpecialHandle field is required." id="NeedSpecialHandle" name="NeedSpecialHandle" type="checkbox" value="true" /> <input type="checkbox" checked="checked" data-val="true" data-val-required="The NeedSpecialHandle field is required." id="NeedSpecialHandle" name="NeedSpecialHandle" value="true" />
</div> </div>
<div> <div>
<label class="order" for="PaymentMethod">PaymentMethod</label> <label class="order" for="PaymentMethod">PaymentMethod</label>

View File

@ -34,7 +34,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
{ "hello", "world" }, { "hello", "world" },
{ "hello", "world2" } { "hello", "world2" }
}, },
"hello=\"HtmlEncode[[world]]\"" "hello=\"HtmlEncode[[world]]\" hello=\"HtmlEncode[[world2]]\""
}, },
{ {
new TagHelperAttributeList new TagHelperAttributeList
@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
{ "hello", "world2" }, { "hello", "world2" },
{ "hello", "world3" } { "hello", "world3" }
}, },
"hello=\"HtmlEncode[[world]]\"" "hello=\"HtmlEncode[[world]]\" hello=\"HtmlEncode[[world2]]\" hello=\"HtmlEncode[[world3]]\""
}, },
{ {
new TagHelperAttributeList new TagHelperAttributeList
@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
{ "HelLO", "world" }, { "HelLO", "world" },
{ "HELLO", "world2" } { "HELLO", "world2" }
}, },
"HelLO=\"HtmlEncode[[world]]\"" "HelLO=\"HtmlEncode[[world]]\" HELLO=\"HtmlEncode[[world2]]\""
}, },
{ {
new TagHelperAttributeList new TagHelperAttributeList
@ -60,7 +60,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
{ "HELLO", "world2" }, { "HELLO", "world2" },
{ "hello", "world3" } { "hello", "world3" }
}, },
"Hello=\"HtmlEncode[[world]]\"" "Hello=\"HtmlEncode[[world]]\" HELLO=\"HtmlEncode[[world2]]\" hello=\"HtmlEncode[[world3]]\""
}, },
{ {
new TagHelperAttributeList new TagHelperAttributeList
@ -68,7 +68,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
{ "HeLlO", "world" }, { "HeLlO", "world" },
{ "hello", "world2" } { "hello", "world2" }
}, },
"HeLlO=\"HtmlEncode[[world]]\"" "HeLlO=\"HtmlEncode[[world]]\" hello=\"HtmlEncode[[world2]]\""
}, },
}; };
} }
@ -76,15 +76,14 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
[Theory] [Theory]
[MemberData(nameof(MultiAttributeCheckBoxData))] [MemberData(nameof(MultiAttributeCheckBoxData))]
public async Task CheckBoxHandlesMultipleAttributesSameNameCorrectly( public async Task CheckBoxHandlesMultipleAttributesSameNameArePreserved(
TagHelperAttributeList outputAttributes, TagHelperAttributeList outputAttributes,
string expectedAttributeString) string expectedAttributeString)
{ {
// Arrange // Arrange
var originalContent = "original content"; var originalContent = "original content";
var originalTagName = "not-input"; var expectedContent = $"<input {expectedAttributeString} type=\"HtmlEncode[[checkbox]]\" id=\"HtmlEncode[[IsACar]]\" " +
var expectedContent = $"{originalContent}<input {expectedAttributeString} id=\"HtmlEncode[[IsACar]]\" " + $"name=\"HtmlEncode[[IsACar]]\" value=\"HtmlEncode[[true]]\" />" +
"name=\"HtmlEncode[[IsACar]]\" type=\"HtmlEncode[[checkbox]]\" value=\"HtmlEncode[[true]]\" />" +
"<input name=\"HtmlEncode[[IsACar]]\" type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />"; "<input name=\"HtmlEncode[[IsACar]]\" type=\"HtmlEncode[[hidden]]\" value=\"HtmlEncode[[false]]\" />";
var context = new TagHelperContext( var context = new TagHelperContext(
@ -94,12 +93,13 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
items: new Dictionary<object, object>(), items: new Dictionary<object, object>(),
uniqueId: "test"); uniqueId: "test");
var output = new TagHelperOutput( var output = new TagHelperOutput(
originalTagName, "input",
outputAttributes, outputAttributes,
getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null)) getChildContentAsync: (useCachedResult, encoder) => Task.FromResult<TagHelperContent>(result: null))
{ {
TagMode = TagMode.SelfClosing, TagMode = TagMode.SelfClosing,
}; };
output.Content.AppendHtml(originalContent); output.Content.AppendHtml(originalContent);
var htmlGenerator = new TestableHtmlGenerator(new EmptyModelMetadataProvider()); var htmlGenerator = new TestableHtmlGenerator(new EmptyModelMetadataProvider());
var tagHelper = GetTagHelper(htmlGenerator, model: false, propertyName: nameof(Model.IsACar)); var tagHelper = GetTagHelper(htmlGenerator, model: false, propertyName: nameof(Model.IsACar));
@ -108,10 +108,9 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
await tagHelper.ProcessAsync(context, output); await tagHelper.ProcessAsync(context, output);
// Assert // Assert
Assert.Empty(output.Attributes); // Moved to Content and cleared Assert.NotNull(output.PostElement);
Assert.Equal(expectedContent, HtmlContentUtilities.HtmlContentToString(output.Content)); Assert.Equal(originalContent, HtmlContentUtilities.HtmlContentToString(output.Content));
Assert.Equal(TagMode.SelfClosing, output.TagMode); Assert.Equal(expectedContent, HtmlContentUtilities.HtmlContentToString(output));
Assert.Null(output.TagName); // Cleared
} }
[Theory] [Theory]
@ -213,13 +212,13 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
// Arrange // Arrange
const string content = "original content"; const string content = "original content";
const string tagName = "input"; const string tagName = "input";
const string isCheckedAttr = " checked=\"HtmlEncode[[checked]]\""; const string isCheckedAttr = "checked=\"HtmlEncode[[checked]]\" ";
var isChecked = (bool.Parse(possibleBool) ? isCheckedAttr : string.Empty); var isChecked = (bool.Parse(possibleBool) ? isCheckedAttr : string.Empty);
var expectedContent = $"{content}<input{isChecked} class=\"HtmlEncode[[form-control]]\" " + var expectedContent = $"<input class=\"HtmlEncode[[form-control]]\" type=\"HtmlEncode[[checkbox]]\" " +
"id=\"HtmlEncode[[IsACar]]\" name=\"HtmlEncode[[IsACar]]\" type=\"HtmlEncode[[checkbox]]\" " + $"{isChecked}id=\"HtmlEncode[[IsACar]]\" name=\"HtmlEncode[[IsACar]]\" " +
"value=\"HtmlEncode[[true]]\" /><input name=\"HtmlEncode[[IsACar]]\" type=\"HtmlEncode[[hidden]]\" " + "value=\"HtmlEncode[[true]]\" /><input name=\"HtmlEncode[[IsACar]]\" type=\"HtmlEncode[[hidden]]\" " +
"value=\"HtmlEncode[[false]]\" />"; "value=\"HtmlEncode[[false]]\" />";
var expectedPostElement = "<input name=\"IsACar\" type=\"hidden\" value=\"false\" />";
var attributes = new TagHelperAttributeList var attributes = new TagHelperAttributeList
{ {
@ -247,10 +246,9 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
tagHelper.Process(context, output); tagHelper.Process(context, output);
// Assert // Assert
Assert.Empty(output.Attributes); Assert.Equal(content, HtmlContentUtilities.HtmlContentToString(output.Content));
Assert.Equal(expectedContent, HtmlContentUtilities.HtmlContentToString(output.Content)); Assert.Equal(expectedContent, HtmlContentUtilities.HtmlContentToString(output));
Assert.Equal(TagMode.SelfClosing, output.TagMode); Assert.Equal(expectedPostElement, output.PostElement.GetContent());
Assert.Null(output.TagName);
} }
// Top-level container (List<Model> or Model instance), immediate container type (Model or NestModel), // Top-level container (List<Model> or Model instance), immediate container type (Model or NestModel),
@ -464,10 +462,10 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
{ {
// Arrange // Arrange
var originalContent = "original content"; var originalContent = "original content";
var originalTagName = "not-input";
var expectedPreContent = "original pre-content"; var expectedPreContent = "original pre-content";
var expectedContent = originalContent + "<input class=\"HtmlEncode[[form-control]]\" /><hidden />"; var expectedContent = "<input class=\"HtmlEncode[[form-control]]\" type=\"HtmlEncode[[checkbox]]\" /><hidden />";
var expectedPostContent = "original post-content"; var expectedPostContent = "original post-content";
var expectedPostElement = "<hidden />";
var context = new TagHelperContext( var context = new TagHelperContext(
tagName: "input", tagName: "input",
@ -480,7 +478,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
{ "class", "form-control" }, { "class", "form-control" },
}; };
var output = new TagHelperOutput( var output = new TagHelperOutput(
originalTagName, "input",
originalAttributes, originalAttributes,
getChildContentAsync: (useCachedResult, encoder) => getChildContentAsync: (useCachedResult, encoder) =>
{ {
@ -501,10 +499,6 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
var tagBuilder = new TagBuilder("input") var tagBuilder = new TagBuilder("input")
{ {
Attributes =
{
{ "class", "form-control" },
},
TagRenderMode = TagRenderMode.SelfClosing TagRenderMode = TagRenderMode.SelfClosing
}; };
htmlGenerator htmlGenerator
@ -530,12 +524,13 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
// Assert // Assert
htmlGenerator.Verify(); htmlGenerator.Verify();
Assert.Empty(output.Attributes); // Moved to Content and cleared Assert.NotEmpty(output.Attributes);
Assert.Equal(expectedPreContent, output.PreContent.GetContent()); Assert.Equal(expectedPreContent, output.PreContent.GetContent());
Assert.Equal(expectedContent, HtmlContentUtilities.HtmlContentToString(output.Content)); Assert.Equal(originalContent, HtmlContentUtilities.HtmlContentToString(output.Content));
Assert.Equal(expectedContent, HtmlContentUtilities.HtmlContentToString(output));
Assert.Equal(expectedPostContent, output.PostContent.GetContent()); Assert.Equal(expectedPostContent, output.PostContent.GetContent());
Assert.Equal(expectedPostElement, output.PostElement.GetContent());
Assert.Equal(TagMode.SelfClosing, output.TagMode); Assert.Equal(TagMode.SelfClosing, output.TagMode);
Assert.Null(output.TagName); // Cleared
} }
[Theory] [Theory]