N
Naveenr.dev
Chapter 104
14 min read2026-08-28

Policy-Driven Field Visibility and Node-Level Dialog Subtree Reuse

Two dialog-authoring techniques beyond dropdown show/hide: tying a field's visibility to a component's design/policy properties with Granite Expression Language, and reusing a specific tab or field from an unrelated component's dialog via a node-level sling:resourceSuperType or a foundation/include — with full, working examples of each.

The dropdown-driven show/hide widget from the earlier chapter reacts to what an author picks, per instance, right now, in the dialog they're editing. Two other real mechanisms solve a related but different problem: showing or hiding a field based on what a template policy allows, and reusing a chunk of one component's dialog inside a completely unrelated component without copy-pasting the markup. Both are genuinely useful, both are easy to get wrong in ways that don't show up until much later, and both are worth having working, copy-pasteable examples for.

Problem

Policy-driven visibility: some dialog fields only make sense if a site's design/policy already allows the feature they configure. A "cards per screen" field is meaningless if the policy has card-carousel mode turned off entirely — showing it anyway just invites authors to configure something with no effect. The dropdown show/hide mechanism can't help here, because it reacts to this dialog's own fields, not to a separate policy configured on the template.

Dialog subtree reuse: a component needs an icon picker, or an "ID / CSS Class" tab, or some other self-contained chunk of dialog markup that another component already has, correctly built and tested. Copy-pasting that markup into every component that needs it means every future fix to that markup has to be repeated everywhere it was copied — the exact kind of duplication most teams try to avoid in Java, but that dialog XML makes easy to fall into since there's no "import" for markup.

Architecture

Policy visibility uses Granite Expression Language, evaluated against a cqDesign object that's automatically bound in dialog rendering context — it exposes the current template policy's own properties (whatever the policy dialog itself defines) as a plain object:

xml
<cardsPerScreen
    granite:hide="${!cqDesign.enableCardCarouselProps}"
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/numberfield"
    fieldLabel="Cards Per Screen"
    name="./cardsPerScreen"/>

granite:hide takes an EL expression; if it evaluates truthy, the field is suppressed from rendering (same rendering-time exclusion sling:hideResource uses, just conditional rather than absolute). cqDesign.enableCardCarouselProps reads a boolean the policy dialog (usually under /conf/.../settings/wcm/policies, authored by whoever defines page templates) sets, not anything on the component dialog itself.

Dialog subtree reuse has two different real mechanisms that look similar but behave differently:

  1. Node-level sling:resourceSuperType — a single node deep inside a dialog (a tab, a field, a container) declares its own sling:resourceSuperType pointing at an absolute path inside a different component's dialog tree. The Sling Resource Merger fills that node's children in from the target, the same merge mechanism covered for whole-component extension, just scoped to one node instead of the component root. Because it's a merge, later changes to the target automatically propagate to everything reusing it — until it doesn't (see Production Troubleshooting).
  2. granite/ui/components/coral/foundation/include — a plain Granite UI component whose only job is: render whatever resource is at path, right here, as if it were declared inline. No inheritance relationship required between the two components at all — it's a literal, request-time include, closer to <sling:include> than to type inheritance.

Repository

Working example 1 — policy-driven visibility, guarding a design-dependent field (fictionalized field/component names, real mechanism):

xml
<!-- _cq_dialog/.content.xml -->
<cardContainer
    granite:hide="${!cqDesign.enableCardCarouselProps}"
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/container">
    <items jcr:primaryType="nt:unstructured">
        <cardsPerScreen
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/coral/foundation/form/numberfield"
            fieldLabel="Cards Per Screen"
            name="./cardsPerScreen"
            min="{Long}1"/>
        <cardsPerScroll
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/coral/foundation/form/numberfield"
            fieldLabel="Cards Per Scroll"
            name="./cardsPerScroll"
            min="{Long}1"/>
    </items>
