Expand my Community achievements bar.

July 31st AEM Gems Webinar: Elevate your AEM development to master the integration of private GitHub repositories within AEM Cloud Manager.
SOLVED

Extend Experience fragments of core using delegation pattern

Avatar

Level 3

Hello Members,

 

We are trying to extend Core Experience fragment to meet our client needs where each page can have its own header and footer.

The approach we are trying now is to unlock the experience fragment in the structure so that the author can configure per page which works fine.

 

However, on top of this we need same Experience fragment header to be set for all the child pages below . I am using Delegate pattern for the same and unfortunately this seems to not work .

 

Note: We are using aem-react-editable-components so the mapping happens in import-component.js and not the usual way with html. 

 

arungm20_0-1702985916183.png

 

Below is my Implementation

 

@Model(adaptables = { Resource.class,
	SlingHttpServletRequest.class }, adapters = { ExperienceFragment.class }, resourceType = CustomExperienceFragmentImpl.RESOURCE_TYPE, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
@Getter
public class CustomExperienceFragmentImpl implements ExperienceFragment {

	protected static final String RESOURCE_TYPE = "myproject/components/core/experiencefragment";

	@Self
	@Getter(AccessLevel.NONE)
	private SlingHttpServletRequest request;

	private String fragmentVariationPath;

	@PostConstruct
	void init() {
		InheritanceValueMap properties = new HierarchyNodeInheritanceValueMap(request.getResource());
		fragmentVariationPath = properties.getInherited(ExperienceFragment.PN_FRAGMENT_VARIATION_PATH, String.class);

	}

	@Self // Indicates that we are resolving the current resource
	@Getter(AccessLevel.NONE)
	@Via(type = ResourceSuperType.class) // Resolve not as this model, but as the model of our supertype (ie: CC Image)
	@Delegate(excludes = DelegationExclusion.class) // Delegate all our methods to the CC Image except those defined below
	private ExperienceFragment delegate;

	@Override
	public String getLocalizedFragmentVariationPath() {
		return fragmentVariationPath!=null ? fragmentVariationPath : delegate.getLocalizedFragmentVariationPath();
	}

	private interface DelegationExclusion {
		String getLocalizedFragmentVariationPath(); // Override the method which determines the source of the asset
	}
}

 

Kindly share your views

 

Thanks

Arun

1 Accepted Solution

Avatar

Correct answer by
Level 2

Hi @arungm20 

 

Please refer to the article below for instructions on configuring dynamic headers and footers. Additionally, explore extending the Experience Fragment model to support localized headers/footers with a custom site structure using the delegation pattern.

https://www.albinsblog.com/2021/10/extend-aem-experience-fragment-localization.html

 

View solution in original post

11 Replies

Avatar

Correct answer by
Level 2

Hi @arungm20 

 

Please refer to the article below for instructions on configuring dynamic headers and footers. Additionally, explore extending the Experience Fragment model to support localized headers/footers with a custom site structure using the delegation pattern.

https://www.albinsblog.com/2021/10/extend-aem-experience-fragment-localization.html

 

Avatar

Level 3

hi @Jeevan-Eranti yes this is the same approach i have implemented unfortunately the delegation does not seem to work for me.

 

The same code is attached as well

 

Thanks

Arun

Avatar

Level 2

@arungm20 
We followed the steps below to implement the Proxy Experience Fragment for the header and footer at the template level:

  1. Integrated the Proxy Experience Fragment component into the template structure at the template level. It's important to note that we kept these components locked.

  2. Introduced two custom fields in the Page component dialog to facilitate the configuration of XF paths for the header and footer.

    Note: XF paths for the header and footer can be defined at the site root page level or any child page if a different header or footer is desired. This allows for flexibility in customizing header and footer content at different levels of the site hierarchy.

    Additionally, a logic has been implemented in the Customized Experience Fragment model to handle the inheritance of XF paths for the header and footer.

    Jeevan923576_0-1703001255921.png Jeevan923576_1-1703001916817.png

     

  3. Provided a sample code snippet for the Customized Experience Fragment model to manage the inheritance logic and delegation pattern.       

 

package com.test.core.models;

import org.apache.sling.models.annotations.Model;
import org.apache.sling.api.resource.Resource;

import java.util.Objects;

import org.apache.commons.lang3.StringUtils;
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.injectorspecific.Self;
import org.apache.sling.models.annotations.Via;
import org.apache.sling.models.annotations.via.ResourceSuperType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import lombok.experimental.Delegate;

import com.adobe.cq.export.json.ExporterConstants;
import com.adobe.cq.wcm.core.components.models.ExperienceFragment;

import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.models.annotations.injectorspecific.ScriptVariable;
import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy;

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;

@Model(adaptables = { Resource.class,
		SlingHttpServletRequest.class }, adapters = ExperienceFragment.class, resourceType = CustomExperienceFragmentImpl.RESOURCE_TYPE, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)

@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)

