Introduction to ASP.NET Core part 18: custom tag helpers cont’d
February 27, 2017 Leave a comment
Introduction
In the previous post we started looking into custom tags. The most straightforward way of creating custom HTML tags is to derive from the TagHelper class and override its Process or ProcessAsync function. Otherwise a custom tag helper is a normal C# class, i.e. there’s no special template involved. We discussed some examples of custom attributes and elements as well.
In this post we’ll look at some more examples of custom tags. We’ll also see how to pass in an object into a custom tag as an argument.
HTML content setter
In the first example we’ll see how to enclose a string value within HTML opening and closing tags. In particular we want to transform…
<p spandec="class-value">Hello from paragraph</p>
…into the following:
<p><span class="class-value">Hello from paragraph</span></p>
“spandec” stands for SpanDecorator. The goal is to read the attribute value of “spandec” and turn it into the value of the class attribute of a span that encloses the string content of “p”. The spandec attribute won’t only work for paragraphs though. We can decorate any element with it that has a text content, like div, h1, td etc.
Add a class called SpanDecoratorTagHelper into the TagHelpers folder:
using Microsoft.AspNetCore.Razor.TagHelpers; using System.Threading.Tasks; namespace DotNetCoreBookstore.TagHelpers { [HtmlTargetElement(Attributes = "spandec")] public class SpanDecoratorTagHelper : TagHelper { public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { TagHelperContent content = await output.GetChildContentAsync(); string textContent = content.GetContent(); string spandecAttrValue = output.Attributes["spandec"].Value.ToString(); output.Attributes.RemoveAll("spandec"); output.PreContent.SetHtmlContent($"<span class=\"{spandecAttrValue}\">"); output.PostContent.SetHtmlContent("</span>"); output.Content.SetContent(textContent); } } }
The HtmlTargetElement declares that the attribute is called “spandec” in the markup. The first two lines of the ProcessAsync method body read the text content of the element which is decorated by the “spandec” attribute. Then we read the text value of the spandec attribute itself and remove it from the final markup. The PreContent and PostContent attributes provide a way to add extra content before and after the text content. We open the span element before and close it after the text. In addition we add the class attribute.
You can try adding the new tag to a cshtml page as shown above, like here in Books/Index.cshtml:
<reverse>Hello world</reverse> <p spandec="class-value">Hello from paragraph</p> @section FunnyMessage { <p>This is a very funny message</p> }
Run the page and check the final markup in the view source. You’ll see that it was transformed as we originally intended.
Passing an object into the tag helper
In this exercise we want to show a single book as a special offer on the index page. We’ll do it by passing in a BookViewModel object into a custom tag. Here’s a reminder of what the BookViewModel object looks like:
public class BookViewModel { public int Id { get; set; } public string Title { get; set; } public string Author { get; set; } }
Here’s the custom tag helper:
using DotNetCoreBookstore.Models; using Microsoft.AspNetCore.Razor.TagHelpers; namespace DotNetCoreBookstore.TagHelpers { [HtmlTargetElement("book-on-offer")] public class SpecialBookOfferTagHelper : TagHelper { public BookViewModel Book { get; set; } public decimal SpecialPrice { get; set; } public override void Process(TagHelperContext context, TagHelperOutput output) { output.TagName = "section"; output.Content.SetHtmlContent( $@"<h1>Special offer!</h1><br /> <ul> <li>Title: {Book.Title}</li> <li>Author: {Book.Author}</li> <li>Price: {SpecialPrice}</li> </ul>"); } } }
We declare the tag to be called “book-on-offer”. The SpecialBookOfferTagHelper class will need a book view model and a special price that will be passed in as parameters though the “book” and “special-price” attributes. We build a short HTML section with a header and a list with the class properties.
Here’s how we can use this custom tag in the Books/Index HTML markup where we passed in a BookIndexViewModel object. Therefore “Model” refers to that in the following example:
<book-on-offer book="Model.BookViewModels.ElementAt(1)" special-price="120" ></book-on-offer>
We simply take the book at position 1 from the list that was passed in from the controller. The following markup will be produced:
<section><h1>Special offer!</h1><br /> <ul> <li>Title: Java for beginners</li> <li>Author: Jane Cook</li> <li>Price: 120</li> </ul></section>
This is another example of how we can mix HTML and C#. The following markup is equally valid:
<book-on-offer book="new BookViewModel() { Author = Model.BookViewModels.ElementAt(1).Author, Title = Model.BookViewModels.ElementAt(1).Title }" special-price="120"></book-on-offer>
Putting a condition to rendering a tag
Say that we only want to filter out some books from a table based on a condition. The following If tag helper can be a solution to suppress an HTML output:
using Microsoft.AspNetCore.Razor.TagHelpers; namespace DotNetCoreBookstore.TagHelpers { [HtmlTargetElement(Attributes = "if")] public class IfTagHelper : TagHelper { public bool If { get; set; } public override void Process(TagHelperContext context, TagHelperOutput output) { if (!If) { output.SuppressOutput(); } } } }
If the “If” condition is not met then we don’t want to show the assigned HTML. In the following example we won’t show any books whose ID is less than 2:
<h1>Id filtered books</h1> <table> <thead> <tr> <th>ID</th> <th>Author</th> <th>Title</th> <th>Details</th> <th>Details with tags</th> </tr> </thead> <tbody> @foreach (var bookViewModel in Model.BookViewModels) { <tr if="bookViewModel.Id > 2"> <td>@bookViewModel.Id</td> <td>@bookViewModel.Author</td> <td>@bookViewModel.Title</td> <td>@Html.ActionLink("Details", "Details", "Books", new { id = bookViewModel.Id }, new { @class = "book-details-link" })</td> <td><a asp-action="Details" asp-controller="Books" asp-route-id="@bookViewModel.Id" class="book-details-link">Details with tag library</a></td> </tr> } </tbody> </table>
Note how the if attribute is applied on the table row in the for-each loop. If you test the above code in e.g. Index.cshtml you’ll see that the two books with IDs 1 and 2 are not shown in the table.
We’ll continue in the next post.
View the list of MVC and Web API related posts here.