Advanced Edition with Detailed Questions, Answers & Code Examples
Enhanced by: Ayush Kumar (Adobe Certified Full Stack & AEM Developer)
AEM Architecture consists of:
CRX (Content Repository Extreme):
Oak:
A complete AEM component consists of:
/apps/myproject/components/content/hero/
├── .content.xml # Component definition
├── cq:dialog/ # Touch UI dialog
├── hero.html # HTL rendering script
├── hero.js # Client-side JavaScript
├── hero.css # Component styles
└── HeroModel.java # Sling Model (if needed)
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0"
xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
jcr:primaryType="cq:Component"
jcr:title="Hero Component"
jcr:description="A responsive hero banner component"
componentGroup="MyProject - Content"
sling:resourceSuperType="core/wcm/components/commons/v1/datalayer"/>
<?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 Configuration"
sling:resourceType="cq/gui/components/authoring/dialog"
extraClientlibs="[myproject.author]"
helpPath="en/docs/myproject/hero">
<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">
<content jcr:primaryType="nt:unstructured"
jcr:title="Content"
sling:resourceType="granite/ui/components/coral/foundation/container"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<title jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Title"
name="./title"
required="{Boolean}true"/>
<subtitle jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Subtitle"
name="./subtitle"/>
<description jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textarea"
fieldLabel="Description"
name="./description"
rows="5"/>
<image jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"
fieldLabel="Background Image"
name="./image"
rootPath="/content/dam/myproject"/>
<ctaText jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="CTA Text"
name="./ctaText"/>
<ctaLink jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"
fieldLabel="CTA Link"
name="./ctaLink"
rootPath="/content/myproject"/>
</items>
</content>
<styling jcr:primaryType="nt:unstructured"
jcr:title="Styling"
sling:resourceType="granite/ui/components/coral/foundation/container"
margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<theme jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/select"
fieldLabel="Theme"
name="./theme">
<items jcr:primaryType="nt:unstructured">
<light jcr:primaryType="nt:unstructured"
text="Light"
value="light"/>
<dark jcr:primaryType="nt:unstructured"
text="Dark"
value="dark"/>
</items>
</theme>
</items>
</styling>
</items>
</tabs>
</items>
</content>
</jcr:root>
<div data-sly-use.hero="com.myproject.core.models.HeroModel"
data-sly-test="${hero.title}"
class="hero hero--${hero.theme @ context='styleToken'}"
style="background-image: url('${hero.backgroundImage}');">
<div class="hero__overlay"></div>
<div class="hero__content">
<h1 class="hero__title">${hero.title}</h1>
<h2 data-sly-test="${hero.subtitle}"
class="hero__subtitle">${hero.subtitle}</h2>
<p data-sly-test="${hero.description}"
class="hero__description">${hero.description @ context='html'}</p>
<a data-sly-test="${hero.ctaText && hero.ctaLink}"
href="${hero.ctaLink @ extension='html'}"
class="hero__cta btn btn--primary">${hero.ctaText}</a>
</div>
</div>
Core Components are pre-built, well-tested components that follow best practices:
<!-- Custom Text Component -->
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0"
xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
jcr:primaryType="cq:Component"
jcr:title="Enhanced Text"
jcr:description="Extended text component with animation"
componentGroup="MyProject - Enhanced"
sling:resourceSuperType="core/wcm/components/text/v2/text"/>
package com.myproject.core.models;
import com.adobe.cq.export.json.ComponentExporter;
import com.adobe.cq.export.json.ExporterConstants;
import com.day.cq.commons.inherit.HierarchyNodeInheritanceValueMap;
import com.day.cq.commons.inherit.InheritanceValueMap;
import com.day.cq.wcm.api.Page;
import com.day.cq.wcm.api.PageManager;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.models.annotations.*;
import org.apache.sling.models.annotations.injectorspecific.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.PostConstruct;
import javax.inject.Inject;
import javax.inject.Named;
import java.util.Calendar;
import java.util.Optional;
@Model(
adaptables = {SlingHttpServletRequest.class, Resource.class},
adapters = {HeroModel.class, ComponentExporter.class},
resourceType = HeroModel.RESOURCE_TYPE,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.JSON)
public class HeroModel implements ComponentExporter {
static final String RESOURCE_TYPE = "myproject/components/content/hero";
private static final Logger LOG = LoggerFactory.getLogger(HeroModel.class);
// Basic property injection from current resource
@ValueMapValue
@Default(values = "Welcome")
private String title;
@ValueMapValue
private String subtitle;
@ValueMapValue
private String description;
@ValueMapValue
@Named("ctaText")
private String callToActionText;
@ValueMapValue
@Named("ctaLink")
private String callToActionLink;
@ValueMapValue
@Default(values = "light")
private String theme;
@ValueMapValue
private String image;
// Request injection
@SlingObject
private SlingHttpServletRequest request;
// Resource injection
@SlingObject
private Resource currentResource;
@SlingObject
private ResourceResolver resourceResolver;
// Service injection
@OSGiService
private PageManager pageManager;
// Custom injection with source
@Inject
@Source("script-bindings")
@Named("currentPage")
private Page currentPage;
// Child resource injection
@ChildResource
@Named("settings")
private Resource settingsResource;
// Request attribute injection
@RequestAttribute
@Named("analytics")
private String analyticsData;
// Computed properties
private String backgroundImage;
private boolean hasContent;
private InheritanceValueMap inheritedProperties;
@PostConstruct
protected void init() {
LOG.debug("Initializing HeroModel for resource: {}", currentResource.getPath());
// Initialize inheritance value map for property inheritance
inheritedProperties = new HierarchyNodeInheritanceValueMap(currentResource);
// Process background image
processBackgroundImage();
// Check if component has meaningful content
hasContent = StringUtils.isNotBlank(title) || StringUtils.isNotBlank(description);
LOG.debug("HeroModel initialized. Has content: {}", hasContent);
}
private void processBackgroundImage() {
if (StringUtils.isNotBlank(image)) {
// Generate responsive image URL with renditions
backgroundImage = image + ".transform/hero-large/image.jpg";
} else {
// Fallback to inherited property or default
String inheritedImage = inheritedProperties.getInherited("defaultHeroImage", String.class);
backgroundImage = Optional.ofNullable(inheritedImage)
.orElse("/content/dam/myproject/defaults/hero-placeholder.jpg");
}
}
// Getter methods with additional logic
public String getTitle() {
return title;
}
public String getSubtitle() {
return subtitle;
}
public String getDescription() {
return description;
}
public String getCallToActionText() {
return callToActionText;
}
public String getCallToActionLink() {
// Process internal links
if (StringUtils.isNotBlank(callToActionLink) && callToActionLink.startsWith("/content/")) {
Page linkedPage = pageManager.getPage(callToActionLink);
if (linkedPage != null) {
return linkedPage.getPath() + ".html";
}
}
return callToActionLink;
}
public String getTheme() {
return theme;
}
public String getBackgroundImage() {
return backgroundImage;
}
public boolean isHasContent() {
return hasContent;
}
// Additional computed properties
public String getPageTitle() {
return currentPage != null ? currentPage.getTitle() : "";
}
public String getLastModified() {
Calendar lastModified = currentResource.getValueMap().get("jcr:lastModified", Calendar.class);
return lastModified != null ? lastModified.getTime().toString() : "";
}
public boolean isAuthorMode() {
return request != null && "author".equals(request.getRequestPathInfo().getSelectors());
}
// JSON Export support
@Override
public String getExportedType() {
return RESOURCE_TYPE;
}
}
@Model - Defines the class as a Sling Model@ValueMapValue - Injects properties from resource's ValueMap@SlingObject - Injects Sling objects (request, resource, etc.)@OSGiService - Injects OSGi services@ChildResource - Injects child resources@PostConstruct - Method called after injection is complete@Default - Provides default values@Named - Maps to different property namespackage com.myproject.core.models;
import com.adobe.cq.wcm.api.PageManager;
import io.wcm.testing.mock.aem.junit5.AemContext;
import io.wcm.testing.mock.aem.junit5.AemContextExtension;
import org.apache.sling.api.resource.Resource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.when;
@ExtendWith({AemContextExtension.class, MockitoExtension.class})
class HeroModelTest {
private final AemContext context = new AemContext();
@Mock
private PageManager pageManager;
private HeroModel heroModel;
@BeforeEach
void setUp() {
// Register the PageManager service
context.registerService(PageManager.class, pageManager);
// Load test content
context.load().json("/com/myproject/core/models/HeroModelTest.json", "/content");
// Create current page
context.currentPage("/content/myproject/homepage");
// Get the hero resource
Resource heroResource = context.resourceResolver()
.getResource("/content/myproject/homepage/jcr:content/hero");
// Adapt to model
heroModel = heroResource.adaptTo(HeroModel.class);
}
@Test
void testBasicProperties() {
assertNotNull(heroModel);
assertEquals("Welcome to Our Site", heroModel.getTitle());
assertEquals("Your journey starts here", heroModel.getSubtitle());
assertEquals("light", heroModel.getTheme());
assertTrue(heroModel.isHasContent());
}
@Test
void testDefaultValues() {
Resource emptyResource = context.create().resource("/content/test/empty");
HeroModel emptyModel = emptyResource.adaptTo(HeroModel.class);
assertEquals("Welcome", emptyModel.getTitle());
assertEquals("light", emptyModel.getTheme());
}
@Test
void testCTALinkProcessing() {