I have a View Component that contains some jQuery in the Razor (.cshtml) file. The script itself is quite specific to the view (deals with some some configuration of third-party libraries), so I would like to keep the script and HTML in the same file for organization's sake.
The problem is that the script is not rendered in the _Layout Scripts section. Apparently this is just how MVC handles scripts with regards to View Components.
I can get around it by just having the scripts in the Razor file, but not inside of the Scripts section.
But then I run into dependency issues - because jQuery is used before the reference to the library (the reference to the library is near the bottom of the _Layout file).
Is there any clever solution to this other than including the reference to jQuery as part of the Razor code (which would impede HTML rendering where ever the component is placed)?
I'm currently not in front of the code, but I can certainly provide it once I get the chance if anyone needs to look at it to better understand it.
What I decided to do is write a ScriptTagHelper that provides an attribute of "OnContentLoaded". If true, then I wrap the inner contents of the script tag with a Javascript function to execute once the document is ready. This avoids the problem with the jQuery library having not loaded yet when the ViewComponent's script fires.
[HtmlTargetElement("script", Attributes = "on-content-loaded")]
public class ScriptTagHelper : TagHelper
{
/// <summary>
/// Execute script only once document is loaded.
/// </summary>
public bool OnContentLoaded { get; set; } = false;
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (!OnContentLoaded)
{
base.Process(context, output);
}
else
{
var content = output.GetChildContentAsync().Result;
var javascript = content.GetContent();
var sb = new StringBuilder();
sb.Append("document.addEventListener('DOMContentLoaded',");
sb.Append("function() {");
sb.Append(javascript);
sb.Append("});");
output.Content.SetHtmlContent(sb.ToString());
}
}
}
example usage within a ViewComponent:
<script type="text/javascript" on-content-loaded="true">
$('.button-collapse').sideNav();
</script>
There might be a better way than this, but this has worked for me so far.
Sections only work in a View don't work in partial views or View Component (
Default.cshtml
executes independently of the mainViewResult
and itsLayout
value isnull
by default. ) and that's by design.
You can use it to render sections
for a Layout
that the partial view
or view component's
view declares. However, sections defined in partials and view components don't flow back to the rendering view or it's Layout.
If you want to use jQuery or other library reference in your partial view or View component then you can pull your library into head
rather than body
in your Layout
page.
Example:
View Component:
public class ContactViewComponent : ViewComponent
{
public IViewComponentResult Invoke()
{
return View();
}
}
Location of ViewComponent:
/Views/[CurrentController]/Components/[NameOfComponent]/Default.cshtml
/Views/Shared/Components/[NameOfComponent]/Default.cshtml
Default.cshtml:
@model ViewComponentTest.ViewModels.Contact.ContactViewModel
<form method="post" asp-action="contact" asp-controller="home">
<fieldset>
<legend>Contact</legend>
Email: <input asp-for="Email" /><br />
Name: <input asp-for="Name" /><br />
Message: <textarea asp-for="Message"></textarea><br />
<input type="submit" value="Submit" class="btn btn-default" />
</fieldset>
</form>
_Layout.cshtml :
<!DOCTYPE html>
<html>
<head>
...
@RenderSection("styles", required: false)
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
</head>
<body>
@RenderBody()
...
//script move to head
@RenderSection("scripts", required: false)
</body>
</html>
View.cshtml:
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
..................
@{Html.RenderPartial("YourPartialView");}
..................
@await Component.InvokeAsync("Contact")
..................
@section Scripts {
<script>
//Do somthing
</script>
}
View Component by grahamehorner: (you can more information here)
The @section scripts don't render in a ViewComponent, view component should have the ability to include scripts within the @section unlike partital views as components may be reused across many views and the component is responsible for it's own functionality.
Here's the solution I'm using. It supports both external script loading and custom inline script. While some setup code needs to go in your layout file (e.g. _Layout.cshtml), everything is orchestrated from within the View Component.
Layout file:
Add this near the top of your page (inside the <head>
tag is a good place):
<script>
MYCOMPANY = window.MYCOMPANY || {};
MYCOMPANY.delayed = null;
MYCOMPANY.delay = function (f) { var e = MYCOMPANY.delayed; MYCOMPANY.delayed = e ? function () { e(); f(); } : f; };
</script>
Add this near the bottom of your page (just before the closing </body>
tag is a good place):
<script>MYCOMPANY.delay = function (f) { setTimeout(f, 1) }; if (MYCOMPANY.delayed) { MYCOMPANY.delay(MYCOMPANY.delayed); MYCOMPANY.delayed = null; }</script>
View Component:
Now anywhere in your code, including your View Component, you can do this and have the script executed at the bottom of your page:
<script>
MYCOMPANY.delay(function() {
console.log('Hello from the bottom of the page');
});
</script>
With External Script Dependency
But sometimes that's not enough. Sometimes you need to load an external JavaScript file and only execute your custom script once the external file is loaded. You also want to orchestrate this all in your View Component for full encapsulation. To do this, I add a little more setup code in my layout page (or external JavaScript file loaded by the layout page) to lazy load the external script file. That looks like this:
Layout file:
Lazy load script
<script>
MYCOMPANY.loadScript = (function () {
var ie = null;
if (/MSIE ([^;]+)/.test(navigator.userAgent)) {
var version = parseInt(RegExp['$1'], 10);
if (version)
ie = {
version: parseInt(version, 10)
};
}
var assets = {};
return function (url, callback, attributes) {
attributes || (attributes = {});
var onload = function (url) {
assets[url].loaded = true;
while (assets[url].callbacks.length > 0)
assets[url].callbacks.shift()();
};
if (assets[url]) {
if (assets[url].loaded)
callback();
assets[url].callbacks.push(callback);
} else {
assets[url] = {
loaded: false,
callbacks: [callback]
};
var script = document.createElement('script');
script.type = 'text/javascript';
script.async = true;
script.src = url;
for (var attribute in attributes)
if (attributes.hasOwnProperty(attribute))
script.setAttribute(attribute, attributes[attribute]);
// can't use feature detection, as script.readyState still exists in IE9
if (ie && ie.version < 9)
script.onreadystatechange = function () {
if (/loaded|complete/.test(script.readyState))
onload(url);
};
else
script.onload = function () {
onload(url);
};
document.getElementsByTagName('head')[0].appendChild(script);
}
};
}());
</script>
View Component:
Now you can do this in your View Component (or anywhere else for that matter):
<script>
MYCOMPANY.delay(function () {
MYCOMPANY.loadScript('/my_external_script.js', function () {
console.log('This executes after my_external_script.js is loaded.');
});
});
</script>
To clean things up a bit, here's a tag helper for the View Component (inspired by @JakeShakesworth's answer):
[HtmlTargetElement("script", Attributes = "delay")]
public class ScriptDelayTagHelper : TagHelper
{
/// <summary>
/// Delay script execution until the end of the page
/// </summary>
public bool Delay { get; set; }
/// <summary>
/// Execute only after this external script file is loaded
/// </summary>
[HtmlAttributeName("after")]
public string After { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
if (!Delay) base.Process(context, output);
var content = output.GetChildContentAsync().Result;
var javascript = content.GetContent();
var sb = new StringBuilder();
sb.Append("if (!MYCOMPANY || !MYCOMPANY.delay) throw 'MYCOMPANY.delay missing.';");
sb.Append("MYCOMPANY.delay(function() {");
sb.Append(LoadFile(javascript));
sb.Append("});");
output.Content.SetHtmlContent(sb.ToString());
}
string LoadFile(string javascript)
{
if (After == NullOrWhiteSpace)
return javascript;
var sb = new StringBuilder();
sb.Append("if (!MYCOMPANY || !MYCOMPANY.loadScript) throw 'MYCOMPANY.loadScript missing.';");
sb.Append("MYCOMPANY.loadScript(");
sb.Append($"'{After}',");
sb.Append("function() {");
sb.Append(javascript);
sb.Append("});");
return sb.ToString();
}
}
Now in my View Component I can do this:
<script delay="true" after="/my_external_script.js"]">
console.log('This executes after my_external_script.js is loaded.');
</script>
You can also leave out the after
attribute if the View Component doesn't have an external file dependency.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With