</cardContainer>
xml
<!-- policy dialog (cq:design_dialog or a policy under /conf) -->
<enableCardCarouselProps
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
    text="Enable Card Carousel Mode"
    name="./enableCardCarouselProps"
    value="{Boolean}true"
    uncheckedValue="{Boolean}false"/>

The uncheckedValue="{Boolean}false" is what makes this safe — it guarantees the property is always written explicitly, true or false, never left unset. That detail matters (see Production Troubleshooting).

Working example 2 — node-level dialog subtree reuse, borrowing an icon picker's fields into an unrelated component's dialog:

xml
<!-- iconpicker component's own dialog subtree, the thing being reused -->
<!-- apps/myapp/components/core/iconpicker/v1/iconpicker/cq:dialog/content/items/icon -->
<icon jcr:primaryType="nt:unstructured">
    <items jcr:primaryType="nt:unstructured">
        <iconName
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/coral/foundation/form/select"
            fieldLabel="Icon"
            name="./iconName">
            <datasource
                jcr:primaryType="nt:unstructured"
                sling:resourceType="myapp/components/core/iconpicker/v1/datasource"/>
        </iconName>
        <iconPosition
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/coral/foundation/form/select"
            fieldLabel="Icon Position"
            name="./iconPosition">
            <items jcr:primaryType="nt:unstructured">
                <left jcr:primaryType="nt:unstructured" text="Left" value="left"/>
                <right jcr:primaryType="nt:unstructured" text="Right" value="right"/>
            </items>
        </iconPosition>
    </items>
</icon>
xml
<!-- a completely unrelated component's dialog, reusing that subtree -->
<ctaIcon
    jcr:primaryType="nt:unstructured"
    sling:resourceSuperType="myapp/components/core/iconpicker/v1/iconpicker/cq:dialog/content/items/icon"
    sling:resourceType="granite/ui/components/coral/foundation/container"/>

That single sling:resourceSuperType line is the entire reuse — no fields are re-declared. The merger resolves ctaIcon's children from the icon picker's icon node, as if they'd been typed out inline.

Working example 3 — foundation/include, embedding a shared "ID / CSS Class" tab verbatim:

xml
<!-- a shared, standalone dialog fragment authored once -->
<!-- apps/myapp/components/commons/idclassconfig/v1/idclassconfig/cq:dialog/content/items/tab -->
<tab jcr:primaryType="nt:unstructured" jcr:title="ID &amp; Class">
    <items jcr:primaryType="nt:unstructured">
        <id
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
            fieldLabel="ID"
            name="./id"/>
        <cssClass
            jcr:primaryType="nt:unstructured"
            sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
            fieldLabel="CSS Class"
            name="./cssClass"/>
    </items>
</tab>
xml
<!-- any component's dialog, including it by path -->
<idClassTab
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/include"
    path="/apps/myapp/components/commons/idclassconfig/v1/idclassconfig/cq:dialog/content/items/tab"/>

How It Works

The two reuse mechanisms solve the same duplication problem with different tradeoffs:

  • Node-level sling:resourceSuperType goes through the Resource Merger, the same mechanism as whole-component extension. That means the same override tooling is available — a component reusing the icon picker's fields this way can still hide or override one of the inherited fields with a same-named node marked sling:hideResource, exactly like the whole-component case. It's real inheritance, just scoped to a single node instead of the component root, and it reads a lot like Java's "protected method override" — you get the base behavior, and you can selectively customize pieces of it.
  • foundation/include has no override machinery at all. It renders the target resource tree exactly as declared, every time, at request time. If you need to change one field in the included fragment for just one consuming component, you can't — you'd have to stop including it and either copy the fragment or restructure it into something the node-level resourceSuperType approach could override instead.

Because of that, the node-level resourceSuperType approach is the right choice when consumers need some customization ability; foundation/include is the right choice when the fragment should always look and behave identically everywhere it appears, and any component that needs to diverge from it shouldn't be reusing it in the first place.

Real Project Example