public class CustomExperienceFragmentImpl implements ExperienceFragment {

	public static final String RESOURCE_TYPE = "test/components/experiencefragment";
	
	public static final String XF_TEMPLATE = "/conf/test/settings/wcm/templates/xf-web-variation";

	@Self
	@Via(type = ResourceSuperType.class)
	@Delegate(excludes = DelegationExclusion.class)
	private ExperienceFragment delegate;

	@Self
	private SlingHttpServletRequest request;

	@SlingObject
	private ResourceResolver resourceResolver;

	@ScriptVariable(injectionStrategy = InjectionStrategy.OPTIONAL)
	private Page currentPage;

	private static final Logger LOGGER = LoggerFactory.getLogger(CustomExperienceFragmentImpl.class);

	private static final String XF_HEADER_VARIATION = "/header/";

	private static final String XF_FOOTER_VARIATION = "/footer/";

	/*
	 * Return the site specific headers and footers, delegate to Core Component
	 * ExperienceFragment Model for remaining scenarios
	 */
	@Override
	public String getLocalizedFragmentVariationPath() {

		try {

			if (currentPage != null && currentPage.getAbsoluteParent(2) != null) {

				String fragmentVariationPath = request.getResource().getValueMap()
						.get(ExperienceFragment.PN_FRAGMENT_VARIATION_PATH, String.class);

				if (StringUtils.isNoneEmpty(fragmentVariationPath)) {

					LOGGER.debug("FragmentVariationPath: {}", fragmentVariationPath);

					if (fragmentVariationPath.contains(XF_HEADER_VARIATION)) {

						InheritanceValueMap ivm = new HierarchyNodeInheritanceValueMap(
								currentPage.getContentResource());
						String headeroverrideXF = ivm.getInherited("headerFragmentPath", String.class);

						if (StringUtils.isNoneEmpty(headeroverrideXF) && resourceExists(headeroverrideXF)
								&& isValidExperienceFragmentVariation(headeroverrideXF)) {
							LOGGER.debug("Handling the header variation. XfVariationPath - {} ", headeroverrideXF);
							return headeroverrideXF;
						}

					} else if (fragmentVariationPath.contains(XF_FOOTER_VARIATION)) {

						InheritanceValueMap ivm = new HierarchyNodeInheritanceValueMap(
								currentPage.getContentResource());
						String footeroverrideXF = ivm.getInherited("footerFragmentPath", String.class);

						if (StringUtils.isNoneEmpty(footeroverrideXF) && resourceExists(footeroverrideXF)
								&& isValidExperienceFragmentVariation(footeroverrideXF)) {
							LOGGER.debug("Handling the footer variation. XfVariationPath - {} ", footeroverrideXF);
							return footeroverrideXF;
						}
					}

				}

			}

		} catch (Exception e) {
			LOGGER.error("Exception thrown at getLocalizedFragmentVariationPath - ExperienceFragmentImpl class " + e);
		}

		return Objects.nonNull(delegate) ? delegate.getLocalizedFragmentVariationPath() : StringUtils.EMPTY;

	}

	private boolean resourceExists(final String path) {
		return (StringUtils.isNotEmpty(path) && this.request.getResourceResolver().getResource(path) != null);
	}
	
	private boolean isValidExperienceFragmentVariation(final String path) {
		
        PageManager pageManager = this.request.getResourceResolver().adaptTo(PageManager.class);
        if (pageManager != null) {
            Page xfVariationPage = pageManager.getPage(path);
            if (xfVariationPage != null) {
            	String xfPageTemplate = xfVariationPage.getTemplate().getPath();
            	return XF_TEMPLATE.equals(xfPageTemplate);
            }
        }
		
		return false;
	}

