AEM Content Fragments with GraphQL – Getting started with GraphQL

Problem Statement:

What is GraphQL?

How GraphQL can be used with Content Fragments?

Introduction:

What is GraphQL?

GraphQL is a query language for APIs and provides a complete and understandable description of the data in your API.

For example:

Let’s consider an external system with the following tables:

1 to 1 relationship between Company table with person table and 1 to 1 person table with awards table.

If I need to get all awards

You might be doing a call to

{domain}/api/awards

awards API flow

To get individual person and awards

{domain}/api/persons?personID{ID}&awards={ID}

person API flow

To get individual company and person and awards

{domain}/api/company?companyNam={NAME}personID={ID}&awards={ID}

Company API flow

But in GraphQL you can send the parameters like a query and get all the related content as well

GraphQL API flow

To use Graph QL you need to prepare schemas and based on the schema you can do filter the data.

For more information on GraphQL, you can be visiting the link

Benefits:

  • Avoiding iterative API requests as with REST,
  • Ensuring that delivery is limited to the specific requirements,
  • Allowing for bulk delivery of exactly what is needed for rendering as the response to a single API query.

How GraphQL can be used with Content Fragments?

GraphQL is a strongly typed API, which means that data must be clearly structured and organized by type.

The GraphQL specification provides a series of guidelines on how to create a robust API for interrogating data on a certain instance. To do this, a client needs to fetch the Schema, which contains all the types necessary for a query.

For Content Fragments, the GraphQL schemas (structure and types) are based on Enabled Content Fragment Models and their data types.

Content Fragments can be used as a basis for GraphQL for AEM queries as:

  • They enable you to design, create, curate and publish page-independent content.
  • The Content Fragment Models provide the required structure by means of defined data types.
  • The Fragment Reference, available when defining a model, can be used to define additional layers of structure.
Model References provided by Adobe

Content Fragments

  • Contain structured content.
  • They are based on a Content Fragment Model, which predefines the structure for the resulting fragment.

Content Fragment Models

  • Are used to generate the Schemas, once Enabled.
  • Provide the data types and fields required for GraphQL. They ensure that your application only requests what is possible, and receives what is expected.
  • The data type Fragment References can be used in your model to reference another Content Fragment, and so introduce additional levels of structure.

Fragment References

  • Is of particular interest in conjunction with GraphQL.
  • Is a specific data type that can be used when defining a Content Fragment Model.
  • References another fragment, dependent on a specific Content Fragment Model.
  • Allows you to retrieve structured data.
    • When defined as a multifeed, multiple sub-fragments can be referenced (retrieved) by the prime fragment.

JSON Preview

To help with designing and developing your Content Fragment Models, you can preview JSON output.

Install:

  1. AEM 6.5.11 (aem-service-pkg-6.5.11.zip)
  2. Graph QL OAK Index (cfm-graphql-index-def-1.0.0.zip)
  3. GraphiQL Developer tool (graphiql-0.0.6.zip)

For AEMacS you will get the content fragment with the latest update.

Go to configuration folder

  1. AEM tools section
  2. General selection in sidebar
  3. Configuration bowser

As shown below:

Configuration Folder

Create a configuration folder and select

  1. Content Fragment Models
  2. GraphQL Persistent Queries

As shown below:

Create a Conf folder with required checkboxes

Go to Assets Model:

  1. AEM tools section
  2. Assets selection in sidebar
  3. Content Fragments Model

As shown below:

Go to Assets CF

Select the folder and create the content fragments as shown below:

CF models

You can also install the package attached here

Go to the following URL to access the GraphiQL developer tool and run the following query:

Note: you can also get all the autosuggestions by using the ctrl+space shortcut

{
  cityByPath(_path: "/content/dam/sample-content-fragments/cities/berlin") {
    item {
      _path
      name
      country
      population
      categories
    }
  }
}
GraphiQL developer tool

Download Sample Package here

You can also find more queries and filters in the following link

AEM Content Fragment with Image support

Problem Statement:

How can I get support for Core Image component within content fragment component?

Why should we use Core Image component?

Requirement:

Get support for OOTB image component within content fragment component

Introduction:

