Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ private ReservedProperties() {
public static final String PN_SHOW_AS_POPUP = "showAsPopup";
public static final String PN_TEXT_IS_RICH = "textIsRich";
public static final String PN_MULTILINE = "multiLine";
public static final String PN_SHOW_CHARACTER_COUNT = "showCharacterCount";
Comment thread
shravani-0811 marked this conversation as resolved.
public static final String PN_DESIGN_DEFAULT_TYPE = Title.PN_DESIGN_DEFAULT_TYPE;
public static final String PN_TITLE_LINK_DISABLED = Title.PN_TITLE_LINK_DISABLED;
public static final String PN_DRAG_DROP_TEXT = "dragDropText";
Expand All @@ -173,6 +174,7 @@ private ReservedProperties() {
public static final String FD_DIALOG_LABEL = "fd:dialogLabel";
public static final String FD_XFA_SCRIPTS = "fd:xfaScripts";
public static final String FD_FILE_ACCEPT_EXTENSIONS = "fd:acceptExtensions";
public static final String FD_SHOW_CHARACTER_COUNT = "fd:showCharacterCount";

public static final String FD_DRAFT_ID = "fd:draftId";
public static final String FD_CHANGE_EVENT_BEHAVIOUR = "fd:changeEventBehaviour";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package com.adobe.cq.forms.core.components.internal.models.v1.form;

import java.util.Date;
import java.util.Map;

import javax.annotation.Nullable;
import javax.annotation.PostConstruct;
Expand All @@ -27,6 +28,7 @@
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import org.jetbrains.annotations.NotNull;

import com.adobe.cq.export.json.ComponentExporter;
import com.adobe.cq.export.json.ExporterConstants;
Expand All @@ -53,6 +55,10 @@ public class TextInputImpl extends AbstractFieldImpl implements TextInput {
@Default(booleanValues = false)
protected boolean multiLine;

@ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL, name = ReservedProperties.PN_SHOW_CHARACTER_COUNT)
@Nullable
protected Boolean showCharacterCount;

@ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL, name = ReservedProperties.PN_FORMAT)
@Nullable
protected String format;
Expand All @@ -75,6 +81,13 @@ public boolean isMultiLine() {
return multiLine;
}

@Override
@Nullable
public Boolean isShowCharacterCount() {
// the character count is only applicable to multi line (textarea) fields
return isMultiLine() ? showCharacterCount : null;
}

@Override
public String getFieldType() {
if (isMultiLine()) {
Expand Down Expand Up @@ -168,4 +181,20 @@ private void initTextInput() {
minimum = null;
}
}

@Override
public @NotNull Map<String, Object> getProperties() {
Map<String, Object> properties = super.getProperties();
// custom (non-spec) property: reported under properties as fd:showCharacterCount, not as a top-level JSON
// field, mirroring how other custom properties (e.g. fd:disabledXfaScripts) are exported. Note this is a
// distinct name from the JCR-authored property (ReservedProperties.PN_SHOW_CHARACTER_COUNT, unprefixed) -
// a colon in a Granite checkbox widget's authored "name" breaks rendering in the AEM Forms authoring editor
// (confirmed empirically), so the fd: namespacing is applied only on the JSON-export side, same as
// fd:xfaScripts (JCR) vs fd:disabledXfaScripts (JSON).
Boolean showCharacterCountValue = isShowCharacterCount();
if (showCharacterCountValue != null) {
properties.put(ReservedProperties.FD_SHOW_CHARACTER_COUNT, showCharacterCountValue);
}
return properties;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,26 @@ default boolean isMultiLine() {
return false;
}

/**
* Returns {@code true} if a live character count should be shown below the field, {@code false} if it should not, or
* {@code null} if not configured. Only applicable when {@link #isMultiLine()} is {@code true}; {@code null} is returned
* for single line fields regardless of how the underlying property was set.
* <p>
* This is a custom (non-spec) property, not part of the Headless Adaptive Form JSON schema, so it is not exported as a
* top-level JSON property; it is instead reported under {@code properties} as {@code fd:showCharacterCount} (see
* {@link com.adobe.cq.forms.core.components.models.form.FormComponent#getProperties()}), mirroring how other custom
* properties (e.g. {@code fd:disabledXfaScripts}) are exported.
*
* @return {@code true} if the character count should be shown, {@code false} if not, or {@code null} if not configured or
* not applicable
* @since com.adobe.cq.forms.core.components.models.form 5.13.0
*/
@Nullable
@JsonIgnore
default Boolean isShowCharacterCount() {
return null;
}

/**
* Returns {@code "off"} if autocomplete if disabled, otherwise {@code "on"} or values listed @see
* <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete">here</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ public class TextInputImplTest {
private static final String PATH_TEXTINPUT_DATALAYER = CONTENT_ROOT + "/textinput-datalayer";
private static final String PATH_TEXTINPUT_CUSTOMIZED = CONTENT_ROOT + "/textinput-customized";
private static final String PATH_TEXTINPUT_2 = CONTENT_ROOT + "/multiline-textinput";
private static final String PATH_TEXTINPUT_CHARACTERCOUNT = CONTENT_ROOT + "/textinput-charactercount";
private static final String PATH_TEXTINPUT_CHARACTERCOUNT_SINGLELINE = CONTENT_ROOT + "/textinput-charactercount-singleline";
private static final String PATH_NUMBER_TEXTINPUT = CONTENT_ROOT + "/number-textinput";
private static final String PATH_NUMBER_TEXTINPUT_EXCLUSIVE = CONTENT_ROOT + "/number-textinput-exclusive";
private static final String PATH_FORMAT_TEXTINPUT = CONTENT_ROOT + "/textinput-format";
Expand Down Expand Up @@ -259,6 +261,47 @@ void testIsMultiLine() {
assertEquals(false, textInputMock.isMultiLine());
}

@Test
void testIsShowCharacterCount() {
TextInput textInput = Utils.getComponentUnderTest(PATH_TEXTINPUT_CUSTOMIZED, TextInput.class, context);
assertEquals(null, textInput.isShowCharacterCount());
TextInput textInputMock = Mockito.mock(TextInput.class);
Mockito.when(textInputMock.isShowCharacterCount()).thenCallRealMethod();
assertEquals(null, textInputMock.isShowCharacterCount());
}

@Test
void testIsShowCharacterCountWhenSet() {
TextInput textInput = Utils.getComponentUnderTest(PATH_TEXTINPUT_CHARACTERCOUNT, TextInput.class, context);
assertEquals(true, textInput.isShowCharacterCount());
assertEquals(100, textInput.getMaxLength().intValue());
}

@Test
void testCharacterCountJSONExport() throws Exception {
TextInput textInput = Utils.getComponentUnderTest(PATH_TEXTINPUT_CHARACTERCOUNT, TextInput.class, context);
Utils.testJSONExport(textInput, Utils.getTestExporterJSONPath(BASE, PATH_TEXTINPUT_CHARACTERCOUNT));
}

@Test
void testShowCharacterCountReportedUnderCustomProperties() {
// custom (non-spec) property: exported under properties as fd:showCharacterCount, not as a top-level JSON field
TextInput textInput = Utils.getComponentUnderTest(PATH_TEXTINPUT_CHARACTERCOUNT, TextInput.class, context);
Map<String, Object> properties = textInput.getProperties();
assertEquals(true, properties.get("fd:showCharacterCount"));
}

@Test
void testIsShowCharacterCountIgnoredForSingleLine() {
// showCharacterCount is only applicable to multi line fields; a single line field must report null
// regardless of what was authored, since the feature has no effect there.
TextInput textInput = Utils.getComponentUnderTest(PATH_TEXTINPUT_CHARACTERCOUNT_SINGLELINE, TextInput.class, context);
assertEquals(false, textInput.isMultiLine());
assertEquals(null, textInput.isShowCharacterCount());
assertFalse("showCharacterCount should not be reported for single line fields",
textInput.getProperties().containsKey("fd:showCharacterCount"));
}

@Test
void testGetPlaceHolder() {
TextInput textInput = Utils.getComponentUnderTest(PATH_TEXTINPUT_CUSTOMIZED, TextInput.class, context);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"id": "textinput-357c2ee34c",
"fieldType": "multiline-input",
"name": "abc",
"type": "string",
"maxLength": 100,
"label": {
"value": "def"
},
"events": {
"custom:setProperty": [
"$event.payload"
]
},
"properties": {
"fd:path": "/content/textinput-charactercount",
"fd:showCharacterCount": true
},
":type": "core/fd/components/form/textinput/v1/textinput"
}
Original file line number Diff line number Diff line change
Expand Up @@ -234,5 +234,24 @@
"sling:resourceType" : "core/fd/components/form/textinput/v1/textinput",
"name" : "abc",
"jcr:title" : "def"
},
"textinput-charactercount" : {
"jcr:primaryType": "nt:unstructured",
"sling:resourceType" : "core/fd/components/form/textinput/v1/textinput",
"name" : "abc",
"jcr:title" : "def",
"multiLine": true,
"fieldType": "multiline-input",
"showCharacterCount": true,
"maxLength": 100
},
"textinput-charactercount-singleline" : {
"jcr:primaryType": "nt:unstructured",
"sling:resourceType" : "core/fd/components/form/textinput/v1/textinput",
"name" : "abc",
"jcr:title" : "def",
"fieldType": "text-input",
"showCharacterCount": true,
"maxLength": 100
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,9 @@
}
textarea.cmp-adaptiveform-textinput__widget {
height: @textarea-height;
}
.cmp-adaptiveform-textinput__charcount {
.shortdescriptionMixin();
color: @light-gray;
text-align: right;
}
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,24 @@
jcr:primaryType="nt:unstructured"
click="[submitForm()]"/>
</submit>
<textcharcount
jcr:created="{Date}2026-09-23T10:00:00.000+05:30"
jcr:createdBy="admin"
jcr:lastModified="{Date}2026-09-23T10:00:00.000+05:30"
jcr:lastModifiedBy="admin"
jcr:primaryType="nt:unstructured"
jcr:title="Text Input with Character Count"
sling:resourceType="forms-components-examples/components/form/textinput"
enabled="{Boolean}true"
showCharacterCount="{Boolean}true"
fieldType="text-input"
hideTitle="{Boolean}false"
maxLength="{Long}20"
multiLine="{Boolean}true"
name="textcharcount"
readOnly="{Boolean}false"
unboundFormElement="{Boolean}false"
visible="{Boolean}true"/>
</guideContainer>
</jcr:content>
</jcr:root>
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ The following properties are written to JCR for this Form Text component and are
12. `./minLengthMessage` - defines the minimum length error message for the field.
13. `./displayPatternType` - defines the display pattern type for formatting the input value (e.g., 'phonenumber', 'socialsecuritynumber', 'email-alphanumeric', 'zipcode')
14. `./displayValueExpression` - defines the expression used to format the display value (e.g., 'formatInput($field.$value, 'phonenumber')')
15. `./showCharacterCount` - if set to `true`, a live character count is displayed below the field. Only applicable when `./multiLine` is `true` (the dialog checkbox is hidden otherwise, and the Sling Model reports `null` for single line fields regardless of the stored value). Combine with `maxLength` to show a running total out of the limit (e.g. `12 / 100`). This is a custom (non-spec) property: it is not exported as a top-level JSON field, but reported under `properties` as `fd:showCharacterCount`, consistent with other custom properties (e.g. `fd:xfaScripts` (JCR) / `fd:disabledXfaScripts` (JSON)). Note the JSON export key intentionally differs from the JCR-authored property name here - a colon in this dialog field's authored `name` breaks Granite checkbox rendering.


### Behavior of `maxLength` and `maxLengthMessage`
Expand Down Expand Up @@ -80,6 +81,7 @@ BLOCK cmp-adaptiveform-textinput
ELEMENT cmp-adaptiveform-textinput__shortdescription
ELEMENT cmp-adaptiveform-textinput__longdescription
ELEMENT cmp-adaptiveform-textinput__errormessage
ELEMENT cmp-adaptiveform-textinput__charcount
```

### Note
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,19 @@
name="./default"/>
<multiLine
jcr:primaryType="nt:unstructured"
granite:class="cmp-adaptiveform-textinput__multiline"
sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
name="./multiLine"
text="Allow multiple lines"
value="true"/>
<showCharacterCount
jcr:primaryType="nt:unstructured"
granite:class="cmp-adaptiveform-textinput__showcharactercount"
sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
name="./showCharacterCount"
text="Show character count"
fieldDescription="Displays a live character count below the field. Only applicable when 'Allow multiple lines' is enabled. Combine with 'Maximum Number of characters' in the Validation tab to show a running total out of the limit."
value="true"/>
<autocomplete
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/autocomplete"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@
cssSelector=".cmp-adaptiveform-textinput__longdescription:hover"/>
</states>
</textinputDescriptionLong>
<textinputCharCount
jcr:primaryType="nt:unstructured"
jcr:title="Character Count"
id="af2_textinputcharcount"
cssSelector=".cmp-adaptiveform-textinput__charcount"
longTitle="Text Input Character Count"
propertySheet="/mnt/overlay/fd/af/components/stylePropertySheet/common">
</textinputCharCount>
<textinputErrorMessage
jcr:primaryType="nt:unstructured"
jcr:title="Text Input Error Message"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,29 @@
TEXTINPUT_VALIDATIONFORMAT = EDIT_DIALOG + " .cmp-adaptiveform-textinput__validationformat",
TEXTINPUT_DISPLAYPATTERN = EDIT_DIALOG + " .cmp-adaptiveform-textinput__displaypattern",
TEXTINPUT_DISPLAYFORMAT = EDIT_DIALOG + " .cmp-adaptiveform-textinput__displayformat",
TEXTINPUT_MULTILINE = EDIT_DIALOG + " .cmp-adaptiveform-textinput__multiline",
TEXTINPUT_SHOWCHARACTERCOUNT = EDIT_DIALOG + " .cmp-adaptiveform-textinput__showcharactercount",
Utils = window.CQ.FormsCoreComponents.Utils.v1;

// "Show character count" is only applicable to multi line (textarea) fields, so hide/show it
// depending on the "Allow multiple lines" checkbox, mirroring the rich-text-title toggle in base's editDialog.js.
function handleShowCharacterCountVisibility(dialog) {
var multiLine = dialog.find(TEXTINPUT_MULTILINE)[0];
var showCharacterCount = dialog.find(TEXTINPUT_SHOWCHARACTERCOUNT)[0];
if (!multiLine || !showCharacterCount) {
return;
}
var toggleVisibility = function() {
if (multiLine.checked) {
Utils.showComponent(showCharacterCount, "div");
} else {
Utils.hideComponent(showCharacterCount, "div");
}
};
toggleVisibility();
multiLine.on("change", toggleVisibility);
}

function handleValidationPatternDropDown(dialog) {
Utils.handlePatternDropDown(dialog,TEXTINPUT_VALIDATIONPATTERN,TEXTINPUT_VALIDATIONFORMAT);
}
Expand Down Expand Up @@ -76,6 +97,8 @@
patternComponent.addEventListener("change", updateDisplayValueExpression);
}

Utils.initializeEditDialog(EDIT_DIALOG)(handleValidationPatternDropDown,handleValidationFormat,handleDisplayPatternDropDown,handleDisplayFormat,handleDisplayValueExpression);
// registered first so it always runs even if a later handler in this list throws (Utils.initializeEditDialog
// has no per-handler error isolation - it forEach()s this list, and one throw aborts everything after it)
Utils.initializeEditDialog(EDIT_DIALOG)(handleShowCharacterCountVisibility,handleValidationPatternDropDown,handleValidationFormat,handleDisplayPatternDropDown,handleDisplayFormat,handleDisplayValueExpression);

})(jQuery);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you please share the canvas theme code changes for the same
repo : https://github.com/adobe/aem-forms-theme-canvas

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,7 @@ textarea.cmp-adaptiveform-textinput__widget{
.cmp-adaptiveform-textinput__questionmark {

}

.cmp-adaptiveform-textinput__charcount {

}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
description: `.${TextInput.bemBlock}__longdescription`,
qm: `.${TextInput.bemBlock}__questionmark`,
errorDiv: `.${TextInput.bemBlock}__errormessage`,
tooltipDiv: `.${TextInput.bemBlock}__shortdescription`
tooltipDiv: `.${TextInput.bemBlock}__shortdescription`,
charCountCurrent: `.${TextInput.bemBlock}__charcount-current`
};

constructor(params) {
Expand Down Expand Up @@ -65,6 +66,23 @@
return this.element.querySelector(TextInput.selectors.qm);
}

/**
* Refreshes the character count span from the widget's current value. Called both on direct
* user input and whenever the model pushes a new value to the widget (updateValue), so the
* count stays in sync with changes coming from rules, prefill, a default value, or reset/clear -
* none of which fire an 'input' event on the widget.
*/
updateCharCount() {
if (this.charCountCurrent) {
this.charCountCurrent.textContent = this.widget.value ? this.widget.value.length : 0;
}
}

updateValue(value) {
super.updateValue(value);
this.updateCharCount();
}

setModel(model) {
super.setModel(model);
if (this.widget.value !== '') {
Expand Down Expand Up @@ -95,6 +113,11 @@
}
});
}
this.charCountCurrent = this.element.querySelector(TextInput.selectors.charCountCurrent);
if (this.charCountCurrent) {
this.updateCharCount();
this.widget.addEventListener('input', () => this.updateCharCount());
}
}
}

Expand Down
Loading
Loading