AEM React Component Development Complete Guide
A complete guide for building authorable React components in AEM, from dialog creation to Sling Model export and React mapping.
Content Objective
- Understand the AEM SPA React component flow
- Create a dialog for author input
- Write a Sling Model with JSON exporter
- Map the AEM component in the React app
- Deploy and validate the component in AEM
Introduction
Adobe Experience Manager (AEM) supports React components through the SPA Editor. This means you can build authorable React components that content editors configure in AEM while React renders the final experience.
The core idea is:
- AEM holds the component structure and authored content
- Sling Models expose the data as JSON
- React maps the JSON
:typeto a component and renders it
Step 1: Create a Dialog
Start by creating the AEM dialog for your Text component. This is the author-facing form that captures content.
Create a new component folder under:
/apps/your-project/components/text
Add a cq:dialog node and define the dialog fields.
Example dialog XML:
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0"
xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
xmlns:granite="http://www.adobe.com/jcr/granite/1.0"
xmlns:cq="http://www.day.com/jcr/cq/1.0"
xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
jcr:primaryType="nt:unstructured"
jcr:title="Text"
sling:resourceType="cq/gui/components/authoring/dialog"
extraClientlibs="[richtext.clientlibs,gpoc.rte.dialogevent]"
helpPath="https://www.adobe.com/go/aem_cmp_text_v2"
trackingFeature="core-components:text:v2">
<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"
maximized="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<properties jcr:primaryType="nt:unstructured"
jcr:title="Properties"
sling:resourceType="granite/ui/components/coral/foundation/container"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<columns jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<column granite:class="cq-RichText-FixedColumn-column"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<text jcr:primaryType="nt:unstructured"
sling:resourceType="cq/gui/components/authoring/dialog/richtext"
fieldDescription="Please enter text"
name="./text"
useFixedInlineToolbar="{Boolean}true">
<uiSettings jcr:primaryType="nt:unstructured">
<cui jcr:primaryType="nt:unstructured">
<inline jcr:primaryType="nt:unstructured"
toolbar="[format#bold,format#italic,format#underline,#justify,#lists,#styles,links#modifylink,links#unlink,#paraformat,findreplace#find,findreplace#replace,spellcheck#checktext,misctools#specialchars,links#anchor,subsuperscript#subscript,subsuperscript#superscript,undo#redo,undo#undo,edit#cut,edit#copy,edit#paste,edit#paste-default,edit#paste-plaintext,edit#paste-wordhtml,misctools#sourceedit,eaem-aem-fonts#applyFont]">
<popovers jcr:primaryType="nt:unstructured">
<justify jcr:primaryType="nt:unstructured"
items="[justify#justifyleft,justify#justifycenter,justify#justifyright]"
ref="justify"/>
<lists jcr:primaryType="nt:unstructured"
items="[lists#unordered,lists#ordered,lists#outdent,lists#indent]"
ref="lists"/>
<paraformat jcr:primaryType="nt:unstructured"
items="paraformat:getFormats:paraformat-pulldown"
ref="paraformat"/>
<styles jcr:primaryType="nt:unstructured"
items="styles:getStyles:styles-pulldown"
ref="styles"/>
</popovers>
</inline>
</cui>
</uiSettings>
</text>
<shortEnabled granite:class="cq-dialog-checkbox-showhide"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/checkbox"
name="./shortEnabled"
text="Shortening enabled?"
uncheckedValue="{Boolean}false"
value="{Boolean}true">
<granite:data jcr:primaryType="nt:unstructured"
cq-dialog-checkbox-showhide-target=".togglefield"/>
</shortEnabled>
<container granite:class="hide togglefield"
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<items jcr:primaryType="nt:unstructured">
<numberOfLines jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/numberfield"
fieldDescription="Number of lines to be presented when collapsed."
fieldLabel="Number of lines"
max="50"
min="5"
name="./numberOfLines"
required="{Boolean}false"/>
<moreLink jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldDescription="Read more link text"
fieldLabel="Read More Text"
maxLength="{Long}50"
name="./moreLink"
required="{Boolean}false"/>
<lessLink jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldDescription="Read less link text"
fieldLabel="Read Less Text"
maxLength="{Long}50"
name="./lessLink"
required="{Boolean}false"/>
</items>
<granite:data jcr:primaryType="nt:unstructured"
showhidetargetvalue="true"/>
</container>
<id jcr:primaryType="nt:unstructured"
sling:hideResource="{Boolean}true"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
name="./id"/>
</items>
</column>
</items>
</columns>
</items>
</properties>
</items>
</tabs>
</items>
</content>
</jcr:root>
This dialog allows authors to enter text, enable shortening, and configure read-more options.
Step 2: Write the Sling Model
The Sling Model exports authored values to JSON for React.
Create a Java model under your core bundle, for example:
/apps/your-project/core/model/text
Example interface:
@ConsumerType
public interface TextModel extends Text {
String getShortEnabled();
String getNumberOfLines();
String getMoreLink();
String getLessLink();
String getClassName();
}
Example implementation:
@Model(
adaptables = { SlingHttpServletRequest.class },
adapters = { TextModel.class, ComponentExporter.class },
resourceType = { TextModelImpl.RESOURCE_TYPE },
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME,
extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class TextModelImpl implements TextModel {
public static final String RESOURCE_TYPE = "yourproject/components/text/v1/text";
@Self
@Via(type = ResourceSuperType.class)
@Delegate(types = Text.class)
private Text text;
@ValueMapValue
private String shortEnabled;
@ValueMapValue
private String numberOfLines;
@ValueMapValue
private String moreLink;
@ValueMapValue
private String lessLink;
@Override
public String getShortEnabled() {
return shortEnabled;
}
@Override
public String getNumberOfLines() {
return numberOfLines;
}
@Override
public String getMoreLink() {
return moreLink;
}
@Override
public String getLessLink() {
return lessLink;
}
@Override
public String getExportedType() {
return RESOURCE_TYPE;
}
}
Important: @Exporter and getExportedType()
When using AEM SPA React components:
@Exporterconverts the Sling Model into JSON.extensions = ExporterConstants.SLING_MODEL_EXTENSIONcreates.model.jsonoutput.getExportedType()sets the JSON:typevalue.
React uses that :type to decide which component should render the data.
Step 3: Generate the JSON
After deploying the Sling Model, open the page model JSON to verify the output:
http://localhost:4502/content/your-site/page.model.json
Look for your Text component’s JSON block. It should include:
- authored values such as
text,shortEnabled,numberOfLines,moreLink,lessLink - the exported
:typefield
Example JSON snippet:
":items": {
"text_367608878": {
"text": "<p>This is text component</p>\r\n",
"shortEnabled": "true",
"numberOfLines": "6",
"moreLink": "read more",
"lessLink": "read less",
":type": "yourproject/components/text/v1/text"
}
},
":itemsOrder": ["text_367608878"],
":type": "wcm/foundation/components/responsivegrid"
Step 4: Map the Component in React
Next, connect the AEM component type to your React component in ui.frontend.
Create the React component, for example:
ui.frontend/apps/global/src/components/atoms/Text/Text.tsx
Example React code:
import React, { useState, useEffect } from 'react';
export const TextEditConfig = {
emptyLabel: 'Text',
isEmpty: (props: any) => !props?.text?.trim(),
};
interface TextProps {
text?: string;
shortEnabled?: string;
numberOfLines?: string;
moreLink?: string;
lessLink?: string;
}
const Text: React.FC<TextProps> = ({ text, shortEnabled, numberOfLines, moreLink, lessLink }) => {
const [isMoreVisible, setIsMoreVisible] = useState(true);
useEffect(() => {
if (shortEnabled === 'true') {
document.querySelectorAll('p').forEach((paraTag) => {
const el = paraTag as HTMLElement;
el.style.overflow = 'hidden';
el.style.display = '-webkit-box';
el.style.webkitBoxOrient = 'vertical';
el.style.webkitLineClamp = numberOfLines || '';
});
}
}, [numberOfLines, shortEnabled]);
const toggleReadMore = () => {
document.querySelectorAll('p').forEach((paraTag) => {
const el = paraTag as HTMLElement;
if (isMoreVisible) {
el.style.overflow = '';
el.style.display = '';
el.style.webkitBoxOrient = '';
el.style.webkitLineClamp = '';
} else {
el.style.overflow = 'hidden';
el.style.display = '-webkit-box';
el.style.webkitBoxOrient = 'vertical';
el.style.webkitLineClamp = numberOfLines || '';
}
});
setIsMoreVisible(!isMoreVisible);
};
if (!text) return null;
return (
<section className="cmp-text">
<div className="a-short" dangerouslySetInnerHTML={{ __html: text }} />
{shortEnabled === 'true' && (
<button type="button" onClick={toggleReadMore}>
{isMoreVisible ? moreLink : lessLink}
</button>
)}
</section>
);
};
export default Text;
Then map the React component to the AEM resource type:
import Text, { TextEditConfig } from './atoms/Text/Text';
import { MapTo } from '@adobe/aem-react-editable-components';
import { mappingComponent } from './mapping-component';
MapTo('yourproject/components/text/v1/text')(
mappingComponent(TextEditConfig, Text)
);
This binding tells React to render your Text component whenever AEM sends JSON with :type equal to yourproject/components/text/v1/text.
Step 5: Deploy and Author
Deploy the backend and frontend code so AEM can recognize the new component.
If you use local AEM packages:
mvn clean install -PautoInstallSinglePackage
If your frontend app is separate, build it too:
npm run build
Then author the component in AEM and confirm it renders in the SPA Editor.
Summary
Building an AEM React component requires three connected pieces:
- a dialog for author input
- a Sling Model exporter for JSON
- a React component mapping for
:type
When these are wired together, your React component becomes authorable inside AEM and renders with live content from the CMS.
If you want, I can also create a companion guide showing the exact folder structure for ui.apps, core, and ui.frontend in an AEM SPA project.