Close Menu
    DevStackTipsDevStackTips
    • Home
    • News & Updates
      1. Tech & Work
      2. View All

      Sunshine And March Vibes (2025 Wallpapers Edition)

      May 16, 2025

      The Case For Minimal WordPress Setups: A Contrarian View On Theme Frameworks

      May 16, 2025

      How To Fix Largest Contentful Paint Issues With Subpart Analysis

      May 16, 2025

      How To Prevent WordPress SQL Injection Attacks

      May 16, 2025

      Microsoft has closed its “Experience Center” store in Sydney, Australia — as it ramps up a continued digital growth campaign

      May 16, 2025

      Bing Search APIs to be “decommissioned completely” as Microsoft urges developers to use its Azure agentic AI alternative

      May 16, 2025

      Microsoft might kill the Surface Laptop Studio as production is quietly halted

      May 16, 2025

      Minecraft licensing robbed us of this controversial NFL schedule release video

      May 16, 2025
    • Development
      1. Algorithms & Data Structures
      2. Artificial Intelligence
      3. Back-End Development
      4. Databases
      5. Front-End Development
      6. Libraries & Frameworks
      7. Machine Learning
      8. Security
      9. Software Engineering
      10. Tools & IDEs
      11. Web Design
      12. Web Development
      13. Web Security
      14. Programming Languages
        • PHP
        • JavaScript
      Featured

      The power of generators

      May 16, 2025
      Recent

      The power of generators

      May 16, 2025

      Simplify Factory Associations with Laravel’s UseFactory Attribute

      May 16, 2025

      This Week in Laravel: React Native, PhpStorm Junie, and more

      May 16, 2025
    • Operating Systems
      1. Windows
      2. Linux
      3. macOS
      Featured

      Microsoft has closed its “Experience Center” store in Sydney, Australia — as it ramps up a continued digital growth campaign

      May 16, 2025
      Recent

      Microsoft has closed its “Experience Center” store in Sydney, Australia — as it ramps up a continued digital growth campaign

      May 16, 2025

      Bing Search APIs to be “decommissioned completely” as Microsoft urges developers to use its Azure agentic AI alternative

      May 16, 2025

      Microsoft might kill the Surface Laptop Studio as production is quietly halted

      May 16, 2025
    • Learning Resources
      • Books
      • Cheatsheets
      • Tutorials & Guides
    Home»Development»Composite Components in AEM SPA (React)

    Composite Components in AEM SPA (React)

    August 20, 2024

    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 

    Facebook Twitter Reddit Email Copy Link
    Previous ArticleLaravel SEO made easy with the Honeystone package
    Next Article Empowering Women in Technology Panel Recap – Breaking Barriers and Building Futures

    Related Posts

    Machine Learning

    Salesforce AI Releases BLIP3-o: A Fully Open-Source Unified Multimodal Model Built with CLIP Embeddings and Flow Matching for Image Understanding and Generation

    May 16, 2025
    Security

    Nmap 7.96 Launches with Lightning-Fast DNS and 612 Scripts

    May 16, 2025
    Leave A Reply Cancel Reply

    Continue Reading

    Rilasciata Nobara 42: una nuova versione “rolling release”

    Linux

    How to Turn a Portion of a Video into an Animated GIF

    Development

    CVE-2025-4154 – PHPGurukul Pre-School Enrollment System SQL Injection Vulnerability

    Common Vulnerabilities and Exposures (CVEs)

    nunomaduro/laravel-optimize-database

    Development

    Highlights

    Databases

    CTF Life Leverages MongoDB Atlas to Deliver Customer-Centric Service

    August 20, 2024

    Hong Kong-based Chow Tai Fook Life Insurance Company Limited (CTF Life) is proud of its…

    5 ways to avoid spyware disguised as legit apps – before it’s too late

    April 9, 2025

    Intel and Google unveil new AI chips to compete with NVIDIA

    April 10, 2024

    Flux AI – Free Online AI Generation Tool for German

    April 17, 2025
    © DevStackTips 2025. All rights reserved.
    • Contact
    • Privacy Policy

    Type above and press Enter to search. Press Esc to cancel.