From Static to Dynamic: The Evolution of Template Management
Do you remember the days of static templates? We had a plethora of templates, each with its own page components and CQ dialogs. It was a maintenance nightmare!
But then came editable templates, and everything changed. With this new approach, we can define a single-page component and create multiple templates from it. Sounds like a dream come true, right?
But there’s a catch. What if we need different dialogs for different templates? Do we really need to create separate template types for each one? That would mean maintaining multiple template types and trying to keep track of which template uses which type. Not exactly the most efficient use of our time.
Managing Page Properties in AEM
In this post, we’ll explore the challenges of template management and how we can overcome them using Granite render conditions and context-aware configurations.
When managing page properties, we’re often faced with a dilemma. While context-aware configurations are ideal for setting up configurations at the domain or language level, they fall short when it comes to managing individual pages.
The usual go-to solution is to update the Page Properties dialog, but this approach has its own set of limitations. So, what’s a developer to do?
Fortunately, there’s a solution that combines the power of Granite render conditions with the flexibility of context-aware configurations.
What is Granite Render Condition?
Render condition is just conditional logic to render a specific section of the component UI. If you want a more detailed description, you can read Adobe’s official documentation.
A Real-World Use Case Using Both Granite Render Condition and Context-Aware Configuration
Say we want to display and hide the page properties tab based on the template name, which can be configured using context-aware configuration without any hardcoded code.
First, we’d need to build the CAC which will contain fields for adding the template names and tab path to show.
We will create a service for context-aware configuration which will read config and provide the mapping.
public interface PageTabsMappingService { List<PageTabsMappingConfig> getPageTabsMappingConfigList(); }
Here PageTabsMappingConfig is just a POJO bean class that consists of a page tab path and template path.
@Data public class PageTabsMappingConfig { private String templatePath; private String tabPath; }
Now let’s create a context-aware configuration implementation class, which will consist of a template path and tabs path configuration ability.
We want this to be more author-friendly, so we will be using a custom data source. This data source can be found in this blog post.
For this example, we need two data sources, one for template path and one for tab paths.
So finally, our configuration will look like this:
@Configuration(label = "Page Tabs Mapping Configuration", description = "Page Tabs Mapping Config", property = {EditorProperties.PROPERTY_CATEGORY + "=TemplateAndTabs"}, collection = true) public @interface PageTabsMappingConfiguration { @Property(label = "Select Template To Be Mapped", description = "Select Template Name To Be Mapped", property = { "widgetType=dropdown", "dropdownOptionsProvider=templateDataSource" },order = 1) String getTemplatePath(); @Property(label = "Select Tab to be mapped", description = "Select Tab to be mapped", property = { "widgetType=dropdown", "dropdownOptionsProvider=tabDataSource" },order = 2) String getTabPath(); }
Now let’s implement Service to read this config.
public interface PageTabsMappingService { List<PageTabsMappingConfig> getPageTabsMappingConfigList(Resource resource); }
@Component(service = PageTabsMappingService.class, immediate = true) @ServiceDescription("Implementation For PageTabsMappingService ") @Slf4j public class PageTabsMappingServiceImpl implements PageTabsMappingService { @Override public List<PageTabsMappingConfig> getPageTabsMappingConfigList(final Resource resource) { final ConfigurationBuilder configurationBuilder = Optional.ofNullable(resource) .map(resource1 -> resource1.adaptTo(ConfigurationBuilder.class)) .orElse(null); return new ArrayList<>(Optional .ofNullable(configurationBuilder) .map(builder -> builder .name(PageTabsMappingConfiguration.class.getName()) .asCollection(PageTabsMappingConfiguration.class)) .orElse(new ArrayList<>())) .stream().map(pageTabsMappingConfiguration ->new PageTabsMappingConfig(pageTabsMappingConfiguration.getTabPath(),pageTabsMappingConfiguration.getTemplatePath())) .collect(Collectors.toList()); } }
In the above code, we are reading context-aware configuration and providing the list for further use.
Now let us create render condition to show and hide tabs in page properties which will utilize the CAC mapping configuration.
We will be using the Sling Model for the same. This will be invoked whenever Page properties tabs are opened, in page editor mode, creation wizard, or on sites wizard.
@Model(adaptables = SlingHttpServletRequest.class) public class TabsRenderConditionModel { @Self private SlingHttpServletRequest request; @OSGiService private PageTabsMappingService pageTabsMappingService; /** * This is to set render condition for tabs. */ @PostConstruct public void init() { final var resource = request.getResource() .getResourceResolver().getResource("/content"); //We are considering root level site config since this will be global. //For multitenant environment you can add additional OSGI Config and use the path accordingly final List<PageTabsMappingConfig> tabRenderConfig = pageTabsMappingService.getPageTabsMappingConfigList(resource); final var name = Optional.ofNullable(request.getResource().getParent()) .map(Resource::getName).orElse(StringUtils.EMPTY); final var props = (ValueMap) request.getAttribute("granite.ui.form.values"); final var template = Optional.ofNullable(props) .map(props1 -> props1.get("cq:template", String.class)) .orElse(StringUtils.EMPTY); final var renderFlag = tabRenderConfig.stream() .anyMatch(tabConfig -> BooleanUtils.and(new Boolean[]{StringUtils.equals(name, tabConfig.getTabName()), StringUtils.equals(template, tabConfig.getTemplatePath())})); request.setAttribute(RenderCondition.class.getName(), new SimpleRenderCondition(renderFlag)); } }
After reading template we simply check if this given tab name mapping exists or not. Based on that, using the simple render condition we are setting a flag for showing and hiding the tab.
Now it is time to use this Sling model in the actual render condition script file. In our project directory let’s assume /apps/my-project/render-conditions/tabs-renderconditions
Create tabs-renderconditions.html
And add content as:
<sly data-sly-use.tab="com.mybrand.demo.models.TabsRenderConditionModel" />
Build a customs tabs under the base page template folder as follows:
/apps/my-project/components/structure/page/base-page/tabs -landing-page-tab -home-page-tab -country-page-tab -state-page-tab -hero-page-tab
And our cq:dialog will be referring the same as this:
<?xml version="1.0" encoding="UTF-8"?> <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" jcr:primaryType="nt:unstructured"> <content jcr:primaryType="nt:unstructured"> <items jcr:primaryType="nt:unstructured"> <tabs jcr:primaryType="nt:unstructured"> <items jcr:primaryType="nt:unstructured"> <additionalHeroPage jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/include" path="/mnt/override/apps/my-project/components/structure/page/tabs/additional-hero-page"/> <additionalStatePage jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/include" path="/mnt/override/apps/my-project/components/structure/page/tabs/additionalstatepage"/> </items> </tabs> </items> </content> </jcr:root>
And our sample tab with render condition config will looks like this:
<additionalHeroPage cq:showOnCreate="{Boolean}true" jcr:primaryType="nt:unstructured" jcr:title="Additional Hero Page Setting" sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns"> <items jcr:primaryType="nt:unstructured"> <column jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/container"> <items jcr:primaryType="nt:unstructured"> <section1 jcr:primaryType="nt:unstructured" jcr:title="Settings" sling:resourceType="granite/ui/components/coral/foundation/form/fieldset"> <items jcr:primaryType="nt:unstructured"> <testProperty cq:showOnCreate="{Boolean}true" jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/textfield" fieldDescription="Test Property" fieldLabel="Test Property" name="./testProp" required="{Boolean}true"> <granite:data jcr:primaryType="nt:unstructured" cq-msm-lockable="./testProp"/> </testProperty> </items> </section1> </items> </column> </items> <granite:rendercondition jcr:primaryType="nt:unstructured" sling:resourceType="my-project/render-conditions/tabs-renderconditions"/> </additionalHomePage>
In the below Template and Tabs CAC configuration, the “Additional Home Page Setting” tab will be displayed in page properties when an author is opening a page created using the hero-page template.
Finally, when you open any page made with a configured template, like the Hero page in the image below, you can see the tabs configured for it.
More Helpful AEM Tips and Tricks
I hope you have a better understanding of how to overcome some of the challenges of managing templates in AEM.
For more AEM tips and tricks, keep up with us on our Adobe blog!