Using the ASP.NET Core Script TagHelper to Polyfill The Latest JavaScript Features

tl;dr

The primary use case for fallbacks in the Script Tag Helper is checking if a CDN is down and loading a local script instead.  Another use case is you can use fallbacks to make it easy to polyfill missing JavaScript features in older browsers, while not having your users using the latest browsers take a network hit.


<script asp-fallback-test="window.Promise"
asp-fallback-src="https://unpkg.com/promise-polyfill@6.0.2">
</script>
<script asp-fallback-test="window.fetch"
asp-fallback-src="https://unpkg.com/whatwg-fetch@2.0.2">
</script>


View this gist on GitHub

Background – What is a Polyfill?

A polyfill is a fallback, written in JavaScript, that fills in a new JavaScript feature that is missing in an older browser and replicates the same functionality that newer browsers provide natively.  This allows JavaScript code using the latest features only available in newer browsers to work on older browsers as well.

To polyfill the API, you simply just check if that API is available, and if not, load the polyfill that adds that API to your browser.  That way, if your user is using the latest browser, you don’t punish them by downloading something they don’t need and hurting their performance.

How a Polyfill works

This JavaScript code will polyfill the “new” fetch API for making AJAX calls, that is currently available in all of the latest major browsers (Chrome, Edge, Firefox, Safari, etc.), but not in any of the Internet Explorer family tree (not even IE11).


window.Promise || document.write('<script src="https://unpkg.com/promise-polyfill@6.0.2"></script>&#39;)
window.fetch || document.write('<script src="https://unpkg.com/whatwg-fetch@2.0.2"></script>&#39;)

First, the code looks to see if the Promise API is available, because fetch relies on Promises in order to work.  If the API is not available, it loads a script from a CDN that polyfills the Promise API.  It does the same thing for the fetch API.

The built-in Script Tag Helper – CDN fallback

The built-in Script Tag Helper in ASP.NET Core has fallback support baked in that generates code very close to this.  The original intent of their fallback support was for attempting to load a script from a CDN.  After loading from that CDN, test to see if a JavaScript API is available, and if it’s not (due to the CDN being down, blocked, or whatever) fall back to loading a local JavaScript file.  That code looks like this:


<script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.2.0.2.min.js"
asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
asp-fallback-test="window.jQuery">
</script>

The fallback works for polyfills too!

What I haven’t seen anyone mention is to use this to conditionally polyfill if a JavaScript feature doesn’t exist.  And it works right out of the box, simply omit the src attribute, and the rest will be taken care of:


<script asp-fallback-test="window.Promise"
asp-fallback-src="https://unpkg.com/promise-polyfill@6.0.2">
</script>
<script asp-fallback-test="window.fetch"
asp-fallback-src="https://unpkg.com/whatwg-fetch@2.0.2">
</script>

The code above will generate nearly identical code to what’s in the first code sample.  As mentioned above, fetch needs Promises to work, so I’m also polyfilling that conditionally as well.  In the real world, I would likely bundle these two together (locally or using something like Polyfill.io) and make one network request, but using unpkg (an NPM CDN) was just for simplicity’s sake.

With the code above, I get the best of both worlds.  My code using fetch runs on older browsers, but my users who use the latest browsers don’t take a network hit for something they don’t need.  This same methodology will work for other ES2015 and above features and is nice for small apps that want to use some of the latest features and not have to deal with the build config of Babel or TypeScript transpiling down to ES5.

I’m not sure if this was an intended feature of the Script Tag Helper, but it’s a good use case for the fallback feature that I hadn’t seen anyone talk about, so I wanted to call attention to it.

And if you are lucky enough to only have to support Chrome at work.  I assume this is you right now reading the hoops us plebes have to go through to get our code to run on all browsers:

https://media.giphy.com/media/l3nWqD4ViFej9REAw/giphy.gif

Custom Tag Helper: Toggling Visibility On Existing HTML elements

A lot of times we need a way to toggle the visibility of an element based on some server-side values.  The most common example is if someone is logged in, show them more options than someone who is not logged in.

Option without Tag Helpers


@if (User.Identity.IsAuthenticated)
{
<div>
This should only be visible if you're logged in
</div>
}

view raw

Index.cshtml

hosted with ❤ by GitHub

This solution always felt a little dirty to me

What I’d like to do is to add an attribute off of div to toggle visibility.  So instead I’d like something that looks like this:


<div is-visible="User.Identity.IsAuthenticated">
This should only be visible if you're logged in
</div>

view raw

Index.cshtml

hosted with ❤ by GitHub

So let’s create a custom Tag Helper to do this

  • Create a TagHelpers folder in the root of your MVC project
  • Create a new class called VisibilityTagHelper
  • Copy and paste the super simple code below


// Add more target elements here on a new line if you want to target more than just div. Example: [HtmlTargetElement("a")] to hide/show links
[HtmlTargetElement("div")]
public class VisibilityTagHelper : TagHelper
{
// default to true otherwise all existing target elements will not be shown, because bool's default to false
public bool IsVisible { get; set; } = true;
// You only need one of these Process methods, but just showing the sync and async versions
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (!IsVisible)
output.SuppressOutput();
base.Process(context, output);
}
public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
if (!IsVisible)
output.SuppressOutput();
return base.ProcessAsync(context, output);
}
}

  • Go to /Views/_ViewImports.cshtml and add a line for all the TagHelpers in your project by doing @addTagHelper *,  .  Example below:


