Expand my Community achievements bar.

SOLVED

AEM 6.4 with SPA Editor - how to use the experience fragments

Avatar

Level 4

Hi,

Using AEM 6.4 with SPA Editor, is there any example/sample of how to use the experience fragments inside the SPA site?

1 Accepted Solution

Avatar

Correct answer by
Level 2

Following the approach suggested by JaideepBrar​ for 6.5 I've managed to get the contents of experience fragments displaying on a react app. This is based on the AEM react SPA tutorial.

First, a model interface for the component that extends the ContainerExporter interface was created. (FYI, the ContainerExporter interface is referenced in some comments within the BaseComponentExporter class created in the tutorial.)

package com.adobe.aem.guides.wkndevents.core.models;

import com.adobe.cq.export.json.ComponentExporter;

import javax.annotation.Nonnull;

import com.adobe.cq.export.json.ContainerExporter;

import org.osgi.annotation.versioning.ConsumerType;

@ConsumerType
public interface ExperienceFragment extends ContainerExporter {

   @Nonnull
   default String getExportedType() {

   throw new UnsupportedOperationException();

  }

}

Some of the logic from the HierarchyPageImpl class of the tutorial was borrowed to implement the interface methods. But the "getItemModels" method was modified so that it retrieved the children of the Experience Fragment page content:

package com.adobe.aem.guides.wkndevents.core.models.impl;

import com.adobe.aem.guides.wkndevents.core.models.ExperienceFragment;

import com.adobe.cq.export.json.ComponentExporter;

import javax.annotation.Nonnull;

import javax.inject.Inject;

import com.adobe.cq.export.json.SlingModelFilter;

import org.apache.commons.lang3.ArrayUtils;

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.jcr.resource.api.JcrResourceConstants;

import org.apache.sling.models.annotations.Exporter;

import org.apache.sling.models.annotations.Model;

import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy;

import org.apache.sling.models.annotations.injectorspecific.Self;

import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;

import org.apache.sling.models.factory.ModelFactory;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import java.util.LinkedHashMap;

import java.util.Map;

@Model(

  adaptables = {SlingHttpServletRequest.class},

  adapters = {ExperienceFragment.class, ComponentExporter.class},

  resourceType = {ExperienceFragmentImpl.RESOURCE_TYPE}

)

@Exporter(

  name = "jackson",

  extensions = {"json"}

)

public class ExperienceFragmentImpl implements ExperienceFragment {

   protected static final String RESOURCE_TYPE = "cq/experience-fragments/editor/components/experiencefragment";

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

   private static final String JCR_CONTENT = "/jcr:content";

   @Self
   private SlingHttpServletRequest request;

   @Inject
   private ResourceResolver resourceResolver;

   @Inject
   private Resource resource;

   @Inject
   private ModelFactory modelFactory;

   @Inject
   private SlingModelFilter slingModelFilter;

   @ValueMapValue(

  injectionStrategy = InjectionStrategy.OPTIONAL
   )

   private String fragmentPath;

   private Map<String, ComponentExporter> childModels = null;

   public ExperienceFragmentImpl() {

   LOG.debug("Creating ExperienceFragmentImpl");

  }

   @Override
  @Nonnull
   public String getExportedType() {

   return this.resource.getResourceType();

  }

   @Nonnull
  @Override
   public Map<String, ? extends ComponentExporter> getExportedItems() {

   if (childModels == null) {

   childModels = getItemModels(request, ComponentExporter.class);

  }

   LOG.debug("childModels: {}", childModels);

   return childModels;

  }

