Dialog Validation
11 – Dialog Validation
Target: 3–4 years experienced AEM Developer Syllabus point covered (20): "How do you write validation JS for a component dialog?" Project domain: a global energy technology company's marketing site.
Before we start — two things that reframe this question
Your syllabus asks how to write validation JavaScript. That's the right question, but there are two things worth knowing before you write a line of it, and both are good things to say in an interview.
First: try not to write any. Granite UI gives you required, and the field types themselves constrain a lot — a pathfield restricted with rootPath cannot produce a bad path, and a select cannot produce a value outside its options. A good fieldDescription prevents more errors than a validator catches, because it stops the author making the mistake rather than telling them off afterwards. Reaching for custom JavaScript first is a mild code smell.
Second, and more important: dialog validation is UX, not enforcement.
It runs in the author's browser. It can be bypassed — by disabling JavaScript, by posting to the node directly, by a package install, by a content migration script, by MSM rolling a value in from a blueprint. So if a constraint genuinely matters for correctness or security, it cannot only live in the dialog.
That distinction is the thing that separates a good answer from a merely correct one. Section 2.8 covers where the real enforcement goes.
1. Introduction
1.1 What dialog validation actually is
Recall from file 02: a dialog is an HTML form, and saving it is a POST handled by the Sling Post Servlet. There is no special "dialog save" API.
So dialog validation is exactly what form validation is anywhere on the web — client-side JavaScript that inspects field values and blocks submission if something is wrong.
AEM provides a framework for it called foundation-validation, part of Granite UI. It is jQuery-based, and it works by letting you register a validator against a CSS selector. Any field matching that selector gets validated by your function.
That selector-based design is worth noticing, because it means a validator is not attached to a specific field — it is attached to a pattern, and any field that matches picks it up. One validator can serve twenty fields across ten components.
1.2 What you get for free
Before any JavaScript:
The point worth making: most validation requirements are better solved by choosing the right field and constraining it than by validating free text afterwards. If an author must pick a product page, a pathfield with rootPath is better than a textfield plus a validator — it makes the wrong answer unreachable rather than rejected.
1.3 A real project example to adapt
"We have three custom validators. One checks that our product code fields match the expected format, because the codes feed an external system and a malformed one fails silently downstream. One enforces a character limit on SEO fields — technically it's a soft limit, so it warns rather than blocks. And one is a cross-field validator on a date range, making sure the end date is after the start.
They live in a clientlib with category
cq.authoring.dialog, which is what AEM loads in the dialog context. And for anything that genuinely matters we don't rely on the dialog alone — the product code is also validated in the Sling Model, because dialog validation is client-side and can be bypassed by a package install or an MSM rollout."
That covers the mechanism, the clientlib, the soft-versus-hard distinction, and the enforcement point.
2. Core Concepts
2.1 The foundation-validation registry
This is the API your syllabus is asking about.
Three things to understand about that:
selector is a CSS selector. Any field in any dialog matching it gets this validator. That is why validators are reusable across components.
validate receives the element and returns either a string (invalid — and the string is the message shown) or nothing (valid). That inverted convention catches people: you return a value to indicate failure, not success.
The empty check. Notice if (value && ...). An empty field is not this validator's problem — that is what required is for. Without that guard, an optional field would be flagged invalid when left blank, which is wrong and infuriating for authors.
2.2 Connecting the dialog field to the validator
The link is the validation property on the field, which Granite renders as a data-validation attribute on the element.
That renders roughly as:
Which is what selector: "[data-validation='energy.productcode']" matches.
Why this indirection is good: the field names a validator; the validator declares what it matches. Neither knows about the other's implementation, so you can change the rule without touching any dialog.
You can select by anything, though. The data-validation attribute is the conventional route, but the selector is a plain CSS selector:
Namespace your validator names. energy.productcode, not productcode. These are global across every dialog in the instance — including Adobe's — so a generic name risks colliding with something you didn't write.
2.3 Where the clientlib goes
This is the part that most often causes "my validation isn't running."
.content.xml:
cq.authoring.dialog is the category AEM loads in the dialog context. Get it wrong and your JavaScript never loads, the validator is never registered, and the field simply saves — with no error anywhere, because nothing failed. It just isn't there.
Related categories worth knowing:
And a point that connects to file 04: this is an author-only clientlib. It must never be embedded into the site bundle that visitors download. Authoring JavaScript shipped to the public is pure waste — and occasionally an information leak, since validators often encode business rules.
2.4 Hard versus soft validation
A distinction worth raising, because it shows you think about authors rather than just rules.
Hard validation blocks the save. Return a message from validate and the field is marked invalid and submission is prevented. Right when the value would actually break something.
Soft validation warns but allows. Sometimes the rule is a guideline — an SEO title should be under 60 characters, but an author may have a good reason to exceed it, and blocking them is presumptuous.
For soft rules, don't return a message. Show a hint instead:
The interview point:
"I'd distinguish between rules that must block the save and rules that are guidance. A malformed product code breaks a downstream integration, so that blocks. An SEO title over sixty characters is a recommendation — the author might have a reason, and blocking them is presumptuous. For those I show a hint and let them save. Over-blocking is how authors end up hating a component."
2.5 Cross-field validation — and the trap in it
The genuinely tricky case: validating one field against another.
That works — and it has a bug that catches nearly everyone.
The validator only runs when its own field changes. So:
- Author sets start = 1 June, end = 1 July. Valid. ✓
- Author then changes start to 1 August.
- The end-date validator does not re-run, because the end date didn't change.
- The dialog saves with an invalid range.
The fix is to trigger revalidation of the dependent field when the other one changes:
This is an excellent thing to raise unprompted, because it demonstrates you've actually built one rather than read about it:
"The trap with cross-field validation is that a validator only fires when its own field changes. So if someone validates the end date against the start date, and the author then goes back and changes the start date, the validator never re-runs and an invalid range saves. You have to explicitly trigger a change event on the dependent field when the other one changes."
2.6 Passing configuration into a validator with granite:data
Rather than hardcoding limits in JavaScript, put them on the field:
granite:data renders as data-* attributes, so in the validator:
Why this matters: one validator now serves every length-limited field in the project, with the limit configured per field in the dialog. Without it you'd need a validator per limit, or the limit hardcoded somewhere a template author can't see.
2.7 Show/hide — adjacent, and frequently asked together
Not validation, but it comes up in the same conversation and AEM gives it to you built in.
Show a group of fields based on a dropdown:
The mechanism: cq-dialog-dropdown-showhide on the select, a cq-dialog-dropdown-showhide-target selector pointing at the groups, and each group carrying showhidetargetvalue for the option that reveals it. There is an equivalent cq-dialog-checkbox-showhide.
Worth mentioning because it's free. Candidates often write custom JavaScript for this, and AEM already does it.
2.8 Where real enforcement lives
The most important section in this file.
Dialog validation runs in the author's browser. It can be bypassed:
- JavaScript disabled, or an error earlier in the clientlib preventing registration
- POSTing to the node directly
- Installing a content package
- A content migration script
- An MSM rollout pushing a value in from a blueprint — a real one on a multi-country site
- Content copied between environments
So for anything that genuinely matters, validate again where it counts:
In the Sling Model, treating the value as untrusted:
Or in a SlingPostProcessor, which runs after the Sling Post Servlet has processed a POST — server-side, so it applies regardless of how the POST arrived.
The interview answer:
"Dialog validation is UX, not enforcement. It runs in the author's browser, so it can be bypassed by disabling JavaScript, by POSTing directly, by a package install, by a content migration, or — the one people forget — by an MSM rollout pushing a value in from a blueprint.
So for anything that actually matters I validate again server-side. Usually in the Sling Model, treating the value as untrusted and exposing an
isReady()that renders nothing rather than something broken. For a hard constraint I'd use aSlingPostProcessor, which runs after the Sling Post Servlet regardless of how the POST arrived.The dialog validator is still worth having — it gives the author immediate, specific feedback at the point they can fix it, which is a much better experience than discovering a broken page later. But it's the first line, not the only one."
3. Internal Working
3.1 The validation lifecycle
sequenceDiagram participant A as Author participant D as Dialog (browser) participant V as foundation-validation participant P as Sling Post Servlet participant J as JCR Note over D,V: On dialog open, the cq.authoring.dialog<br/>clientlib registers all validators A->>D: types in a field D->>V: change / blur event V->>V: find validators whose SELECTOR matches V->>V: run validate(el) alt returns a string V->>D: mark invalid, show the message A->>D: clicks Save D->>D: BLOCKED else returns nothing V->>D: field is valid A->>D: clicks Save D->>P: POST./field=value P->>J: write the property end
Three things to draw out:
Validators register when the clientlib loads, not per field. If the clientlib isn't in the dialog context, nothing registers and every field silently passes.
Validation runs on change and blur, and again on submit. So a field the author never touched still gets checked at submit time — which is how required catches an untouched empty field.
Once it reaches the Sling Post Servlet, no client-side validation exists any more. That is the whole argument in 2.8, expressed as a diagram.
3.2 Why "my validation isn't running" is nearly always one of three things
graph TD A["The validator never fires"] --> B{"Is the clientlib loading?<br/>network tab, dialog context"} B -->|No| C["WRONG CATEGORY<br/>needs cq.authoring.dialog"] B -->|Yes| D{"Does the selector match?<br/>inspect the field element"} D -->|No| E["The data-validation attribute<br/>isn't rendering, or the<br/>selector is wrong"] D -->|Yes| F{"Any JS error in the console<br/>BEFORE registration?"} F -->|Yes| G["An earlier error stopped<br/>the file executing"] F -->|No| H["Check the validate function —<br/>is it returning a string?"]
The third branch is the sneaky one. A JavaScript error anywhere earlier in the same file — or in another file loaded before it in the same clientlib — stops execution, so $.validator.register never runs. The symptom is a validator that simply doesn't exist, with the real error sitting further up the console.
8. Coding Examples
8.1 The complete validator clientlib
/apps/energy/clientlibs/clientlib-authoring/.content.xml
js.txt
js/validators.js
js/listeners.js — the cross-field fix
8.2 The dialog fields
8.3 The server-side counterpart
The layer that actually holds.
8.4 Show/hide without custom JavaScript
Zero custom JavaScript. This is built into AEM, and candidates frequently write their own. There is an equivalent cq-dialog-checkbox-showhide.
9. Common Mistakes
10. Best Practices
Prefer prevention. Constrain the field type before validating free text. A pathfield with rootPath beats a textfield plus a regex. A fieldDescription stating the format prevents more errors than any message catches.
Distinguish rules from guidelines. Hard-block what would actually break; warn on what's merely advisable. Over-blocking is how authors come to resent a component and start working around it.
Write messages an author can act on. Include the expected format and an example. "Invalid" is useless.
Namespace validators, and configure them with granite:data rather than hardcoding, so one validator serves many fields.
Guard the empty case. Emptiness is required 's job.
Handle the cross-field trap explicitly — a dependent validator needs a trigger from the field it depends on.
Always validate server-side too for anything that matters, and log at warn when the server rejects something, because a bypassed value arrived some way nobody will report.
Keep the validate function cheap. It runs on every change. No network calls.
Keep authoring clientlibs out of the site bundle.
11. Debugging Tips
The three causes, in order, from section 3.2:
1. Is the clientlib loading? Open a dialog, look at the network tab, and search for your category. If it isn't there, the category is wrong — and remember this failure is completely silent, because a missing validator doesn't error, it just doesn't exist.
2. Does the selector match? Inspect the field element in the DOM and confirm the data-validation attribute is actually rendered with the value you expect. If the field has no attribute, the validation property is missing or misspelled in the dialog XML.
3. Is there a JavaScript error earlier in the file? This is the sneaky one. An error anywhere before $.validator.register stops execution, so registration never happens. The real error is further up the console than where you're looking.
A quick way to confirm registration — in the browser console with a dialog open:
Zero means the field isn't rendering the attribute. Non-zero means the field is fine and the problem is registration.
When validation blocks but shows no message: the validate function is returning something non-string-like, or the field wrapper structure isn't what your custom display code expects.
When it works on one dialog and not another: the selector is matching more or less than you intended. Test it in the console on both dialogs.
12. Performance Notes
The validate function runs on every change event. A regex is fine. A repository lookup or an HTTP call is not — the dialog becomes laggy and the author feels it on every keystroke.
Never call an external system from a validator. Beyond the latency, it means the author is blocked when a system they've never heard of is down. If you genuinely need to check against an external source, make it an explicit action the author triggers, not something that runs while they type.
Keep the authoring clientlib small and separate. It only loads on author, so it doesn't affect site performance — but only if it isn't embedded in the site bundle.
input events fire per keystroke. If you bind to them for a live character count, that's fine for a string length check and wrong for anything heavier.
15. Comparison Tables
Validation layers
Hard versus soft
Clientlib categories
Constrain versus validate
Next topic
12-MSM-and-Translation — your syllabus point 21 in full: what MSM is, blueprint versus live copy versus language copy, how rollout configurations work, inheritance and how it's cancelled, and how translation actually runs on a multi-country project.
Topic 11 of the AEM Interview Preparation repository. Teaching style, energy-sector project domain.