AEM Dialog Components #1: Textfield - Single Line Text Input

AEM Dialog Components #1: Textfield

What is a Textfield?

The Textfield is a Granite UI component that allows authors to enter single-line text in AEM component dialogs.

Common use cases:

  • Titles and headings
  • URLs or links
  • Author name
  • IDs or codes
  • Email, phone
  • Any short text input

Basic Configuration

Minimal XML Dialog

<title
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
    fieldLabel="Title"
    name="./title"/>

Essential properties:

  • sling:resourceType - Always granite/ui/components/coral/foundation/form/textfield
  • fieldLabel - Label visible to the author
  • name - JCR path where to save the value (always use ./)

Main Properties

1. Required (Mandatory Field)

<title
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
    fieldLabel="Title"
    name="./title"
    required="{Boolean}true"/>

Behavior: The author cannot save the dialog without filling in the field.


2. Default Value

<title
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
    fieldLabel="Title"
    name="./title"
    value="Default Title"/>

When to use it: Provide an initial value that the author can modify.


3. Placeholder

<email
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
    fieldLabel="Email"
    name="./email"
    emptyText="example@domain.com"/>

Behavior: Shows suggestive text when the field is empty.


4. Maxlength (Maximum Length)

<shortTitle
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
    fieldLabel="Short Title"
    name="./shortTitle"
    maxlength="{Long}50"/>

Important: Use {Long} for the number type hint!


5. Validation (Regex Validation)

IMPORTANT: There is no validation="email" attribute in Granite UI. To validate email, URL or other patterns you must use granite:data with regex.

Email Validation

<email
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
    fieldLabel="Email"
    name="./email"
    required="{Boolean}true">
    <granite:data
        jcr:primaryType="nt:unstructured"
        validation-regex="^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
        validation-regex-message="Enter a valid email address"/>
</email>

Custom Regex Validation

<phoneNumber
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
    fieldLabel="Phone Number"
    name="./phoneNumber"
    required="{Boolean}true">
    <granite:data
        jcr:primaryType="nt:unstructured"
        validation-regex="^[0-9]{10}$"
        validation-regex-message="Enter exactly 10 digits"/>
</phoneNumber>

Note: The regex syntax varies slightly between AEM 6.3 and AEM 6.4+ for backslash escaping.


6. Disabled and ReadOnly

<!-- Disabled: disabled field, not editable -->
<id
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
    fieldLabel="Component ID"
    name="./id"
    disabled="{Boolean}true"/>

<!-- ReadOnly: visible but not editable -->
<createdBy
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
    fieldLabel="Created By"
    name="./createdBy"
    readOnly="{Boolean}true"/>

Difference:

  • disabled - Grayed out field, not sent to server
  • readOnly - Visually normal field but not editable

Practical Examples

Example 1: Title with Maximum Length

<title
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
    fieldLabel="Title"
    name="./title"
    required="{Boolean}true"
    maxlength="{Long}120"
    emptyText="Enter component title (max 120 chars)"/>

Use case: Titles for SEO or cards with character limit.


Example 2: Email with Validation

<email
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
    fieldLabel="Email Address"
    name="./email"
    fieldDescription="Contact email for this component"
    required="{Boolean}true"
    emptyText="user@example.com">
    <granite:data
        jcr:primaryType="nt:unstructured"
        validation-regex="^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
        validation-regex-message="Enter a valid email address"/>
</email>

Use case: Contact forms, author info.


Example 3: URL with Validation

<externalLink
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
    fieldLabel="External Link"
    name="./externalLink"
    fieldDescription="Full URL including https://"
    required="{Boolean}true"
    emptyText="https://example.com">
    <granite:data
        jcr:primaryType="nt:unstructured"
        validation-regex="^https?://[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(/.*)?$"
        validation-regex-message="Enter a valid URL (http:// or https://)"/>
</externalLink>

Use case: External links, API integrations.


Example 4: Custom CSS Class

<cssClass
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
    fieldLabel="Custom CSS Class"
    name="./cssClass"
    fieldDescription="Additional CSS classes (space-separated)"
    emptyText="my-class another-class"/>

Use case: Allow authors to add custom CSS classes.


Example 5: Tracking/Analytics Code

<trackingId
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
    fieldLabel="Tracking ID"
    name="./trackingId"
    fieldDescription="Google Analytics tracking ID"
    required="{Boolean}true"
    emptyText="UA-123456-1">
    <granite:data
        jcr:primaryType="nt:unstructured"
        validation-regex="^UA-[0-9]+-[0-9]+$"
        validation-regex-message="Invalid GA format (UA-XXXXXX-X)"/>
</trackingId>

Use case: Analytics configuration, tracking codes.


Complete Dialog with Textfield

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
    xmlns:granite="http://www.adobe.com/jcr/granite/1.0"
    xmlns:cq="http://www.day.com/jcr/cq/1.0"
    xmlns:jcr="http://www.jcp.org/jcr/1.0"
    xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
    jcr:primaryType="nt:unstructured"
    jcr:title="Hero Component"
    sling:resourceType="cq/gui/components/authoring/dialog">
    <content
        jcr:primaryType="nt:unstructured"
        sling:resourceType="granite/ui/components/coral/foundation/container">
        <items jcr:primaryType="nt:unstructured">
            <tabs
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/tabs">
                <items jcr:primaryType="nt:unstructured">
                    <content
                        jcr:primaryType="nt:unstructured"
                        jcr:title="Content"
                        sling:resourceType="granite/ui/components/coral/foundation/container">
                        <items jcr:primaryType="nt:unstructured">

                            <!-- Title Field -->
                            <title
                                jcr:primaryType="nt:unstructured"
                                sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                                fieldLabel="Title"
                                name="./title"
                                required="{Boolean}true"
                                maxlength="{Long}120"/>

                            <!-- Subtitle Field -->
                            <subtitle
                                jcr:primaryType="nt:unstructured"
                                sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                                fieldLabel="Subtitle"
                                name="./subtitle"
                                maxlength="{Long}200"
                                emptyText="Optional subtitle"/>

                            <!-- CTA Button Text -->
                            <ctaText
                                jcr:primaryType="nt:unstructured"
                                sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                                fieldLabel="Button Text"
                                name="./ctaText"
                                value="Learn More"
                                maxlength="{Long}30"/>

                            <!-- CTA Link -->
                            <ctaLink
                                jcr:primaryType="nt:unstructured"
                                sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                                fieldLabel="Button Link"
                                name="./ctaLink"
                                emptyText="/content/mysite/page"/>

                        </items>
                    </content>
                </items>
            </tabs>
        </items>
    </content>
