VAr
VAr

Reputation: 2601

How to restrict a component to add only once per page

How to restrict a CQ5/Custom component to add only once per page.? I want to restrict the drag and drop of component into the page when the author is going to add the same component for the second time into the same page.

Upvotes: 4

Views: 5963

Answers (7)

pzrq
pzrq

Reputation: 1907

If you are building a new AEM component, please just pick one of the other answers. I'd suggest strongly to do it before your component gets to production!

However, while not directly answering the author's question, this hopefully helps others who stumble here, via Google or otherwise.

I don't think any of the solutions to this point seriously consider the migration of existing AEM components in a project, for example if the creators (jokes aside, sorry), perhaps some of them in upstream library code, extensively used document.getElementById amongst other things, and so could take a very significant effort to migrate fully to much more respectable alternatives like document.getElementsByClassName.

For that reason that migrations are hard, it could be quite reasonable to be pragmatic, for example by making it clearer as part of the AEM author and review workflows (as others have written, thanks!) that the component is only built to be a one only, such as by displaying an error message to the author, as a better trade off.

I went with:

  1. In my-component.html, add something like:
  <div class="hide my-component-error-message">
    <h3 class="error-title">Error</h3>
    <p class="error-paragraph">
      My component can only be authored once per page. 
      Please delete the existing components before adding only one new component.
    </p>
  </div>
  1. In my-component.js (inspired by this AEM cookie post) using a guard clause:
const MAX_OF_MY_COMPONENT_PER_PAGE = 1;
const ON_AEM_AUTHOR_INSTANCE = document.cookie.split(';').filter(item => item.includes('wcmmode')).length > 0;
document.addEventListener("DOMContentLoaded", function () {
  const myComponentsInDOM = [...document.querySelectorAll('#myComponentId')];
  if (ON_AEM_AUTHOR_INSTANCE && myComponentsInDOM.length > MAX_OF_MY_COMPONENT_PER_PAGE) {
    [
      ...document.getElementsByClassName('my-component-error-message')
    ].map(element => element.removeClass('hide'));
    return;
  }
  // Continue with normal one only component code
})
  1. (Optional) CSS if not using for example AEM GraniteUI:
.hide {
  display: none;
}

Upvotes: 0

Srikanth
Srikanth

Reputation: 36

None of the options looks easy to implement. The best approach I found is to use the ACS Commons Implementation which is very easy and can be adopted into any project.

Here is the link and how to configure it: https://github.com/Adobe-Consulting-Services/acs-aem-commons/pull/639

Enjoy coding !!!

Upvotes: 1

Brenn
Brenn

Reputation: 1384

None of these options are that great. If you truly want a robust solution to this problem (limit the number of items on the page without hardcoding location) then the best way is with a servlet filter chain OSGI service where you can administer the number of instances and then use a resource resolver to remove offending instances.

The basic gist is:

  1. Refresh the page on edit using cq:editConfig
  2. Create an OSGI service implementing javax.servlet.Filter that encapsulates your business rules.
  3. Use the filter to remove excess components according to business rules
  4. Continue page processing.

For more details see here: Using a servlet filter to limit the number of instances of a component per page or parsys

This approach will let you administer the number of items per page or per parsys and apply other possibly complex business rules in a way that the other offered solutions simply cannot.

Upvotes: 0

Pratik Rewatkar
Pratik Rewatkar

Reputation: 31

Thanks Rampant, I have followed your method and link stated. Posting link again : please follow this blog It was really helpful. I am posting the implementation whatever I have done. It worked fine for me. One can definitely improve the code quality, this is raw code and is just for reference.

1.Servlet Filter

Keep this in mind that,if any resource gets refereshed, this filter will execute. So you need to filter the contents at your end for further processing. P.S. chain.doFilter(request,response); is must. or cq will get hanged and nothing will be displayed.

@SlingFilter(generateComponent = false, generateService = true, order = -700,
scope = SlingFilterScope.REQUEST)
@Component(immediate = true, metatype = false)
public class ComponentRestrictorFilter implements Filter {
public void init(FilterConfig filterConfig) throws ServletException {}

@Reference
private ResourceResolverFactory resolverFactory;

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
  throws IOException, ServletException {
WCMMode mode = WCMMode.fromRequest(request);
if (mode == WCMMode.EDIT) {
  SlingHttpServletRequest slingRequest = (SlingHttpServletRequest) request;
  PageManager pageManager = slingRequest.getResource().getResourceResolver().adaptTo(PageManager.class);
  Page currentPage = pageManager.getContainingPage(slingRequest.getResource());
  logger.error("***mode" + mode);
  if (currentPage != null )) {

ComponentRestrictor restrictor = new ComponentRestrictor(currentPage.getPath(), RESTRICTED_COMPONENT);
    restrictor.removeDuplicateEntry(resolverFactory,pageManager);
  }
  chain.doFilter(request, response);
}
 }

   public void destroy() {}
}

