Customize the Client Context

——————————————————————————————————

September 20:

Important change: if you installed the code before please check the new code. The modifications solve the issue with the segment definition :

  • file json.jsp: sample data are in a different format in order to enable the use of the component “Generic Store Property” when defining a segment
  • customstore.js: the loadData function has been modified to take in account the new data format.
  • customstore_ui.js: the setTraitValue function has been modified for the new data format.

Section 9 has been modified. The “Generic Store Property” component is now used.

——————————————————————————————————

This article is the first of a series on Adobe Experience Manager 5.6. My objective is to write some simple step by step tutorials for developpers starting with Adobe Experience Manager (AEM). As usual the reference is the documentation. It’s available here.

I’ll start by the customization of the Client Context. The Client Context (CC) is a key component of the AEM personnalization framework. The first time, the Client Context appears quite complex to extend but it’s so powerful that it’s worth doing it! I hope that this article will help to start.

The use case for the tutorial is to personnalize content based on information retrieved from an external CRM system. On the author instance the editor must be able to change the value of the information for simulation. Of course we’ll simulate the external CRM system and we’ll concentrate on the Client Context only.

So we need two things:

  1. A way to retrieve and keep the data, both on the author and publish instances. This is the role of the Session Store object. It’s a JavaScript object created on the client side. It has also a back-end part to retrieve data from the CRM system.
  2. A way to display data in the Client Context. This is the role of the Context Store component.

There are several methods to implement such a use case. Whenever possible I prefer the manual method, i.e trying to implement everything: it’s less productive than copy/paste existing components, but when things go wrong it’s safe to understand how things work together.

Step 1: prepare for debugging

Creating a new session store component means working with JavaScript. It’s always a good idea to change the default settings for the HTML Library Manager for debugging.
Open the OSGi console, choose “OSGi>Configuration” and search for “HTML Libray Manager”. Set these settings:

  • enable Debug
  • enable Debug console

Remove the /var/classes and /var/clientlibs folders with the CRXDE. Also remove the cache from your browser.

Step 2: create the Session Store

The Session Store is a javascript object available both on the author and publish instances. Because we don’t want the Context Store component to be loaded on the publish instance (remenber it’s only used by editor for simulation), we create two javascript libraries with the following categories:

 Library name Categories
kernel personalization.stores.kernel
ui personalization.stores.ui

Setting the categories assure that the libraries are correctly loaded by the system when needed (the kernel on both author and publish instances and the ui only on the author instance.)

In the kernel library, create the customstore.js file with the following code:

// Create the session store called "customstore"
if (!CQ_Analytics.CustomStoreMgr ) {<
 // Create the session store as a JSONStore
 CQ_Analytics.CustomStoreMgr = CQ_Analytics.JSONStore.registerNewInstance("customstore");
 // Function to load the data for the current user
 CQ_Analytics.CustomStoreMgr.loadData = function() {
  console.info("Loading CustomStoreMgr data");
  var authorizableId = CQ_Analytics.ProfileDataMgr.getProperty("authorizableId");
  var url = "/apps/customstore/components/loader.json";
  url = CQ_Analytics.Utils.addParameter(url, "authorizableId", authorizableId);
  try {
   var object = CQ.shared.HTTP.eval(url);
   if (object) { this.data = object; }
  } catch(error) { console.log("Error", error); } };
}

The code is quite strait forward. The JSONStore component stores data as a json object. The loadData() function retrieves data from the back-end using the
“/apps/customstore/components/loader.json” URL and set the data into the store.

Step 3: simulate the CRM system

The CRM system is called from the client (the session store component, ie the loadData function) and send back the JSON object. For our simple use case, the CRM is simulated by a jsp page. In real life, it could be a JSP, a servlet,…

Remember that Apache Sling comes into the game with AEM when calling the URL “/apps/customstore/components/loader.json”.

With the CRXDE create a new component named “loader” in /apps/customstore/components. Set the property “sling:resourceType=/apps/customstore/components/personnalization/loader”.

Create a json.jsp file in the /apps/customstore/components/personnalization/loader directory. It’s this file that will be excuted when calling the URL “/apps/customstore/components/loader.json”.

Code for json.jsp:


