Updated May 5 2016: Updated code to work with ASP.NET Core RC2
In a previous blog post we talked about how to create a simple tag helper in ASP.NET Core MVC. In today’s post we take this one step further and create a more complex tag helper that is made up of multiple parts.
A Tag Helper for Bootstrap Modal Dialogs
<div class="modal fade" tabindex="-1" role="dialog"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> <h4 class="modal-title">Modal title</h4> </div> <div class="modal-body"> <p>One fine body…</p> </div> <div class="modal-footer"> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> <button type="button" class="btn btn-primary">Save changes</button> </div> </div> </div> </div> |
Using a tag helper here would help simplify the markup but this is a little more complicated than the Progress Bag example. In this case, we have HTML content that we want to add in 2 different places: the
element and the
element.
The solution here wasn’t immediately obvious. I had a chance to talk to Taylor Mullen at the MVP Summit ASP.NET Hackathon in November and he pointed me in the right direction. The solution is to use 3 different tag helpers that can communicate with each other through the
TagHelperContext
.
Ultimately, we want our tag helper markup to look like this:
<modal title="Modal title"> <modal-body> <p>One fine body…</p> </modal-body> <modal-footer> <button type="button" class="btn btn-default" data-dismiss="modal">Close</button> <button type="button" class="btn btn-primary">Save changes</button> </modal-footer> </modal> |
This solution uses 3 tag helpers:
modal
, modal-body
and modal-footer
. The contents of the modal-body
tag will be placed inside the
while the contents of the
tag will be placed inside the
element. The modal
tag helper is the one that will coordinate all this.Restricting Parents and Children
First things first, we want to make sure that
and
can only be placed inside the
tag and that the
tag can only contain those 2 tags. To do this, we set the RestrictChildren
attribute on the modal tag helper and the ParentTag
property of the HtmlTargetElement
attribute on the modal body and modal footer tag helpers:[RestrictChildren("modal-body", "modal-footer")] public class ModalTagHelper : TagHelper { //... } [HtmlTargetElement("modal-body", ParentTag = "modal")] public class ModalBodyTagHelper : TagHelper { //... } [HtmlTargetElement("modal-footer", ParentTag = "modal")] public class ModalFooterTagHelper : TagHelper { //... } |
Now if we try to put any other tag in the
tag, Razor will give me a helpful error message.Getting contents from the children
The next step is to create a context class that will be used to keep track of the contents of the 2 child tag helpers.
public class ModalContext { public IHtmlContent Body { get; set; } public IHtmlContent Footer { get; set; } } |
At the beginning of the ProcessAsync method of the Modal tag helper, create a new instance of
ModalContext
and add it to the currentTagHelperContext
:public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { var modalContext = new ModalContext(); context.Items.Add(typeof(ModalTagHelper), modalContext); //... } |
Now, in the modal body and modal footer tag helpers we will get the instance of that
ModalContext
via the TagHelperContext
. Instead of rendering the output, these child tag helpers will set the the Body
and Footer
properties of the ModalContext
.[HtmlTargetElement("modal-body", ParentTag = "modal")] public class ModalBodyTagHelper : TagHelper { public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { var childContent = await output.GetChildContentAsync(); var modalContext = (ModalContext)context.Items[typeof(ModalTagHelper)]; modalContext.Body = childContent; output.SuppressOutput(); } } |
Back in the modal tag helper, we call
output.GetChildContentAsync()
which will cause the child tag helpers to execute and set the properties on the ModalContext
. After that, we just set the output as we normally would in a tag helper, placing the Body
and Footer
in the appropriate elements.public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { var modalContext = new ModalContext(); context.Items.Add(typeof(ModalTagHelper), modalContext); await output.GetChildContentAsync(); var template = $@"<div class='modal-dialog' role='document'> <div class='modal-content'> <div class='modal-header'> <button type = 'button' class='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>×</span></button> <h4 class='modal-title' id='{context.UniqueId}Label'>{Title}</h4> </div> <div class='modal-body'>"; output.TagName = "div"; output.Attributes["role"] = "dialog"; output.Attributes["id"] = Id; output.Attributes["aria-labelledby"] = $"{context.UniqueId}Label"; output.Attributes["tabindex"] = "-1"; var classNames = "modal fade"; if (output.Attributes.ContainsName("class")) { classNames = string.Format("{0} {1}", output.Attributes["class"].Value, classNames); } output.Attributes.SetAttribute("class", classNames); output.Content.AppendHtml(template); if (modalContext.Body != null) { output.Content.AppendHtml(modalContext.Body); //Setting the body contents } output.Content.AppendHtml("</div>"); if (modalContext.Footer != null) { output.Content.AppendHtml("<div class='modal-footer'>"); output.Content.AppendHtml(modalContext.Footer); //Setting the footer contents output.Content.AppendHtml("</div>"); } output.Content.AppendHtml("</div></div>"); } |
Conclusion
Composing complex tag helpers with parent / child relationships is fairly straight forward. In my opinion, the approach here is much easier to understand than the “multiple transclusion” approach used to solve the same problem in Angular 1. It would be easy to unit test and as always, Visual Studio provides error messages directly in the HTML editor to guide anyone who is using your tag helper.
You can check out the full source code on the Tag Helper Samples repo.