← Back to knowledge base |
  • 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. 

The script is executed on the live site but not in the Page builder

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 a div with the hello-world class.
  • app.js: The client-side JavaScript file selecting all elements with the hello-world class and adding the hello-world--red class.
  • app.css: The main CSS file defining .hello-world as default black and .hello-world--red as red.
Widget code that does not execute well in Page builder

We need to ensure the script in the app.js file executes for all widget instances in these scenarios:

  1. On the live site when a page loads.
  2. In the Page Builder when a page loads.
  3. In the Page Builder when a widget is added to a page.
  4. 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 or ReadOnly 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 the DOMContentLoaded 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.

The script is executed on the live site and in the Page builder

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
Milan Lund