- Xperience by Kentico
How to initialize JavaScript in Xperience by Kentico Page Builder Widgets
In this article, you will learn how to initialize client-side JavaScript within widgets so that it runs seamlessly in both the Page Builder environment and on the live site.
As web developers 👨💻, we often encounter issues when working with client-side JavaScript within widgets in the Page Builder in Xperience by Kentico. Typically, we link our JavaScript file near the closing </body>
tag, expecting it to manipulate the DOM nodes rendered by the widgets. This works well on a live site, but in the Page Builder, only static markup from the widget's view file gets rendered, and client-side JavaScript doesn't execute as expected.
Developers implementing widgets that rely on client-side JavaScript, such as carousels or Google Maps, will most likely encounter this problem. I’ve previously addressed this in a short article on Kentico Xperience 13's Page Builder, but I now realize that my solution was somewhat shortsighted and requires a more detailed approach.
Upon revisiting the documentation, I found only a brief mention and a generic code snippet on this topic. To me, these resources were insufficient for practical implementation in a real web project. Consequently, I developed a more comprehensive guide to address this issue. Let's break down the problem and explore the scenarios requiring the execution of widget-specific client-side JavaScript.
🙈 The Problem
To illustrate, I created a simple widget that renders the text Hello, World! in default black ⚫. A deferred script linked in the page turns this text red 🔴. On the live site, the script executes correctly, but in the Page Builder, the text remains black.
This happens because the script executes as soon as the Page Builder's static markup is parsed, before the widget is fully available. Here’s the structure of our implementation:
_Layout.cshtml
: The main layout file linking JavaScript and CSS files._Default.cshtml
: The widget's view file rendering static text inside adiv
with thehello-world
class.app.js
: The client-side JavaScript file selecting all elements with thehello-world
class and adding thehello-world--red
class.app.css
: The main CSS file defining.hello-world
as default black and.hello-world--red
as red.
We need to ensure the script in the app.js
file executes for all widget instances in these scenarios:
- On the live site when a page loads.
- In the Page Builder when a page loads.
- In the Page Builder when a widget is added to a page.
- In the Page Builder when a widget is moved.
😊 The Solution
I'll demonstrate the solution using our Hello, World! widget and extending it. This concept is essential for handling more complex scenarios, such as rendering carousels, slideshows, or other client-side JavaScript-based elements within widgets. The solution might seem a bit overwhelming at first, so I recommend reviewing the code sample provided below the description.
We need to distinguish between two contexts in Xperience by Kentico: the live site and the Page Builder. Calling the pageBuilderDataContext.Retrieve().GetMode()
method in our .cshtml
view file, we check the context. The possible return values are:
Off
which stands for the live site context,Edit
orReadOnly
which stands for the Page builder context.
If we are in the context of the Page builder then we:
- render a custom
data-page-builder
attribute in the<html>
tag to be read by our client-side script. - render inline JavaScript code in the widget's view file to execute our client-side script only for the given widget instance.
Live Site Rendering
In our client-side script, we check the attribute in the <html>
tag to determine if we are on the live site. If so, we execute the logic that changes the text color for all widget instances.
The helloWorld
function is defined in the global scope and accepts an id
parameter:
- Without the
id
parameter, it changes the color for all.hello-world
elements (used for the live site). - With the
id
parameter, it targets a specific element (used for the Page Builder).
Page Builder Rendering
For the Page Builder, the coloring logic must be scoped to a single widget instance. In the widget's view file, we generate a random GUID to use as the id
attribute for the .hello-world
element. This id
is also passed to the helloWorld
function, which executes within an inline script in the Page Builder. This approach ensures the script runs only for the relevant widget instance.
However, we encounter additional complexity if the widget is rendered within a widget zone or through code:
- Widget Zone: The
helloWorld
function is available immediately and can be executed. - Code: The
helloWorld
function might be called before it's defined, so we delay execution until theDOMContentLoaded
event.
This approach ensures that all scenarios requiring script execution are covered.
The code
Below, I share the code for the files to demonstrate how the above-described solution comes together:
The Site layout: _Layout.cshtml
file
@inject IPageBuilderDataContextRetriever pageBuilderDataContext
<!DOCTYPE html>
@* Add "data-page-builder" if page is rendered in a Page builder when a page is in Edit or Read-only mode *@
<html @(pageBuilderDataContext.Retrieve().GetMode() != PageBuilderMode.Off ? "data-page-builder" : "")>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset="UTF-8" />
<link href="~/css/app.css" rel="stylesheet" type="text/css" asp-append-version="true" />
@RenderSection("styles", required: false)
<page-builder-styles />
</head>
<body>
<main>
@RenderBody()
</main>
<page-builder-scripts />
@RenderSection("scripts", required: false)
<script src="~/js/app.js" defer asp-append-version="true"></script>
</body>
</html>
The Widget view: _Default.cshtml
file
@using Project.Widgets
@model HelloWorldViewModel
@inject IPageBuilderDataContextRetriever pageBuilderDataContext
@if(!string.IsNullOrEmpty(Model.Text))
{
@*
Add a unique id attribute through which we can initialize
client-side JavaScript on a single instance of the widget
in Page Builder.
*@
var id = Guid.NewGuid().ToString();
<div class="hello-world" id="@id">@Model.Text</div>
@* This gets rendered in the Page Builder when a page is in Edit or Read-only mode *@
@if(pageBuilderDataContext.Retrieve().GetMode() != PageBuilderMode.Off)
{
<script>
if (typeof window.helloWorld !== "undefined") {
@*
Initialize when the widget gets rendered
in the Page Builder inside a widget zone.
*@
window.helloWorld("@id");
} else {
document.addEventListener("DOMContentLoaded", function() {
if (typeof window.helloWorld !== "undefined") {
@*
Initialize when the widget gets rendered
in the Page Builder in code.
*@
window.helloWorld("@id");
}
});
}
</script>
}
}
The client-side JavaScript: app.js
file
(() => {
const makeRed = (element) => {
if (!element) return;
// Add a class to make the text red
element.classList.add("hello-world--red");
};
window.helloWorld = (id) => {
if (typeof id !== "undefined") {
// When id is provided, only make that element red
makeRed(document.getElementById(id));
} else {
// When id is not provided, make all elements with class "hello-world" red
document.querySelectorAll(".hello-world").forEach((element) => {
makeRed(element);
});
}
};
// Initialize when rendered on the live site
if (!document.documentElement.hasAttribute("data-page-builder")) {
window.helloWorld();
}
})();
🏁 Conclusion
Executing client-side JavaScript on a widget's markup typically fails in the Page Builder due to asynchronous widget rendering. To resolve this, scripts must be executed differently on the live site and in the Page Builder:
- Live Site: Execute scripts as usual.
- Page Builder: Ensure the script executes for each widget instance and that the script is available when needed.
I’ve successfully applied this concept to complex widgets like carousels, lightbox slideshows, accordions, Google Maps, and filters. I hope this guide helps you address similar issues in your projects.
About the author
Milan Lund is a Freelance Web Developer with Kentico Expertise. He specializes in building and maintaining websites in Xperience by Kentico. Milan writes articles based on his project experiences to assist both his future self and other developers.
Find out more