The Image Component features adaptive image selection and responsive behaviour with lazy loading for the page visitor as well as easy image placement.

The Image Component comes with robust responsive features ready right out of the box. At the page template level, the design dialog can be used to define the default widths of the image asset. The Image Component will then automatically load the correct width to display depending on the browser window’s size. As the window is resized, the Image Component dynamically loads the correct image size on the fly. There is no need for component developers to worry about defining custom media queries since the Image Component is already optimized to load your content.

In addition, the Image Component supports lazy loading to defer loading of the actual image asset until it is visible in the browser, increasing the responsiveness of your pages.

Create Custom Image content fragment component as shown below and add the following conditions into the element.html file:

Image Contentfragment Component

In the above HTL we are trying to tell if the element name contains the “image” (eg: primaryimage) keyword then don’t print it instead of that call an image model and get synthetic image resource

Note: Make sure your content fragment element name (field name) contains image word

Create a Sling mode ImageContentFragment Interface as shown below:

package com.mysite.core.models;

import org.apache.sling.api.resource.Resource;
import org.osgi.annotation.versioning.ConsumerType;

@ConsumerType
public interface ImageContentFragment {
    /**
     * Getter for Image Synthetic Resource
     *
     * @return resource
     */
    default Resource getImageResource() {
        throw new UnsupportedOperationException();
    }
}

Create a Sling mode implementation ImageContentFragmentImpl class as shown below, this class is used to create a synthetic image resource for Adaptive Image Servlet to work:

package com.mysite.core.models.impl;

import java.util.HashMap;
import javax.annotation.PostConstruct;
import com.mysite.core.models.ImageContentFragment;
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.api.resource.ValueMap;
import org.apache.sling.api.wrappers.ValueMapDecorator;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy;
import org.apache.sling.models.annotations.injectorspecific.RequestAttribute;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import com.adobe.cq.wcm.core.components.models.contentfragment.ContentFragment;
import com.adobe.cq.wcm.core.components.models.contentfragment.DAMContentFragment.DAMContentElement;
import com.adobe.granite.ui.components.ds.ValueMapResource;
import com.day.cq.commons.DownloadResource;
import lombok.Getter;

@Model(adaptables = SlingHttpServletRequest.class, adapters = {ImageContentFragment.class}, resourceType = ImageContentFragmentImpl.RESOURCE_TYPE)
public class ImageContentFragmentImpl implements ImageContentFragment{

    public static final String RESOURCE_TYPE = "mysite/components/image";
    private static final String IMAGE = "/image";

    @SlingObject
    private ResourceResolver resourceResolver;

    @SlingObject
    protected Resource resource;

    @ValueMapValue(name = ContentFragment.PN_PATH, injectionStrategy = InjectionStrategy.OPTIONAL)
    private String fragmentPath;

    @RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
    private DAMContentElement imageElement;

    @ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL)
    private String fileReference;

    @Getter
    private Resource imageResource;

    @PostConstruct
    private void initModel() {
        if (StringUtils.isNotEmpty(fragmentPath) && null != imageElement && null != imageElement.getValue()) {
            createSyntheticResource(imageElement.getValue().toString(), resource.getPath() + IMAGE);
        }
    }

    private void createSyntheticResource(String imagePath, String path) {
        ValueMap properties = new ValueMapDecorator(new HashMap<>());
        properties.put(DownloadResource.PN_REFERENCE, imagePath);
        imageResource = new ValueMapResource(resourceResolver, path, ImageContentFragmentImpl.RESOURCE_TYPE, properties);
    }
}

Create an ImageDelegate Lombok based delegation class as shown below, for this example, I am using Image V3 component and this class is used to modify the image srcset method to add image path into the image URL:

package com.mysite.core.models.impl;

import com.adobe.cq.export.json.ExporterConstants;
import com.adobe.cq.wcm.core.components.models.Image;
import lombok.experimental.Delegate;
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.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Via;
import org.apache.sling.models.annotations.injectorspecific.Self;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.models.annotations.via.ResourceSuperType;
import org.apache.sling.servlets.post.SlingPostConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;

