Monday, May 12, 2014

Sling I18n and IDE integration

CQ uses Sling I18n support plus provides a number of its own useful utilities/classes. For example it has Translator Tool for convenient editing localized resources directly in running CQ instance, it has I18n class which helps you to programmatically extract localized strings using standard java ResourceBundle dictionaries, it has JSP tag <cq:setContentBundle> for setting I18n localization context and some other useful features.

Inside your JSPs you can use standard JSTL fmt:message tag to retrieve localized string, i.e.
<fmt:message var="placeholderMsg" key='components.button.placeholder'/>

What frustrates me though, is the lack of IDE integration. Sling/CQ uses hierarchical structure for representing resource bundles as compared with standard properties-based approach. Therefore there is no out-of-the-box support inside modern IDEs: you cannot use key completion feature, IntelliSense, automatic relocation to the localized string inside your resource file, etc. For example, IntelliJIDEA always shows keys inside fmt:message tag in red color, because it cannot interpret xml files as resources.

Pic 1. No Sling I18n integration in IDE 

Simple solution I found is to use standard properties files for I18n support, so IDE can work with it using its "out-of-the-box properties editor", and later convert these properties files to Sling XMLs when you create CQ package. For maven-based project you can use standard exec-maven-plugin; the same is true for ant-based projects (exec task).

Example of maven plugin description:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>1.3</version>
    <executions>
        <execution>
            <phase>compile</phase>
            <goals>
                <goal>java</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <mainClass>com.yvv.cq5.build.I18nPropertiesConverter</mainClass>
        <classpathScope>compile</classpathScope>
        <arguments>
            <!-- First argument is the folder with properties files to be read -->
            <argument>${basedir}/src/main/content/jcr_root/apps/myproject/i18n</argument>
            <!-- Second argument - output folder for converted XML files -->
            <argument>${basedir}/target/vault-work/jcr_root/apps/myproject/i18n</argument>
        </arguments>
    </configuration>
</plugin>

I18nPropertiesConverter is a simple java utility which checks input folder (argument 1) for the existence of properties files, reads them and converts data into sling xml files which will be written to the output folder (argument 2). This utility can be downloaded here.

The result is full I18n support of your project which comes in your IDE "for free". Below you can see how it looks like in IDEA (11.*).


Pic 2. Out-Of-The-Box I18n integration in IDE 

Wednesday, April 30, 2014

Dropdown initialization inside CQ dialogs

If you need to open component configuration dialog which contains a dropdown, you obviously have to initialize dropdown with the value which user selected previously. ExtJS 3, which is extensively used in CQ5.* author environment, provides listener "selectionchanged", which works fine for checkboxes and radio buttons, but doesn't work for dropdowns. There's another listener which does this job very well - "afterlayout".

Example below shows how to initialize a dropdown with 2 values in the dialog form. By default "Object-Oriented" value is selected, however if the user has previously selected other value, it will be set using aforementioned listeners.

<type jcr:primaryType="cq:Widget"
      name="./type"
      fieldDescription="Enter type"
      fieldLabel="Type"
      type="select"
      defaultValue="oop"
      xtype="selection">
    <options jcr:primaryType="cq:WidgetCollection">
        <parallax jcr:primaryType="nt:unstructured"
                  text="Object-Oriented"
                  value="oop"/>
        <normal jcr:primaryType="nt:unstructured"
                text="Functional"
                value="function"/>
    </options>
    <listeners
        jcr:primaryType="nt:unstructured"
        afterlayout="function(component){
            initializeType(component);
            }"
        selectionchanged="function(selection, value, isChecked){
            initializeType(selection);
            }">
</type>

where initializeType is defined in a separate javascript file which is located inside clientlib:

initializeType = function(component) {
    if (component && component.getValue()) {
        if (component.getValue() === 'oop') {
            // do some OOP logic 
        } else if (component.getValue() === 'function') {
            // do some function logic ;-)
        }
    }
};

Tuesday, April 29, 2014

CQ Page Properties from Javascript

To get CQ page properties inside javascript you can use core CQ JS API. It can be convenient if you need to get this information inside your custom JS widgets.

            var pageData = CQ.HTTP.get(CQ.HTTP.externalize(CQ.utils.WCM.getPagePath() + "/jcr:content.json"));