	private interface DelegationExclusion {
		String getLocalizedFragmentVariationPath();
	}

}

 

 

Avatar

Level 2

@arungm20 

You need to define the static fragmentVariationPath in the editable template's structure. There is an option to override the fragment variation (for the header and footer) at the page properties level.

if (fragmentVariationPath.contains(XF_HEADER_VARIATION)) {
    InheritanceValueMap ivm = new HierarchyNodeInheritanceValueMap(currentPage.getContentResource());
    String headerOverrideXF = ivm.getInherited("headerFragmentPath", String.class);

    if (StringUtils.isNotEmpty(headerOverrideXF) && resourceExists(headerOverrideXF)
            && isValidExperienceFragmentVariation(headerOverrideXF)) {
        LOGGER.debug("Handling the header variation. XF Variation Path - {} ", headerOverrideXF);
        return headerOverrideXF;
    }
}

// If the specified XF path is not found or is invalid, fallback to the template's default variation
return Objects.nonNull(delegate) ? delegate.getLocalizedFragmentVariationPath() : StringUtils.EMPTY;

 

In the above code, the if condition checks whether the fragmentVariationPath configured in the current page contains the header variation (XF_HEADER_VARIATION). If so, it attempts to retrieve an overridden header XF path from the page properties. If a valid and existing XF path is found, it is returned; otherwise, the code falls back to the default XF path specified in the editable template's structure using the delegate.getLocalizedFragmentVariationPath().

Avatar

Level 3

Thanks @Jeevan-Eranti  for explanations. and Indeed i understood and

it works for me with the one we set at page root level. However the items are i get are from the master which is set in template i.e from the path fragmentVariationPath. and hence i am confused. I expected that the Experience fragment delegate will take the configured path at the page root level and render the items from this fragment but seems not..

 

Also i have just copied your sample code and adjusted to our project

 

:items: {
experiencefragment-header: {
localizedFragmentVariationPath: "/content/experience-fragments/mysite/vu/en/site/sample/master", ---> this is the overridden path from root page

:items: {
---> All the items i get here are from the path configured in template due to the Core component
}

 

 

Hence i am curious what i miss and which works for you

 

Thanks

Arun

Avatar

Level 3

hi @Jeevan-Eranti,

 

After few hours of debugging, i could find the issue. I played your same code on WKND project and below are my findings

 

1. This approach is working fine where we use the experiencefragment.html for rendring but not with react based project which uses model.json

2. I have noticed that the model.json is not updated with the overridden path in WKND as well.

3. Reason for this is the core Experience fragment has a private method to get the children of the path i.e localizedFragmentVariationPath which always gives the path configured in structure as this is derived from ExperienceFragmentDataImpl model which is internal and was not able to extend or override.

 

Hence i had to use a workaround now to set this path using reflection so that the model.json gets updated and react picks the items rendered to show on page

 

Thank you very much for the idea provided earlier.

If you can test the model.json in your project where this is implemented i can be 100% sure that its a problem in Core component then

 

Thanks

Arun

Avatar

Level 1

Hi @arungm20 

 

I'm facing the similar issue in AEM SPA project while implementing localization of XF's. Can you please provide what's the workaround (Reflection) and how to use in detail to override localizedFragmentVariationPath in model.json.

 

Thanks,

Sairam 

Avatar

Level 3

Thanks @Jeevan-Eranti i like this approach and i see that localizedFragmentVariationPath is updated to what is configured in the parent.. However i am not sure if we need to override below 2 methods from ExperienceFragmentImpl.java as the private methods in these methods gets invoked and shows the items of the fragments configured from template. Unfortunately it does not get the items from the updated fragmentVariation.

 

Can you share your views on how it works for you?

    public @NotNull Map<String, ? extends ComponentExporter> getExportedItems() {
        return this.getChildren();
    }

    public @NotNull String[] getExportedItemsOrder() {
        return (String[])this.getChildren().keySet().toArray(new String[0]);
    }

 Thanks

Arun

Avatar

Level 3

Thanks @arunpatidar but this post uses slightly. however i would need to do in Model so that its available in model.json and react would pick the path