<%@ page session="false" import="com.day.cq.security.User,
                    com.day.cq.security.profile.Profile,
                    com.day.cq.security.profile.ProfileManager,
                    org.apache.sling.commons.json.io.JSONWriter"
%><%@include file="/libs/foundation/global.jsp" %><%

    response.setContentType("application/json");
    response.setCharacterEncoding("utf-8");

    String authorizableId = request.getParameter("authorizableId");

    Profile profile = null;
    ProfileManager pMgr = sling.getService(ProfileManager.class);

    JSONWriter w = new JSONWriter(response.getWriter());

    w.object();

	if (!authorizableId.equals("anonymous")) {

		if (authorizableId != null) {
            try {
                profile = pMgr.getProfile(authorizableId, resourceResolver.adaptTo(Session.class));
            } catch (RepositoryException e) {
                slingResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED, "");
            }
        } else {
			profile = resourceResolver.adaptTo(User.class).getProfile();
        }

        if (profile != null) {

            // here we simulate the result from a back-end system</pre>
w.key("AUTOMATICPAYMENTDEBIT/key").value("AUTOMATICPAYMENTDEBIT");
w.key("AUTOMATICPAYMENTDEBIT/label").value("Automatic payment debit");
w.key("AUTOMATICPAYMENTDEBIT/value").value("true");
w.key("ONLINESTATEMENT/key").value("ONLINESTATEMENT");
w.key("ONLINESTATEMENT/label").value("Online statement");
w.key("ONLINESTATEMENT/value").value("false");
<pre>        }

	}

	w.endObject();

%>

Step 4: time for test!

Let’s check that everything is correctly set. Open any sample site (like Geometrixx Outdoors), and open a javascript console. I use the Chrome console.

1: Check that the session store is correctly created. If not, you have a javascript error or you didn’t set the category.

2: Call the loadData function and verify that the data are correctly retrieved.

LoadData

Step 5: create the UI part

For now the session store is almost finished. Let’s work on the user interface, the Context Store component. Create a new CQ component in /apps/customstore/components called “customcontextstore” with the properties :

  • sling:resourceSuperType = cq/personalization/components/contextstores/genericstoreproperties
  • componentGroup = Client Context

Using the genericstoreproperties component and not the jsonpstore, means that we’ll have to manage by ourselves the display of the properties. That’s the funny part ;-)

Create the customcontextstore.jsp file for the component:

<%--   customcontextstore component. --%><%
%><%@taglib prefix="personalization" uri="http://www.day.com/taglibs/cq/personalization/1.0" %><%
%><div class="cq-cc-store"><personalization:storeRendererTag store="customstore"/></div>

The tag <personalization:storeRendererTag> generates a script like this one:

<div id="current_page_path-customstore" class="cq-cc-customstore"></div>
<script type="text/javascript">
if (window.CQ_Analytics) {
 CQ_Analytics.ClientContextUtils.renderStore('current_page_path-customsore','customstore');
}
</script>

The renderStore method calls the render method on the session store and register a method on the update event (check the code in the javascript console.) So we only need to implement a renderer method. But we only want this method to be loaded on the author instance, so we implement this method in the ui library.

Create a file customstore_ui.js in the ui library:

if (CQ_Analytics.CustomStoreMgr ) {

    // HTML template
    CQ_Analytics.CustomStoreMgr.template =
        "<input class='customstore-input' type='checkbox' id='customstore-input-%key%' name='%key%' value='%key%' %checked%>" +
        "<label for='customstore-input-%key%' class='%checkedClass%'>" +
        "<div class='toggle'><div class='green'></div><div class='red'></div></div>" +
        "%label%</label>";

    CQ_Analytics.CustomStoreMgr.templateRenderer = function(key, label, value) {

         var checkedString = ""; var checkedClass = "";
         if (value==="true") {
             checkedString = "checked='checked'";
             checkedClass  = "checked";
         }
         var template = CQ_Analytics.CustomStoreMgr.template;
         return template.replace(/%label%/g, label)
             .replace(/%key%/g, key)
             .replace(/%checked%/g, checkedString)
             .replace(/%checkedClass%/g, checkedClass);
     }

    CQ_Analytics.CustomStoreMgr.renderer = function(store, divId) {

        // first load data
		// CQ_Analytics.CustomStoreMgr.loadData();

		$CQ("#" + divId).children().remove();

		var name = CQ_Analytics.ProfileDataMgr.getProperty("formattedName");
		var templateRenderer = CQ_Analytics.CustomStoreMgr.templateRenderer;

        // Set title
		$CQ("#" + divId).addClass("cq-cc-customstore");
		var div = $CQ("<div>").html(name + " services");
		$CQ("#" + divId).append(div);

		var data = this.getJSON();

        if (data) {
            for (var i in data) {
                if (typeof data[i] === 'object') {
                    $CQ("#" + divId).append(templateRenderer(data[i].key,data[i].label,data[i].value));
                }
            }
        }

    }

    CQ_Analytics.CustomStoreMgr.setTraitValue = function(trait, newValue) {

        var data = CQ_Analytics.CustomStoreMgr.data;
        if (data) {
            data[trait + '/value'] = newValue;
        }
    };

}

