AEM Dialog with extraClientlibs – Do Not Shoot Yourself in the Foot

Evgeniy Fitsner Software Engineer
9 min read
AEM Dialog with extraClientlibs – Do Not Shoot Yourself in the Foot

AEM component dialogs let you attach custom JavaScript via the extraClientlibs property. It is a powerful feature - and a silent source of bugs. As your project accumulates components, each with its own dialog scripts, those scripts start colliding in ways that are hard to diagnose.

This post explains how extraClientlibs work, why the conflicts happen, and three rules that keep your dialogs clean.

How extraClientlibs Work

When an author opens a component dialog, AEM retrieves the dialog’s XML definition and looks up the extraClientlibs property. Each category listed there is loaded as a client library - CSS and JS files are injected into the page on the spot.

The key detail: dialog clientlib JS is never unloaded. Unlike page-level scripts that are replaced on navigation, dialog scripts remain on the page until the author reloads the editor. If an author opens dialog A, then closes it and opens dialog B, both A’s and B’s scripts are active at the same time.

This is by design - AEM has no “dialog-closed” lifecycle event to hook into. But it means every dialog script you load accumulates in the page for the entire editing session.

The Problem in Practice

Consider a project with three components, each registering a dialog-ready listener:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Product Wizard - opens a multi-step wizard
$(document).on("dialog-ready", function () {
  // bind wizstep navigation
});

// Carousel - toggles slide visibility based on checkbox
$(document).on("dialog-ready", function () {
  // show/hide slides
});

// Teaser - auto-fills the CTA link from the image dam:link
$(document).on("dialog-ready", function () {
  // resolve link from DAM metadata
});

Each handler fires on every dialog-ready event, regardless of which component is being edited. As the project grows, the chance of one handler interfering with another increases - hidden fields get toggled, click handlers fire on the wrong form, validation runs against the wrong schema.

The worst part: the bug only appears when a specific sequence of dialogs is opened, making it intermittent and hard to reproduce.

1. Avoid Global Categories

The cq.authoring.dialog category is loaded on every dialog in AEM. Adding your component’s JS to it guarantees that your code runs in every dialog on the page, for every component, whether it needs to or not.

Anti-pattern:

1
2
3
4
5
6
7
<!-- /apps/myproject/components/product/wizard/cq:dialog/.content.xml -->
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0"
          xmlns:jcr="http://www.jcp.org/jcr/1.0"
          jcr:primaryType="cq:Dialog"
          extraClientlibs="[cq.authoring.dialog]">
  <!-- dialog fields -->
</jcr:root>

Reserve cq.authoring.dialog for truly global utilities - things like shared validation helpers or Coral UI polyfills that are safe across all dialogs. Component-specific logic does not belong here.

2. Use Separate, Descriptive Categories

Create a dedicated clientlib category for each component (or logical group of components). Use a reverse-domain naming convention to avoid collisions:

1
2
3
4
5
6
7
<!-- /apps/myproject/components/product/wizard/cq:dialog/.content.xml -->
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0"
          xmlns:jcr="http://www.jcp.org/jcr/1.0"
          jcr:primaryType="cq:Dialog"
          extraClientlibs="[myproject.components.product.wizard.authoring]">
  <!-- dialog fields -->
</jcr:root>

The corresponding clientlib folder:

1
2
3
4
5
<!-- /apps/myproject/clientlibs/product-wizard-authoring/.content.xml -->
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0"
          jcr:primaryType="cq:ClientLibraryFolder"
          categories="[myproject.components.product.wizard.authoring]"
          dependencies="[cq.authoring.dialogpage]" />

Why this matters: category names are global. If two teams independently create a category called product.dialog, the second deployment overwrites the first. A long, path-based name guarantees uniqueness and makes the purpose self-documenting.

3. Add Dialog Resource Type Validation

Even with separate categories, the same dialog-ready event still fires for every dialog on the page. The safeguard is to verify that the script is running inside the intended dialog before executing any logic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
(function (document, $, authorUtils) {
  "use strict";

  var DIALOG_RESOURCE_TYPE = "myproject/components/product/wizard";

  function isTargetDialog(formElement, resourceType) {
    var resourceTypeInput = formElement.find("input[name='./sling:resourceType']");
    return resourceTypeInput.val() === resourceType;
  }

  $(document).on("dialog-ready", function () {
    var formElement = $(this).find("coral-dialog form.foundation-form");

    if (!isTargetDialog(formElement, DIALOG_RESOURCE_TYPE)) {
      return;
    }

    // Your dialog-specific logic starts here
    var $wizard = formElement.find(".wizard-steps");

    $wizard.on("click", ".wizard-nav-next", function () {
      // advance to next step
    });

    $wizard.on("click", ".wizard-nav-prev", function () {
      // return to previous step
    });
  });
})(document, Granite.$, Granite.author);

