N
Naveenr.dev
Chapter rw-40
8 min read2026-08-28
📖 AEM - Adobe Experience Manager SeriesChapter rw-40 · 13 chapters

Real-World Scenarios: The Carousel With No Slides to Navigate

A hero carousel that renders navigation arrows and indicators but no slides — tracing it back to an inherited Core Component dialog tab that got hidden without anything replacing what it authored, and the contract test that would have caught it pre-launch.

Background reading: Extending a Core Component's Dialog and Hiding Inherited Fields covers the mechanics this post assumes.

Problem Statement

QA flagged a hero carousel on a staging page: the carousel shell rendered — left/right arrows, a row of empty indicator dots — but there was nothing to carousel through. No slide content, no error in the browser console, no server-side exception in the logs. The dialog authors used to add slides also had no obvious way to add content anymore; the "Items" tab authors expected simply wasn't there.

The component extended Adobe's Core Component Carousel via sling:resourceSuperType, which is where both halves of the bug lived. A newer variant of the carousel dialog had hidden the inherited children-editor tab (sling:hideResource="{Boolean}true" on a node named to match the Core Component's own "Items" tab), copied from a sibling component where hiding that tab was correct because that sibling managed its content through a multifield instead. On this hero carousel, nothing replaced it — there was no multifield, no alternate mechanism, just an empty tab-shaped hole where the only way to add child slides used to be.

Approach and Why

The fix had two parts, and only doing the first one would have left the underlying gap in place for the next copy-paste:

  1. Immediate fix: un-hide the inherited "Items" tab on this component, since nothing else provides its capability here — restoring the only real authoring path for adding slides.
  2. Prevent recurrence: the actual root cause was a hidden-tab snippet copied from one component to another without re-checking whether the target component had a replacement mechanism. A contract test asserting "if this component extends a Core Component with a children-editor, and it hides that tab, something else in the dialog must be authoring child content" catches the specific mistake without needing to re-litigate every future hide decision by hand.

The fix does not touch the Core Component's own carousel JS or HTL — the inherited rendering logic was never broken, it was correctly initializing against zero child resources because zero child resources actually existed. Fixing the symptom at the JS layer (defensive-coding around an empty carousel) would have masked the real problem instead of restoring the missing authoring path.

POC

java
class HeroCarouselDialogContractTest {

    private static Document loadComponentDefinition() throws Exception {
        return parse("src/main/content/jcr_root/apps/myapp/components/herocarousel/.content.xml");
    }

    private static Document loadDialog() throws Exception {
        return parse("src/main/content/jcr_root/apps/myapp/components/herocarousel/_cq_dialog/.content.xml");
    }

    @Test
    void hidingTheInheritedChildrenEditorRequiresAReplacementAuthoringPath() throws Exception {
        Document componentDefinition = loadComponentDefinition();
        String superType = componentDefinition.getDocumentElement()
            .getAttribute("sling:resourceSuperType");

        assumeTrue(superType.contains("carousel"),
            "only applies to components extending a Core Component with a children editor");

        Document dialog = loadDialog();
        boolean childrenEditorHidden = findHiddenChildrenEditorNode(dialog).isPresent();
        boolean hasReplacementAuthoringField = dialogHasMultifieldOrChildResourceField(dialog);

        if (childrenEditorHidden) {
            assertThat(hasReplacementAuthoringField)
                .as("children editor is hidden but no multifield/child-resource field replaces it - "
                    + "authors would have no way to add slide content")
                .isTrue();
        }
    }

    @Test
    void componentWithNoAuthoringPathForChildrenFailsRenderPrecondition() throws Exception {
        // A lighter integration check: render the component against a page with
        // zero child resources and confirm the model surfaces that as a known,
        // named state rather than silently rendering empty navigation controls.
        HeroCarouselModel model = context.request(componentResource).adaptTo(HeroCarouselModel.class);

        assertThat(model.getSlides()).isEmpty();
        assertThat(model.hasAuthorableSlideMechanism())
            .as("model should be able to report whether authors have any way to add slides")
            .isTrue();
    }
}

Implementation

Dialog fix — restore the inherited tab on this component (delete the local override, letting the merge fall through to the Core Component's own node unmodified):

xml
<!-- BEFORE: copied from a sibling component where this hide was correct -->
<containerItems
    jcr:primaryType="nt:unstructured"
    jcr:title="Items"
    sling:hideResource="{Boolean}true"
    sling:resourceType="granite/ui/components/coral/foundation/container">
    ...
</containerItems>

<!-- AFTER: node removed entirely - the Core Component's own "Items" tab
     is no longer overridden, so the merge exposes it unmodified -->

Contract-test helper distinguishing "has a real replacement authoring path" from "has nothing":

java
final class DialogAuthoringPathInspector {

    private DialogAuthoringPathInspector() {
    }

    static boolean dialogHasMultifieldOrChildResourceField(Document dialog) {
        NodeList allNodes = dialog.getElementsByTagName("*");
        for (int i = 0; i < allNodes.getLength(); i++) {
            Element element = (Element) allNodes.item(i);
            String resourceType = element.getAttribute("sling:resourceType");
            if (resourceType.contains("foundation/form/multifield")) {
                return true;
            }
        }
        return false;
    }

    static Optional<Element> findHiddenChildrenEditorNode(Document dialog) {
        NodeList allNodes = dialog.getElementsByTagName("*");
        for (int i = 0; i < allNodes.getLength(); i++) {
            Element element = (Element) allNodes.item(i);
            boolean isHidden = "true".equalsIgnoreCase(
                element.getAttribute("sling:hideResource").replaceAll("[{}Boolean]", ""));
            boolean nameLooksLikeChildrenEditor = element.getTagName().toLowerCase(Locale.ROOT)
                .contains("containeritems");
            if (isHidden && nameLooksLikeChildrenEditor) {
                return Optional.of(element);
            }
        }
        return Optional.empty();
    }
}

Rollout Steps

  1. Restored the inherited "Items" tab on the affected hero carousel component and verified in a local author instance that authors could add child slides again.
  2. Added the contract test to the component's test module; ran it against every other component in the codebase extending a Core Component with a children editor — found one additional component with the same copy-pasted hide, still in an unreleased feature branch, caught before it reached staging.
  3. Documented, in the team's component-authoring notes, that the hidden-tab pattern is per-component and must never be copy-pasted without confirming a replacement authoring mechanism exists.
  4. Re-tested the carousel end-to-end on staging with real slide content restored, confirmed navigation, indicators, and auto-rotation all worked as expected once actual child resources existed.

Why This Approach Held Up

The contract test targets the actual causal chain — "hidden children editor" plus "no replacement" — rather than this one component's name, so it caught a second live instance of the same mistake immediately and will catch future ones automatically as new carousel-extending components get added. Restoring the tab rather than patching around an empty carousel in JS kept the fix at the layer where the actual defect lived: the dialog stopped offering authors a way to do the one thing the component needed them to do.

Enjoyed this chapter?

Get an email when I publish the next chapter. No spam — just new technical deep-dives.

Comments

Share feedback or questions about this blog post.

No comments yet. Be the first to share your thoughts.