Hello AEM Community,
I'm working on a component dialog in AEM where a dropdown drives visibility of field containers using a custom JS-based show/hide logic that supports multiple values per target.
Goal / Functionality:
A select dropdown has 4 options: Images, Articles, Posts, and Videos
Images, Articles, and Posts share a common image upload field
Videos shows a separate video path and poster field
Both single and multifield dropdowns are supported
Validation is handled properly — hidden required fields are not submitted, and visible fields are validated correctly
Issue:
While the custom JS handles visibility and validation well for the video path field, I’ve noticed that the image field (used in the shared container) allows submission even when required and empty.
Oddly enough, the video path field is correctly validated.
Dialog XML Snippet:
xml
Copy
Edit
<tab
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/select"
fieldLabel="Select Show Tab"
granite:class="cq-dialog-dropdown-showhide"
defaultValue="about"
name="./maintab">
<granite:data
jcr:primaryType="nt:unstructured"
cq-dialog-dropdown-showhide-target=".connect-layout-show-hide-main"/>
<items jcr:primaryType="nt:unstructured">
<images jcr:primaryType="nt:unstructured" text="Images" value="images"/>
<articles jcr:primaryType="nt:unstructured" text="Articles" value="articles"/>
<posts jcr:primaryType="nt:unstructured" text="Posts" value="posts"/>
<videos jcr:primaryType="nt:unstructured" text="Videos" value="videos"/>
</items>
</tab>
<main-video-container
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container"
granite:class="hide connect-layout-show-hide-main">
<granite:data
jcr:primaryType="nt:unstructured"
showhidetargetvalue="videos"
validation-ignore="{Boolean}true"/>
<items jcr:primaryType="nt:unstructured">
<videosrc
granite:class="mybrowser video"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Video Path"
name="./mainVideoSrc"
required="{Boolean}true">
<granite:data jcr:primaryType="nt:unstructured" assettype="*" mimeType="*" mode="single" rootpath="/content/dam"/>
</videosrc>
<video-poster
granite:class="mybrowser"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Video Poster"
name="./mainVideoPoster">
<granite:data jcr:primaryType="nt:unstructured" assettype="images" mimeType="image/*" mode="single" rootpath="/content/dam"/>
</video-poster>
</items>
</main-video-container>
<main-image-container
sling:resourceType="granite/ui/components/coral/foundation/container"
granite:class="hide connect-layout-show-hide-main">
<granite:data
jcr:primaryType="nt:unstructured"
showhidetargetvalue="images,articles,posts"
validation-ignore="{Boolean}true"/>
<items jcr:primaryType="nt:unstructured">
<imagepath
granite:class="assetbrowser"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Image"
name="./mainimagepath"
required="{Boolean}true">
<granite:data
jcr:primaryType="nt:unstructured"
assettype="images"
mimeType="image/*"
mode="single"
rootpath="/content/dam"/>
</imagepath>
</items>
</main-image-container>
Custom Show/Hide + Validation JS
javascript
Copy
Edit
(function(document, $) {
"use strict";
$(document).on("foundation-contentloaded", function(e) {
Coral.commons.ready(function() {
showHideHandler($(".cq-dialog-dropdown-showhide", e.target));
showHideMultifieldHandler($(".cq-dialog-dropdown-showhide-multival", e.target));
});
setTimeout(function() {
$(".cq-dialog-dropdown-showhide-multival").each(function() {
showHideMultifieldHandler($(this));
});
}, 100);
});
$(document).on("change", ".cq-dialog-dropdown-showhide", function() {
if ($(this).closest('.coral3-Multifield').length > 0) {
showHideHandler($(this));
}
});
$(document).on("selected", ".cq-dialog-dropdown-showhide-multival", function(e) {
showHideMultifieldHandler($(this));
});
$(document).on("click", ".cq-dialog-submit", function(e) {
$(".hide").find("[required]").each(function() {
var $field = $(this);
if (!$field.attr("data-temp-submission-required")) {
$field.attr("data-temp-submission-required", "true");
$field.removeAttr("required");
$field.removeAttr("aria-required");
}
});
$(".hide input, .hide coral-textfield").each(function() {
if ($(this).attr("type") !== "hidden") {
clearFieldValue($(this));
}
});
});
function showHideHandler(el) {
el.each(function(i, element) {
if ($(element).is("coral-select")) {
Coral.commons.ready(element, function(component) {
multiSelectShowHide(element);
component.on("change", function() {
multiSelectShowHide(element);
});
});
}
});
}
function multiSelectShowHide(element) {
let target = $(element).data("cqDialogDropdownShowhideTarget");
let $target = $(target);
let selectedValue = element.value;
if (target) {
$target.each(function() {
let $currentTarget = $(this);
let targetValues = $currentTarget.data('showhidetargetvalue');
if (targetValues) {
let shouldShow = targetValues.replace(/ /g, '').split(',').includes(selectedValue);
if (shouldShow) {
$currentTarget.removeClass('hide');
restoreRequiredFields($currentTarget);
} else {
$currentTarget.addClass('hide');
storeAndRemoveRequiredFields($currentTarget);
}
}
});
}
}
function showHideMultifieldHandler(el) {
el.each(function(i, element) {
if ($(element).is("coral-select")) {
Coral.commons.ready(element, function(component) {
showHideMultifield(component, element);
component.on("change", function() {
showHideMultifield(component, element);
});
});
}
});
}
function showHideMultifield(component, element) {
var target = $(element).data("cqDialogDropdownShowhideTarget");
var $target = $(target);
var elementIndex = $(element).closest('coral-multifield-item').index();
if (target) {
var value = component.value;
$target.each(function() {
var tarIndex = $(this).closest('coral-multifield-item').index();
if (elementIndex == tarIndex) {
var $currentTarget = $(this);
var targetValues = $currentTarget.data('showhidetargetvalue');
if (targetValues) {
var shouldShow = targetValues.replace(/ /g, '').split(',').includes(value);
if (shouldShow) {
$currentTarget.removeClass("hide");
restoreRequiredFields($currentTarget);
} else {
$currentTarget.addClass("hide");
storeAndRemoveRequiredFields($currentTarget);
}
}
}
});
}
}
function storeAndRemoveRequiredFields($container) {
$container.find("[required], [aria-required='true']").each(function() {
var $field = $(this);
$field.attr("data-original-required", "true");
$field.removeAttr("required").removeAttr("aria-required");
clearFieldValue($field);
clearValidationErrors($field);
});
}
function restoreRequiredFields($container) {
$container.find("[data-original-required='true']").each(function() {
var $field = $(this);
$field.attr("required", "true").attr("aria-required", "true");
$field.removeAttr("data-original-required");
});
}
function clearFieldValue($field) {
try {
if ($field.is("input[type='text'], textarea")) {
$field.val("");
} else if ($field.is("coral-textfield")) {
$field[0].value = "";
} else if ($field.is("coral-select")) {
$field[0].value = "";
} else if ($field.is("coral-checkbox, coral-radio")) {
$field[0].checked = false;
} else if ($field.is("coral-autocomplete")) {
if (typeof $field[0].clear === "function") $field[0].clear();
}
} catch (e) {
console.warn("Clear field error:", e);
}
}
function clearValidationErrors($field) {
try {
$field.removeAttr("aria-invalid").removeClass("is-invalid");
$field.closest(".coral-Form-field").removeClass("is-invalid").find(".coral-Form-fielderror").remove();
if ($field[0]) $field[0].invalid = false;
} catch (e) {
console.warn("Clear validation error:", e);
}
}
function getFieldValue($field) {
try {
return $field[0]?.value || $field.val();
} catch (e) {
return $field.val();
}
}
$(document).on("foundation-validation-valid", function(e) {
$(".hide [required]").each(function() {
$(this).removeAttr("required").removeAttr("aria-required").attr("data-temp-submission-required", "true");
});
});
if ($.validator && typeof $.validator.register === "function") {
$.validator.register({
selector: "[required]:visible:not(.hide):not(.hide *)",
validate: function(el) {
const $field = $(el);
if ($field.closest('.hide').length > 0) return null;
const value = getFieldValue($field);
if (!value || value.trim() === '') {
return Granite.I18n.get('This field is required.');
}
return null;
}
});
}
$(document).on("foundation-contenthide foundation-contentunloaded", function(e) {
$("[data-original-required='true'], [data-temp-submission-required='true']").each(function() {
$(this).attr("required", "true").attr("aria-required", "true").removeAttr("data-original-required").removeAttr("data-temp-submission-required");
});
});
})(document, Granite.$);
Why is the imagepath field not enforcing required validation while others like videosrc are?
Is my custom JS too heavy? Can it be made more efficient without losing functionality?
Is this overall approach still considered best-practice in AEM (6.5+) dialogs, or is there a more Coral 3/native way to handle this?
Has anyone faced this inconsistency with asset-based textfields before?
Appreciate any insights from the community!
Topics help categorize Community content and increase your ability to discover relevant content.
Views
Replies
Total Likes
Hi @SriMi1 while imagepath and videosrc looks same, the issue may arise from
granite:class="assetbrowser".Temporarily remove granite:class="assetbrowser" from imagepath and test.
Js Work:
Instead of $field.attr("required", "true"); use $field[0].setAttribute("required", "true");
You're calling restoreRequiredFields and storeAndRemoveRequiredFields in several places.
change to smaller helper method
function toggleRequiredState($container, shouldShow) {
if (shouldShow) {
$container.removeClass('hide');
restoreRequiredFields($container);
} else {
$container.addClass('hide');
storeAndRemoveRequiredFields($container);
}
}
Use this inside both your multiSelectShowHide() and showHideMultifield() methods.
Extend your validator to explicitly check the .assetbrowser field:
$.validator.register({
selector: ".assetbrowser[required]",
validate: function(el) {
const value = getFieldValue($(el));
if (!value || value.trim() === "") {
return Granite.I18n.get("This image is required.");
}
return null;
}
});
Hi @SriMi1 ,
In Coral 3 dialogs, textfield-based asset pickers used with required=true and dynamic hide/show logic, lose native validation if you don’t re-trigger Coral’s internal validation pipeline or don’t preserve required attribute correctly when toggled.
Additionally, the granite/ui/components/coral/foundation/form/textfield used for imagepath often doesn’t trigger .is-invalid state until the field is touched/edited and clearing it via JS alone won’t mark it invalid unless forced validation is retriggered.
Try below steps:
Step 1: Update Dialog Field - Switch to pathbrowser
Change this field:
<sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
To
<sling:resourceType="granite/ui/components/coral/foundation/form/pathbrowser"
Because:
- Pathbrowser provides native DAM asset support.
- It fully supports required validation with Coral 3.
- And it triggers validation more reliably than the raw textfield-based picker.
Step 2: Ensure required is restored only when visible
In your JS, this block is essential and must be triggered after show/hide logic:
function restoreRequiredFields($container) {
$container.find("[data-original-required='true']").each(function () {
const $field = $(this);
$field.attr("required", "true").attr("aria-required", "true");
$field.removeAttr("data-original-required");
});
}
Make sure this is called right after showing the container using removeClass('hide').
Step 3: Re-trigger Validation When Dialog is Submitted
Right before submit, force all visible required fields to validate:
$(document).on("click", ".cq-dialog-submit", function (e) {
// force visible required fields to validate
$("form.foundation-form").trigger("submit");
});
This triggers Coral’s native .reportValidity() style validation across visible inputs - including pathbrowser.
Step 4: Re-enable Native Field Classes
Sometimes, your fields don't get is-invalid class updates if the field is wrapped inside containers without correct Coral markup. Double-check the XML structure so granite:class="coral-Form-fieldwrapper" is wrapping fields where necessary.
Sample Working XML
<imagepath
granite:class="assetbrowser"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathbrowser"
fieldLabel="Image"
name="./mainimagepath"
required="{Boolean}true">
<granite:data
jcr:primaryType="nt:unstructured"
assettype="images"
mimeType="image/*"
mode="single"
rootpath="/content/dam"/>
</imagepath>
Regards,
Amit
Hi @SriMi1
Please check if this helps
https://www.linkedin.com/pulse/aem-hideshow-drop-down-select-options-more-than-one-values-vikraman/
@SriMi1 Did you find the suggestions helpful? If you need more information, please let us know. If a response resolved your issue, kindly mark it as correct to help others in the future. Alternatively, if you discovered a solution on your own, we'd appreciate it if you could share it with the community. Thank you.
Views
Replies
Total Likes
Views
Like
Replies