From f8b0249918c4ec2ffda48e3b066c32c4eef6c6fc Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Tue, 14 Oct 2014 15:13:20 -0700 Subject: [PATCH] Add Label TagHelper. - Validated label TagHelper functionality. #1249 --- .../LabelTagHelper.cs | 57 +++++ .../LabelTagHelperTest.cs | 199 ++++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100644 src/Microsoft.AspNet.Mvc.TagHelpers/LabelTagHelper.cs create mode 100644 test/Microsoft.AspNet.Mvc.TagHelpers.Test/LabelTagHelperTest.cs diff --git a/src/Microsoft.AspNet.Mvc.TagHelpers/LabelTagHelper.cs b/src/Microsoft.AspNet.Mvc.TagHelpers/LabelTagHelper.cs new file mode 100644 index 0000000000..33cc3f047a --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.TagHelpers/LabelTagHelper.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Microsoft.AspNet.Razor.TagHelpers; + +namespace Microsoft.AspNet.Mvc.TagHelpers +{ + /// + /// implementation targeting <label> elements with for attributes. + /// + [ContentBehavior(ContentBehavior.Modify)] + public class LabelTagHelper : TagHelper + { + // Protected to ensure subclasses are correctly activated. Internal for ease of use when testing. + [Activate] + protected internal ViewContext ViewContext { get; set; } + + // Protected to ensure subclasses are correctly activated. Internal for ease of use when testing. + [Activate] + protected internal IHtmlGenerator Generator { get; set; } + + /// + /// An expression to be evaluated against the current model. + /// + public ModelExpression For { get; set; } + + /// + public override void Process(TagHelperContext context, TagHelperOutput output) + { + if (For != null) + { + var tagBuilder = Generator.GenerateLabel(ViewContext, + For.Metadata, + For.Name, + labelText: null, + htmlAttributes: null); + + if (tagBuilder != null) + { + output.MergeAttributes(tagBuilder); + + // We check for whitespace to detect scenarios such as: + // + if (string.IsNullOrWhiteSpace(output.Content)) + { + output.Content = tagBuilder.InnerHtml; + } + + output.TagName = tagBuilder.TagName; + } + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LabelTagHelperTest.cs b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LabelTagHelperTest.cs new file mode 100644 index 0000000000..4c353d62b4 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.TagHelpers.Test/LabelTagHelperTest.cs @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// 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.AspNet.Mvc.ModelBinding; +using Microsoft.AspNet.Mvc.Razor; +using Microsoft.AspNet.Mvc.Rendering; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Xunit; + +namespace Microsoft.AspNet.Mvc.TagHelpers +{ + public class LabelTagHelperTest + { + // Model (List or Model instance), container type (Model or NestModel), model accessor, + // property path, TagHelperOutput.Content values. + public static TheoryData, string, TagHelperOutputContent> TestDataSet + { + get + { + var modelWithNull = new Model + { + NestedModel = new NestedModel + { + Text = null, + }, + Text = null, + }; + var modelWithText = new Model + { + NestedModel = new NestedModel + { + Text = "inner text", + }, + Text = "outer text", + }; + var models = new List + { + modelWithNull, + modelWithText, + }; + + return new TheoryData, string, TagHelperOutputContent> + { + { null, typeof(Model), () => null, "Text", + new TagHelperOutputContent(Environment.NewLine, "Text") }, + + { modelWithNull, typeof(Model), () => modelWithNull.Text, "Text", + new TagHelperOutputContent(Environment.NewLine, "Text") }, + { modelWithText, typeof(Model), () => modelWithText.Text, "Text", + new TagHelperOutputContent(Environment.NewLine, "Text") }, + { modelWithText, typeof(Model), () => modelWithNull.Text, "Text", + new TagHelperOutputContent("Hello World", "Hello World") }, + { modelWithText, typeof(Model), () => modelWithText.Text, "Text", + new TagHelperOutputContent("Hello World", "Hello World") }, + + { modelWithNull, typeof(NestedModel), () => modelWithNull.NestedModel.Text, "NestedModel.Text", + new TagHelperOutputContent(Environment.NewLine, "Text") }, + { modelWithText, typeof(NestedModel), () => modelWithText.NestedModel.Text, "NestedModel.Text", + new TagHelperOutputContent(Environment.NewLine, "Text") }, + { modelWithNull, typeof(NestedModel), () => modelWithNull.NestedModel.Text, "NestedModel.Text", + new TagHelperOutputContent("Hello World", "Hello World") }, + { modelWithText, typeof(NestedModel), () => modelWithText.NestedModel.Text, "NestedModel.Text", + new TagHelperOutputContent("Hello World", "Hello World") }, + + // Note: Tests cases below here will not work in practice due to current limitations on indexing + // into ModelExpressions. Will be fixed in https://github.com/aspnet/Mvc/issues/1345. + { models, typeof(Model), () => models[0].Text, "[0].Text", + new TagHelperOutputContent(Environment.NewLine, "Text") }, + { models, typeof(Model), () => models[1].Text, "[1].Text", + new TagHelperOutputContent(Environment.NewLine, "Text") }, + { models, typeof(Model), () => models[0].Text, "[0].Text", + new TagHelperOutputContent("Hello World", "Hello World") }, + { models, typeof(Model), () => models[1].Text, "[1].Text", + new TagHelperOutputContent("Hello World", "Hello World") }, + + { models, typeof(NestedModel), () => models[0].NestedModel.Text, "[0].NestedModel.Text", + new TagHelperOutputContent(Environment.NewLine, "Text") }, + { models, typeof(NestedModel), () => models[1].NestedModel.Text, "[1].NestedModel.Text", + new TagHelperOutputContent(Environment.NewLine, "Text") }, + { models, typeof(NestedModel), () => models[0].NestedModel.Text, "[0].NestedModel.Text", + new TagHelperOutputContent("Hello World", "Hello World") }, + { models, typeof(NestedModel), () => models[1].NestedModel.Text, "[1].NestedModel.Text", + new TagHelperOutputContent("Hello World", "Hello World") }, + }; + } + } + + [Theory] + [MemberData(nameof(TestDataSet))] + public async Task ProcessAsync_GeneratesExpectedOutput( + object model, + Type containerType, + Func modelAccessor, + string propertyPath, + TagHelperOutputContent tagHelperOutputContent) + { + // Arrange + var expectedAttributes = new Dictionary + { + { "class", "form-control" }, + { "for", propertyPath } + }; + var metadataProvider = new DataAnnotationsModelMetadataProvider(); + + // Property name is either nameof(Model.Text) or nameof(NestedModel.Text). + var metadata = metadataProvider.GetMetadataForProperty(modelAccessor, containerType, propertyName: "Text"); + var modelExpression = new ModelExpression(propertyPath, metadata); + var tagHelper = new LabelTagHelper + { + For = modelExpression, + }; + + var tagHelperContext = new TagHelperContext(allAttributes: new Dictionary()); + var htmlAttributes = new Dictionary + { + { "class", "form-control" }, + }; + var output = new TagHelperOutput("A random tag name", htmlAttributes, tagHelperOutputContent.OriginalContent); + var expectedTagName = "label"; + var htmlGenerator = new TestableHtmlGenerator(metadataProvider); + var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider); + tagHelper.ViewContext = viewContext; + tagHelper.Generator = htmlGenerator; + + // Act + await tagHelper.ProcessAsync(tagHelperContext, output); + + // Assert + Assert.Equal(expectedAttributes, output.Attributes); + Assert.Equal(tagHelperOutputContent.ExpectedContent, output.Content); + Assert.False(output.SelfClosing); + Assert.Equal(expectedTagName, output.TagName); + } + + [Fact] + public async Task TagHelper_LeavesOutputUnchanged_IfForNotBound2() + { + // Arrange + var expectedAttributes = new Dictionary + { + { "class", "form-control" }, + }; + var expectedContent = "original content"; + var expectedTagName = "original tag name"; + + var metadataProvider = new DataAnnotationsModelMetadataProvider(); + var metadata = metadataProvider.GetMetadataForProperty( + modelAccessor: () => null, + containerType: typeof(Model), + propertyName: nameof(Model.Text)); + var modelExpression = new ModelExpression(nameof(Model.Text), metadata); + var tagHelper = new LabelTagHelper(); + + var tagHelperContext = new TagHelperContext(allAttributes: new Dictionary()); + var output = new TagHelperOutput(expectedTagName, expectedAttributes, expectedContent); + + var htmlGenerator = new TestableHtmlGenerator(metadataProvider); + Model model = null; + var viewContext = TestableHtmlGenerator.GetViewContext(model, htmlGenerator, metadataProvider); + var activator = new DefaultTagHelperActivator(); + activator.Activate(tagHelper, viewContext); + + // Act + await tagHelper.ProcessAsync(tagHelperContext, output); + + // Assert + Assert.Equal(expectedAttributes, output.Attributes); + Assert.Equal(expectedContent, output.Content); + Assert.Equal(expectedTagName, output.TagName); + } + + public class TagHelperOutputContent + { + public TagHelperOutputContent(string outputContent, string expectedContent) + { + OriginalContent = outputContent; + ExpectedContent = expectedContent; + } + + public string OriginalContent { get; set; } + public string ExpectedContent { get; set; } + } + + private class Model + { + public string Text { get; set; } + + public NestedModel NestedModel { get; set; } + } + + private class NestedModel + { + public string Text { get; set; } + } + } +} \ No newline at end of file