@Model(adaptables = SlingHttpServletRequest.class, adapters = {Image.class}, resourceType = ImageDelegate.RESOURCE_TYPE, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class ImageDelegate implements Image {
    private static final Logger LOGGER = LoggerFactory.getLogger(ImageDelegate.class);
    public static final String RESOURCE_TYPE = "mysite/components/content/image";
    public static final String CONTENT_DAM_PATH = "/content/dam";
    public static final String HTTPS = "https://";
    public static final String PN_DISPLAY_SIZES = "sizes";
    public static final String WIDTH = "{.width}";

    @SlingObject
    private ResourceResolver resourceResolver;

    @SlingObject
    protected Resource resource;

    @Self
    @Via(type = ResourceSuperType.class)
    @Delegate(excludes = DelegationExclusion.class)
    private Image image;

    @Override
    public String getSrc() {
        return prepareSuffix(image.getSrc());
    }

    @Override
    public String getSrcset() {
        int[] widthsArray = image.getWidths();
        String srcUritemplate = image.getSrcUriTemplate();
        String[] srcsetArray = new String[widthsArray.length];
        if (widthsArray.length > 0 && srcUritemplate != null) {
            String srcUriTemplateDecoded = "";
            try {
                srcUriTemplateDecoded = prepareSuffix(URLDecoder.decode(srcUritemplate, StandardCharsets.UTF_8.name()));
            } catch (UnsupportedEncodingException e) {
                LOGGER.error("Character Decoding failed for {}", resource.getPath());
            }
            if (srcUriTemplateDecoded.contains(WIDTH)) {
                for (int i = 0; i < widthsArray.length; i++) {
                    if (srcUriTemplateDecoded.contains("="+WIDTH)) {
                        srcsetArray[i] = srcUriTemplateDecoded.replace(WIDTH, String.format("%s", widthsArray[i])) + " " + widthsArray[i] + "w";
                    } else {
                        srcsetArray[i] = srcUriTemplateDecoded.replace(WIDTH, String.format(".%s", widthsArray[i])) + " " + widthsArray[i] + "w";
                    }
                }
                return StringUtils.join(srcsetArray, ',');
            }
        }
        return null;
    }

    private String prepareSuffix(String imageSrc) {
        if(StringUtils.isNotEmpty(imageSrc) && !StringUtils.containsIgnoreCase(imageSrc, CONTENT_DAM_PATH)) {
            int endIndex = imageSrc.lastIndexOf(SlingPostConstants.DEFAULT_CREATE_SUFFIX);
            String intermittenResult = imageSrc.substring(0, endIndex);
            endIndex = intermittenResult.lastIndexOf(SlingPostConstants.DEFAULT_CREATE_SUFFIX);
            return intermittenResult.substring(0, endIndex)+image.getFileReference();
        }
        return imageSrc;
    }

    private interface DelegationExclusion {
        String getSrc();
        String getSrcset();
    }
}

In the above code, we are trying to prepend the URL with an image path, because we are using a synthetic image component for the content fragment

For more information on Lombok based component delegation please click on the title to check:

AEM Core Component Delegation

Create a new Adaptive Image servlet and EnhancedRendition class, this servlet is used for displaying appropriate rendition of image component based on width selected based on browser width and pixel ratio:

public class AdaptiveImageServlet  extends SlingSafeMethodsServlet {
    @Override
    protected void doGet(@NotNull SlingHttpServletRequest request, @NotNull SlingHttpServletResponse response) throws IOException {
        try {
            RequestPathInfo requestPathInfo = request.getRequestPathInfo();
            List<String> selectorList = selectorToList(requestPathInfo.getSelectorString());
            String suffix = requestPathInfo.getSuffix();
            String imagePath = suffix;
            String imageName = StringUtils.isNotEmpty(suffix) ? FilenameUtils.getName(suffix) : "";
            Resource component = request.getResource();
            ImageComponent imageComponent = new ImageComponent(component, imagePath);

        } catch (IllegalArgumentException e) {
            LOGGER.error("Invalid image request {}", e.getMessage());
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
        }
    }
    private static class ImageComponent {
        Source source = Source.NONEXISTING;
        Resource imageResource;

        ImageComponent(@NotNull Resource component, String imagePath) {
            if (StringUtils.isNotEmpty(imagePath)) {
                imageResource = component.getResourceResolver().getResource(imagePath);
                source = Source.ASSET;
            }
        }
    }
}

Adaptive Image servlet is a modified version of the Core component Adaptive Image servlet because we are using synthetic image component and Enhanced Rendition class is a support class to get the best image rendition.

Create a simple content fragment Model as shown below:

Make sure the contentreference field name contains the image

Sample Content Fragment Model

Create a new content fragment under asset path (/content/dam/{project path}) as shown below:

Content Fragment

Create a sample page and add the content fragment component and select all the fields as a multifield as shown below:

Custom Content Fragment Component Authoing

As we can see we are able to fetch Core component Image component with Image widths configured into Image component policy (design dialog)

Content Fragment with OOTB Image

You can also get the actual working code from the below link:

https://github.com/kiransg89/ImageContentFragment

FAQ / Q&A using Content Fragments AEM

Problem Statement:

How can I get support for the Core Accordion component within the content fragment component?

Why should we use the Core Accordion component?

Requirement:

Get support for OOTB Accordion component within content fragment component

Introduction:

The Core Component Accordion component allows for the creation of a collection of components, composed as panels, and arranged in an accordion on a page, similar to the Tabs Component, but allows for expanding and collapsing of the panels.

  • The accordion’s properties can be defined in the configure dialog.
  • The order of the panels of the accordion can be defined in the configure dialog as well as the select panel popover.
  • Defaults for the Accordion Component when adding it to a page can be defined in the design dialog.

How to use Accordion for FAQ feature?

Manual authoring:

Drag and drop Accordion component, go to component dialog and click on add and select the component of your choice as shown below:

Accordion Dialog, insert panel

Once the component is selected add the question into the field

Author questions

Click on the panel selector icon and go to the individual item and make necessary changes

Panel Selection
Author answer

You can view it as published as shown below:

Published view of the Q&A

You can also rearrange the order of results from dialog

Dialog level reordering

Automated process:

Create a Sample model which can have a max of 10 Q&A’s as shown below:

Q&A content fragment

Create a custom Accordion component as shown below:

<div data-sly-use.accordion="com.adobe.cq.wcm.core.components.models.Accordion"
     data-sly-use.accordioncf="com.mysite.core.models.AccordionContentFragmentModel"
     data-panelcontainer="${wcmmode.edit && 'accordion'}"
     id="${accordion.id}"
     class="cmp-accordion"
     data-cmp-is="accordion"
     data-cmp-data-layer="${accordion.data.json}"
     data-cmp-single-expansion="${accordion.singleExpansion}"
     data-placeholder-text="${wcmmode.edit && 'Please drag Accordion item components here' @ i18n}">
    <h1>${accordioncf.faqTitle}</h1>
    <div data-sly-test="${accordion.items.size > 0}"
         data-sly-repeat.item="${accordion.items}"
         class="cmp-accordion__item"
         data-cmp-hook-accordion="item"
         data-cmp-data-layer="${item.data.json}"
         id="${item.id}"
         data-cmp-expanded="${item.name in accordion.expandedItems}">
        <h4 data-sly-element="${accordion.headingElement @ context='elementName'}"
            class="cmp-accordion__header">
            <button id="${item.id}-button"
                    class="cmp-accordion__button${item.name in accordion.expandedItems ? ' cmp-accordion__button--expanded' : ''}"
                    aria-controls="${item.id}-panel"
                    data-cmp-hook-accordion="button">
                <span class="cmp-accordion__title">${item.title}</span>
                <span class="cmp-accordion__icon"></span>
            </button>
        </h4>
        <div data-sly-resource="${item.name @ decorationTagName='div'}"
             data-cmp-hook-accordion="panel"
             id="${item.id}-panel"
             class="cmp-accordion__panel${item.name in accordion.expandedItems ? ' cmp-accordion__panel--expanded' : ' cmp-accordion__panel--hidden'}"
             role="region"
             aria-labelledby="${item.id}-button">
        </div>
    </div>
    <sly data-sly-resource="${resource.path @ resourceType='wcm/foundation/components/parsys/newpar', appendPath='/*', decorationTagName='div', cssClassName='new section aem-Grid-newComponent'}"
         data-sly-test="${(wcmmode.edit || wcmmode.preview) && accordion.items.size < 1}"></sly>
</div>

Create a Sling model interface called AccordionContentFragmentModel

package com.mysite.core.models;

import org.osgi.annotation.versioning.ConsumerType;

@ConsumerType
public interface AccordionContentFragmentModel {

    /**
     * Getter for checking editor template
     *
     * @return String content fragment FAQ Title
     */
    default String getFaqTitle() {
        throw new UnsupportedOperationException();
    }
}

Create a model implementation called as AccordionContentFragmentModelImpl

Make sure all the below code executes only on the author environment

We are checking for; how many content fragment fields are not empty and also we are checking for children’s under the current path.

If there is a difference, then we are going to delete the unwanted children’s and add the new children’s as shown below:

package com.mysite.core.models.impl;

import com.day.cq.commons.jcr.JcrConstants;
import com.day.cq.wcm.api.Page;
import com.mysite.core.models.AccordionContentFragmentModel;
import com.mysite.core.models.CFModel;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.PersistenceException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.OSGiService;
import org.apache.sling.models.annotations.injectorspecific.ScriptVariable;
import org.apache.sling.models.annotations.injectorspecific.Self;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.models.factory.ModelFactory;
import org.apache.sling.settings.SlingSettingsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.StreamSupport;

@Model(adaptables = SlingHttpServletRequest.class, adapters = {AccordionContentFragmentModel.class})
public class AccordionContentFragmentModelImpl implements AccordionContentFragmentModel {

    private static final Logger LOG = LoggerFactory.getLogger(AccordionContentFragmentModelImpl.class);

    private static final String CF_FRAGMENT_PATH = "fragmentPath";
    private static final String CF_ELEMENTS_NAME = "elementNames";
    private static final String CF_FAQ_QUESTION = "question";
    private static final String CF_FAQ_ANSWER = "answer";
    private static final String CF_FAQ_ITEMS = "/item_";
    private static final String CF_FAQ_PANEL_TITLE = "cq:panelTitle";

    private static final String CF_ARTICLE_FRAGMENT_PATH = "articleFragmentPath";
    private static final String CF_FAQ_ITEM = "item_";
    private static final String UNDERSCORE = "_";
    private static final String PUBLISH_ENV = "publish";

    private static final Map<Integer, String> NUMBERS = new HashMap<>();
    static {
        NUMBERS.put(1, "One");
        NUMBERS.put(2, "Two");
        NUMBERS.put(3, "Three");
        NUMBERS.put(4, "Four");
        NUMBERS.put(5, "Five");
        NUMBERS.put(6, "Six");
        NUMBERS.put(7, "Seven");
        NUMBERS.put(8, "Eight");
        NUMBERS.put(9, "Nine");
        NUMBERS.put(10, "Ten");
    }

    static Map<String, Object> defualtFaqProperties = new HashMap<>();
    static {
        defualtFaqProperties.put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED);
        defualtFaqProperties.put(ResourceResolver.PROPERTY_RESOURCE_TYPE, "mysite/components/contentfragment");
        defualtFaqProperties.put("containerOpted", "dynamic");
        defualtFaqProperties.put("displayMode", "singleText");
        defualtFaqProperties.put("paragraphScope", "all");
        defualtFaqProperties.put("variationName", "master");
    }

    @Self
    private SlingHttpServletRequest request;

    @ScriptVariable
    private Resource resource;

    @ScriptVariable
    private Page currentPage;

    @SlingObject
    private ResourceResolver resourceResolver;

    @OSGiService
    private SlingSettingsService slingSettings;

    @OSGiService
    private ModelFactory modelFactory;

    private List<Integer> faqCount = new ArrayList<>();

    @PostConstruct
    private void initModel() {
        if(!isPublish(slingSettings)) {
            String pageArticleFragmentPath = currentPage.getProperties().get(CF_ARTICLE_FRAGMENT_PATH, StringUtils.EMPTY);
            if(StringUtils.isNotEmpty(pageArticleFragmentPath)) {
                Resource contentFragmentResource = resourceResolver.resolve(pageArticleFragmentPath);
                CFModel cfModel = modelFactory.getModelFromWrappedRequest(request, contentFragmentResource, CFModel.class);
                getBodyElementCounts(cfModel);
                List<Integer> childrens = StreamSupport.stream(resource.getChildren().spliterator(), false)
                        .map(accRes -> StringUtils.substringAfter(accRes.getName(), UNDERSCORE))
                        .map(NumberUtils::toInt)
                        .sorted()
                        .collect(Collectors.toList());
                if(!faqCount.equals(childrens)) {
                    deleteInvalidResource(childrens);
                    addFaqItem(faqCount, resourceResolver, resource, pageArticleFragmentPath, cfModel);
                }
            }
        }
    }

    private void deleteInvalidResource(List<Integer> childrens) {
        childrens.removeAll(faqCount);
        if(!childrens.isEmpty()) {
            childrens.stream().forEach(this::deleteResource);
            commitChanges();
        }
    }
    private void commitChanges() {
        if(resourceResolver.hasChanges()) {
            try {
                resourceResolver.commit();
            } catch (PersistenceException e) {
                LOG.error("Unable save the cahnges {}",e.getMessage());
            }
        }
    }

    private void deleteResource(Integer num) {
        try {
            resourceResolver.delete(resource.getChild(CF_FAQ_ITEM+num));
        } catch (PersistenceException e) {
            LOG.error("Unable to delete the resource {}", e.getMessage());
        }
    }

    public void getBodyElementCounts(CFModel cfModel) {
        faqCount = IntStream.rangeClosed(1, 20).filter(num -> checkFragment(num, cfModel, CF_FAQ_QUESTION)).boxed().collect(Collectors.toList());
    }

    public static boolean checkFragment(int num, CFModel cfModel, String elementName) {
        return StringUtils.isNotEmpty(cfModel.getElementContent(elementName+ NUMBERS.get(num)));
    }

    public static void addFaqItem(List<Integer> faqCount, ResourceResolver resourceResolver, Resource resource, String pageArticleFragmentPath, CFModel cfModel) {
        while(!faqCount.isEmpty()) {
            try {
                Map<String, Object> faqProperties = defualtFaqProperties;
                faqProperties.put(CF_FRAGMENT_PATH, pageArticleFragmentPath);
                faqProperties.put(CF_ELEMENTS_NAME, CF_FAQ_ANSWER+ NUMBERS.get(faqCount.get(0)));
                faqProperties.put(CF_FAQ_PANEL_TITLE, cfModel.getElementContent(CF_FAQ_QUESTION+ NUMBERS.get(faqCount.get(0))));
                ResourceUtil.getOrCreateResource(resourceResolver, resource.getPath()+CF_FAQ_ITEMS+faqCount.get(0), faqProperties, StringUtils.EMPTY, false);
                faqCount.remove(0);
            } catch (PersistenceException e) {
                LOG.error("Unable to persist the resource {}", e.getMessage());
            }
        }
    }

    public static boolean isPublish(SlingSettingsService slingSettings) {
        return slingSettings.getRunModes().contains(PUBLISH_ENV);
    }
}

