Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 211
Alan Sangeeth S Avatar asked Jul 09 '15 09:07

Alan Sangeeth S


People also ask

What is the difference between Htmlhelper and Taghelper?

HtmlHelpers vs. Unlike HtmlHelpers, a tag helper is a class that attaches itself to an HTML-compliant element in a View or Razor Page. The tag helper can, through its properties, add additional attributes to the element that a developer can use to customize the tag's behavior.

How might you supply data from a database into a tag helper's output?

Passing a model Data to a Tag Helper We can also pass the model data to the tag helper via model binding by creating properties of type "ModelExpression". Using HtmlAttributeName attribute, we can create a friendly attribute name. The ModelExpression describes a model expression passed to the tag helper.

How do I disable tag helper at the element level?

The opt-out character (“!”) is used to disable the Tag Helper at the element level. With the opt-out character, the HTML will not be generated for the label tag in the above case. We can use this opt-out character if we want to conditionally control rendering of the HTML elements.

How would the form tag helper create a form element for posting data back to the server?

The Form Tag HelperGenerates the HTML <FORM> action attribute value for a MVC controller action or named route. Generates a hidden Request Verification Token to prevent cross-site request forgery (when used with the [ValidateAntiForgeryToken] attribute in the HTTP Post action method)


2 Answers

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!

like image 165
Christos K. Avatar answered Oct 21 '22 06:10

Christos K.


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!

like image 40
N. Taylor Mullen Avatar answered Oct 21 '22 06:10

N. Taylor Mullen