A design system team built a reusable icon picker (icon name + position) as its own small component, purely so its dialog subtree could be reused via node-level sling:resourceSuperType — several unrelated components (a hero carousel, a CTA button, a promo banner) each pulled in .../iconpicker/v1/iconpicker/cq:dialog/content/items/icon for their own icon fields, rather than each maintaining a copy of the same select-with-datasource markup. This worked well for a long time: one datasource implementation, one set of icon-position options, maintained in exactly one place.

The trouble started when the icon picker team added a new required field — an accessible label for screen readers, needed for a compliance push — directly into that shared icon node. Every component reusing it via sling:resourceSuperType immediately, silently inherited the new field, since the merge pulls in whatever the target currently contains, not a pinned snapshot of what it contained when the reuse was first wired up. Authors editing the hero carousel, the CTA button, and the promo banner all started seeing a new required field they'd never been told about, appearing on components none of those consuming teams had touched — and in components where the dialog's "required" validation was strict, authors were blocked from saving until they filled it in, with no clear explanation of where the field came from.

Production Troubleshooting

  • Node-level sling:resourceSuperType reuse creates a live dependency, not a snapshot. Anyone changing a dialog subtree that's reused this way needs to know who else reuses it — which nothing in the tooling surfaces automatically. A grep for the exact target path across the whole content tree, before making a change, is the only reliable way to find every consumer.
  • foundation/include doesn't have this problem the same way, but it isn't override-friendly. Choose it deliberately when "must be identical everywhere" is actually the requirement, not just because it's simpler to wire up.
  • Always pair a policy checkbox used for granite:hide conditions with an explicit uncheckedValue. Without it, an author who never touches the checkbox leaves the property completely unset in the policy node — and ${!cqDesign.somePropertyThatWasNeverSet} evaluates the same way ${!undefined} does in Granite EL: it resolves to true, meaning the guarded field shows by default, the opposite of what "should be hidden until explicitly enabled" usually intends. An explicit uncheckedValue guarantees the property is always present with a real boolean, removing that ambiguity entirely.
  • cqDesign is only populated in contexts where a design/policy actually applies — a dialog rendered somewhere that isn't resolved against a template policy (rare, but possible in certain preview or test harnesses) can have an empty cqDesign, and any granite:hide expression referencing it needs to fail safe (typically toward showing the field) rather than throwing.

Why Architects Care

Both mechanisms trade a small amount of authoring convenience for a real, ongoing coupling relationship that isn't visible anywhere in a pull request diff for the component that changes. A one-line addition to a shared dialog fragment can ripple into save-blocking validation errors on components three teams away, and nothing in code review catches it unless the reviewer already knows every consumer by memory. Treat a widely-reused dialog subtree with the same change-management discipline as a public API — announce changes, and where possible, make new fields optional rather than required, specifically because dialog reuse has no versioning and no opt-in mechanism.

Summary

  • granite:hide with a Granite EL expression against cqDesign.<property> conditionally hides a field based on the current template policy, not the dialog's own fields — a different axis of "show/hide" from the per-instance dropdown mechanism.
  • Always give a policy checkbox an explicit uncheckedValue when it drives a granite:hide condition, to avoid the unset-property-evaluates-truthy ambiguity.
  • Node-level sling:resourceSuperType, applied to a single tab or field node (not the whole component), reuses another component's dialog subtree through the Resource Merger — live, overridable, but creating an invisible ongoing dependency on whoever owns the target.
  • foundation/include embeds a dialog fragment by path at request time, with no inheritance and no override capability — better suited when a fragment must render identically everywhere.
  • Changing a widely-reused dialog subtree (via either mechanism) needs the same change-management rigor as a public API change, since nothing in the tooling tracks or warns its consumers.

What's Next

The companion real-world post traces exactly the icon-picker scenario above: a new required field added to a shared subtree quietly blocking saves on three unrelated components, and the review process put in place afterward to catch this before it ships again.

Want to See This Applied to a Real Problem?

See A Shared Icon Picker's New Required Field Blocked Saves on Three Unrelated Components for the full incident and the consumer-discovery check added to CI.

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.