It seems like dark mode is taking over everything from IDE’s, to various apps and services (including the new MacOs Mojave).
The use case is pretty simple. In some dialogs, you have RTE’s and want to author text in white color. But that text becomes invisible against the default, white background of dialog boxes in AEM (I believe 6.3+). See for your self:
So I wondered “Why not have a button that toggles between the light and dark mode?” It’s possible because Coral already supports that!
Here is an example:
First, add this code into a clientlib with categories="[cq.authoring.editor.hook]"
(function(){ function getButton (clickHandler) { var btn = new Coral.Button() btn.set({ variant: Coral.Button.variant.MINIMAL, size: Coral.Button.size.MEDIUM, icon: "wand" }); btn.classList.add("cq-dialog-header-action"); btn.on("click", function(e){ e.preventDefault(); clickHandler(e); }); return btn; } $(document).on("dialog-loaded", function(event) { var $dialog = event.dialog; if (!$dialog || !$dialog.length) return; var $dialogWrapper = $dialog.closest('coral-dialog'); var $dialogActions = $dialogWrapper.find(".cq-dialog-actions"); // add switch button and handler var switchButton = getButton(function(){ $dialogWrapper.toggleClass('coral--dark') }); // add button to dialog actions $dialogActions.prepend(switchButton) }); })();
Please note that this was tested on AEM 6.4. I’m pretty sure it would also wok on AEM 6.3 but have not tested. Please let me know if you did!
There you have it! Dark Mode all the things! The code should be extremely simple and easy to follow. But comment if you have questions or want further assistance.
]]>In AEM 6.3+, there is a feature that allows content authors to “Request Publication” or “Request Unpublication.” The option becomes available when the content author does not have replication permission crx:replicate
on the page they are currently authoring. See image below:
Once the author clicks on “Request Publication,” the “Publish Page” wizard opens (see image below), and it offers a few options:
The “Request Unpublication” wizard is very similar, except it offers the option to unpublish a page but does not look for references.
By default, the OOTB “Request For Activation” workflow will be triggered for the page and all of the references/configs/policies selected in the wizard.
The OOTB “Request For Activation” workflow is located here:
/libs/settings/workflow/models/request_for_activation
There is a second way to publish/unpublish pages, and that is the “Manage Publication” wizard. Here is how to get to it:
Once you click on “Manage Publication,” the wizard opens:
Once you go through the wizard, again, the OOTB “Request For Activation” workflow will be triggered.
Customizing All The Wizards
DISCLAIMER: Everything here was discovered and done on AEM 6.4 SP3. Although, I’d think it should be the same on AEM 6.3+. Let me know in the comments if you find otherwise.
There is one small section on how to customize the wizard in the AEM docs here. But it only covers the “Manage Publication” wizard and not the other two wizards: “Publish Page” and “Unpublish Page Wizard.”
Location in libs: /libs/wcm/core/content/common/managepublicationwizard
but it’s an override of /libs/cq/gui/content/common/managepublicationwizard
Create the same wizard path, but under /apps
, so: /apps/cq/gui/content/common/managepublicationwizard
You could also overlay
/libs/wcm/core/content/common/managepublicationwizard
, I went with/apps/cq/gui/content/common/managepublicationwizard
because I already started doing it and did not want to change.
Then you can use this node structure:
Please note the use of
sling:resourceSuperType="/libs/cq/gui/content/common/managepublicationwizard"
(See Konrad’s comment below)
<?xml version="1.0" encoding="UTF-8"?> <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/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" sling:resourceSuperType="/libs/cq/gui/content/common/managepublicationwizard"> <body jcr:primaryType="nt:unstructured"> <items jcr:primaryType="nt:unstructured"> <form jcr:primaryType="nt:unstructured"> <granite:data jcr:primaryType="nt:unstructured" requestActivationWorkflow="/etc/workflow/models/custom-request-for-activation/jcr:content/model" requestDeactivationWorkflow="/etc/workflow/models/custom-request-for-deactivation/jcr:content/mode"/> </form> </items> </body> </jcr:root>
Location in libs: /libs/wcm/core/content/sites/publishpagewizard
Create the same wizard path, but under /apps
, so: /apps/wcm/core/content/sites/publishpagewizard
. Then you can use this node structure:
<?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="cq:Page"> <jcr:content jcr:primaryType="nt:unstructured"> <body jcr:primaryType="nt:unstructured"> <items jcr:primaryType="nt:unstructured"> <form jcr:primaryType="nt:unstructured"> <items jcr:primaryType="nt:unstructured"> <wizard jcr:primaryType="nt:unstructured" jcr:title="Custom Publish"> <items jcr:primaryType="nt:unstructured"> <publishstep jcr:primaryType="nt:unstructured"> <items jcr:primaryType="nt:unstructured"> <fixedColumns jcr:primaryType="nt:unstructured"> <items jcr:primaryType="nt:unstructured"> <fixedColumn1 jcr:primaryType="nt:unstructured"> <items jcr:primaryType="nt:unstructured"> <references jcr:primaryType="nt:unstructured" sling:resourceType="cq/gui/components/siteadmin/admin/publishwizard/references" requestActivationWorkflow="/etc/workflow/models/custom-request-for-activation/jcr:content/model"/> </items> </fixedColumn1> </items> </fixedColumns> </items> </publishstep> </items> </wizard> </items> </form> </items> </body> </jcr:content> </jcr:root>
Location in libs: /libs/wcm/core/content/sites/unpublishpagewizard
Create the same wizard path, but under /apps
, so: /apps/wcm/core/content/sites/unpublishpagewizard
. Then you can use this node structure:
<?xml version="1.0" encoding="UTF-8"?> <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:granite="http://www.adobe.com/jcr/granite/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="cq:Page"> <jcr:content jcr:primaryType="nt:unstructured"> <body jcr:primaryType="nt:unstructured"> <items jcr:primaryType="nt:unstructured"> <form jcr:primaryType="nt:unstructured"> <items jcr:primaryType="nt:unstructured"> <wizard jcr:primaryType="nt:unstructured" jcr:title="Custom Unpublish"> <items jcr:primaryType="nt:unstructured"> <unpublishstep jcr:primaryType="nt:unstructured"> <items jcr:primaryType="nt:unstructured"> <fixedColumns jcr:primaryType="nt:unstructured"> <items jcr:primaryType="nt:unstructured"> <fixedColumn1 jcr:primaryType="nt:unstructured"> <items jcr:primaryType="nt:unstructured"> <references jcr:primaryType="nt:unstructured" sling:resourceType="cq/gui/components/siteadmin/admin/unpublishwizard/references" requestDeactivationWorkflow="/etc/workflow/models/custom-request-for-deactivation/jcr:content/model"/> </items> </fixedColumn1> </items> </fixedColumns> </items> </unpublishstep> </items> </wizard> </items> </form> </items> </body> </jcr:content> </jcr:root>
requestActivationWorkflow
requestDeactivationWorkflow
/etc/workflow/models/custom-request-for-deactivation/jcr:content/mode
even though my actual workflow model is under path /conf/global/settings/workflow/models/custom-request-for-activation
I am not sure why this is, but I found it to work. If you know the answer to this, let me know in the comments!
A package with all the overlays above: Download Package
We all run into instances where it is necessary to make some dialog fields mandatory. A myriad of cases come to mind, but one of the most popular scenarios is to require Alt text for images.
In a previous post, one of my Perficient Digital colleagues shared best practices on how to validate authoring fields. In this post, I am going to add a small extension that will allow you to disable the submit button until all the fields are valid.
Overlay the following code into your /apps folder:
/libs/cq/gui/components/authoring/dialog/dialog.jsp
From here you can edit the dialog.jsp by adding the class <foundation-valid-bind> to the dialog submit button as shown below:
<div class="cq-dialog-actions u-coral-pullRight"> <button is="coral-button" icon="helpCircle" variant="minimal" <%= getHelpAttrs(slingRequest, cfg, xssAPI, i18n).build() %>></button> <button is="coral-button" icon="close" variant="minimal" class="cq-dialog-header-action cq-dialog-cancel" title="<%= i18n.get("Cancel")%>"></button> <button is="coral-button" icon="check" variant="minimal" class="cq-dialog-header-action cq-dialog-submit foundation-validation-bind" title="<%= i18n.get("Done") %>"></button> </div>
Add the required (boolean) = true property to the following code:
/apps/weretail/components/content/heroimage/cq:dialog/content/items/column/items/title1
Try adding it to any retail site page and author the dialog. You’ll notice that when the title is missing, the dialog submit button will be disabled.
There is also an alternative way to accomplish this without overlaying the dialog by listening for a ‘foundation-content-loaded’ event on your listener script. Query for the submit button and mark the attribute as disabled.
Now you know how to disable the submit button with foundation API.
Out of the box, AEM provides a pattern based logging system which comes pre-configured with an MessageFormat pattern for logging. This is a somewhat legacy messaging format, which, for most applications, has been updated with a “Logback” implementation. The main reason for said replacement is the flexibility that logback techniques can offer, including a more customized log pattern support, in addition to the ability to add in mapped variables, or in logback terminology, Mapped Diagnostic Context, or MDC. AEM comes bundled with SLF4J, and therefore already has support for logback patterns, and more importantly, MDC. What is missing here is how to populate the variable which needs to be logged on each request, and how to manipulate your logging patterns to accommodate said variable.
To utilize MDC, there are two main steps:
In AEM, every request flows through a filter chain, meaning we need to populate the MDC variable early enough in the filter that we can successfully log those variables later. The easiest way to do this is to set the scope of a custom filter to be at the request level, order 0:
/** * The SlingFilterScope.REQUEST is important as MDC needs to be configured prior * to any logger being executed in the page rendering. */ @SlingFilter( label = "Sample MDC Filter", description = "Sample implementation of custom MDC properties.", order = 0, scope = SlingFilterScope.REQUEST)
Then you need to import the MDC object into your class:
import org.slf4j.MDC;
The rest of the logic is up to you! The request object gives you direct access to the following:
And of course, you can always use a customized service user for more targeted JCR access. For sake of simplicity, this example includes logic to read a request parameter named “appId”.
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { final SlingHttpServletRequest request = (SlingHttpServletRequest) servletRequest; try { insertIntoMDC(request); filterChain.doFilter(request, servletResponse); } finally { clearMDC(); } } private void clearMDC() { for (String key : MDC_CUSTOM_KEYS) { MDC.remove(key); } } private void insertIntoMDC(SlingHttpServletRequest request) { //logic can go here to take value from request object. // Can also utilize ResourceResolver from same request. // If needed, can also write a generic OSGi Configuration // (String Array Properties) to read from standard request objects, // i.e cookies, headers, and parameters. MDC.put(APPLICATION_ID,request.getParameter(APPLICATION_ID)); }
That is it! Deploy the filter and we will have (the ability) to log a passed in request parameter into your log.
Here’s the full class for those copy/pasters:
package com.perficient.commons.core.filters; import org.apache.felix.scr.annotations.sling.SlingFilter; import org.apache.felix.scr.annotations.sling.SlingFilterScope; import org.apache.sling.api.SlingHttpServletRequest; import org.slf4j.MDC; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import java.io.IOException; /** * The SlingFilterScope.REQUEST is important as MDC needs to be configured prior * to any logger being executed in the page rendering. */ @SlingFilter( label = "Sample MDC Filter", description = "Sample implementation of custom MDC properties.", order = 0, scope = SlingFilterScope.REQUEST) public class MDCLoggerFilter implements javax.servlet.Filter{ public static final String APPLICATION_ID = "appId"; private static final String[] MDC_CUSTOM_KEYS = { APPLICATION_ID, }; public void init(FilterConfig filterConfig) throws ServletException { } public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { final SlingHttpServletRequest request = (SlingHttpServletRequest) servletRequest; try { insertIntoMDC(request); filterChain.doFilter(request, servletResponse); } finally { clearMDC(); } } private void clearMDC() { for (String key : MDC_CUSTOM_KEYS) { MDC.remove(key); } } private void insertIntoMDC(SlingHttpServletRequest request) { //logic can go here to take value from request object. // Can also utilize ResourceResolver from same request. // If needed, can also write a generic OSGi Configuration // (String Array Properties) to read from standard request objects, // i.e cookies, headers, and parameters. MDC.put(APPLICATION_ID,request.getParameter(APPLICATION_ID)); } public void destroy() { } }
This step actually helped uncover an issue in the latest AEM versions. At time of writing, AEM “Logging Logger Configurations” do not properly leverage the “Apache Sling Logging Logger” pattern variable. Instead, it references the “default” pattern, found in the configuration for org.apache.sling.commons.log.LogManager. As a work-around, we will update the “default” LogManager instead of updating the application-specific “Logging Logger” pattern. You can perform this update directly in AEM:
Or, my recommendation, create an associated OSGi configuration within your code repository’s associated config folder ( /apps/<project>/config
) named org.apache.sling.commons.log.LogManager.xml
with the following contents:
<?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="sling:OsgiConfig" org.apache.sling.commons.log.pattern="%d{dd.MM.yyyy HH:mm:ss.SSS} *%level* [%X{appId:-NoAppId}] [%thread] %logger %msg %ex%n" org.apache.sling.commons.log.file.size="'.'yyyy-MM-dd" org.apache.sling.commons.log.file="logs/error.log" org.apache.sling.commons.log.file.number="7" org.apache.sling.commons.log.level="info" org.apache.sling.commons.log.maxOldFileCountInDump="3" org.apache.sling.commons.log.numOfLines="10000" org.apache.sling.commons.log.maxCallerDataDepth="7" org.apache.sling.commons.log.packagingDataEnabled="{Boolean}false"/>
You will notice that in the above example I use a different format for the pattern string. One that may look more familiar is the default MessagePattern format:
{0,date,yyyy-MM-dd HH:mm:ss.SSS} {4} [{3}] {5}
In this default pattern, each number corresponds with a given data point, as described in the official docs. In our case, you see variables prefixed with a percentage sign. Odd! Well, fortunately, there is also a mapping for these, which we can also map using the official logback documentation.
The most interesting of the group is the %X
variable. This is what exposes your custom MDC variables. In the above example, %X{appId:-NoAppID}
” defines a placeholder for a property named “appId” as well as its default text, “NoAppId”. The standard for supplying defaults for any MDC object is to use the “:-” delimeter followed by the default text. Therefore, %X{appId}
would output an empty string if null, whereas %X{appId:-NoAppId}
would output “NoAppId” if null.
Similarly, to output all of the configured MDC variables, simply do not specify the variable you want to output: %X
. If configured this way, the entire MDC map would be output as a comma separated list.
For a much deeper look on all possible variable mappings, I would highly suggest taking a look at the official logback documentation, which I will link to again here: https://logback.qos.ch/manual/layouts.html#conversionWord
Once you’ve pushed the updated configuration and your custom sling filter, the changes should be active! If you followed the exact steps above, you’ll see a lot of “NoAppID” messages. Do not fret – this is because we never requested a page with the “appId” request parameter. For example, a simple we-retail request ( http://localhost:4502/content/we-retail/ca/en/experience.html ) should result in:
24.09.2018 12:41:36.819 *WARN* [NoAppId] [0:0:0:0:0:0:0:1 [1537818095824] GET /content/we-retail/ca/en/experience.html HTTP/1.1] com.adobe.granite.ui.clientlibs.impl.HtmlLibraryManagerImpl No library configured at /etc/designs/we-retail 24.09.2018 12:41:36.915 *INFO* [NoAppId] [0:0:0:0:0:0:0:1 [1537818095824] GET /content/we-retail/ca/en/experience.html HTTP/1.1] com.day.cq.wcm.core.impl.designer.SystemDesign Initialized system design at /etc/designs/default in 13ms 24.09.2018 12:41:38.432 *WARN* [NoAppId] [0:0:0:0:0:0:0:1 [1537818095824] GET /content/we-retail/ca/en/experience.html HTTP/1.1] com.adobe.granite.ui.clientlibs.impl.HtmlLibraryManagerImpl No library configured at /apps/clientlibs/granite/jquery-ui 24.09.2018 12:41:40.303 *INFO* [NoAppId] [0:0:0:0:0:0:0:1 [1537818100301] GET /home/users/Z/ZxNe0kWWedMwcd2ZAFhp.infinity.json HTTP/1.1] org.apache.sling.engine.impl.SlingRequestProcessorImpl service: Resource /home/users/Z/ZxNe0kWWedMwcd2ZAFhp.infinity.json not found
However, if we change the URL to add our appId, http://localhost:4502/content/we-retail/ca/en/experience.html?kp.appId=RMAPP, we get the following:
24.09.2018 12:43:39.305 *WARN* [RMAPP] [0:0:0:0:0:0:0:1 [1537818219292] GET /content/we-retail/ca/en/experience.html HTTP/1.1] com.adobe.granite.ui.clientlibs.impl.HtmlLibraryManagerImpl No library configured at /apps/clientlibs/granite/jquery-ui 24.09.2018 12:43:39.305 *WARN* [RMAPP] [0:0:0:0:0:0:0:1 [1537818219292] GET /content/we-retail/ca/en/experience.html HTTP/1.1] com.adobe.granite.ui.clientlibs.impl.HtmlLibraryManagerImpl No library configured at /etc/cloudsettings/default/contexthub.kernel 24.09.2018 12:43:39.309 *WARN* [RMAPP] [0:0:0:0:0:0:0:1 [1537818219292] GET /content/we-retail/ca/en/experience.html HTTP/1.1] com.adobe.granite.ui.clientlibs.impl.HtmlLibraryManagerImpl No library configured at /etc/designs/we-retail 24.09.2018 12:43:39.436 *WARN* [RMAPP] [0:0:0:0:0:0:0:1 [1537818219292] GET /content/we-retail/ca/en/experience.html HTTP/1.1] com.adobe.granite.ui.clientlibs.impl.HtmlLibraryManagerImpl No library configured at /apps/clientlibs/granite/jquery-ui
Obviously, the true power of these entries will show when leveraging JCR and the Resource Resolver. Potentially a topic for another day. Have fun and good luck tinkering!
I want to give appropriate recognition to the initial implementation in which this was derived: https://github.com/chetanmeh/sling-logback. Unfortunately, this package no longer functions without modification on an 6.3 instance. The logic for setting OSGi configuration properties for header, cookie, and parameters could be mirrored in this example if desired.
]]>You probably already know that Adobe added support for composite multifield in 6.3. Adobe also added support for nested multifield and nested composite multifield in AEM 6.4.
Take a look at the AEM 6.4 docs for multifield
Let’s define them
Allows authors the ability to add a list of items, each item let’s call it a fieldset, has only one field. Example, a list of emails.
Same as a normal multifield
, but can handle multiple fields in the fieldset. Example, a list of addresses where each address has multiple fields: street, city, state and zip.
As of 6.3+ Composite multifields are denoted with
composite="true"
attribute
A composite multifield
that contains a multifield
as one of the items in the fieldset. Example, a list of users where each user has a name and a list of social media links.
A composite multified that contains another composite multifield as one of the items in the fieldset. Example, a list companies that contain a list of departments, where each department has a name and manager.
+ companies + company1 - name + department1 - name - manager + department2 - name - manager ... + departmentN + company2 - name + department1 - name - manager ... + departmentN ... +companyN
Let’s look at how we can build a component that allows authoring and displaying the companies as described above:
Consider the following dialog XML:
<?xml version="1.0" encoding="UTF-8"?> <jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/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" jcr:title="Companies" sling:resourceType="cq/gui/components/authoring/dialog"> <content jcr:primaryType="nt:unstructured" 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"> <companies jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/multifield" fieldDescription="Click 'Add Field' to add a new company." composite="{Boolean}true"> <field jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/container" name="./companies"> <items jcr:primaryType="nt:unstructured"> <name jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/textfield" fieldLabel="Comp. Name" name="name"/> <departments jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/multifield" fieldDescription="Click 'Add Field' to add a new department." fieldLabel="Departments" composite="{Boolean}true"> <field jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/container" name="./departments"> <items jcr:primaryType="nt:unstructured"> <name jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/textfield" fieldLabel="Dep. Name" name="name"/> <manager jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/textfield" fieldLabel="Manager" name="manager"/> </items> </field> </departments> </items> </field> </companies> </items> </column> </items> </content> </jcr:root>
When using that dialog, you’ll see something like this:
When submitting that dialog, it will be stored into the following structure:
How can we model this structure with sling models? You could write a model where you iterate over the resource tree and create some data structure to make it easier to use in HTL. But that seems like a lot of work, is there really no better way?
sling models can handle collections so we can write a model like the following:
package com.sample.models; import java.util.List; import javax.inject.Inject; import org.apache.sling.api.resource.Resource; import org.apache.sling.models.annotations.DefaultInjectionStrategy; import org.apache.sling.models.annotations.Model; @Model( adaptables = {Resource.class}, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL) public interface CompaniesModel { @Inject List<Company> getCompanies(); // the name `getCompanies` corresponds to the multifield name="./companies" /** * Company model * has a name and a list of departments */ @Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL) interface Company { @Inject String getName(); @Inject List<Department> getDepartments(); // the name `getDepartments` corresponds to the multifield name="./departments" } /** * Department model * has a name and a manager */ @Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL) interface Department { @Inject String getName(); @Inject String getManager(); } }
I think this is pretty self explanatory, and really easy to reason about.
name
properties in your dialog xml, see comments in the code.@Inject List<Company> getCompanies();
we are basically telling sling, that under the component resource node, there is a node named companies
which has child nodes that should be modeled after (adapted to) Company
model.Interface
‘s here since there is no need to process any of the user inputs (no business logic), but you can switch that into class
‘s, just make sure that if you nest classes they are public
or don’t nest them and rather add them to their own class file.<sly data-sly-use.companiesModel="com.sample.models.CompaniesModel"/> <sly data-sly-test.empty="${!companiesModel.companies}" /> <div data-sly-test="${wcmmode.edit && empty}" class="cq-placeholder" data-emptytext="${component.title}"></div> <sly data-sly-test="${!empty}"> <div> <ul data-sly-list.company="${companiesModel.companies}"> <li>${company.name} <ul data-sly-list.department="${company.departments}"> <li> <b>Department:</b> ${department.name} <br/> <b>Manager</b>: ${department.manager} </li> </ul> </li> </ul> </div> </sly>
Which, after being authored, displays the following:
Hope this example helps you model complex nested composite multifields.
That’s it for now, till the next one!
Last week a new version of ACS AEM Commons, 3.14.0 dropped. This latest release included a new feature for creating configurable reports. These reports allow semi-technical users and administrators to create and execute custom reports and export the results as a Comma-Separated Values (CSV) file.
Content reports allow authors and content managers to get easy access to critical information such as:
Unfortunately, in AEM OOTB, this cannot be done without developer support. Now, with the ACS AEM Commons Report Builder, all of these reports can be created by any semi-technical author.
The ACS AEM Commons Report Builder is easy and intuitive to use. I wrote up extensive documentation on how to use if you have any questions, but there are essentially three ways to use the tool.
Website users can easily run reports, including providing parameters to configure the results.
Advanced authors and administrators can configure the report columns, parameters, and queries.
Developers can extend the Report Builder by adding new Report Executors, Column types, and Parameter types.
Report Builder is available in ACS AEM Commons version 3.14.0+ and is compatible with AEM 6.3+.
]]>
In Adobe Experience Manager (AEM), Granite UI is the foundation UI framework to build touch-enabled UI consoles and component dialogs. It provides a set of out-of-the-box (OOTB) components that you can use to build consoles or component dialogs. In this blog, I want to talk about a small tech gem that a lot of AEM developers are not aware of, Granite UI common attributes. In fact, I firstly encountered them in the AEM WCM core components, then when I was reading the Granite UI migration guide for the dialog conversion from Coral UI 2 to Coral UI 3 (AEM 6.2 to 6.3), I saw that this is listed to streamline the API and replace existing properties.
Note: this was tested on AEM 6.3. All the source code used in this blog can be found in my GitHub project.
To use Granite UI common attributes, you will add the following xml namespace to your console or dialog content.xml in order for it to build.
xmlns:granite="http://www.adobe.com/jcr/granite/1.0"
In your component development, you may use a lot of class and id attributes to apply styles and/or target your dialog field. Now you should use Granite UI common attributes instead. Below is a list of Granite UI common attributes that I think developer uses a lot in their component dialog:
granite:class
granite:id
granite:title
Note: this is for HTML title attribute. Do not confuse this with jcr:title, which is a JCR property name. This should not replace jcr:title attribute.
granite:hidden
Note: this is for HTML hidden attribute. Do not confuse this with granite:hide, which is another Granite UI attribute that can be used to dynamically render components. (see below section)
If you are a front-end web developer, you’ve probably known how much you use the HTML5 data-* attribute, either to target specific element, toggle elements, trigger events or to pass data from front end to back end…Similarly, as an AEM developer, you should know that data-* attribute can also be used to pass data for Adobe Analytics, component dialogs…
While you may have been already using data-* attributes in your component dialog by defining a custom unknown attributes, you should replace them with granite:data, in order to follow the best practices.
I have created two examples to demonstrate different use cases of granite:data attribute. They are both in the Icon component in my blog project.
One example of granite:data attribute is that I can use it to pass error message for field validation, continued with the dialog validator from my last blog. In my icon-picker-validation.js, I can simply call the data-error-message attribute that’s populated to the dialog field markup (see below section) and display the message when validator doesn’t pass. This way the error message can be extracted from the logic and set in the field, in case you want to reuse the validator with a different error message for another field.
The second example of granite:data attribute is that I can use it to show and hide dialog fields based on dropdown selection. And this comes OOTB (/libs/cq/gui/components/authoring/dialog/dropdownshowhide/clientlibs/dropdownshowhide/js/dropdownshowhide.js). You can implement this like the example fields below (no more event listeners or long inline extjs widgets api code like what you did in Classic UI).
<showhidedropdown granite:class="cq-dialog-dropdown-showhide" jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/select" fieldLabel="Show hide toggle" name="./showhidedropdown"> <granite:data jcr:primaryType="nt:unstructured" cq-dialog-dropdown-showhide-target=".text-showhide-target"/> <items jcr:primaryType="nt:unstructured"> <hide jcr:primaryType="nt:unstructured" text="Hide container" value="hide"/> <show jcr:primaryType="nt:unstructured" text="Show container" value="show"/> </items> </showhidedropdown> <extrafieldcontainer granite:class="hide text-showhide-target" jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/container"> <granite:data jcr:primaryType="nt:unstructured" showhidetargetvalue="show"/> <items jcr:primaryType="nt:unstructured"> <text jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/foundation/form/textfield" fieldLabel="Text" name="./text"/> </items> </extrafieldcontainer>
As the snippet shown above, the data-cq-dialog-dropdown-showhide-target is the hook, with value set to the class of the container, “.text-showhide-target”. The container also has “.hide” initially. This is working on the container level, however, it doesn’t work on individual field.
There’s more advanced things you can do to show and hide resource dynamically with FilteringResourceWrapper or render condition. FilteringResourceWrapper leverages granite:hide attribute with the wrapper class, while render condition uses granite:rendercondition node with script to define the render condition logic.
Most of the time you will not develop a Granite UI component since it’s not an open source project to the public. But in case you are doing custom dialog field or extending the OOTB Granite UI component, like the example I have above, icon picker component, it’s good to know that you can use the component helper api to populate Granite UI common attributes in the jsp and render those attributes.
Tag tag = cmp.consumeTag(); AttrBuilder attrs = tag.getAttrs(); cmp.populateCommonAttrs(attrs);
Granite UI common attributes help define component global attributes and provide the capability to apply and encapsulate Granite specific implementations. It’s recommended by Adobe to use these shared intentions in developing new server components. And more common attributes may be added for future releases of Granite UI. You should be aware of these attributes and start incorporating them into your Touch UI development.
]]>Making a list and checking it twice…
‘Tis the season for lists. Our children are making lists, and we too are staying organized with lists – both personally and while at work. Whether its all the people you have to buy presents for in your eight-sibling family, or a slightly larger (okay – much, much larger) undertaking such as re-platforming your CMS, there’s a lot to plan and account for to ensure success.
When it comes to the latter, there’s no doubt that designing, building, and implementing top-notch experiences requires a great deal of planning, strategy, and time. You also need to determine what is the right platform for your business.
As mentioned in the recent Forrester Wave for Web Content Management Systems (Q1 2017), features previously considered advanced, such as separation of content and presentation, what-you-see-is-what-you-get (WYSIWYG) authoring with drag-and-drop tools, and configurable approval processes, are now table stakes. Today’s platforms must include these features while also providing semantic insight, APIs, cloud, and continuous delivery.
Among the 15 web CMS vendors evaluated, Forrester recognized Adobe Experience Manager (AEM) as a leader. AEM falls under the Adobe Marketing Cloud, which is part of the larger Adobe Experience Cloud. It enables your organization to deliver experiences that are compelling, personal, useful, and everywhere.
Of course, this post isn’t about Santa coming to town, but we do have a list of our own to help you with your migration to AEM.
And if you’re still in the beginning stages of planning for your migration to Adobe Experience Manager, we have a comprehensive guide on the very topic, available below. If you are looking for a qualified partner to assist, feel free to reach out. Cheers!
]]>Oftentimes, AEM developers will be asked to develop a validator for the component dialog. Back in the Classic UI dialog days, you would probably write a JavaScript function for dialog before submitting an event. In Touch UI dialog, if you’ve Googled around, you probably found a lot articles/codes to use jQuery based validator, i.e. $.validator.register({})
. Recently, I found out that this jQuery-based validator is deprecated starting in AEM 6.2 (see screenshot below), where the new best practice is to use foundation-validation. In this blog, I am going to walk you through foundation-validation, and the things you can do with it, using a sample icon component. You can find all the source codes in my GitHub project.
The sample icon picker validator mentioned in this blog is tested in AEM 6.3.
A lot of the validation business requirements for dialog I’ve seen are for mandatory fields. If you are using Granite UI components, this, for the most part, is already resolved by setting required="{Boolean}true"
. But, for instance, if you have a multi-field, or RTE (resourceType of cq/gui/components/authoring/dialog/richtext), or an icon picker from ACS commons, or even your custom-developed dialog field, you may need to write a validator for a mandatory field and/or any other custom validation requirements.
If you are looking for an RTE validator that is easy to customize and scalable, we have a good blog post about it, which also uses foundation-validation.
There are two ways you can call the validator for your dialog field:
1. Validation/custom data attribute
Most Granite UI components have a validation attribute that you can use to trigger a custom validator. It adds a data-validation attribute to the component markup; then, in your custom validator, you can use the data-validation attribute and value as a selector to trigger the validation.
If you are not using Granite UI components, you can add any custom data attribute to your dialog field node and use that as a selector for your validator. Of course, you can also use the same “validation” attribute, but just to make sure the component is adding that data – or data-validation attribute to a -foundation-submittable element (refer to “What is –foundation-submittable?” section). If not, you can’t use it directly as a selector; instead, you will need to register a new selector to the foundation registry.
These validations will be triggered when the dialog submit button is clicked. It will stop the dialog submit event first, scan through the dialog to look for fields with a validator, validate those fields and show errors, if any. If all fields are valid, it will proceed with the dialog submit event.
<icon jcr:primaryType="nt:unstructured" sling:resourceType="acs-commons/components/authoring/graphiciconselect" fieldDescription="Icon to display" fieldLabel="Icon" name="./icon" class="icon-picker-base" validation="icon-picker"> <datasource jcr:primaryType="nt:unstructured" sling:resourceType="acs-commons/components/utilities/genericlist/datasource" path="/etc/acs-commons/lists/font-awesome-icons"/> </icon>
2. Foundation validation API
You can also trigger the dialog validation from foundation validation API. This can be tied to any event on the dialog field, and you can toggle the error UI based on the validity of the field. See example in “Even Better” section.
Basically it’s similar to the applicable elements in the jQuery validator, plus some coral and foundation elements (see the OOTB list screenshot below). Only these submittable elements can be used as a selector and trigger the foundation validation. You can still register a new selector if the data attribute is not in a –foundation-submittable field. For example, the data-validation attribute is added to a <span> element in icon picker.
You can register a custom selector if the dialog component is a composite field or is not adding the data-validation attribute to a –foundation-submittable element, i.e. input, select, button, textarea…
The best practice for validation selector is to use fast flat selector, i.e. “[data-validation=icon-picker]”. Below is the code snippet of how you can register a selector:
registry.register("foundation.validation.selector", { submittable: "[data-validation=icon-picker]", candidate: "[data-validation=icon-picker]:not([disabled]):not([readonly])", exclusion: "[data-validation=icon-picker] *" });
This is based on AEM 6.2 and 6.3:
/libs/granite/ui/components/foundation/clientlibs/foundation/js/coral/validations.js
/libs/granite/ui/components/coral/foundation/clientlibs/foundation/js/validation/validation.js
Foundation validation uses .adaptTo()
for adapting elements; it’s the same idea as Sling adapter. You can register an adapter for your element and return custom properties/functions. For example, this is useful when I need to set my icon picker field valid/invalid. I can create an adapter for my icon picker and encapsulate my logic there, so I can reuse it in other places.
function createGenericIsInvalid(el) { return function() { return el.attr("aria-invalid") === "true"; }; } function createGenericSetInvalid(el) { return function(value) { el.attr("aria-invalid", "" + value).toggleClass("is-invalid", value); }; } registry.register("foundation.adapters", { type: "foundation-field", selector: "[data-validation=icon-picker]", adapter: function(el) { var field = $(el); var button = field.children(".icons-selector"); var select = field.children("select"); return { isDisabled: function() { return select.prop("disabled"); }, setDisabled: function(disabled) { select.prop("disabled", disabled); input.prop("disabled", disabled); }, isInvalid: createGenericIsInvalid(field), setInvalid: createGenericSetInvalid(field) }; } });
Besides selector, there are validate, show, and clear properties. They are all pretty self explanatory. For validate, if it returns any string value, it means it’s invalid; otherwise it’s valid. Below is the snippet of my icon picker validator. I am validating if any icon other than the empty icon is selected from the icon picker, meaning this is a mandatory field.
registry.register("foundation.validation.validator", { selector: "[data-validation=icon-picker]", validate: function (element) { var field, value; field = $(element); value = $(field).find(".selected-icon>i").attr("class"); if (value == "fip-icon-block") { return "Please select the icon"; } else { return; } }, show: function(element, message, ctx) { $(element).closest(".icon-picker-base").adaptTo("foundation-field").setInvalid(true); ctx.next(); }, clear: function(element, ctx) { $(element).closest(".icon-picker-base").adaptTo("foundation-field").setInvalid(false); ctx.next(); } });
You can use foundation validation API to check validity and/or update U accordingly. For example, I want to check validity whenever someone selects a new icon from icon picker (instead of when they click dialog submit) and I want to toggle the red error triangle and message based on the validity of the select. I can bind the click event from icon selector, trigger a function to call the foundation validation API. I will find the icon picker element and adapt that to “foundation-validation,” and then I can call the foundation validation API.
var validateHandler = function(e) { var iconpicker = $(document).find("[data-validation=icon-picker]"); var api = $(iconpicker).adaptTo("foundation-validation"); if (api) { api.checkValidity(); api.updateUI(); } }; $(document).on("dialog-ready", function () { var container = $(this).find("div.fip-icons-container"); if (container.length > 0) { $('.fip-icons-container').on('click', function(){ setTimeout(validateHandler, 200); }); } });
Feel free to comment on your specific questions for foundation-validation. I understand that when you have to go through all the documentations and source codes without any working reference can be very challenging, so I hope this blog gives you pointers and examples to lighten up your Touch UI dialog validation implementation.
]]>If you have been using Adobe Experience Manager (AEM) to host your sites for a while, and are now looking to upgrade to AEM 6.3 (whether considering an in-place upgrade or a fresh new install), I am sure you’ve already found some good public documentation from Adobe. (If you are wondering if you should upgrade, here are some compelling reasons from Adobe, as well as my previous blog post.)
Recently, I worked on accessing the AEM 6.2 to 6.3 upgrade along with my colleagues for our client, and I want to share some additional thoughts you may find useful when upgrading to 6.3. Although this blog is for 6.3, the pointers and idea should really be applicable for an AEM upgrade in general.
Thorough planning and assessment are critical when upgrading the AEM platform for a large enterprise with a multi-tenant project, like my client. It should involve architects, project manager, dev manager, infrastructure, deployment teams and Adobe professionals to understand the scope, the impact, the approaches, the things that may break after the upgrade and the codes that need to be updated for each tenant, etc. A dedicated dev environment is recommended to set up for the upgrade in order to access and test the environment and codes. Upgrade learning and findings should be well documented and shared with all parties. After planning and assessment, your dev manager should decide if a dedicated technical release is required for the upgrade, assuming tenant teams are continuing new feature development for their products.
No matter how you created your project structure at first, probably from Adobe’s AEM maven archetype or ACS common’s lazybones AEM multi module project, you should update your POM files to use the latest and newest files, and to get deployed properly in AEM 6.3. Below is a list of dependencies and plugins that I found during my assessment that can be updated:
You may want to ask to which of the above dependencies and plugins needs to be updated. Great question. Good news, Adobe just released AEM project archetype 12 for 6.3 a couple days ago! AEM project archetype 12 is created to generate a project structure template for AEM 6.3 and above. You can create a new AEM project and reference to the POM files generated from archetype 12 to see exactly what you need to update. That’s what I did for this blog’s purpose and I pushed my code to my Github project.
After the update, run the maven build and make sure the build is successful and bundles are active state. Fix any issues.
Also, a side note: WCM IO has an AEM dependencies tooling that will provide you all maven dependencies (including those that are not defined by Uber Jar) for a specific AEM version. This can provide a safety net for your project in case you miss any dependencies in your POM, but it will not overwrite the ones that you explicitly declare in your POM. Maybe it’s a good idea for Adobe/developer to write an automatic tool for POM updates?
Even though your build might be successful, sometimes you can still find your application bundles in “resolved” status, most likely due to some of your bundles’ imported packages are outside of the supported range by OOTB AEM. While you are scratching your head to figure out why your application is not working, it’s always a good practice to validate your bundles status is at “active” state.
Below is a list of common components/features that break after we deploy our code to AEM 6.3:
As part of your technical upgrade assessment, you should look into your project codes and see if you are using any deprecated APIs in AEM 6.3.
Full deprecated API list can be referenced here: https://docs.adobe.com/docs/en/aem/6-3/develop/ref/javadoc/deprecated-list.html
API changes between AEM 6.2 and 6.3 can be referenced here: https://docs.adobe.com/docs/en/aem/6-3/develop/ref/diff-previous/changes.html
Deprecated and removed features list can be referenced here: https://docs.adobe.com/docs/en/aem/6-3/release-notes/deprecated-removed-features.html
Be aware that WCMUse has been deprecated since AEM6.1, so for component classes that need updating, it’s recommended to implement Sling Models instead of WCMUsePojo. How? You should reference to my How To Switch From WCMUsePojo To Sling Models blog series.
To make your AEM upgrade a more seamless project, Adobe’s developed pre-upgrade and post-upgrade tasks that you can run, just follow the instructions from Adobe. We tried to run the pre-upgrade task on AEM6.2, but it had this error below:
*ERROR* [sling-threadpool-3a1e6c9e-47ac-4e73-8445-85babb95cf17-(apache-sling-job-thread-pool)-33-Pre Upgrade Tasks Queue(com/adobe/aem/preupgrade/job/RunAllPreUpgradeTasks)] com.adobe.aem.upgrade.prechecks.tasks.impl.RunAllPreUpgradeTasksImpl RunAllPreUpgradeTasks task failed.
Solution: configure workflow purge configuration and auditlog configuration in configuration manager. (For more detail, see Prakash Venkatesh’s blog post.)
This tool was firstly developed by Adobe to upgrade the CRX from Apache Jackrabbit 2 base (CRX2) to Apache Jackrabbit Oak base (CRX3). But don’t let the name fool you, it can actually be run as a standalone tool to migrate content from one repository to another, even they are the same version.
This tool is run in command line, either as a new option when you start the 6.3 quickstart jar file, or as a standalone Java program. There are many parameters you can pass in to customize content migration. The latest full list of parameters can be found here. One advantage of using crx2oak to migrate content, versus a content package, is that you can keep the version history data, even orphaned history data.
Sounds great, right? However, this tool is not that easy to use, with limited documentation and help pages. I’ve tried to use the tool (crx2oak-1.6.8-all-in-one.jar) as a standalone and migrate content from 6.3 to 6.3 and from 6.2 to 6.3, and both had different kinds of exceptions (see below). I have seen the same issues reported by different users in Adobe forum, so be mindful of that, look for updates of the tool and consult with Adobe support team when you had difficulty using the tool on AEM 6.3.
I have seen the same issues reported by different users in the Adobe forum, so be mindful of that, and look for updates to the tool and consult with the Adobe support team if you have difficulty using the tool on AEM 6.3.
ERROR c.a.g.c.c.MigrationSpecGenerator: I/O error during processing migration specification: org.apache.jackrabbit.oak.segment.file.InvalidFileStoreVersionException: Using oak-segment-tar, but oak-segment should be used ERROR c.a.g.c.e.MigrationEngine: The internal error occurred during migration. The migration process has been aborted and it is not complete! java.lang.RuntimeException: javax.jcr.RepositoryException: Failed to copy content
After many trials and errors, I finally found a combination of commands that worked and had a successful run to migrate content from my local AEM 6.2 to AEM 6.3. Thanks to this post from Adobe help and this post in Adobe forum.
This is the command I use, in case you need to reference: (and I place crx2oak-1.6.8-all-in-one.jar and logback.xml in the same directory as AEM 6.3 quickstart jar)
java -Dlogback.configurationFile=logback.xml \ -Xmx10g \ -jar crx2oak-1.6.8-all-in-one.jar \ segment-old:/Users/eddieyao/Projects/perficient/blog/author-aem-6.2/crx-quickstart/repository /Users/eddieyao/Projects/perficient/blog/author-aem-6.3-new/crx-quickstart/repository \ --include-paths=/content \ --datastore /Users/eddieyao/Projects/perficient/blog/author-aem-6.3-new/crx-quickstart/repository/datastore \ --copy-versions=true \ --copy-orphaned-versions=false
Overall, an AEM upgrade is not an easy task, especially when you have a multi-tenant project, many custom implementations, integrated with many solutions and configured Mongo DB or node and data stores. It requires extensive assessment, strategic planning, thorough testing, strong collaboration and technical implementation. It’s always a good idea to have your AEM experts around, form your team with a trusted Adobe solution partner and consult with Adobe for custom recommendations when doing the upgrade.
In earlier releases of Adobe Experience Manager (AEM), there’s no way of knowing whether the editing page is in the workflow process. Authors need to go into workflow inbox to look and see if they have any pages/items assigned as part of the workflow. With the release of AEM 6.3, Adobe has increased productivity with faster workflow-related tasks and the ability to notify authors about the workflow during page editing. Whether the page is added to the workflow through launcher, workflow payload, or start workflow, editing authors will see the workflow notifications on the page (until the workflow completes all the stages).
Once an author opens the page during workflow execution, they may see a blue bar on the page with the following message:
This notification indicates this page is under workflow and there are actions that need to be performed by authors (or any other user group) who are part of this workflow participant steps. On the right side of the notification, there will be a number with arrows for navigation if this page is subject to more than one workflow. Authors can navigate using arrows and perform the actions on the selected workflow.
On the notifications, authors may see links that require actions to be completed for workflow. Those actions include:
Complete: Authors can complete an item to allow the workflow to progress to the next step. In a normal workflow process, authors will use this action to go to next step.
On this action you can indicate:
Step Back: This step is optional; not all the workflows will have this step. If you discover that a step, or series of steps, needs to be repeated you can step back. This allows you to select a step that occurred earlier in the workflow for reprocessing. The workflow returns to the step you specify, then proceeds from there.
On this action you can indicate:
Delegate: If a step has been assigned to you, but for any reason you are unable to take action, you can delegate the step to another user or group.
On this action you can indicate:
View Details: View details of the workflow work item and take appropriate actions. The workflow details are shown in tabs and appropriate actions are available in the toolbar. One cool feature of workflow info tab is stages. If workflow stages have been configured for the model, you can view the progress according to these in workflow info tab.
I will demonstrate how to create stages as a workflow modeler and display to the participant users in the workflow. Let’s take a simple OOTB workflow request for activation and add stages.
Are you also loving the new workflow features of AEM 6.3? What feature is your favorite? Drop us a comment below and let us know.
]]>You may have been on Adobe Experience Manager (AEM) for more than three years, no matter if you are an admin, developer or author, you probably got used to the legacy Classic UI interface and dialogs. Personally, I tend to use the Classic UI site admin console to view/manage content pages, because I like how it easily lets me navigate through the tree and give me a lot of page information in one panel. However, with release of AEM 6.3, Adobe officially announced that Classic UI will be deprecated in April 2018, and completely removed from AEM in April 2019. This means that Adobe will explicitly display the deprecation message in Classic UI in next year’s release, and it will be completely gone from OOTB AEM in 2 years. With that said, it may be sooner than you thought if you don’t put it in your roadmap and take action for architecture, development and trainings, especially if you are still on or heavily rely on Classic UI. Related to this topic, I am going to try out the new dialog conversion tool (version 2) Adobe developed and released in June this year in this blog, and let you know the enhancements, concepts, steps, test result and pros and cons.
(dialog conversion tool v2 user interface)
First of all, AEM dialog conversion tool v2.0.0 can convert both Classic UI dialogs and Granite UI/CoralUI 2 dialogs to Granite UI/CoralUI 3 dialogs. If you have not already known, AEM’s touch-enabled UI is built with Adobe’s Granite UI and CoralUI. CoralUI is the front-end implementation (HTML, CSS, JS) of Adobe’s visual style for touch enabled UI. It is developed to provide consistent UX among different Adobe products (like many of Adobe Experience Cloud products). Granite UI components are server side components that are built with CoralUI, CRX, Apache Felix and Apache Sling technologies. They are used in Granite-based platform like AEM. They are also reusable and modular building blocks for most Touch UI dialogs and consoles in AEM. So if you want to write a custom Touch UI console or dialog that has the same look and feel as the OOTB AEM environment, you can either assemble and configure different Granite UI components, or write CoralUI html and define custom action and services.
CoralUI 3 is the latest version of CoralUI that comes with AEM 6.3. If you are on AEM 6.2, it has partially CoralUI 2 and 3, as it was transitioning to 3. AEM 6.1 has CoralUI 2. To check version of CoralUI, see your AEM developing reference materials and Touch UI documentation.
Now you may wonder what is the difference between CoralUI 3 and 2? Answer is not much. Except some platform and component changes, one of the major changes I found is that CoralUI 3 uses web component’s custom elements. This encapsulates the detailed presentation markups and behavior into custom HTML tag, so developers can write coral elements in the HTML file and don’t need to worry about how it works behind the scene. For example, if I need a checkbox in my page, I’ll just write:
<coral-checkbox value="kittens"> CoralUI Rocks </coral-checkbox>
On CoralUI 2, it uses generic HTML tags with CoralUI classes and some with data attributes. For example, the same checkbox will look like this:
<label class="coral-Checkbox"> <input class="coral-Checkbox-input" type="checkbox" name="c2" value="2"> <span class="coral-Checkbox-checkmark"></span> <span class="coral-Checkbox-description">Unchecked</span> </label>
Another noticeable change is that on AEM 6.2, the component Touch UI dialog has dark grey background color, and on AEM 6.3, it’s white.
(touch ui dialog in aem 6.3) (touch ui dialog in aem 6.2)
For the complete CoralUI 3 enhancements, see the CoralUI 3 documentation.
I have tested and installed the tool on AEM 6.1-6.3 instances. Unfortunately, it only works with 6.3. There are different errors on 6.1 and 6.2. On 6.2, you won’t be able to see the path field to search for dialogs, and didn’t show any dialogs when click on show dialogs button. On 6.1, there’s a package dependency error that requires a higher version of AEM product codes.
(path field not shown in AEM 6.2) (package dependency issue in AEM 6.1)
Use of the tool is simple, go to Global Navigation -> Tools -> Operations -> Dialog Conversion. Put in the path of your component root folder and it will list all dialogs with type classic or coral 2 (Granite UI/CoralUI 3 dialogs will not be listed). Then just select the ones you want to convert and click convert button. If success, it will prompt you to another screen with converted dialog information. If not, it will show you the error message.
After that, you can simply use FileVault (VLT), either command line or the VLT plugin in your IDE, to get the converted dialog from CRX to your code.
I understand that your Classic UI dialog, or even Granite UI/CoralUI 2 dialog may have custom dialog fields (or properties). These fields may be extended from OOTB Ext JS widgets, Granite UI components, or custom developed scripts. Custom xtype or sling:resourceType will be processed by the conversion tool as it is unless you have a custom rewrite rule associated with it. For that, I think it’s necessary to know more about the dialog conversion tool behind the scene.
The dialog conversion tool is built on the concept of graph rewriting and rewrite rule, which is a paring of a pattern and a replacement graph.
The tool will take all the selected dialog paths from request parameter and send them to a dialog rewriter class, in which it’s performing the node rewriting algorithm. The basics of the algorithm is that it will traverse all the dialog nodes from top level, all the way to bottom, see if there’s a matching rule for the subtree rooted at each level, then overwrite the node based on matching rule, and start the traversal from top node again; if no match is found, it will keep the original node as it is; traversal will stop until all nodes are final (each node that is processed will be put into a linked hash set, so it will be skipped by the tool in the next traversal).
There are two types of rewrite rules: JCR node based and Java class based.
The dialog conversion tool already provides good amount of OOTB rewrite rules: 3 Java based, 21 JCR node based for Classic UI and 35 JCR node based for Granite UI/CoralUI 2.
Properties of the xtype or sling:resourceType can also be defined in the rewrite rules, otherwise they will be omitted by the tool. The replacement tree can define mapped properties that will inherit the value of a property in the original tree. Here’s an example: /libs/cq/dialogconversion/rules/classic/textfield/replacement/text
. If your Classic UI dialog text field has a field label property named “./fieldLabel”, the value you defined for that field in Classic will be copied to the converted Granite UI/CoralUI 3 dialog.
You can copy /libs/cq/dialogconversion/rules
to /apps to modify existing and/or add new rules to this new instance. You can also implement com.adobe.cq.dialogconversion.DialogRewriteRule
interface or extend com.adobe.cq.dialogconversion.AbstractDialogRewriteRule
class.
I will try not to go over complicated with custom rewrite rules. In my opinion, if you spend a lot of time configuring custom rewrite rules or even write a custom rewrite rule class, it’s better to let the tool convert the xtype/sling:resourceType as it is and then spend the time to develop the Granite UI/CoralUI 3 dialog directly, as the tool is supposed to aid your dialog conversion process, it does not and can not take full control of the conversion.
I have tested the tool with Classic UI dialog, design dialog and a Granite UI/CoralUI 2 dialog. The result is that all three were successful. The tool is able to convert most of the OOTB xtype and sling:resourceType. I just found that it omits the fields inside a multifield. For custom xtype and sling:resourceType, it kept as it is. The Granite UI/CoralUI 2 dialog node will be appended with “.coral2” in name after the conversion.
I have put the sample component dialogs I used to test and the converted dialogs onto my Blog GitHub project: https://github.com/guangweiyao/blog. Feel free to check them out.
(converted touch ui dialog)
Pros:
Cons:
/libs/cq/dialogconversion/rules
to /apps
, it doesn’t support the newer overlay mechanism/libs/cq/dialogconversion/rules
to /apps
in order to modify existing and/or add new rulesDespite some space for improvement, I think the new AEM Dialog Conversion Tool Version 2 can help you or your client accelerate the component dialog conversion process and align with Adobe’s Classic UI deprecation timeline, so you can plan for paying down your technical debt and building beautiful and touch-enabled UI component dialogs.
]]>