After that you can retrieve any property you need (assuming it's present in JCR):


            var resourceType = pageData ? CQ.Util.formatData(CQ.HTTP.eval(pageData))['sling:resourceType'] : null;

Please do not overuse it because it invokes additional ajax call to server. It's OK to use it in edit mode on author instance.

EditConfig tips

A couple of months ago I read very good article from Dan Klco about EditConfigs. Some weeks later I had to practice it myself, so here are some tips from my side.

1) Custom listeners. Some of my components needed to execute javascripts after their configuration is changed. There are build-in listeners shortcuts:

  • REFRESH_PAGE, 
  • REFRESH_PARENT, 
  • REFRESH_SELF, 
  • REFRESH_SELFMOVED, 
  • REFRESH_INSERTED
Most important and widely used are first three. Refreshing whole page is generally a bad idea as it's a bad user experience. Ideally you should use refresh_parent or refresh_self and only a part of the page will be updated via ajax call.
In my case it was not enough to use these shortcuts, because apart from refreshing a parent container, some page-scoped javascripts need to be executed. Therefore I had to write my own listener.

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0"
          cq:actions="[text:My Component,EDIT,DELETE,COPYMOVE]"
          cq:layout="editbar"
          jcr:primaryType="cq:EditConfig"
          cq:disableTargeting="{Boolean}true"
          cq:dialogMode="floating">
    <cq:listeners
            jcr:primaryType="cq:EditListenersConfig"
            afterinsert="function(path, definition) { CQWidgets.refreshParent(this); }"
            afteredit="function(path, definition) { CQWidgets.refreshParent(this); }"/>
</jcr:root>

where CQWidgets is a separate JS file which is included inside clientlib in edit mode.

var CQWidgets = function () {

    return {
        refreshParent: function (editBar) {
            editBar.refreshParent();
            MyComponentController.initializeSliders(); // this is where my javascript logic is being executed
        }
    }
}();


2) EditConfig actions.
<TBD>

Content Finder Visibility

By default Content Finder is visible only for the following paths:

  • "/content/*",
  • "/etc/scaffolding/*",
  • "/etc/workflow/packages/*"

You can check permissions for each individual tab inside Content Finder here - "/libs/wcm/extensions/contentfinder".

Sometimes you need to have administration page of your application which usually resides under /etc/<your_app_name> folder. Therefore you will NOT see content finder in edit mode.
In order for it to appear there you will have to either customize existing scripts (by copying it under /apps) or - better way - you will create special folder in your application which has properties extensionType=contentfinder_extension and extensionGroup=tabs. It tells ContentFinder manager to include scripts under this folder as separate tabs.

I personally prefer to keep folder structure similar to original CQ hierarchy, so inside my application I create folder hierarchy:
<app_folder>
    wcm
        extensions
            contentfinder (which has properties above)

Under contentfinder I will then add my own tabs. They can be the same as default ones (f.e. images.js) or include my own custom logic. In both cases it is required to set allowedPaths property inside these scripts which points to your page's path. For example:

    "allowedPaths": [
        "/etc/myproject/*"
    ]

Sidekick Customization

Sometimes it is necessary to override default behaviour of CQ Sidekick. For example, you need to reload the page when author presses "Preview" button to apply some javascript logic or rename buttons in Sidekick, etc.

There are two ways to do it:
1). Straightforward (ugly). You need to copy script init.jsp from "/libs/wcm/core/components/init" directly under your application folder, customize it and include it from your page. (You can also copy this script under /apps but that would affect all other applications on this CQ instance).
Let's reload the page when author clicks on Preview button.

For that we need to change these lines:
    CQ.WCM.launchSidekick("<%= currentPage.getPath() %>", {
        propsDialog: "<%= dlgPath == null ? "" : dlgPath %>",
        locked: <%= currentPage.isLocked() %>,
        previewReload: "true"
    });
Notice previewReload property, it does the trick.

Next we need to include this script from our page component:
    <cq:include script="/apps/skywalker/init.jsp" />

2) Programmatic (preferrable approach).
Inside your page component please use the following code:

<script>
    function checkSidekickStatus() {
        if (CQ.WCM.isSidekickReady()) {
            CQ.WCM.getSidekick().previewReload = true;
            clearTimeout(timeout);
        }
    }
    var timeout = setInterval(checkSidekickStatus, 1000);
</script>

In other words, we wait until sidekick is loaded and then modify its properties. In this case we set previewReload to true. To check all available properties of sidekick please visit http://dev.day.com/docs/en/cq/current/widgets-api/index.html and search for "sidekick" there.