@addTagHelper *, WebApplication1

  • Build your project *this is important or you won’t get intellisense*
  • Start using it such as the example shown below (same as above)


<div is-visible="User.Identity.IsAuthenticated">
This should only be visible if you're logged in
</div>

view raw

Index.cshtml

hosted with ❤ by GitHub

  • That’s it!

 

Or view the GitHub Commit that makes this happen

I also have a repo, where the first two commits are just the initial commit with a File => New Project.  This 3rd commit outlines all the changes needed to make to get this to work if you’d rather learn via a Commit.

 

Breaking Down The Code

The code for the Visibility Tag Helper is pretty straight forward, but here’s a breakdown of it:

  • HtmlTargetElement
    • You first decorate your class with the HtmlTargetElement attribute to tell it that you’re targeting div’s.
      [HtmlTargetElement("div")]
    • You can target more than just divs by adding more attributes.
      [HtmlTargetElement("div")]
      [HtmlTargetElement("a")]
    • You could add the is-visible attribute to ALL elements by using *.  This could be quite handy
      [HtmlTargetElement("*")]
    • UPDATE: 10/4/2018 – Per Andrew’s comment below, you can have this target all elements like above, but skip going through this TagHelper rendering by saying only run it if they have the is-visible attribute.  This should save some perf as well as not give you purple tags in Visual Studio on all of your HTML elements.
    • [HtmlTargetElement("*", Attributes = "is-visible")]
  • Inherit from TagHelper.
    • Naming the TagHelper doesn’t matter, as we are choosing the HtmlTargetElement.  If we were making our own tag, then the tag would be based off of the name.
      public class VisibilityTagHelper : TagHelper
      
  • Add a property to store the visibility of the tag.
    • We need to default it to true, otherwise none of the HTML tags specified in the HtmlTargetElement will be visible, because booleans default to false.  THAT IS REALLY IMPORTANT.  The name will be kebab-cased which means each capital letter will be split by a dash.  Example IsVisible becomes is-visible when using the tag.
      public bool IsVisible { get; set; } = true;
      
  •   Override the Process method
    • If the tag is set to not IsVisible, then we need to supress the output which will not render the tag at all.  (Thanks to Damian Edwards for the tip about SuppressOutput())
      public override void Process(TagHelperContext context, TagHelperOutput output)
      {
          if (!IsVisible)
              output.SuppressOutput();
          base.Process(context, output);
       }
      
  • That’s about it!  Super simple.

 

Update 1/3/2017: As mentioned above – thanks to Damian Edwards for informing me of the output.SuppressOutput() method.  That made things simpler.