2.ComponentRestrictor class

    public class ComponentRestrictor {

      private String targetPage;
      private String component;
      private Pattern pattern;
      private Set<Resource> duplicateResource = new HashSet<Resource>();
      private Logger logger = LoggerFactory.getLogger(ComponentRestrictor.class);
      private Resource resource = null;
      private ResourceResolver resourceResolver = null;
      private ComponentRestrictorHelper helper = new ComponentRestrictorHelper();


      public ComponentRestrictor(String targetPage_, String component_){
        targetPage = targetPage_ + "/jcr:content";
        component = component_;
      }

      public void removeDuplicateEntry(ResourceResolverFactory resolverFactory, PageManager pageManager) {
        pattern = Pattern.compile("([\"']|^)(" + component + ")(\\S|$)");
        findReference(resolverFactory, pageManager);

      }

      private void findReference(ResourceResolverFactory resolverFactory, PageManager pageManager) {
        try {

          resourceResolver = resolverFactory.getAdministrativeResourceResolver(null);
          resource = resourceResolver.getResource(this.targetPage);
          if (resource == null)
            return;
          search(resource);
          helper.removeDuplicateResource(pageManager,duplicateResource);

        } catch (LoginException e) {
          logger.error("Exception while getting the ResourceResolver " + e.getMessage());
        }
        resourceResolver.close();

      }

      private void search(Resource parentResource) {
        searchReferencesInContent(parentResource);
        for (Iterator<Resource> iter = parentResource.listChildren(); iter.hasNext();) {
          Resource child = iter.next();
          search(child);
        }
      }



      private void searchReferencesInContent(Resource resource) {
        ValueMap map = ResourceUtil.getValueMap(resource);

        for (String key : map.keySet()) {
          if (!helper.checkKey(key)) {
            continue;
          }

          String[] values = map.get(key, new String[0]);
          for (String value : values) {
            if (pattern.matcher(value).find()) {
              logger.error("resource**" + resource.getPath());
              duplicateResource.add(resource);
            }
          }
        }
      }
    }

3.To remove the node/ resource Whichever resource you want to remove/delete just use PageManager api

pageManeger.delete(resource,false);

That's it !!! You are good to go.

Upvotes: 3

IT Gumby
IT Gumby

Reputation: 1077

It sounds like there needs to be clarification of requirements (and understanding why).

If the authors can be trained, let them manage limits of components through authoring and review workflows.

If there is just 1 fixed location the component can appear, then the page component should include the content component, and let the component have an "enable" toggle property to determine if it should render anything. The component's group should be .hidden to prevent dragging from the sidekick.

If there is a fixed set of locations for the component, the page component can have a dropdown of the list of locations (including "none"). The page render component would then conditionally include the component in the correct location. Again, prevent dragging the component from the sidekick.

In the "hard to imagine" case that the component can appear anywhere on the page, added by authors, but limited to only 1 instance - use a wrapper component to manage including the (undraggable) component. Let the authors drag the wrapper on the page as many times as they want, but the wrapper should query the page's resources and determine if it is the first instance, and if so, include the end component. Otherwise, the wrapper does nothing.

In our experience (>2years on CQ), implementing this type of business rules via code creates a brittle solution. Also, requirements have a habit of changing. If enforced via code, development work is required instead of letting authors make changes faster & elegantly.

Upvotes: 1

Bruce Lefebvre
Bruce Lefebvre

Reputation: 411

One option is to include the component directly in the JSP of the template and exclude it from the list of available components in the sidekick. To do so, add the component directly to your JSP (foundation carousel in this example):

<cq:include path="carousel" resourceType="foundation/components/carousel" />

To hide the component from the sidekick, either set:

componentGroup: .hidden

or exclude it from the list of "Allowed Components" using design mode.

If you need to allow users to create a page without this component you can provide a second template with the cq:include omitted.

Upvotes: 5

Kaiser Shahid
Kaiser Shahid

Reputation: 241

you can't prevent that without doing some massive hacking to the ui code, and even then, you've only prevented it from one aspect of the ui. there's still crxde, and then the ability to POST content.

if this is truly a requirement, the best approach might be the following:

  1. have the component check for a special value in the pageContext object (use REQUEST_SCOPE)
  2. if value is not found, render component and set value
  3. otherwise, print out a message that component can only be used once

note that you can't prevent a dialog from showing, but at the very least the author has an indication that that particular component can only be used once.

Upvotes: 0

Related Questions