</jcr:root>

Usage in HTL

Simple Value Reading

<h1 class="hero-title">${properties.title}</h1>
<p class="hero-subtitle">${properties.subtitle}</p>

<a href="${properties.ctaLink}" class="btn">
  ${properties.ctaText}
</a>

With Fallback and Validation

<!-- Title with fallback -->
<h1 data-sly-test="${properties.title}">
  ${properties.title}
</h1>
<h1 data-sly-test="${!properties.title}">
  Default Title
</h1>

<!-- CTA only if link is present -->
<a data-sly-test="${properties.ctaLink}"
   href="${properties.ctaLink}"
   class="btn">
  ${properties.ctaText || 'Read More'}
</a>

With Sling Model

@Model(adaptables = Resource.class)
public class HeroModel {

    @ValueMapValue
    private String title;

    @ValueMapValue
    private String subtitle;

    @ValueMapValue
    private String ctaText;

    @ValueMapValue
    private String ctaLink;

    public String getTitle() {
        return title != null ? title : "Default Title";
    }

    public String getSubtitle() {
        return subtitle;
    }

    public String getCtaText() {
        return ctaText != null ? ctaText : "Learn More";
    }

    public String getCtaLink() {
        return ctaLink;
    }

    public boolean hasSubtitle() {
        return subtitle != null && !subtitle.isEmpty();
    }

    public boolean hasCtaLink() {
        return ctaLink != null && !ctaLink.isEmpty();
    }
}

HTL with Model:

<div data-sly-use.model="com.mysite.models.HeroModel" class="hero">
  <h1 class="hero-title">${model.title}</h1>

  <p data-sly-test="${model.hasSubtitle}" class="hero-subtitle">
    ${model.subtitle}
  </p>

  <a data-sly-test="${model.hasCtaLink}"
     href="${model.ctaLink}"
     class="hero-btn">
    ${model.ctaText}
  </a>
</div>

Advanced Properties

1. fieldDescription (Field Description)

<apiKey
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
    fieldLabel="API Key"
    name="./apiKey"
    fieldDescription="Enter your API key from the admin panel"
    required="{Boolean}true"/>

Shows help text below the field.


2. granite:class (Custom CSS)

<customField
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
    fieldLabel="Custom Field"
    name="./customField"
    granite:class="custom-textfield-style"/>

Adds custom CSS classes to the field.


3. autocomplete

<username
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
    fieldLabel="Username"
    name="./username"
    autocomplete="username"/>

Common values: username, email, name, tel, url.


Common Issues

❌ Issue 1: Value not saved

<!-- WRONG - missing ./ in name -->
<title
    name="title"/>

<!-- CORRECT -->
<title
    name="./title"/>

Solution: Always use ./ prefix in name.


❌ Issue 2: Validation with wrong syntax

<!-- WRONG - validation="email" doesn't exist -->
<email
    validation="email"
    required="{Boolean}true"/>

<!-- CORRECT - use granite:data with regex -->
<email
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
    name="./email"
    required="{Boolean}true">
    <granite:data
        jcr:primaryType="nt:unstructured"
        validation-regex="^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
        validation-regex-message="Invalid email"/>
</email>

Solution: There is no validation="email". Use granite:data with validation-regex.


❌ Issue 3: Maxlength ignored

<!-- WRONG - maxlength without type hint -->
<title
    maxlength="50"/>

<!-- CORRECT -->
<title
    maxlength="{Long}50"/>

Solution: Always use {Long} for numeric values.


❌ Issue 4: Required not working

<!-- WRONG - required as string -->
<title
    required="true"/>

<!-- CORRECT -->
<title
    required="{Boolean}true"/>

Solution: Use {Boolean}true, not string.


Best Practices

  1. Always use ./ in name: name="./title"
  2. Type hints for booleans and numbers: required="{Boolean}true", maxlength="{Long}50"
  3. EmptyText for UX: Provide examples in placeholder
  4. Maxlength for limits: Prevent too long inputs
  5. FieldDescription for complexity: Explain non-obvious fields
  6. Validation for critical data: Email, URL, specific patterns
  7. Required only if necessary: Don't force optional fields
  8. Useful default values: Speed up authoring

When NOT to Use Textfield

Don't use textfield when:

  • Multi-line text needed → Use Textarea
  • Formatted text needed → Use RichText Editor
  • Selection from options needed → Use Select or Radio
  • AEM path selection needed → Use PathBrowser
  • Number with spinner needed → Use NumberField
  • Date needed → Use DatePicker
  • Color needed → Use ColorField

Next Lesson

In the next lesson we'll see the Textarea component for multi-line text.

Resources:


Guide #1 of the AEM Dialog Components series. Next lesson →