How to render scripts, generated in TagHelper process method, to the bottom of the page rather than next to the tag element?
Asked Answered
P

7

6

I am generating scripts in process method of 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());
}
}

Now it place the script very next to the tag element as its sibling.

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

Philippines answered 9/7, 2015 at 9:31 Comment(0)
C
10

I have created a pair of custom tag helpers that are able to solve your problem.

The first one is <storecontent> and it just stores the html content wrapped inside it in the TempData dictionary. It provides no direct output. The content may be an inline script or any other html. Many tag helpers of this kind can be placed in various locations e.g. in partial views.

The second tag helper is <renderstoredcontent> and it renders all the previously stored contents at the desired location e.g at the end of 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);
        }
    } 
} 

Basic usage:

In some view or partial view:

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

In another location:

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

And finally at the desired location where both scripts should be rendered:

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

That's it.

A few notes:

  1. There can be any number of <storecontent> tags. The asp-key attribute is required, at least as empty "". If you specify specific values for this attribute you can group the stored content and render specific groups at different locations. E.g. if you specify some content with asp-key="scripts" and some other content with asp-key="footnotes" then you can render only the first group as some location using:

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

The other group "footnotes" can be rendered at another location.

  1. The <storecontent> must be defined before the <renderstoredcontent> is applied. In ASP.NET the response is generated in a reverse hierarchical order, firstly the innermost partial views are generated, 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 a partial view and then render the scripts at the end of the body section in the layout page.

  2. Don't forget to reference your custom tag helpers in the _ViewImports.cshtml file using the command @addTagHelper "*, YourProjectHere"

Sorry for the long post, and I hope it helps!

Chocolate answered 12/8, 2015 at 14:36 Comment(1)
Thank you sou much.Advantageous
S
5

Create a BodyTagHelper which inserts a value into TagHelperContext.Items and is then set in your custom TagHelper.

Full 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!

Skepticism answered 5/8, 2015 at 17:55 Comment(0)
E
1

Have a @section scripts {} that's rendered on Layout with @RenderSection("scripts") and place your tag helper inside the a scripts section. When rendered, It will be placed where is defined on 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 on any other cshtml file,

<p>Some page</p>
@section scripts {
    <mytaghelper>foo</mytaghelper>
}
Endometriosis answered 9/7, 2015 at 17:42 Comment(0)
H
0

I don't believe it is possible from inside the tagHelper to add script at the bottom or anywhere else but the location of the tag that the taghelper is rendering. I think if the taghelper depends on some external js file it should not be the responsibility of the taghelper itself to add the script. For example the built in validation taghelpers like:

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

all the validation taghelper does is decorate the span with data- attributes, it does not add any scripts to the page and the data- attributes will just be ignored if the scripts are missing.

consider that a view may have multiple validation taghelpers used and we would not want each one adding another script.

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

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

one possible strategy to automate script inclusion is your tagHelper could take IHttpContextAccessor in its constructor so it will be injected by DI, then you could access the HttpContext.Items collection and add a variable to indicate the need for a script, then in a partial view that adds scripts you could detect the added variable to decide which script(s) to include.

But myself I think it is more straightforward to just add the script where needed to support the use of the taghelper rather than trying to get fancy and add things automatically.

This idea only would work for external js files not for js that is written dynamically inside the taghelper, but it is better not to have such scripts and only use external script files if possible. If you really need to generate script inside the taghelper I think you will only be able to render it at the location of the element the taghelper is processing.

Hydrate answered 9/7, 2015 at 14:47 Comment(0)
L
0

I know this thread is old, but if someone is looking for an easy fix to run some javascript here is a way.

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

But often your tag helper takes data as input that is relevant for the client side script. To be able to run this data through a js function you could 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 when the scripts are ready they can look for your tag helper and execute the proper function with the relevant data.

Longways answered 15/7, 2017 at 13:8 Comment(0)
C
0

Maybe not the most elegant solution but still working:

Wrap the tag that you want to generate inside a span and then append some HTML to the InnerHtml of this span:

 myTag = new TagBuilder("span");
 var mymask = htmlGenerator.GenerateTextBox(...);
 myTag.InnerHtml.AppendHtml(mymask);
 myTag.InnerHtml.AppendHtml(@"<script>...</script>");
Capitation answered 20/2, 2019 at 14:31 Comment(0)
T
-1

Instead of putting the javascript at the bottom of the page you can go a step further and entirely seperate your html (the taghelper) from your javascript. Write your Javascript so that it will find your taghelper and initialize itself.

As an example here is a Taghelper/Javascript I use that takes a UTC datetime and displays it in the users local time, formatted as datetime, time or date.

The 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 moment.js but irrelevant 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");
    });
}
Turfman answered 30/6, 2016 at 1:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.