DamianEdwards / TagHelperPack

A set of useful, and possibly opinionated, Tag Helpers for ASP.NET Core
MIT License
337 stars 59 forks source link

JavaScript Collocation TagHelper #42

Open khalidabuhakmeh opened 2 years ago

khalidabuhakmeh commented 2 years ago

This is in reference to a .NET 6 feature known as JavaScript collocation. https://docs.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/?view=aspnetcore-6.0#load-a-script-from-an-external-javascript-file-js-collocated-with-a-component

I wanted to submit a PR but noticed you are multi-targeting .NET 3.1 and .NET 4.7.1 with this package. Seeing that JavaScript collocation is part of .NET 6 I'm not sure what to do. In any case, I've included the code below in the chance you'd like to add it to your library.

using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.FileProviders;

public class ViewScriptTagHelper : TagHelper
{
    private readonly IWebHostEnvironment environment;
    private readonly IFileVersionProvider fileVersionProvider;
    private const string AppendVersionAttributeName = "append-version";

    public ViewScriptTagHelper(IWebHostEnvironment environment, IFileVersionProvider fileVersionProvider)
    {
        this.environment = environment;
        this.fileVersionProvider = fileVersionProvider;
    }

    [ViewContext] 
    public ViewContext? ViewContext { get; set; }

    /// <summary>
    /// Value indicating if file version should be appended to src urls.
    /// </summary>
    /// <remarks>
    /// A query string "v" with the encoded content of the file is added.
    /// </remarks>
    [HtmlAttributeName(AppendVersionAttributeName)]
    public bool? AppendVersion { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        // remove the `page-script` tag if script doesn't exist
        output.TagName = null;
        output.TagMode = TagMode.StartTagAndEndTag;

        var viewPath = ViewContext?.View.Path;
        var src = $"{viewPath}.js";

        /* When the app is published, the framework automatically moves the script to the web root.
           So we should check both places, with the content root first for development */
        var exists = environment.ContentRootFileProvider.GetFileInfo(src).Exists ||
                   environment.WebRootFileProvider.GetFileInfo(src).Exists;

        if (exists)
        {
            // switch it to script now
            output.TagName = "script";
            output.Content = new DefaultTagHelperContent();

            if (AppendVersion == true)
            {
                // people love their cache busting versions
                src = fileVersionProvider.AddFileVersionToPath(src, src);
            }

            output.Attributes.Add("src", src);
        }
    }
}

Usage

Add the following to the end of your _Layout.cshtml file.

<view-script append-version="true" />
kfrancis commented 2 years ago

@khalidabuhakmeh Hmm, we're trying to use this ourselves but in production, it doesn't seem to work. The files are correctly under wwwroot, but the script tag isn't being rendered.

khalidabuhakmeh commented 2 years ago

Be sure you're importing the correct namespace with @addTagHelepers call in your ViewImports file.

DamianEdwards commented 2 years ago

Just noticed this! This would be great to add. If you're still up for it, you can add net6.0 as a TFM to multi-target to and only include this new Tag Helper in that target (via compiler directives) no problem.

kfrancis commented 2 years ago

Be sure you're importing the correct namespace with @addTagHelepers call in your ViewImports file.

Hmm, AFAIK it was correct. Still doesn't work in actual production here .. hmm.

khalidabuhakmeh commented 2 years ago

Honestly, I forget why I wrote this in the first place. 😅

I'll have to try it again or find the sample somewhere. Please hold as I try this in the next couple of days.

khalidabuhakmeh commented 2 years ago

OK, I remember now. I created a sample that shows it working. It's actually a good idea! (I have a couple of those a year). I hope this helps @kfrancis.

Screenshot 2022-08-09 at 15 54 41

https://github.com/khalidabuhakmeh/ColocationTagHelperSample

kfrancis commented 2 years ago

@khalidabuhakmeh So, still can't get it to work. We have a ton of razor pages, so perhaps it cares about the namespace really particularly? In your sample, the namespace is just basically ColocationTagHelperSample.Pages The scripts in production are making it to the wwwroot folder, but that's it. No script tag is omitted by the tag helper. Hmm. #perplexed

khalidabuhakmeh commented 2 years ago

Are you attempting colocation at a top level Razor element (View, Razor Page Razor component)?

This will not work with partials.

kfrancis commented 2 years ago

Only top-level razor pages.

khalidabuhakmeh commented 2 years ago

I'm not sure what to tell you at this point, without seeing your solution :(

AlexZeitler commented 2 years ago

I'm facing the same issue like @kfrancis. Tag works in dev but no script tag rendered in prod.

AlexZeitler commented 2 years ago

I'm facing the same issue like @kfrancis. Tag works in dev but no script tag rendered in prod.

The problem is caused by this line:

    var fileInfo = environment.ContentRootFileProvider.GetFileInfo(src) ??
                   environment.WebRootFileProvider.GetFileInfo(src);

environment.ContentRootFileProvider.GetFileInfo(src) never becomes null hence fileInfo.Exists() will be false in prod.

environment.WebRootFileProvider.GetFileInfo(src).Exists() is true in prod as expected.

This works both in dev and prod:

    var exists = environment.ContentRootFileProvider.GetFileInfo(src).Exists ||
                   environment.WebRootFileProvider.GetFileInfo(src).Exists;

    if (exists)
khalidabuhakmeh commented 2 years ago

Well... damn. Sorry folks. Thanks, @AlexZeitler, for catching that.

khalidabuhakmeh commented 2 years ago

I've updated the code above for brave souls who don't read below the big fancy code blocks.

kfrancis commented 2 years ago

Thank you @AlexZeitler for finding that!