You can author the component by selecting the fragment path.

Notes: Please make sure we don’t author anything manually by clicking on add item

Author content fragment path

After authoring you can reload the page to see the content is being populated into the accordion model and all the changes made on the content fragment will be pulled into the component

Content fragment data is pulled automatically

You can also rearrange the order of the FAQ from dialog level or from the panel selector.

Rearrange from panel selector

Flexibility Vs Automation:

This component will automatically pull Q&A from content fragment but if there is any update on the question field then it won’t reflect hence please remove the field which is not pulling the latest results and refresh the page so that it pulls the latest.

Why are we seeing above issue?

We are unable to pull the updated questions into the component because this will remove the flexibility of rearranging the panels. Based on your requirement you can remove this functionality.

you can also find the working code from here:

https://github.com/kiransg89/AccordionContentFragment

AEM Core Component Delegation

Problem Statement:

The article aims to explain the process of delegating OOTB components in AEM and customizing them by adding new parameters and methods.


Requirement:

This article is about delegating the Out Of The Box (OOTB) components of Adobe Experience Manager (AEM) using Lombok delegation. The author explains how to add new parameters and methods to the components using delegation. Specifically, the article covers how to delegate the OOTB image component, add custom logic to its existing methods, and introduce new methods such as getCredit() and getSizes().