   /**
  * Returns a map (resource name => Sling Model class) of the given resource children's Sling Models that can be adapted to {@link T}.
  *
  * @param slingRequest The current request.
  * @param modelClass  The Sling Model class to be adapted to.
  * @return Returns a map (resource name => Sling Model class) of the given resource children's Sling Models that can be adapted to {@link T}.
  */
   @Nonnull
   private <T> Map<String, T> getItemModels(@Nonnull SlingHttpServletRequest slingRequest,

   @Nonnull Class<T> modelClass) {

  Map<String, T> itemWrappers = new LinkedHashMap<>();

   if(StringUtils.isBlank(fragmentPath)) {

   return itemWrappers;

  }

  Resource experienceFragmentContent = resourceResolver.getResource(fragmentPath + JCR_CONTENT);

   if (experienceFragmentContent == null) {

   return itemWrappers;

  }

  Iterable<Resource> iterable = slingModelFilter.filterChildResources(experienceFragmentContent.getChildren());

   if (iterable == null) {

   return itemWrappers;

  }

   for (final Resource child : iterable) {

   LOG.debug("child.getPath(): {}", child.getPath());

   T model = modelFactory.getModelFromWrappedRequest(slingRequest, child, modelClass);

   LOG.debug("model: {}", model);

  itemWrappers.put(child.getName(), model);

  }

   return  itemWrappers;

  }

   @Nonnull
  @Override
   public String[] getExportedItemsOrder() {

  Map<String, ? extends ComponentExporter> models = getExportedItems();

   if (models.isEmpty()) {

   return ArrayUtils.EMPTY_STRING_ARRAY;

  }

   return models.keySet().toArray(ArrayUtils.EMPTY_STRING_ARRAY);

  }

}

Because the ContainerExporter interface was used in the component exporter the Container module to be used from the cq-react-editable-components JavaScript library as opposed to the Component module:

/*
  ExperienceFragment.js

  Maps to cq/experience-fragments/editor/components/experiencefragment
*/

import React from 'react';

import {MapTo, Container} from '@adobe/cq-react-editable-components';

require('./ExperienceFragment.scss');

/**
* Default Edit configuration for the Text component that interact with the Core ExperienceFragment component.
*
* @type EditConfig
*/
const ExperienceFragmentConfig = {

   emptyLabel: 'Experience Fragment',

   isEmpty: function(props) {

   var itemsOrder = props["cqItemsOrder"];

   return !(itemsOrder && itemsOrder.length);

  }

};

/**
* Text React component
*/
export default class ExperienceFragment extends Container {

   render() {

   return (<div className="ExperienceFragment">

  { this.childComponents }

  </div>);
   }

}

MapTo('cq/experience-fragments/editor/components/experiencefragment')(ExperienceFragment, ExperienceFragmentConfig);

But this will only show components in the experience fragment where the type has already been mapped. So after completing the tutorial it will only display the image, text and list components used in a fragment.

I hope this answers the question.

View solution in original post

16 Replies

Avatar

Level 10

Could you explain your use case?

SPA editor is for editing SPA components that are mapped/binded to AEM components. I'm not able to understand what are you trying to do with SPA editor & XFs.

Avatar

Employee Advisor

Experience Fragments are not yet supported(6.4 and below) in the SPA Editor. That being said, there is an approach mentioned for AEM 6.5 which can be used for XF where SPA app consumes JSON which is provided by content services (Sling Model Exporter).

Avatar

Level 4

Hi @JaideepBrar

Could you please let me know where can I find the approach of SPA app consuming XF JSON? I am trying to enable content fragment component in an SPA editor template based page(created from maven archetype), but it is not showing up for authoring(although node is formed in crx/de) I understand not all components are enabled in SPA authoring and we have to map using mapto react library, but could you let us know how to use XF where SPA app consumes JSON by content services (Sling Model Exporter) ? Is there any article/page for this approach ?

Avatar

Level 1

HI cqsapientu69896437 Did you find a solution for your question?

JaideepBrar​: is there any document to refer for using XF in SPA.

Avatar

Level 1

I would also appreciate a response to this question!

Avatar

Correct answer by
Level 2

Following the approach suggested by JaideepBrar​ for 6.5 I've managed to get the contents of experience fragments displaying on a react app. This is based on the AEM react SPA tutorial.

First, a model interface for the component that extends the ContainerExporter interface was created. (FYI, the ContainerExporter interface is referenced in some comments within the BaseComponentExporter class created in the tutorial.)

