How do I render the scripts generated in the TagHelper process method at the bottom of the page instead of next to the tag element?

I am generating scripts in a method method of the TagHelper class as follows

[TargetElement("MyTag")]
    public Class MYClass: TagHelper{
      public override void Process(TagHelperContext context, TagHelperOutput output)
        {
StringBuilder builder = new StringBuilder();

                builder.Append("<script>");
                builder.Append("//some javascript codes here);
                builder.Append("</script>");
                output.Content.Append(builder.ToString());
}
}

      

It now puts the script next to the tag element as its sibling.

I need to put scripts at the end of the body section.

+2


source to share


6 answers


Create BodyTagHelper

one that inserts the value into TagHelperContext.Items

and then set in your custom TagHelper

.

Complete bit of code:



public class BodyContext
{
    public bool AddCustomScriptTag { get; set; }
}

public class BodyTagHelper : TagHelper
{
    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        var builder = new StringBuilder();

        var bodyContext = new BodyContext();

        context.Items["BodyContext"] = bodyContext;

        // Execute children, they can read the BodyContext
        await context.GetChildContentAsync();

        if (bodyContext.AddCustomScriptTag)
        {
            // Add script tags after the body content but before end tag.
            output
                .PostContent
                .Append("<script>")
                .Append("//some javascript codes here")
                .Append("</script>");
        }
    }
}

[TargetElement("MyTag")]
public class MYClass : TagHelper
{
    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        // Do whatever you want

        object bodyContextObj;
        if (context.Items.TryGetValue("BodyContext", out bodyContextObj))
        {
            // Notify parent that we need the script tag
            ((BodyContext)bodyContextObj).AddCustomScriptTag = true;
        }
    }
}

      

Hope this helps!

+3


source


I've created a couple of custom tag helpers that might solve your problem.

The first one is <storecontent>

and it just stores the html content enclosed inside it in the TempData dictionary. It does not provide direct output. The content can be inline script or any other html. Many helpers of this type can be placed in different places, for example. in partial views.

The second tag helper <renderstoredcontent>

and it displays all previously saved content in the right place, for example at the end of the body element.

Code for StoreContentTagHelper.cs

:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;


namespace YourProjectHere.TagHelpers
{
    [TargetElement("storecontent", Attributes = KeyAttributeName)]
    public class StoreContentTagHelper : TagHelper
    {
        private const string KeyAttributeName = "asp-key";
        private const string _storageKey = "storecontent";
        private const string _defaultListKey = "DefaultKey";

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

        [HtmlAttributeName(KeyAttributeName)]
        public string Key { get; set; }

        public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            output.SuppressOutput();
            TagHelperContent childContent = await context.GetChildContentAsync();

            var storageProvider = ViewContext.TempData;
            Dictionary<string, List<HtmlString>> storage;
            List<HtmlString> defaultList;

            if (!storageProvider.ContainsKey(_storageKey) || !(storageProvider[_storageKey] is Dictionary<string,List<HtmlString>>))
            {
                storage = new Dictionary<string, List<HtmlString>>();
                storageProvider[_storageKey] = storage;
                defaultList = new List<HtmlString>();
                storage.Add(_defaultListKey, defaultList);
            }
            else
            {
                storage = ViewContext.TempData[_storageKey] as Dictionary<string, List<HtmlString>>;
                if (storage.ContainsKey(_defaultListKey))
                {
                    defaultList = storage[_defaultListKey];

                }
                else
                {
                    defaultList = new List<HtmlString>();
                    storage.Add(_defaultListKey, defaultList);
                }
            }

            if (String.IsNullOrEmpty(Key))
            {
                defaultList.Add(new HtmlString(childContent.GetContent()));
            }
            else
            {
                if(storage.ContainsKey(Key))
                {
                    storage[Key].Add(new HtmlString(childContent.GetContent()));
                }
                else
                {
                    storage.Add(Key, new List<HtmlString>() { new HtmlString(childContent.GetContent()) });
                }
            }
        }
    } 
} 

      

Code for RenderStoredContentTagHelper.cs

:

using System;
using System.Linq;
using System.Collections.Generic;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;


namespace YourProjectHere.TagHelpers
{
    [TargetElement("renderstoredcontent", Attributes = KeyAttributeName)]
    public class RenderStoredContentTagHelper : TagHelper
    {
        private const string KeyAttributeName = "asp-key";
        private const string _storageKey = "storecontent";

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

        [HtmlAttributeName(KeyAttributeName)]
        public string Key { get; set; }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.TagName = String.Empty;

            var storageProvider = ViewContext.TempData;
            Dictionary<string, List<HtmlString>> storage;

            if (!storageProvider.ContainsKey(_storageKey) || !(storageProvider[_storageKey] is Dictionary<string, List<HtmlString>>))
            {
                return;
            }

            storage = storageProvider[_storageKey] as Dictionary<string, List<HtmlString>>;
            string html = "";

            if (String.IsNullOrEmpty(Key))
            {
                html = String.Join("", storage.Values.SelectMany(x => x).ToList());
            }
            else
            {
                if (!storage.ContainsKey(Key)) return;
                html = String.Join("", storage[Key]);
            }

            TagBuilder tagBuilder = new TagBuilder("dummy");
            tagBuilder.InnerHtml = html;
            output.Content.SetContent(tagBuilder.InnerHtml);
        }
    } 
} 

      

Primary use:

From some point of view or partial view:

<storecontent asp-key="">
  <script>
    your inline script...
  </script>
</storecontent>

      

In the other place:



<storecontent asp-key="">
  <script src="..."></script>
