About Composite Components
Composite components are combinations of multiple components that reside or can be accessed within a single parent/container component. The main goal of developing components in such a way is to make life easier for authors by being able to drag and drop only one component rather than multiple. It also helps in building reusable modular components that can function independently or as a part of other composite components. In some cases, the parent/container component has its own dialog while in others it does not. The authors can individually edit each of the components as they would if they were added to the page as a standalone component.Â
Composite Components in SPA Versus Non-SPA
Developing composite components in a non-SPA/HTL world is pretty straightforward. All we need to do is add the resource path of the child component using data-sly-resource inside the HTL of the parent component. We’ll touch more on this below.Â
To achieve the same on AEM SPA architecture it involves a few more steps. This is because only the JSON data of the authored components is exposed via the Sling model exporter while the React component does the heavy lifting of mapping to them and reading the data. If the JSON does not have a nested structure (child components listed under the parent) the React component does not know that we are trying to build a composite component. Once we have the nested structure available, we can iterate and initialize each as a standalone React component. Â
More details are mentioned below in the implementation section.Â
Implementing in Non-SPAÂ
The HTL/non-SPA way of building this component can be achieved by the following steps. Â
If the parent/container component does not have a dialog or design dialog, we need to create a cq:template node of type nt:unstructured and under cq:template node., mMake a node of type nt:unstructured with sling:resourceType having the value of the path to the component included. Â
Repeat the same for all the child components. In the HTL of the parent/container component add the tag below to simply include the component at render time:Â
<sly data-sly-resource=â€${‘<component name>’ @ resourceType=’<resource path of the component>’ }â€/>Â
Below is an example of a title and image (composite) component built using a core title and image. The parent/container component does not have a dialog, so we need to create a cq:template as mentioned above. The HTL will need to include the image and title components by using data-sly-resource.Â
 <sly data-sly-resource=â€${‘title’ @ resourceType=’weretail/components/content/title’ }â€/>Â
 <sly data-sly-resource=â€${‘image’ @ resourceType=’weretail/components/content/image}â€/>Â