package com.adobe.aem.guides.wkndevents.core.models;

import com.adobe.cq.export.json.ComponentExporter;

import javax.annotation.Nonnull;

import com.adobe.cq.export.json.ContainerExporter;

import org.osgi.annotation.versioning.ConsumerType;

@ConsumerType
public interface ExperienceFragment extends ContainerExporter {

   @Nonnull
   default String getExportedType() {

   throw new UnsupportedOperationException();

  }

}

Some of the logic from the HierarchyPageImpl class of the tutorial was borrowed to implement the interface methods. But the "getItemModels" method was modified so that it retrieved the children of the Experience Fragment page content:

package com.adobe.aem.guides.wkndevents.core.models.impl;

import com.adobe.aem.guides.wkndevents.core.models.ExperienceFragment;

import com.adobe.cq.export.json.ComponentExporter;

import javax.annotation.Nonnull;

import javax.inject.Inject;

import com.adobe.cq.export.json.SlingModelFilter;

import org.apache.commons.lang3.ArrayUtils;

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.jcr.resource.api.JcrResourceConstants;

import org.apache.sling.models.annotations.Exporter;

import org.apache.sling.models.annotations.Model;

import org.apache.sling.models.annotations.injectorspecific.InjectionStrategy;

import org.apache.sling.models.annotations.injectorspecific.Self;

import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;

import org.apache.sling.models.factory.ModelFactory;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import java.util.LinkedHashMap;

import java.util.Map;

@Model(

  adaptables = {SlingHttpServletRequest.class},

  adapters = {ExperienceFragment.class, ComponentExporter.class},

  resourceType = {ExperienceFragmentImpl.RESOURCE_TYPE}

)

@Exporter(

  name = "jackson",

  extensions = {"json"}

)

public class ExperienceFragmentImpl implements ExperienceFragment {

   protected static final String RESOURCE_TYPE = "cq/experience-fragments/editor/components/experiencefragment";

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

   private static final String JCR_CONTENT = "/jcr:content";

   @Self
   private SlingHttpServletRequest request;

   @Inject
   private ResourceResolver resourceResolver;

   @Inject
   private Resource resource;

   @Inject
   private ModelFactory modelFactory;

   @Inject
   private SlingModelFilter slingModelFilter;

   @ValueMapValue(

  injectionStrategy = InjectionStrategy.OPTIONAL
   )

   private String fragmentPath;

   private Map<String, ComponentExporter> childModels = null;

   public ExperienceFragmentImpl() {

   LOG.debug("Creating ExperienceFragmentImpl");

  }

   @Override
  @Nonnull
   public String getExportedType() {

   return this.resource.getResourceType();

  }

   @Nonnull
  @Override
   public Map<String, ? extends ComponentExporter> getExportedItems() {

   if (childModels == null) {

   childModels = getItemModels(request, ComponentExporter.class);

  }

   LOG.debug("childModels: {}", childModels);

   return childModels;

  }

   /**
  * Returns a map (resource name => Sling Model class) of the given resource children's Sling Models that can be adapted to {@link T}.
  *
  * @param slingRequest The current request.
  * @param modelClass  The Sling Model class to be adapted to.
  * @return Returns a map (resource name => Sling Model class) of the given resource children's Sling Models that can be adapted to {@link T}.
  */
   @Nonnull
   private <T> Map<String, T> getItemModels(@Nonnull SlingHttpServletRequest slingRequest,

   @Nonnull Class<T> modelClass) {

  Map<String, T> itemWrappers = new LinkedHashMap<>();

   if(StringUtils.isBlank(fragmentPath)) {

   return itemWrappers;

  }

  Resource experienceFragmentContent = resourceResolver.getResource(fragmentPath + JCR_CONTENT);

   if (experienceFragmentContent == null) {

   return itemWrappers;

  }

  Iterable<Resource> iterable = slingModelFilter.filterChildResources(experienceFragmentContent.getChildren());

   if (iterable == null) {

   return itemWrappers;

  }

   for (final Resource child : iterable) {

   LOG.debug("child.getPath(): {}", child.getPath());

   T model = modelFactory.getModelFromWrappedRequest(slingRequest, child, modelClass);

   LOG.debug("model: {}", model);

  itemWrappers.put(child.getName(), model);

  }

   return  itemWrappers;

  }