How the guard works:

  1. $(document).on("dialog-ready", ...) fires every time any dialog opens.
  2. $(this).find("coral-dialog form.foundation-form") locates the currently active dialog form.
  3. formElement.find("input[name='./sling:resourceType']") reads the hidden input that AEM injects into every dialog form - it contains the component’s sling:resourceType.
  4. If the resource type does not match, the handler exits immediately. No event listeners are bound, no DOM is touched.

This pattern costs three lines of overhead but eliminates an entire class of cross-dialog bugs.

Complete Working Example

Here is a full file layout you can copy into your project:

1
2
3
4
5
6
7
8
9
10
11
12
13
apps/
  myproject/
    components/
      product/
        wizard/
          .content.xml          ← component definition
          cq:dialog/
            .content.xml        ← dialog with extraClientlibs
    clientlibs/
      product-wizard-authoring/
        .content.xml            ← clientlib folder
        js/
          dialog.js             ← dialog logic with resource type guard

Dialog definition (cq:dialog/.content.xml):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
          xmlns:cq="http://www.day.com/jcr/cq/1.0"
          xmlns:jcr="http://www.jcp.org/jcr/1.0"
          xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
          jcr:primaryType="nt:unstructured"
          sling:resourceType="cq/gui/components/authoring/dialog"
          extraClientlibs="[myproject.components.product.wizard.authoring]">
  <content jcr:primaryType="nt:unstructured"
           sling:resourceType="granite/ui/components/coral/foundation/container">
    <items jcr:primaryType="nt:unstructured">
      <!-- your dialog fields here -->
    </items>
  </content>
</jcr:root>

Clientlib folder (clientlibs/product-wizard-authoring/.content.xml):

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0"
          jcr:primaryType="cq:ClientLibraryFolder"
          categories="[myproject.components.product.wizard.authoring]"
          dependencies="[cq.authoring.dialogpage]" />

Dialog JS (clientlibs/product-wizard-authoring/js/dialog.js):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(function (document, $) {
  "use strict";

  var DIALOG_RESOURCE_TYPE = "myproject/components/product/wizard";

  function isTargetDialog(form, resourceType) {
    return form.find("input[name='./sling:resourceType']").val() === resourceType;
  }

  $(document).on("dialog-ready", function () {
    var form = $(this).find("coral-dialog form.foundation-form");
    if (!isTargetDialog(form, DIALOG_RESOURCE_TYPE)) {
      return;
    }

    // Dialog-specific logic below this line
    // ----------------------------------------
  });
})(document, Granite.$);

Debugging Tips

Check which clientlib categories are loaded

Open the browser’s Network tab, filter by clientlib or your project namespace. When you open a dialog, a new request appears for each extraClientlibs category. If you see a category that should not be there, check the dialog’s .content.xml.

Verify the resource type guard

In the browser console, after opening a dialog:

1
2
3
4
5
// Check which resource type the current dialog has
document.querySelector('form.foundation-form input[name="./sling:resourceType"]').value;

// Check all loaded dialog-ready handlers
$._data(document, "events")["dialog-ready"];

The first command tells you whether your guard constant matches the actual resource type. The second shows how many handlers are registered - if you see more than expected, some component is missing its guard.

Toggle script-only clientlibs for testing

If you suspect a specific clientlib is causing a conflict, temporarily remove it from the extraClientlibs array in the dialog XML and reload the editor. If the problem disappears, you have isolated the culprit.

Summary

Rule What to do Why
Avoid global categories Never put component-specific JS in cq.authoring.dialog It runs in every dialog, for every component
Use descriptive categories Name categories project.component-group.feature.authoring Prevents name collisions across teams and deployments
Validate resource type Guard every dialog-ready handler with sling:resourceType check Prevents cross-dialog bugs when multiple scripts accumulate on the page

Following these three rules eliminates an entire category of AEM dialog bugs - the kind that only appear after a specific sequence of dialog opens and disappear on page reload.

Contents