Implementing in AEM SPA Editor
When implementing this in AEM SPA Editor (React), the process is very different because the component is completely rendered as a part of the SPA application. As mentioned above, in AEM SPA the authored data is still stored in the JCR but exposed as JSON which gets mapped to its React counterpart rather than reading it in traditional HTL. Â
We’ll showcase how we can build a header component in AEM SPA (React) by leveraging a mix of core and custom components. The parent component which is the header in this case will be updated to export the data in a nested (JSON) structure for all the components authored as its children. This will help the React component read the data as one element having child nodes and then iterate over them. After this, we need to include the object returned by each node which will then initialize the child component. The child components are:Â
Logo (Core Image)
Primary Navigation (Core Navigation)
Secondary Navigation
Search
My Account
This implementation requires us to build:
Proxies of Core Image & Core Navigation
Search & My Account Components
An ExportChildComponents sling model
A parent/container header component
1. Proxies of Core Image & Core Navigation
Create a proxy of the core image and update the title as a logo.
Create a proxy of the core navigation component.
For the sake of this demo, the proxies do not have any customizations.
2. Search & My Account Components
Create a standalone search component.
It can be reused in the header as well as dragged and dropped anywhere.
Create a My Account component with fields to configure account management.
3. An ExportChildComponents Sling model
The export Child Components Sling model implements the Core Container Sling model.
import com.adobe.cq.export.json.ComponentExporter;
import com.drew.lang.annotations.NotNull;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Exporter;
import com.adobe.cq.export.json.ExporterConstants;
import com.adobe.cq.wcm.core.components.models.Container;
import java.util.Map;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.HashMap;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.injectorspecific.*;
import org.apache.sling.models.factory.ModelFactory;
import com.adobe.cq.export.json.SlingModelFilter;
//Sling Model annotation
@Model(adaptables = SlingHttpServletRequest.class,
adapters = { ExportChildComponents.class,ComponentExporter.class },
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter( // Exporter annotation that serializes the model as JSON
name = ExporterConstants.SLING_MODEL_EXPORTER_NAME,
extensions = ExporterConstants.SLING_MODEL_EXTENSION,
selector = ExporterConstants.SLING_MODEL_SELECTOR)
public class ExportChildComponents implements Container{
private Map<String, ? extends ComponentExporter> childrenModels;
private String[] exportedItemsOrder;
@ScriptVariable
private Resource resource;
@Self
private SlingHttpServletRequest request;
@OSGiService
private SlingModelFilter slingModelFilter;
@OSGiService
private ModelFactory modelFactory;
@NotNull
@Override
public Map<String, ? extends ComponentExporter> getExportedItems() {
if (childrenModels == null) {
childrenModels = getChildrenModels(request, ComponentExporter.class);
}
Map<String, ComponentExporter> exportChildrenModels = new HashMap<String, ComponentExporter>();
exportChildrenModels.putAll(childrenModels);
return exportChildrenModels;
}
@NotNull
@Override
public String[] getExportedItemsOrder() {
if (exportedItemsOrder == null) {
Map<String, ? extends ComponentExporter> models = getExportedItems();
if (!models.isEmpty()) {
exportedItemsOrder = models.keySet().toArray(ArrayUtils.EMPTY_STRING_ARRAY);
} else {
exportedItemsOrder = ArrayUtils.EMPTY_STRING_ARRAY;
}
}
return Arrays.copyOf(exportedItemsOrder,exportedItemsOrder.length);
}
private Map<String, T> getChildrenModels(@NotNull SlingHttpServletRequest request, @NotNull Class
modelClass) {
Map<String, T> models = new LinkedHashMap<>();
for (Resource child : slingModelFilter.filterChildResources(resource.getChildren())) {
T model = modelFactory.getModelFromWrappedRequest(request, child, modelClass);
if (model != null) {
models.put(child.getName(), model);
}
}
return models;
}
}
…
It helps to iterate over child components saved under the parent node.
Then export all of them as a single JSON output via their Sling model exporter.
import com.adobe.cq.export.json.ComponentExporter;
{
“:items”: {
“root”: {
“columnClassNames”: {
“header”: “aem-GridColumn aem-GridColumn–default–12”
},
“gridClassNames”: “aem-Grid aem-Grid–12 aem-Grid–default–12”,
“columnCount”: 12,
“:items”: {
“header”: {
“id”: “header-346949959”,
“:itemsOrder”: [
“logo”,
“navigation”,
“search”,
“account”
],
“:type”: “perficient/components/header”,
“:items”: {
“logo”: {
“id”: “image-ee58d4cd48”,
“linkType”: “”,
“tagLinkType”: “”,
“displayPopupTitle”: true,
“decorative”: false,
“srcUriTemplate”: “/content/experience-fragments/perficient/us/en/site/header/master/_jcr_content/root/header/logo.coreimg{.width}.png/1705295980301/perficient-logo-horizontal.png”,
“lazyEnabled”: true,
“title”: “Logo”,
“uuid”: “799c7831-264c-42c0-be02-1d0cbb747bd2”,
“areas”: [],
“alt”: “NA”,
“src”: “/content/experience-fragments/perficient/us/en/site/header/master/_jcr_content/root/header/logo.coreimg.png/1705295980301/perficient-logo-horizontal.png”,
“widths”: [],
“:type”: “perficient/components/logo”
},
“navigation”: {
“id”: “navigation-1727181688”,
“items”: [
{
“id”: “navigation-cd54619f8f-item-12976048d7”,
“path”: “/content/react-spa/us/en/cases”,
“level”: 0,
“active”: false,
“current”: false,
“title”: “Link 1”,
“url”: “/content/react-spa/us/en/cases.html”,
“lastModified”: 1705203487624,
“:type”: “perficient/components/structure/page”
},
{
“id”: “navigation-cd54619f8f-item-438eb66728”,
“path”: “/content/react-spa/us/en/my-onboarding-cases”,
“level”: 0,
“active”: false,
“current”: false,
“title”: “Link 2”,
“url”: “/content/react-spa/us/en/my-onboarding-cases.html”,
“lastModified”: 1705203496764,
“:type”: “perficient/components/structure/page”
},
{
“id”: “navigation-cd54619f8f-item-e8d85a3188”,
“path”: “/content/react-spa/us/en/learning-resources”,
“level”: 0,
“active”: false,
“current”: false,
“title”: “Link 3”,
“url”: “/content/react-spa/us/en/learning-resources.html”,
“lastModified”: 1705203510651,
“:type”: “perficient/components/structure/page”
},
{
“id”: “navigation-cd54619f8f-item-d1553683aa”,
“path”: “/content/react-spa/us/en/Reports”,
“children”: [],
“level”: 0,
“active”: false,
“current”: false,
“title”: “Link 5”,
“url”: “/content/react-spa/us/en/Reports.html”,
“lastModified”: 1705203601382,
“:type”: “perficient/components/structure/page”
}
],
“:type”: “perficient/components/navigation”
},
“search”: {
“id”: “search-1116154524”,
“searchFieldType”: “navbar”,
“:type”: “perficient/components/search”
},
“account”: {
“id”: “account-1390371799”,
“accountConfigurationFields”: [],
“logoutLinkType”: “”,
“helpText”: “Account”,
“logoutText”: “Sign Out”,
“:type”: “perficient/components/account”
}
}
}
},
“:itemsOrder”: [
“header”
],
“:type”: “perficient/components/core/container”
}
}
}
…
4. Parent/Container Header Component
Create a Header Component which is an extension of the core container component.
Include the Search Component dialog as an additional tab in the header component dialog.
Include Accounts tab which is a custom tab with fields for user account management.
Create cq:template having child nodes pointing to their corresponding resource paths.
This will help export the authored data using the Sling model exporter.
Create HeaderModelImpl that extends ExportChildComponents (created in step 3). This exports the JSON data of not only the parent but also its children (logo, navigation & search).
import com.adobe.cq.export.json.ComponentExporter;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import com.adobe.cq.export.json.ComponentExporter;
import com.adobe.cq.export.json.ExporterConstants;
import com.chc.ecchub.core.models.ExportChildComponents;
import com.chc.ecchub.core.models.nextgen.header.HeaderModel;
@Model(adaptables = SlingHttpServletRequest.class,
adapters = { HeaderModel.class, ComponentExporter.class},
resourceType = HeaderModelImpl.RESOURCE_TYPE,
defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter( // Exporter annotation that serializes the model as JSON
name = ExporterConstants.SLING_MODEL_EXPORTER_NAME,
extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class HeaderModelImpl extends ExportChildComponents implements HeaderModel{
static final String RESOURCE_TYPE = “perficient/components/header”;
@Override
public String getExportedType() {
return RESOURCE_TYPE;
}
}
…
Create React equivalent for a header that extends the core container and maps to the header component created above.
import { Container, MapTo, withComponentMappingContext } from ‘@adobe/aem-react-editable-components’;
import { useState } from “react”;
import { NavigationComp, NavigationConfig } from “./Navigation”;
import { LogoComp, LogoConfig } from “../../components/Logo”;
import { SearchComp } from “../../components/Search”;
import { SecondaryNavigation } from “./SecondaryNavigation”;
export const HeaderEditConfig = {
emptyLabel: ‘Header’
};
export default class Header extends Container {
render() {
return (
<>
<HeaderComp headerData={super.childComponents} headerProps={this.props} hasAuthorization={hasAuthorization}/>
</>
)
}
}
const selectedComponent = (props, selectedCqType) => {
let selectedComponentProps;
props?.headerData?.forEach((data) => {
if (data.props?.cqType === selectedCqType) {
selectedComponentProps = data;
}
});
return selectedComponentProps;
}
const HeaderComp = (props) => {
const [searchModalOverlay, setSearchModalOverlay] = useState(false);
const searchProps = selectedComponent(props, ‘perficient/components/search’);
const logoProps = selectedComponent(props, ‘perficient/components/logo’);
const navProps = selectedComponent(props, ‘perficient/components/navigation’);
return (
<>
<header data-analytics-component=”Header”>
<SearchComp
searchProps={searchProps?.props}
onToggleSearch={setSearchModalOverlay}
searchModal={searchModalOverlay}
/>
<div className=”header__container”>
<div className=”header__container-wrapper”>
<div className=”company-info”>
{logoProps}
</div>
<div className=”desktop-only”>
<nav className=”navigation”>
{navProps}
</nav>
<nav className=”header__secondary-nav”>
<SecondaryNavigation
notificationRead={notificationRead}
isNotificationRead={isNotificationRead}
snProps={props}
onToggleSearch={setSearchModalOverlay}
/>
</nav>
</div>
</div>
</div>
</header>
</>
)
}
MapTo(‘ECCHub/components/nextgen/header/navigation’)(NavigationComp, NavigationConfig);
MapTo(‘ECCHub/components/nextgen/header/logo’)(LogoComp, LogoConfig);
MapTo(‘ECCHub/components/nextgen/header/header’)(withComponentMappingContext(Header), HeaderEditConfig);
Â
Additionally, add a map for the logo and navigation since they are exported as child components. Since the header extends the core container the child component properties are available via super.childcomponents
import { useEffect } from ‘react’;
import { MapTo } from ‘@adobe/aem-react-editable-components’;
import * as constants from ‘../../../../utils/constants’;
import { toggleHeaderForUsers } from ‘../../../../utils/genericUtil’;
import { useDispatch } from “react-redux”;
import { BreadcrumbActions } from ‘../../../../store/Breadcrumb/BreadcrumbSlice’;
export const NavigationConfig = {
emptyLabel: ‘Navigation’,
isEmpty: function() {
return true;
}
};
export const NavigationComp = (navProps) => {
return (
<ul className=”top-menu” data-analytics-component=”Top Navigation”>
{/* Maps through authored navigation links and renders the link title and URL. */}
{navProps?.items.map((item, index) =>
<li key={index}><a href={item?.url} title={item?.title}>{item?.title}</a></li>
)}
</ul>
);
}
MapTo(“perficient/components/navigation”)(NavigationComp, NavigationConfig);
Â
Â
Worth the Investment
Composite components are a useful feature to leverage as they immensely help the authors while retaining the modularity and reusability of the individual components. Although creating the experience on the SPA framework compared to the HTL approach requires additional effort it is still worth the investment as it is a one-time setup and can be used for any components any number of times as it is quite generic.
Â
Source: Read MoreÂ