Real cases ยท 1 of 25
How Granite UI's dropdown-driven show/hide widget works inside a dialog and inside a multifield, why "hidden" fields aren't the same as "deleted" fields, and how dead hidden fields quietly pile up in real dialogs after a redesign.
A content author opens a component dialog, picks "Video" from a dropdown, and a Video URL field appears. Pick "Image" instead and a Path field appears in its place. Nobody wrote a line of JavaScript for that โ it's a Granite UI convention baked into the dialog XML. The part that trips teams up isn't the show/hide behavior itself. It's what happens to the fields underneath it once the dialog gets redesigned a second or third time.
A component dialog needs to show different fields depending on what an author picks in another field โ a classic case is a media tile that can be either a video or an image, where showing both a Video URL field and an Image Path field at once is confusing and half the fields will always be irrelevant. AEM's Touch UI (Coral) dialogs solve this with a pure-authoring-time mechanism: no server round trip, no custom widget code, just a granite:class + granite:data convention that toggles visibility client-side when the driving field's value changes.
The same problem shows up inside a multifield โ an author adds several rows to a list, and each row itself needs the same show/hide behavior scoped to just that row, not the whole dialog.
The part that isn't obvious from the Granite UI docs: when this pattern gets added to a dialog that used to have a flat, non-conditional layout, the old fields don't disappear. They get marked hidden and left in the resource tree โ which means a "hidden" field in a dialog is not the same thing as a deleted field, and it's easy for that distinction to matter later.
Granite UI's show/hide mechanism has three moving parts, all declared as data attributes on plain widget resources โ no custom Java, no custom JS:
select (dropdown) field carries granite:class="cq-dialog-dropdown-showhide-multifield" and a granite:data node with cq-dialog-dropdown-showhide-target pointing at a CSS selector for the elements it controls.granite:class (the same selector the driver points at) and its own granite:data node with showhidetargetvalue โ the driver's value that makes this container visible.cq-dialog-dropdown-showhide.js, shipped with Granite UI, not something you write) reads those two data attributes and toggles container visibility whenever the driver's value changes, including on initial dialog load.Inside a multifield, the same three pieces are declared once, inside the multifield's <field> node โ Granite instantiates a fresh copy of that whole subtree (driver + targets) for every row the author adds, so each row's dropdown only ever controls its own row's containers, never a sibling row's.
select (mediaType)
granite:class="cq-dialog-dropdown-showhide-multifield"
granite:data: cq-dialog-dropdown-showhide-target=".media-type-target"
option: value="video"
option: value="image"
container (granite:class="media-type-target")
granite:data: showhidetargetvalue="video"
-> Video URL field, Video Title field
container (granite:class="media-type-target")
granite:data: showhidetargetvalue="image"
-> Image Path field, Alt Text field
A real dialog implementing this (from a media-tile component in a production AEM 6.5 codebase, field and component names generalized here):
<!-- _cq_dialog/.content.xml -->
<mediaItems
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
composite="{Boolean}true"
fieldLabel="Media Items"
required="{Boolean}true">
<field
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container"
name="./mediaItemList">
<items jcr:primaryType="nt:unstructured">
<mediaType
granite:class="cq-dialog-dropdown-showhide-multifield"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/select"
fieldLabel="Media Type"
name="./mediaType">
<granite:data
jcr:primaryType="nt:unstructured"
cq-dialog-dropdown-showhide-target=".media-type-target"/>
<items jcr:primaryType="nt:unstructured">
<video jcr:primaryType="nt:unstructured" text="Video" value="video"/>
<image jcr:primaryType="nt:unstructured" text="Image" value="image"/>
</items>
</mediaType>
<videoFields
granite:class="media-type-target"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<videoUrl
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Video URL"
name="./videoUrl"/>
</items>
<granite:data jcr:primaryType="nt:unstructured" showhidetargetvalue="video"/>
</videoFields>
<imageFields
granite:class="media-type-target"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<imagePath
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"
fieldLabel="Image Path"
name="./imagePath"
rootPath="/content/dam"/>
</items>
<granite:data jcr:primaryType="nt:unstructured" showhidetargetvalue="image"/>
</imageFields>
</items>
</field>
</mediaItems>
sling:hideResource="{Boolean}true" is the other half of this story, and it's a different mechanism from showhidetargetvalue. Granite UI's dialog rendering skips any resource marked sling:hideResource entirely โ it isn't rendered, isn't in the DOM, and can't be submitted. It's used for two genuinely different reasons that are easy to conflate:
sling:resourceSuperType and the Sling Resource Merger), sling:hideResource on a node with the same name as an inherited node suppresses that inherited field from rendering. This is the "hide an inherited field" mechanism (covered in the companion chapter on extending Core Components).The second case is the one worth paying attention to, because it's invisible unless someone opens the raw .content.xml. In the dialog above, imagine an earlier version of this component had flat, non-multifield ./imagePath and ./altText fields directly on the dialog root โ added before anyone needed multiple media items per component. When the multifield was introduced, the simplest edit was to mark the old fields sling:hideResource="{Boolean}true" rather than delete them (faster, and "just in case" something still reads them). Nothing in the dialog's own XML tells you those fields are dead โ the resource tree doesn't distinguish "correctly excluded via merge" from "abandoned leftover."
A retail client's product-tile component had exactly this history: version 1 supported a single image, authored via flat ./imagePath/./altText fields. Version 2 added the video-or-image choice and the multifield structure shown above, and the old flat fields were hidden rather than deleted โ the commit message said "keep for now in case migration script needs them." Eighteen months later, a new developer doing an unrelated dialog cleanup found the hidden imagePath/altText nodes, assumed they were an inherited field being intentionally suppressed (the merge-exclusion use case), and left them alone rather than investigating further โ a reasonable but wrong guess, since there was no sling:resourceSuperType on this component at all. The Sling Model backing the component had already been fully migrated to read only from the multifield's child resources; the flat fields had zero code path reading them. They were pure dead weight in the content structure, discoverable only by grepping the Java model for the exact property name and finding nothing.
sling:hideResource always means "inherited and intentionally suppressed." Check whether the component even has a sling:resourceSuperType before assuming that's the reason. If it doesn't, the hidden field is almost certainly a leftover, not a merge exclusion.showhidetargetvalue matching is a plain string comparison against the driver's value. If a dropdown's option values are ever renamed (say, "wistiaVideo" becomes "video" for consistency), every showhidetargetvalue referencing the old string silently stops matching โ the container just never shows again, with no error anywhere.granite:class selector is scoped to that row. A class name that happens to collide with something elsewhere in the dialog (outside the multifield) can cause a driver in one row to toggle a container in a completely different part of the dialog.Show/hide widgets and multifields are both zero-code, purely declarative โ which is exactly why they accumulate technical debt invisibly. There's no compiler, no lint rule, and usually no code review scrutiny applied to a .content.xml dialog file the way there is to a Java class. A field that's dead weight in a dialog doesn't throw an exception or fail a test; it just sits there, authorable, silently ignored, until someone burns an hour investigating why filling it in "does nothing."
cq-dialog-dropdown-showhide-multifield + a target selector), one or more targets (showhidetargetvalue), and a stock Granite client library that wires them together โ no custom code needed.sling:hideResource is a different, broader mechanism (full exclusion from rendering) used for two different reasons: suppressing an inherited field via dialog merge, or masking a leftover field after a redesign โ and the dialog XML itself doesn't tell you which.sling:resourceSuperType as a strong signal it's dead, and confirm by grepping the Sling Model/HTL for the property name before assuming it's still load-bearing.The companion piece on extending Adobe's Core Components covers the other half of sling:hideResource โ using it deliberately, through dialog merge, to suppress a field your component inherits from a Core Component's own dialog.
See Dead Hidden Fields After a Multifield Migration for a full walkthrough of tracing exactly this situation down to a dead property, plus a template-contract test that catches it before it ships again.
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.