Create also a default theme for the ui library with the css below:

.cq-clientcontext .cq-cc-customstore {
    line-height: 23px;
}

.cq-clientcontext .cq-cc-customstore input[type=checkbox] {
    display: none;
}

.cq-clientcontext .cq-cc-customstore label {
    padding: 2px 5px;
    margin: 2px 4px 2px 0px;
    font-size: 12px;
    border-radius: 5px;
    display: inline-block;
    background-color: #444;
    color: #ccc;
}

.cq-clientcontext .cq-cc-customstore label:hover {
    background-color: #393939;
}

.cq-clientcontext .cq-cc-customstore label.checked {
    background-color: black;
    color: white;
}

.cq-clientcontext .cq-cc-customstore label.checked:hover {
    background-color: #222;
}

.cq-clientcontext .cq-cc-customstore label .toggle {
    display: inline-block;
    width: 16px;
    background-color: #555;
    height: 9px;
    border-radius: 5px;
    top: 1px;
    margin-right: 5px;
    border: 1px solid #777;
    position: relative;
}

.cq-clientcontext .cq-cc-customstore label .green,
.cq-clientcontext .cq-cc-customstore label .red {
    background-color: #D00;
    width: 9px;
    height: 9px;
    border-radius: 5px;
    display: block;
    float: right;
}

.cq-clientcontext .cq-cc-customstore label .green {
    background-color: #0D0;
    display: none;
    float: left;
}

.cq-clientcontext .cq-cc-customstore label.checked .red {
    display: none;
}

.cq-clientcontext .cq-cc-customstore label.checked .green {
    display: block;
}

Step 6: test
Reload your test page. The CustomStoreMgr session store should now have the new methods. It means that the ui part has been correctly loaded. Add the context store component in the client context.

You should see the title for the current user:

ClientContextStep6

In the javascript console, load the data and trigger an update event. The data are displayed!

ClientContextStep61

That’s it we are almost done. The final step is to load data automatically and keep the display correctly updated each time we change the profile. We also want the editor to change the value of the data (by a simple click.)

Step 7: keep everything synchronized

It’s not the easiest part. Code is quite easy to write but trying not to fire an event twice is a challenge.

  • Let’s update the data each time we change the current profile in the Client Context. Add this code in the customstore.js
CQ_Analytics.CCM.addListener("configloaded", function()
{ CQ_Analytics.ProfileDataMgr.addListener("update", function()
    {
      this.loadData();
      this.fireEvent("update");
    },
   CQ_Analytics.CustomStoreMgr);},
CQ_Analytics.CustomStoreMgr
);
  • Load the data for the first time. Add this code in customstore.js
CQ_Analytics.CustomStoreMgr.addListener("initialize", function() {
    this.loadData();
 });
CQ_Analytics.CustomStoreMgr.init();

In order to avoid to call several time the loadData method, we need to keep the current profileId and call the method only it it has changed.
We also need to retrieve data only if the component Context Store has been added into the Client Context. I use the variable “initialized” (boolean) but we could use any instance variable.
The customstore.js becomes:


// Create the session store called "customstore"
if (!CQ_Analytics.CustomStoreMgr ) {

    // Create the session store as a JSONStore
    CQ_Analytics.CustomStoreMgr = CQ_Analytics.JSONStore.registerNewInstance("customstore");

	CQ_Analytics.CustomStoreMgr.currentId = "";

    // Function to load the data for the current user
    CQ_Analytics.CustomStoreMgr.loadData = function() {

        var authorizableId = CQ_Analytics.ProfileDataMgr.getProperty("authorizableId");
        var url = "/apps/customstore/components/loader.json";

        if ( (authorizableId !== CQ_Analytics.CustomStoreMgr.currentId) & CQ_Analytics.CustomStoreMgr.initialized ) {

			console.info("Loading CustomStoreMgr data");

            url = CQ_Analytics.Utils.addParameter(url, "authorizableId", authorizableId);

            try {

                var object = CQ.shared.HTTP.eval(url);
                if (object) { this.data = object; }

            } catch(error) {
                console.log("Error", error);
            }

			CQ_Analytics.CustomStoreMgr.currentId = authorizableId;

        }

    };

    CQ_Analytics.CCM.addListener("configloaded", function() {

        CQ_Analytics.ProfileDataMgr.addListener("update", function() {
			this.loadData();
            this.fireEvent("update");
        }, CQ_Analytics.CustomStoreMgr);

	}, CQ_Analytics.CustomStoreMgr);

    CQ_Analytics.CustomStoreMgr.addListener("initialize", function() {
		this.loadData();
    });

    CQ_Analytics.CustomStoreMgr.initialized = false;

}

When do we set the initialized variable to true? in the script init.js.jsp associate with the Context Store. Create a file init.js.jsp associated with the Context Store component. Code is like:

<%@ page session="false" contentType="text/javascript" %><% %><%@ include file="/libs/foundation/global.jsp" %><% %>

if (CQ_Analytics.CustomStoreMgr) {
CQ_Analytics.CustomStoreMgr.init();
}

Step 8: change event
Each time the editor click on a label, we change the value and send an update event. Add this code at the end at the render method:

$CQ(".customstore-input").change(function(){
   var value = false;
   if ($CQ(this).attr("checked")) {
      value = true;
   }
   var key = $CQ(this).attr("name");
   $CQ("label[for='customstore-input-" + key + "']").toggleClass('checked');
   var newValue = (value === true)?"true":"false";
   CQ_Analytics.CustomStoreMgr.setTraitValue(key,newValue);
   CQ_Analytics.ProfileDataMgr.fireEvent("update");
});

Remenber that the html tags are created dynamically, so the event handler must be added after each creation.

Step 9: use the data for personnalization
The last step is to personnalize content based on the value of the data. We do this by using a segment based on the value of the property of the datastore.

Create a segment, for instance ONLINESTATEMENT and add a component “Generic Store Property”:

Segment

Then you can create you campaign based on this segment.

That’s it for this first tutorial. Many part of the customstore can be optimized and you can start exploring the API to add more functionnalities.

You can find all this code on my github :  https://github.com/Breiz41/customstore.git

Enjoy!

7 Responses to Customize the Client Context

  1. harii, says:

    Hi,
    I tried downloading the code from GIT and added the script in the segmentation but getting the error that “CQ_Analytics.CustomStoreMgr is undefined”. Do we have to do any other changes after loading the code in crxde lite

    • Samuel Blin says:

      Hi Harii,
      Same error as Prasad. Please first load the component in the client context. Fix to come.
      Samuel

  2. prasad says:

    Hi,

    After trying these steps, i could not able to see CQ_Analytics.CustomStoreMgr in my browser console. Do i miss any step?

    • Samuel Blin says:

      Hi,
      The code should work without any customization.
      Be sure to open the right console: when in chrome choose the “cq-cf-frame” and not the “top-frame” console.
      Also be sure to clean the cache (see step 1)
      Samuel

      • prasad says:

        Please check the screen shot below where i can not load the customstoremgr.

        • prasad says:

          Hi Samuel,

          Looks like the clientlibs are not loading in the segmentation. Do we have to enable these through the
          any where in the jsps. Also, do we have to load this component on the segmentation page before writing any script for the and condition. Please suggest

          • Samuel Blin says:

            Hi Prasad,
            You’re correct, there is an issue in the segmentation if the component is not already loaded. The workaround is to first load the component in the clientcontext.
            I’ll provide a fix later.
            Samuel