</storecontent>

      

And finally, in the desired location where both scripts should appear:

<renderstoredcontent asp-key=""></renderstoredcontent>

      

What is it.

A few notes:

  • There can be any number of tags <storecontent>

    . The attribute asp-key

    is required at least as empty "". If you provide specific values ​​for this attribute, you can group the saved content and display specific groups in different locations. For example. if you specify some content with asp-key="scripts"

    and some other content with asp-key="footnotes"

    , you can only display the first group as some location using:

<renderstoredcontent asp-key="scripts"></renderstoredcontent>

Another group of "footnotes" can be displayed elsewhere.

  1. <storecontent>

    must be determined prior to use <renderstoredcontent>

    . ASP.NET generates the response in reverse hierarchical order, first generating the innermost partial views, then the parent partial view, then the main view, and finally the layout page. Therefore, you can easily use these tag helpers to define scripts in partial view and then render scripts at the end of the body section on the layout page.

  2. Remember to reference your custom helper tags in the _ViewImports.cshtml file using the command @addTagHelper "*, YourProjectHere"

Sorry for the long post and I hope this helps!

+2


source


You have @section scripts {}

that displayed in the layout with @RenderSection("scripts")

, and place the tag helper in the scripting section. When displayed, it will be placed where defined in the layout (at the bottom of your html).

<!DOCTYPE html>
<html>
<head>
</head>
<body>
    <div>
        <p>some html ... bla bla bla</p>
        @RenderBody()
    </div>
    @RenderSection("scripts", required: false)
</body>
</html>

      

then in any other cshtml file,

<p>Some page</p>
@section scripts {
    <mytaghelper>foo</mytaghelper>
}

      

+1


source


I don't believe it is possible to add a script inside the tagHelper at the bottom or anywhere other than the location of the tag that the taghelper is executing. I think that if the taghelper depends on some external js file, it should be the responsibility of the taghelper itself to add the script. For example, built-in validation markers such as:

<span asp-validation-for="Email" class="text-danger"></span>

      

all the taghelper check does is decorate the span with data attributes, doesn't add any scripts to the page, and the data attributes are simply ignored if scripts are missing.

think that a view can have multiple validation labels and we don't want each one to add another script.

In the VS starter web app template, you can see the validation scripts are added by the partial view at the bottom of the view (like Login.cshtml)

@{await Html.RenderPartialAsync("_ValidationScriptsPartial"); }

      

one possible strategy to automate script inclusion is your tagHelper can accept an IHttpContextAccessor in its constructor, so it will be injected by DI, then you can access the HttpContext.Items collection and add a variable indicating the script is needed, then in a partial view that adds scripts , you can discover the added variable to decide which script to include.

But I myself find it easier to just add a script where needed to support taghelper usage, rather than trying to get fancy and add things automatically.

This idea will only work for external js files, not js written dynamically inside taghelper, but it is better not to have such scripts and only use external script files if possible. If you really need to generate the script inside the taghelper, I think you will only be able to display it where the taghelper is handled.

0


source


Instead of putting javascript at the bottom of the page, you can go a step further and completely separate your html (taghelper) from your javascript. Write your Javascript so that it finds your taghelper and initializes itself.

Taghelper / Javascript is used here as an example, which takes UTC date and time and displays it in the user's local time, formatted as date, time or date.

Tag Helper

[HtmlTargetElement("datetime", Attributes = "format,value")]
public class DateTimeTagHelper : TagHelper {

    [HtmlAttributeName("format")]
    public DateTimeFormat Format { get; set; }

    [HtmlAttributeName("value")]
    public DateTime Value { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output) {

        output.TagName = "span";
        output.TagMode = TagMode.StartTagAndEndTag;

        output.Attributes.Add("class", "datetime_init");
        output.Attributes.Add("format", Format);
        output.Attributes.Add("value", Value.ToString("u"));

    }
}

      

Javascript (requires a moment .js, but not relevant to the concept)

$(document).ready(function () {
    DateTime_Init();
}

function DateTime_Init() {
    $(".datetime_init").each(function () {
        var utctime = $(this).attr("value");
        var localTime = moment.utc(utctime).toDate();

        switch($(this).attr("format")) {
            case "Date":
                $(this).html(moment(localTime).format('DD/MM/YYYY'));
                break;
            case "Time":
                $(this).html(moment(localTime).format('HH:mm'));
                break;
            default:
                $(this).html(moment(localTime).format('DD/MM/YYYY HH:mm'));
                break;
        }

        //Ensure this code only runs once
        $(this).removeClass("datetime_init");
    });
}

      

0


source


I know this thread is out of date, but if anyone is looking for an easy fix to run some javascript this is the way to go.

First, the ViewComponents expose the server side, so naturally the client scripts will not be ready at this point. As others have pointed out, you can display some section scripts where needed, which is what your tag helper will interpret, this is great for decoupling and you just include the script where needed.

But often your tag helper takes data as input, which is important to the client-side script. To be able to run this data through a js function, you can do something like this.

TagHelper.cs

var data= $@"
        '{Id}', 
        '{Title}', 
        {JsonConvert.SerializeObject(MyList)}";

output.Attributes.SetAttribute("data-eval", data);

      

site.js

$(".tag-helper-class").each((i, e) => {
    const jq = $(e);

    const data= jq.data("eval");

    if (!data) {
        return;
    }
    jq.attr("data-eval", "");
    eval(`myJsFunction(${data})`);
});

      

Now that the scripts are ready, they can search for your tag helper and execute the appropriate function with the appropriate data.

0


source







All Articles