Expand my Community achievements bar.

Join expert-led, customer-led sessions on Adobe Experience Manager Assets on August 20th at our Skill Exchange.

AEM Dialog Show/Hide with Multiple Dropdown Values and Custom Validation — Issue with Image Field Not Validating

Avatar

Level 1


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.$);
Looking for Suggestions
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

Topics help categorize Community content and increase your ability to discover relevant content.

4 Replies

Avatar

Level 4

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;
}
});

Avatar

Community Advisor

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"

Avatar

Community Advisor

Avatar

Administrator

@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.



Kautuk Sahni