{"id":80396,"date":"2026-07-07T17:03:44","date_gmt":"2026-07-07T11:33:44","guid":{"rendered":"https:\/\/www.tothenew.com\/blog\/?p=80396"},"modified":"2026-07-17T07:51:18","modified_gmt":"2026-07-17T02:21:18","slug":"how-we-can-show-hide-fields-in-an-aem-multifield-based-on-dropdown-selection-in-aemaacs","status":"publish","type":"post","link":"https:\/\/www.tothenew.com\/blog\/how-we-can-show-hide-fields-in-an-aem-multifield-based-on-dropdown-selection-in-aemaacs\/","title":{"rendered":"How we can Show\/Hide Fields in an AEM Multifield Based on Dropdown Selection in AEMaaCS"},"content":{"rendered":"<p>If you&#8217;ve built a &#8220;header&#8221; or &#8220;navigation menu&#8221; dialog in AEM, you&#8217;ve probably hit this requirement before: a content author adds multiple menu items, and for each one picks whether it&#8217;s an Internal Link or an External Link. Based on that choice, only the relevant field should show up: a path field for internal links, a text field for external.<\/p>\n<p>Sounds simple. AEM even has a built-in feature for exactly this. It works fine for a single dropdown. Put it inside a multifield, though, and it falls apart. Here&#8217;s how I fixed it, and what went wrong first.<\/p>\n<p>&nbsp;<\/p>\n<h2><span style=\"text-decoration: underline;\">The Requirement:<\/span><\/h2>\n<p>Build a header component with a &#8220;Configure Menu Items&#8221; multifield. For each menu item, the author enters Link Text and picks a Link Type from a dropdown, Internal or External. Depending on the choice:<\/p>\n<ul>\n<li>Internal Link &#8211; show a path browser to pick an internal page<\/li>\n<li>External Link &#8211; show a plain text field for a URL<\/li>\n<\/ul>\n<p>Both fields needed to be mandatory, but only the one actually visible. And authors should be able to add as many menu items as they want, with each one behaving independently of the others.<\/p>\n<p>&nbsp;<\/p>\n<h2><span style=\"text-decoration: underline;\">The Solution<\/span><\/h2>\n<h3>Dialog XML:<\/h3>\n<p>Here&#8217;s the relevant part of the dialog <strong>_cq_dialog.xml<\/strong>. Each multifield row (<strong>.\/menuItems<\/strong>) contains a <strong>linkType<\/strong> select, and two sibling containers &#8211; one for the internal link pathfield, one for the external link textfield.<\/p>\n<blockquote>\n<pre><code>&lt;multiField\r\n    jcr:primaryType=\"nt:unstructured\"\r\n    sling:resourceType=\"granite\/ui\/components\/coral\/foundation\/form\/multifield\"\r\n    composite=\"{Boolean}true\"\r\n    fieldDescription=\"Click the '+' to add a new menu item\"\r\n    fieldLabel=\"Configure Menu Items\"&gt;\r\n    &lt;field\r\n        jcr:primaryType=\"nt:unstructured\"\r\n        sling:resourceType=\"granite\/ui\/components\/coral\/foundation\/container\"\r\n        name=\".\/menuItems\"&gt;\r\n        &lt;items jcr:primaryType=\"nt:unstructured\"&gt;\r\n            &lt;linkType\r\n                jcr:primaryType=\"nt:unstructured\"\r\n                sling:resourceType=\"granite\/ui\/components\/coral\/foundation\/form\/select\"\r\n                fieldLabel=\"Select Link Type\"\r\n                name=\".\/linkType\"&gt;\r\n                &lt;items jcr:primaryType=\"nt:unstructured\"&gt;\r\n                    &lt;internalLink\r\n                        jcr:primaryType=\"nt:unstructured\"\r\n                        text=\"Internal Link\"\r\n                        value=\"internalLink\" \/&gt;\r\n                    &lt;externalLink\r\n                        jcr:primaryType=\"nt:unstructured\"\r\n                        text=\"External Link\"\r\n                        value=\"externalLink\" \/&gt;\r\n                &lt;\/items&gt;\r\n            &lt;\/linkType&gt;\r\n            &lt;linkText\r\n                jcr:primaryType=\"nt:unstructured\"\r\n                sling:resourceType=\"granite\/ui\/components\/coral\/foundation\/form\/textfield\"\r\n                fieldDescription=\"Enter the text for this link.\"\r\n                fieldLabel=\"Link Text\"\r\n                required=\"{Boolean}true\"\r\n                name=\".\/linkText\" \/&gt;\r\n            &lt;internalLinkContainer\r\n                granite:class=\"internal-link-option\"\r\n                jcr:primaryType=\"nt:unstructured\"\r\n                sling:resourceType=\"granite\/ui\/components\/coral\/foundation\/container\"&gt;\r\n                &lt;items jcr:primaryType=\"nt:unstructured\"&gt;\r\n                    &lt;internalLink\r\n                        jcr:primaryType=\"nt:unstructured\"\r\n                        sling:resourceType=\"granite\/ui\/components\/coral\/foundation\/form\/pathfield\"\r\n                        fieldDescription=\"Select the internal page for this link.\"\r\n                        fieldLabel=\"Internal Link\"\r\n                        name=\".\/internalLink\"\r\n                        required=\"{Boolean}true\"\r\n                        rootPath=\"\/content\/aem-learning\" \/&gt;\r\n                &lt;\/items&gt;\r\n            &lt;\/internalLinkContainer&gt;\r\n            &lt;externalLinkContainer\r\n                granite:class=\"hide external-link-option\"\r\n                jcr:primaryType=\"nt:unstructured\"\r\n                sling:resourceType=\"granite\/ui\/components\/coral\/foundation\/container\"&gt;\r\n                &lt;items jcr:primaryType=\"nt:unstructured\"&gt;\r\n                    &lt;externalLink\r\n                        jcr:primaryType=\"nt:unstructured\"\r\n                        sling:resourceType=\"granite\/ui\/components\/coral\/foundation\/form\/textfield\"\r\n                        fieldDescription=\"Enter the URL for this link.\"\r\n                        fieldLabel=\"External Link\"\r\n                        required=\"{Boolean}true\"\r\n                        name=\".\/externalLink\" \/&gt;\r\n                &lt;\/items&gt;\r\n            &lt;\/externalLinkContainer&gt;\r\n        &lt;\/items&gt;\r\n    &lt;\/field&gt;\r\n&lt;\/multiField&gt;<\/code><\/pre>\n<\/blockquote>\n<p>Notice there&#8217;s no <strong>cq-dialog-dropdown-showhide<\/strong> class anywhere. That&#8217;s on purpose, and I&#8217;ll explain why below.<\/p>\n<p>&nbsp;<\/p>\n<h3><span style=\"text-decoration: underline;\">The clientlib JS:<\/span><\/h3>\n<blockquote>\n<pre style=\"font-family: Consolas,Monaco,monospace; font-size: 13px; line-height: 1.5;\"><code>(function (document, $) {\r\n    \"use strict\";\r\n\r\n    $(document).on(\"dialog-ready\", function () {\r\n        process();\r\n\r\n        $(\"coral-multifield[data-granite-coral-multifield-name='.\/menuItems']\").on(\"click\", \"button[coral-multifield-add]\", function () {\r\n            process();\r\n        });\r\n    });\r\n\r\n    function process() {\r\n        const multifield = document.querySelector('coral-multifield[data-granite-coral-multifield-name=\".\/menuItems\"]');\r\n        if (multifield) {\r\n            const items = multifield.querySelectorAll('coral-multifield-item');\r\n\r\n            items.forEach(function (item, index) {\r\n                const itemContent = item.querySelector('coral-multifield-item-content');\r\n                const linkTypeEle = itemContent.querySelector(`coral-select[name*=\"linkType\"]`);\r\n\r\n                if (!linkTypeEle) {\r\n                    console.warn(\"Link type dropdown not found for item index:\", index);\r\n                    return;\r\n                }\r\n                applyToggle(linkTypeEle, linkTypeEle.value);\r\n\r\n                linkTypeEle.addEventListener(\"change\", function (e) {\r\n                    applyToggle(this, this.value);\r\n                });\r\n            });\r\n        } else {\r\n            console.warn(\"Multifield not found with selector: coral-multifield[data-granite-coral-multifield-name=\\\".\/menuItems\\\"]\");\r\n        }\r\n    }\r\n\r\n    function applyToggle(item, val) {\r\n        const itemContent = item.closest(\"coral-multifield-item-content\");\r\n        const internalContainer = itemContent.querySelector(\".internal-link-option\");\r\n        const externalContainer = itemContent.querySelector(\".external-link-option\");\r\n\r\n        const internalField = internalContainer.querySelector(\"foundation-autocomplete\");\r\n        const externalField = externalContainer.querySelector(\"input[type='text']\");\r\n\r\n        if (val === \"internalLink\") {\r\n            internalContainer.classList.remove(\"hide\");\r\n            internalField.removeAttribute(\"disabled\");\r\n            externalContainer.classList.add(\"hide\");\r\n            externalField.setAttribute(\"disabled\", \"true\");\r\n        } else if (val === \"externalLink\") {\r\n            internalContainer.classList.add(\"hide\");\r\n            internalField.setAttribute(\"disabled\", \"true\");\r\n            externalContainer.classList.remove(\"hide\");\r\n            externalField.removeAttribute(\"disabled\");\r\n        }\r\n    }\r\n\r\n}(document, Granite.$));<\/code><\/pre>\n<\/blockquote>\n<p>&nbsp;<\/p>\n<p><span style=\"text-decoration: underline;\"><strong>How it works:<\/strong><\/span> Drop this JS into a clientlibrary folder, and reference the category via the <strong>extraClientlibs<\/strong> property on the dialog. On <strong>dialog-ready<\/strong>, we loop through every existing row and wire a change listener onto its own dropdown. When the &#8220;+&#8221; button is clicked to add a new row, we just re-run the same loop. The old rows already have listeners, so nothing breaks for them, and the new row gets wired up too. <strong>applyToggle<\/strong> always scopes its lookup to the current row&#8217;s <strong>coral-multifield-item-content<\/strong>, so flipping the dropdown in row 1 never touches row 2.<\/p>\n<p>&nbsp;<\/p>\n<h2><span style=\"text-decoration: underline;\">Why This Needed Custom Handling:<\/span><\/h2>\n<p>AEM ships a built-in feature for exactly this kind of show\/hide. Add the <strong>cq-dialog-dropdown-showhide<\/strong> class to your select, point it at a target selector with <strong>data-cq-dialog-dropdown-showhide-target<\/strong>, and give your containers a <strong>data-showhidetargetvalue<\/strong> attribute matching the dropdown value. No JS required, in theory.<\/p>\n<p>It worked fine for a single dropdown in a normal dialog. The moment I put it inside a <strong>granite\/ui\/components\/coral\/foundation\/form\/multifield<\/strong> with <strong>composite=&#8221;{Boolean}true&#8221;<\/strong>, it broke.<\/p>\n<p>&nbsp;<\/p>\n<h2><span style=\"text-decoration: underline;\">Challenges While Building This:<\/span><\/h2>\n<h3>#1: Selecting a value in one row affected every row<\/h3>\n<p>This was the big one, and it took a while to actually understand why it was happening. I&#8217;d add two menu items, set the first one to &#8220;External Link,&#8221; and suddenly the second item&#8217;s fields would change too, even though I never touched its dropdown.<\/p>\n<p>The root cause: AEM&#8217;s OOTB show\/hide handler runs document.querySelectorAll() against the target selector globally, across the entire dialog. It has no concept of &#8220;which row triggered this change.&#8221; Since every row&#8217;s target container shared the exact same class (<strong>linktype-option-showhide-target<\/strong> in earlier iterations, later .internal-link-option \/ .external-link-option), the handler toggled visibility on all of them at once, no matter which row&#8217;s dropdown actually fired.<\/p>\n<p>This is a classic trap with multifields anything that works fine for a single instance of a field often breaks down once you have an array of repeating items, because most OOTB handlers aren&#8217;t written with multifield scoping in mind.<\/p>\n<h3>#2: The Add Button and New Multifield Items<\/h3>\n<p>The next challenge was handling rows added dynamically via the multifield&#8217;s &#8220;+&#8221; (Add) button. Since the dialog XML only defines a &lt;template&gt; for new items, any JavaScript wiring done at dialog-ready time obviously won&#8217;t touch rows added afterward.<\/p>\n<p>A few things were tried before landing on something solid:<\/p>\n<ul>\n<li>First we tried listening to the multifield&#8217;s <strong>coral-collection:add<\/strong> event, but it fired more than once per item (once for the item itself, and again for nested elements) and caused duplicate bindings.<\/li>\n<li>Then we tried a <strong>MutationObserver<\/strong> combined with a small <strong>setTimeout<\/strong> to wait for the new row to render before wiring it up, but honestly, this never felt fully reliable. The timing depended on when Coral finished decorating the new row in the DOM, and we weren&#8217;t confident it would behave the same way across different browsers or even different AEM Cloud instances.<\/li>\n<li>In the end, we went with the simplest option: just re-run the same <strong>process()<\/strong> function on every click of the Add button. It&#8217;s a bit less &#8220;clever,&#8221; but it&#8217;s predictable, doesn&#8217;t depend on timing, and just works.<\/li>\n<\/ul>\n<h3>#3: Required validation blocking save on the hidden field<\/h3>\n<p>This one was sneaky. Both the Internal Link and External Link fields were marked required=&#8221;{Boolean}true&#8221; in the dialog XML, which is correct, but AEM&#8217;s foundation validation doesn&#8217;t care whether a field is visually hidden. If it&#8217;s marked required and tries to save with an empty value, it blocks the dialog from saving.<\/p>\n<p>So picking &#8220;External Link,&#8221; filling the URL, and hitting save would still throw an error on the hidden Internal Link field.<\/p>\n<p>The fix that worked cleanly, without fighting against AEM&#8217;s own validation engine, was using the disabled attribute instead of trying to toggle <strong>required\/aria-required<\/strong> on and off. A disabled field is automatically skipped by both native browser validation and AEM&#8217;s foundation validation, no custom validator registration needed, no risk of conflicting with OOTB behavior. Whichever field is currently hidden gets disabled; the visible one gets re-enabled.<\/p>\n<p>&nbsp;<\/p>\n<h2><span style=\"text-decoration: underline;\">Conclusion<\/span><\/h2>\n<p>What looks like a five-minute &#8220;just use the OOTB show\/hide&#8221; task turns into a real rabbit hole once multifields, Coral custom elements, and AEM&#8217;s validation engine are all in play. The fix isn&#8217;t complicated once you understand why the built-in feature breaks in a multifield context. It just comes down to scoping everything to the current row and letting <strong>disabled<\/strong> handle validation instead of wrestling with <strong>required<\/strong>.<\/p>\n<p>Hopefully this saves someone else the few hours of head-scratching it took to track down.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>If you&#8217;ve built a &#8220;header&#8221; or &#8220;navigation menu&#8221; dialog in AEM, you&#8217;ve probably hit this requirement before: a content author adds multiple menu items, and for each one picks whether it&#8217;s an Internal Link or an External Link. Based on that choice, only the relevant field should show up: a path field for internal links, [&hellip;]<\/p>\n","protected":false},"author":2303,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":6},"categories":[5868],"tags":[8675,5313,5314,8672,4847,5008,8674,8678,8676,8688,8690,8687,8691,8689],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/80396"}],"collection":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/users\/2303"}],"replies":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/comments?post=80396"}],"version-history":[{"count":11,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/80396\/revisions"}],"predecessor-version":[{"id":80705,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/80396\/revisions\/80705"}],"wp:attachment":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/media?parent=80396"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/categories?post=80396"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/tags?post=80396"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}