Delegate OOTB image component and add custom logic on existing methods like getWidth(), getHeight(), and getSrcSet() and also add new methods getCredit() and getSizes()


Introduction:

Any field or no-argument method can be annotated with @Delegate to let Lombok generate delegate methods that forward the call to this field (or the result of invoking this method).

Lombok delegates all public methods of the field’s type (or method’s return type), as well as those of its supertypes except for all methods declared in java.lang.Object.

You can pass any number of classes into the @Delegate annotation’s types parameter. If you do that, then Lombok will delegate all public methods in those types (and their supertypes, except java.lang.Object) instead of looking at the field/method’s type.

All public non-Object methods that are part of the calculated type(s) are copied, whether or not you also wrote implementations for those methods. That would thus result in duplicate method errors. You can avoid these by using the @Delegate(excludes=SomeType.class) parameter to exclude all public methods in the excluded type(s), and their supertypes.

The below image shows how the Lombok delegation happens:

Lombok request handling and delegation
Lombok request delegation

Whenever we make a request to a class (for the Image Sling model) the Lombok delegation will copy all the public methods and exclude the methods which we want to override and provide the custom implementation to those methods.

Image Component Delegation

For our requirement, I am creating a class called ImageDelegate and implementing the Image component interface.

I will be making the call to the Image component using @self via ResourceSuperType and using @Delegate annotation I will be excluding some of the methods inside DelegationExclusion interface class