   @Nonnull
  @Override
   public String[] getExportedItemsOrder() {

  Map<String, ? extends ComponentExporter> models = getExportedItems();

   if (models.isEmpty()) {

   return ArrayUtils.EMPTY_STRING_ARRAY;

  }

   return models.keySet().toArray(ArrayUtils.EMPTY_STRING_ARRAY);

  }

}

Because the ContainerExporter interface was used in the component exporter the Container module to be used from the cq-react-editable-components JavaScript library as opposed to the Component module:

/*
  ExperienceFragment.js

  Maps to cq/experience-fragments/editor/components/experiencefragment
*/

import React from 'react';

import {MapTo, Container} from '@adobe/cq-react-editable-components';

require('./ExperienceFragment.scss');

/**
* Default Edit configuration for the Text component that interact with the Core ExperienceFragment component.
*
* @type EditConfig
*/
const ExperienceFragmentConfig = {

   emptyLabel: 'Experience Fragment',

   isEmpty: function(props) {

   var itemsOrder = props["cqItemsOrder"];

   return !(itemsOrder && itemsOrder.length);

  }

};

/**
* Text React component
*/
export default class ExperienceFragment extends Container {

   render() {

   return (<div className="ExperienceFragment">

  { this.childComponents }

  </div>);
   }

}

MapTo('cq/experience-fragments/editor/components/experiencefragment')(ExperienceFragment, ExperienceFragmentConfig);

But this will only show components in the experience fragment where the type has already been mapped. So after completing the tutorial it will only display the image, text and list components used in a fragment.

I hope this answers the question.

Avatar

Employee
After some pushing from my end, we now got Experience Fragments baked in OOTB. Use out archetype 28. Check my youtube video: https://youtube.com/watch?v=Wn6pz5TI4Kc&t=230s

Avatar

Level 2

lawrencer38818131

thanks for your reply. I'm using 6.5 + sp1. Managed to create XF and render it in SPA template and on a page.

unfortunately still requires overhead for using it in XF editor, as originally it doesn't include React and json model, so no content is visible from Author UI, I just added components via CRXDE

Avatar

Level 2

so for backend I used answer from lawrencer38818131

to enable react rendering in XF edit mode, I just enabled React related client library. It can be done either via Page/Template Policies or if you point to your own page via resource type and this page will include the mentioned client library.

Works ok on AEM 6.5 + sp1

Avatar

Level 1

We have added the react components in the XF created but i am not able to see the added components in the created XF but when i drag drop the XF on the page and configure the path the added components in the XF are visible. So problem i am facing is how do i configure the components added in the XF.

Avatar

Level 2

Hi, please, check that React library code is present on XF page, if no - add the required client library, in policies for example

Avatar

Level 1

Hi ivana ,

can you please elaborate this - 'add the required client library' ?

Avatar

Level 2

Hello. If you refer to We Retail Journal example, the related client lib config is defined in the file

aem-sample-we-retail-journal/clientlib.config.js at master · adobe/aem-sample-we-retail-journal · Gi...

in the We Retail case it is - 'we-retail-journal-react'

Avatar

Level 1
Thanks. Done with Experience Fragment Compatibility with React

Avatar

Level 1

https://github.com/adobe/aem-react-core-wcm-components-examples.. In this repo, you can find the examples for loading react components in experience fragment editor. 

https://github.com/adobe/aem-react-core-wcm-components-examples/tree/master/ui.apps/src/main/content...

 

Also get the corresponding model (java) files from the bundle. After this, you need to register the xf page(appname/components/xfpage) component by extending the page component 

Avatar

Employee

After some pushing from my end, we now got Experience Fragments baked in OOTB. Use out archetype 28. Check my youtube video: https://youtube.com/watch?v=Wn6pz5TI4Kc&t=230s