Reputation: 2601
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
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:
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>
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
})
.hide {
display: none;
}
Upvotes: 0
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
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:
cq:editConfig
javax.servlet.Filter
that encapsulates your business rules.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
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
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
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
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:
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