I will be adding custom code to the overridden methods and also I am able to introduce new methods getCredit(), getSizes()

package com.mysite.core.models.impl;

import com.adobe.cq.export.json.ExporterConstants;
import com.adobe.cq.wcm.core.components.models.Image;
import com.day.cq.dam.api.DamConstants;
import com.day.cq.dam.commons.util.DamUtil;
import com.day.cq.wcm.api.designer.Style;
import com.drew.lang.annotations.Nullable;
import lombok.experimental.Delegate;
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.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Via;
import org.apache.sling.models.annotations.injectorspecific.*;
import org.apache.sling.models.annotations.via.ResourceSuperType;
import org.apache.sling.servlets.post.SlingPostConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;

@Model(adaptables = SlingHttpServletRequest.class, adapters = {
  Image.class
}, resourceType = ImageDelegate.RESOURCE_TYPE, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class ImageDelegate implements Image {
  private static final Logger LOGGER = LoggerFactory.getLogger(ImageDelegate.class);
  public static final String RESOURCE_TYPE = "mysite/components/content/image";
  public static final String CONTENT_DAM_PATH = "/content/dam";
  public static final String HTTPS = "https://";
  public static final String PN_DISPLAY_SIZES = "sizes";
  public static final String WIDTH = "{.width}";

  @SlingObject
  private ResourceResolver resourceResolver;

  @SlingObject
  protected Resource resource;

  @ValueMapValue(injectionStrategy = InjectionStrategy.OPTIONAL)
  @Nullable
  protected String credit;

  @ScriptVariable
  protected Style currentStyle;

  @RequestAttribute(injectionStrategy = InjectionStrategy.OPTIONAL)
  String host;

  @Self
  @Via(type = ResourceSuperType.class)
  @Delegate(excludes = DelegationExclusion.class)
  private Image image;

  @Override
  public String getSrc() {
    return prepareSuffix(image.getSrc());
  }

  @Override
  public String getWidth() {
    return DamUtil.resolveToAsset(resourceResolver.resolve(image.getFileReference())).getMetadataValueFromJcr(DamConstants.TIFF_IMAGEWIDTH);
  }

  @Override
  public String getHeight() {
    return DamUtil.resolveToAsset(resourceResolver.resolve(image.getFileReference())).getMetadataValueFromJcr(DamConstants.TIFF_IMAGELENGTH);
  }

  @Override
  public String getSrcset() {
    int[] widthsArray = image.getWidths();
    String srcUritemplate = image.getSrcUriTemplate();
    String[] srcsetArray = new String[widthsArray.length];
    if (widthsArray.length > 0 && srcUritemplate != null) {
      String srcUriTemplateDecoded = "";
      try {
        srcUriTemplateDecoded = HTTPS + host + prepareSuffix(URLDecoder.decode(srcUritemplate, StandardCharsets.UTF_8.name()));
      } catch (UnsupportedEncodingException e) {
        LOGGER.error("Character Decoding failed for {}", resource.getPath());
      }
      if (srcUriTemplateDecoded.contains(WIDTH)) {
        for (int i = 0; i < widthsArray.length; i++) {
          if (srcUriTemplateDecoded.contains("=" + WIDTH)) {
            srcsetArray[i] = srcUriTemplateDecoded.replace(WIDTH, String.format("%s", widthsArray[i])) + " " + widthsArray[i] + "w";
          } else {
            srcsetArray[i] = srcUriTemplateDecoded.replace(WIDTH, String.format(".%s", widthsArray[i])) + " " + widthsArray[i] + "w";
          }
        }
        return StringUtils.join(srcsetArray, ',');
      }
    }
    return null;
  }

  public String getCredit() {
    if (StringUtils.isEmpty(credit)) {
      return DamUtil.resolveToAsset(resourceResolver.resolve(image.getFileReference())).getMetadataValueFromJcr(DamConstants.DC_CREATOR);
    }
    return credit;
  }

  public String getSizes() {
    return currentStyle.get(PN_DISPLAY_SIZES, StringUtils.EMPTY);
  }

  private String prepareSuffix(String imageSrc) {
    if (StringUtils.isNotEmpty(imageSrc) && !StringUtils.containsIgnoreCase(imageSrc, CONTENT_DAM_PATH)) {
      int endIndex = imageSrc.lastIndexOf(SlingPostConstants.DEFAULT_CREATE_SUFFIX);
      String intermittenResult = imageSrc.substring(0, endIndex);
      endIndex = intermittenResult.lastIndexOf(SlingPostConstants.DEFAULT_CREATE_SUFFIX);
      return intermittenResult.substring(0, endIndex) + image.getFileReference();
    }
    return imageSrc;
  }

  private interface DelegationExclusion {
    String getSrc();
    String getSrcset();
    String getWidth();
    String getHeight();
  }
}

Sightly code:

Sightly call to the Image component will remain the same but I will be getting overridden method returns and new method returns as well

<div data-sly-use.image="com.adobe.cq.wcm.core.components.models.Image"
     data-sly-use.templates="core/wcm/components/commons/v1/templates.html"
     data-sly-test="${image.src}"
     data-cmp-dmimage="${image.dmImage}"
     data-asset-id="${image.uuid}"
     id="${image.id}"
     data-cmp-data-layer="${image.data.json}"
     class="cmp-image"
     itemscope itemtype="http://schema.org/ImageObject">
    <a data-sly-unwrap="${!image.imageLink.valid}"
       class="cmp-image__link"
       data-sly-attribute="${image.imageLink.htmlAttributes}"
       data-cmp-clickable="${image.data ? true : false}">
        <img srcset="${image.srcset}" src="${image.src}"
             loading="${image.lazyEnabled ? 'lazy' : 'eager'}"
             class="cmp-image__image cmp-image__image@tablet"
             itemprop="contentUrl"
             sizes="${image.sizes}"
             width="${image.width}" height="${image.height}"
             alt="${image.alt || true}" title="${image.displayPopupTitle && image.title}"/>
    </a>
    <span class="cmp-image__title" itemprop="caption" data-sly-test="${!image.displayPopupTitle && image.title}">${image.title}</span>
    <meta itemprop="caption" content="${image.title}" data-sly-test="${image.displayPopupTitle && image.title}">
</div>
<sly data-sly-call="${templates.placeholder @ isEmpty = !image.src, classAppend = 'cmp-image cq-dd-image'}"></sly>
sizes int the image
The image is from the wknd site