How can I add Page specific javascript in SXA. I've done it using PlainHTML rendering, but what if my script is big and I want to call it from outside
-
Could you elaborate on using the page's body css class for a page-based solution? My organization is attempting to use url query parameters to customize text on a page and I'm struggling to find good information on how to utilize javascript on a page – Michael Dec 18 '20 at 14:55
I love this question because I have an answer. Not long ago I built a module for SXA that would do this. I may not have tested this on your version of SXA, but it should give you an idea how to do it.
Check out this link for more details.
- I added a processor called AddPlainIncludes to pull items from the media library and add to the page. SXA comes with a class called
PlainInclude
that you can use. - The processor looks for items in the media library, similar to a Theme but called Asset. The structure is similar but has additional properties for CDN.
I built the module so I could load scripts from a CDN.
var urlInclude = new PlainInclude
{
SortOrder = num,
Name = plainIncludeItem.Name,
Type = assetType,
Content = urlContent
};
assetsList.Add(urlInclude);
Here is the processor:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sitecore;
using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.StringExtensions;
using Sitecore.XA.Foundation.IoC;
using Sitecore.XA.Foundation.Multisite;
using Sitecore.XA.Foundation.SitecoreExtensions.Extensions;
using Sitecore.XA.Foundation.Theming.Bundler;
using Sitecore.XA.Foundation.Theming.Configuration;
using Sitecore.XA.Foundation.Theming.Pipelines.AssetService;
namespace Westco.XA.Foundation.Theming.Pipelines.AssetService
{
public class AddPlainIncludes : AddAssetsProcessor
{
public override void Process(AssetsArgs args)
{
if (!IsSxaPage()) return;
var assetConfiguration = AssetConfigurationReader.Read();
var contextItem = Context.Item;
if (contextItem == null) return;
var siteItem = ServiceLocator.Current.Resolve<IMultisiteContext>().GetSiteItem(contextItem);
var settingsItem = siteItem?.Children["Settings"];
if (settingsItem == null) return;
var assetIds = new string[0];
var siteAssetsIds = settingsItem[Templates.Page.Fields.AssociatedAssets]?.Split('|');
var pageAssetsIds = contextItem[Templates.Page.Fields.AssociatedAssets]?.Split('|');
if (siteAssetsIds != null && siteAssetsIds.Any())
{
assetIds = assetIds.Union(siteAssetsIds).ToArray();
}
if (pageAssetsIds != null && pageAssetsIds.Any())
{
assetIds = assetIds.Union(pageAssetsIds).ToArray();
}
if (!assetIds.Any()) return;
var assetsList = new List<AssetInclude>();
const int num = 0;
var plainIncludeItems = contextItem.Axes.SelectItems($"/sitecore/media library//*[@@templateid='{Templates.PlainInclude.Id}']");
if (!plainIncludeItems.Any()) return;
foreach (var assetId in assetIds)
{
if (assetId.IsNullOrEmpty() || !ID.IsID(assetId)) continue;
var plainIncludeItem = plainIncludeItems.FirstOrDefault(pii => pii.ID.ToString() == assetId);
if (plainIncludeItem == null) continue;
var mode = GetEnumFieldValue(plainIncludeItem.Fields[Templates.PlainInclude.Fields.Mode]);
if (string.IsNullOrEmpty(mode) || mode.Equals("Disabled", StringComparison.OrdinalIgnoreCase)) continue;
var assetTypeEx = (AssetTypeEx)Enum.Parse(typeof(AssetTypeEx), mode);
var assetType = assetTypeEx == AssetTypeEx.Style ? AssetType.Style : AssetType.Script;
var attributes = new Dictionary<string, string>();
if (!plainIncludeItem[Templates.PlainInclude.Fields.SriHash].IsNullOrEmpty())
{
attributes.Add("integrity", plainIncludeItem[Templates.PlainInclude.Fields.SriHash]);
}
if (!plainIncludeItem[Templates.PlainInclude.Fields.Cors].IsNullOrEmpty())
{
attributes.Add("crossorigin", GetEnumFieldValue(plainIncludeItem.Fields[Templates.PlainInclude.Fields.Cors]));
}
switch (assetTypeEx)
{
case AssetTypeEx.ScriptAsync:
attributes.Add("async", "");
break;
case AssetTypeEx.ScriptDefer:
attributes.Add("defer", "");
break;
}
var url = plainIncludeItem[Templates.PlainInclude.Fields.AssetUrl];
var joinedAttributes = string.Join(" ",
attributes.Where(a => !a.Value.IsNullOrEmpty()).Select(a => $"{a.Key}=\"{a.Value}\""));
joinedAttributes += string.Join(" ",
attributes.Where(a => a.Value.IsNullOrEmpty()).Select(a => $"{a.Key}"));
var urlContent = GetUrlContent(assetType, url, joinedAttributes);
if (string.IsNullOrEmpty(urlContent)) continue;
var urlInclude = new PlainInclude
{
SortOrder = num,
Name = plainIncludeItem.Name,
Type = assetType,
Content = urlContent
};
assetsList.Add(urlInclude);
var isFallbackEnabled = MainUtil.GetBool(plainIncludeItem[Templates.PlainInclude.Fields.IsFallbackEnabled], false);
var fallbackTest = plainIncludeItem[Templates.PlainInclude.Fields.FallbackTest];
if (isFallbackEnabled && !string.IsNullOrEmpty(fallbackTest))
{
var tagBuilder = new StringBuilder();
foreach (var script in ProcessAssets(plainIncludeItem, AssetType.Script,
assetConfiguration.ScriptsMode))
{
tagBuilder.Append(script);
}
foreach (var link in ProcessAssets(plainIncludeItem, AssetType.Style,
assetConfiguration.StylesMode))
{
tagBuilder.Append(link);
}
if (tagBuilder.Length > 0)
{
var fallbackInclude = new PlainInclude
{
SortOrder = num,
Name = plainIncludeItem.Name + "-fallback",
Type = AssetType.Script,
Content = "<script>{0} || document.write('{1}')</script>".FormatWith(fallbackTest,
tagBuilder.ToString())
};
assetsList.Add(fallbackInclude);
}
}
var rawContent = GetRawContent(assetType, plainIncludeItem[Templates.PlainInclude.Fields.RawContent]);
if (string.IsNullOrEmpty(rawContent)) continue;
var rawInclude = new PlainInclude
{
SortOrder = num,
Name = plainIncludeItem.Name,
Type = assetType,
Content = rawContent
};
assetsList.Add(rawInclude);
}
if (assetsList.Any())
{
args.AssetsList.InsertRange(0, assetsList.ToArray());
}
}
private IEnumerable<string> ProcessAssets(Item plainIncludeItem, AssetType assetType, AssetServiceMode serviceMode)
{
Item[] selectedItems = null;
if (assetType == AssetType.Script)
{
selectedItems = plainIncludeItem.Axes.SelectItems("./Scripts//*[@Extension='js']");
}
else if (assetType == AssetType.Style)
{
selectedItems = plainIncludeItem.Axes.SelectItems("./Styles//*[@Extension='css']");
}
var assets = (selectedItems != null && selectedItems.Any()) ? new List<Item>(selectedItems) : new List<Item>();
var isOptimized = true;
switch (serviceMode)
{
case AssetServiceMode.Disabled:
assets = assets.Where(i => i.TemplateID != Sitecore.XA.Foundation.Theming.Templates.OptimizedFile.ID).ToList();
isOptimized = false;
break;
case AssetServiceMode.Concatenate:
case AssetServiceMode.ConcatenateAndMinify:
var optimizedItem = assets.FirstOrDefault(i => i.TemplateID == Sitecore.XA.Foundation.Theming.Templates.OptimizedFile.ID);
if (optimizedItem != null && !IsNotEmpty(optimizedItem))
{
assets = new List<Item> { optimizedItem };
}
else
{
optimizedItem = new AssetBundler().GetOptimizedItem(plainIncludeItem, (OptimizationType)assetType, serviceMode);
if (optimizedItem != null && IsNotEmpty(optimizedItem))
{
assets = new List<Item> { optimizedItem };
}
else
{
assets = new List<Item>();
}
}
break;
}
switch (assetType)
{
case AssetType.Script:
return assets.Select(s => "<script src=\"{0}\">\\x3C/script>".FormatWith(s.BuildAssetPath(isOptimized)));
case AssetType.Style:
return
assets.Select(s => "<link href=\"{0}\" rel=\"stylesheet\" />".FormatWith(s.BuildAssetPath(isOptimized)));
default:
return new List<string>();
}
}
private static bool IsNotEmpty(Item optimizedScriptItem)
{
var mediaStream = ((MediaItem)optimizedScriptItem).GetMediaStream();
return mediaStream?.Length > 0L;
}
private static string GetUrlContent(AssetType assetType, string url, string joinedAttributes)
{
var content = string.Empty;
if (assetType == AssetType.Script)
{
if (!string.IsNullOrEmpty(url))
{
content += "<script src=\"{0}\" {1}></script>".FormatWith(url, joinedAttributes);
}
}
else if (assetType == AssetType.Style)
{
if (!string.IsNullOrEmpty(url))
{
content += "<link href=\"{0}\" rel=\"stylesheet\" {1} />".FormatWith(url, joinedAttributes);
}
}
return content;
}
private static string GetRawContent(AssetType assetType, string rawContent)
{
var content = string.Empty;
switch (assetType)
{
case AssetType.Script:
if (!string.IsNullOrEmpty(rawContent))
{
content += "<script>{0}</script>".FormatWith(rawContent);
}
break;
case AssetType.Style:
if (!string.IsNullOrEmpty(rawContent))
{
content += "<style>{0}</style>".FormatWith(rawContent);
}
break;
}
return content;
}
private static string GetEnumFieldValue(LookupField field)
{
var enumItem = field?.TargetItem;
return enumItem?.Fields[Sitecore.XA.Foundation.Common.Templates.Enum.Fields.Value].Value;
}
}
}
Once you get the processor working you can add it like so:
<?xml version="1.0" encoding="utf-8" ?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
<sitecore>
<pipelines>
<assetService>
<processor type="Westco.XA.Foundation.Theming.Pipelines.AssetService.AddPlainIncludes, Westco.XA.Foundation.Theming" />
</assetService>
</pipelines>
</sitecore>
</configuration>
Assuming that you want to store your script in your Sitecore instance, you could upload a script inside your theme. It will be included automatically. If you need it page-based, you can use the Page's Body Css Class (or ID) to only apply it to that page. If you really must use an external file like from a CDN, you could always just update the DOM in your custom JS file similar to this.