Introduction to ASP.NET Core part 18: custom tag helpers cont’d

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.

Advertisements

About Andras Nemes
I'm a .NET/Java developer living and working in Stockholm, Sweden.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

ultimatemindsettoday

A great WordPress.com site

Elliot Balynn's Blog

A directory of wonderful thoughts

Robin Sedlaczek's Blog

Developer on Microsoft Technologies

HarsH ReaLiTy

My goal with this blog is to offend everyone in the world at least once with my words… so no one has a reason to have a heightened sense of themselves. We are all ignorant, we are all found wanting, we are all bad people sometimes.

Softwarearchitektur in der Praxis

Wissenswertes zu Webentwicklung, Domain-Driven Design und Microservices

the software architecture

thoughts, ideas, diagrams,enterprise code, design pattern , solution designs

Technology Talks

on Microsoft technologies, Web, Android and others

Software Engineering

Web development

Disparate Opinions

Various tidbits

chsakell's Blog

Anything around ASP.NET MVC,WEB API, WCF, Entity Framework & AngularJS

Cyber Matters

Bite-size insight on Cyber Security for the not too technical.

Guru N Guns's

OneSolution To dOTnET.

Johnny Zraiby

Measuring programming progress by lines of code is like measuring aircraft building progress by weight.

%d bloggers like this: