Showing posts with label Tab Component. Show all posts
Showing posts with label Tab Component. Show all posts

Saturday, November 14, 2020

Custom Tab Component in AEM

Although the best way to implement tab component in AEM is via inheriting it from core tab component, but there could be certain scenarios where we need to use a custom Tab component. In such scenarios, we can easily create a tab component by using a multifield for addition of n number of tabs and by providing a responsivegrid/parsys for adding the component inside each tab. But, the problem arises when we need to rearrange tabs after authoring or deleting any tab, the challenge is to maintain the components authored for each tab. In this article, we will be discussing different ways to create the custom Tab component.


To begin with, lets create a custom tab component which have the below files/nodes inside it:


Next, we can start by taking any HTML for tabs component, for this article I have used a pretty basic HTML from W3School. We can separate the HTML, JS and CSS files based on standard practice and can place the JS and CSS to the concerned clientlib folders. Once, we are done with this we can try including this component in any page where it is allowed and we will be able to see the static markup getting loaded with tab functionalities.

Next, we will be creating a dialog for it and will try to make it authorable. Observe the below xml code for dialog:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?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="nt:unstructured"
    jcr:title="Autopace Tabs"
    sling:resourceType="cq/gui/components/authoring/dialog"
    extraClientlibs="[autopace.tabs]">
    <content
        jcr:primaryType="nt:unstructured"
        sling:resourceType="granite/ui/components/coral/foundation/container">
        <items jcr:primaryType="nt:unstructured">
            <tabs
                jcr:primaryType="nt:unstructured"
                sling:resourceType="granite/ui/components/coral/foundation/tabs"
                maximized="{Boolean}true">
                <items jcr:primaryType="nt:unstructured">
                    <tabitems
                        jcr:primaryType="nt:unstructured"
                        jcr:title="Items"
                        sling:resourceType="granite/ui/components/coral/foundation/container"
                        margin="{Boolean}true">
                        <items jcr:primaryType="nt:unstructured">
                            <columns
                                jcr:primaryType="nt:unstructured"
                                sling:resourceType="granite/ui/components/coral/foundation/fixedcolumns"
                                margin="{Boolean}true">
                                <items jcr:primaryType="nt:unstructured">
                                    <column
                                        jcr:primaryType="nt:unstructured"
                                        sling:resourceType="granite/ui/components/coral/foundation/container">
                                        <items jcr:primaryType="nt:unstructured">
	                                        <tabItems
	                                            jcr:primaryType="nt:unstructured"
	                                            sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
	                                            composite="{Boolean}true"
	                                            fieldDescription="Click 'Add Field' to add new item."
	                                            fieldLabel="Tab Items">
	                                            <field
	                                                jcr:primaryType="nt:unstructured"
	                                                sling:resourceType="granite/ui/components/coral/foundation/container"
	                                                name="./tabItems">
	                                                <items jcr:primaryType="nt:unstructured">
	                                                    <title
	                                                    	granite:class="tab-title"
	                                                        jcr:primaryType="nt:unstructured"
	                                                        sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
	                                                        fieldLabel="Title"
	                                                        name="./title"/>
	                                                    <tabId
	                                                    	granite:class="tab-id"
	                                                        jcr:primaryType="nt:unstructured"
	                                                        sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
	                                                        name="./tabId"/>
	                                                </items>
	                                            </field>
	                                        </tabItems>
                                        </items>
                                    </column>
                                </items>
                            </columns>
                        </items>
                    </tabitems>
                </items>
            </tabs>
        </items>
    </content>
</jcr:root>


In the above XML code, if we observe closely, we will see that in the multifield , we have two fields, one is for title and the other is for tabId. The trick here is if we provide an option to author tabId for each tab added to the component, we can use that tabId to name the parsys/responsivegrid present under that particular tab. For example: if we have added "Tab1 Title" as title with id name as "tab1-id", then we can use "tab1-id" to create the parsys/responsivegrid node for that particular tab and all the components added inside that Tab parsys/responsivegrid will be added under a node with name "tab1-id". In that way, even if we change the positions of tabs later, we will be able to render the current components added for the tab as those will be present under node "tab1-id".

Now lets have a look at the HTML code for the same and try to understand the ID login:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<sly data-sly-use.tabs="org.autopace.autopace.core.models.AutopaceTabs" />
<sly data-sly-test="${wcmmode.edit}" data-emptytext="Autopace Tabs" class="cq-placeholder" />

<h2>Tabs</h2>
<div class="tab">
	<sly data-sly-list.item="${tabs.tabItems}">
	<button class="tablinks" onclick="openCity(event, '${item.title @ context='html'}')">${item.title}</button>
	</sly>
</div>

<sly data-sly-list.item="${tabs.tabItems}">
<div id="${item.title}" class="tabcontent">
	<sly data-sly-resource="${item.tabId @ resourceType='wcm/foundation/components/responsivegrid'}"/>
</div>
</sly>


We can observe that from line #11 to line #15, we are running a loop to create the responsive grid for each tab added to the component. In line #13, we can observe that "item.tabId" is the name for each tab's responsive grid. we just need to make sure that the tabId authored is unique and we can manage the components added inside. Also, if the author uses the same Id for more than one tab, then the components displayed for all those tabs will be same. Also, In line #1, we can see that we have used the sling model, below is the sling model code snippet for reference:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package org.autopace.autopace.core.models;

import java.util.List;

import javax.inject.Inject;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Via;

import com.adobe.cq.export.json.ExporterConstants;

@Model(adaptables = SlingHttpServletRequest.class, adapters = {
		AutopaceTabs.class }, resourceType = AutopaceTabs.RESOURCE_TYPE_TABS, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class AutopaceTabs {

	public static final String RESOURCE_TYPE_TABS = "autopace/components/content/tabs";

	@Inject
	@Via("resource")
	private List<AutopaceTabsItem> tabItems;

	public List<AutopaceTabsItem> getTabItems() {
		return tabItems;
	}

}


 Below is the code for AutopaceTabsItem.java which is acting like a bean class for each tab item:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package org.autopace.autopace.core.models;

import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;

import com.adobe.cq.export.json.ExporterConstants;

@Model(adaptables = Resource.class, defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter(name = ExporterConstants.SLING_MODEL_EXPORTER_NAME, extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class AutopaceTabsItem {

	@ValueMapValue
	private String title;

	@ValueMapValue
	private String tabId;

	public String getTitle() {
		return title;
	}

	public String getTabId() {
		return tabId;
	}

}


So far we discussed, how we can create a authorable custom tab component and can retain the authored components intact to there individual tabs by using the tabId. Now lets try to automate this process so that authoring of Id is not required. We will be writing our custom JS code which will assign an ID to each tab i.e. to each addition to the multifield.

If we observe again in the XML provided for dialog above, we will see that at line #6, I have used an extraClientlib, this Clientlib will be called whenever we will be authoring the dialog. Lets create a Clientlib with autopace.tabs category and add the below JS to it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(function($) {
	$.validator.register({
		selector : "input.tab-title",
		validate : validate,
		show : show,
		clear : clear
	});
	function validate($el) {
		var multifieldItem = $el.closest('.coral3-Multifield-item');
		var val = Math.floor(1000 + Math.random() * 9000);
		var tabName = "tabpar_" + val;
		var parsysNameField = $(multifieldItem).find("input.tab-id");
		if (!parsysNameField.val()) {
			parsysNameField.val(tabName);
		}

		return;
	}
	function show($el, message) {
	}
	function clear($el) {
	}
}(jQuery));


The above JS will help us auto-populate an ID for each addition to multifield. It will make sure that whenever authors click on ADD in the multifield, the tabId field is auto-populated and author needs to only provide a title for the tabs. In this JS, we are creating a random Number at line #10. This random number is appended to "tabpar_" at line #11 and then this newly created ID is getting assigned to the tabId field of each addition at line #14. Also, the class name used at line #3 i.e. "tab-title" and at line #12 i.e. "tab-id" are the custom classes added to the dialog fields in the XML at line #43 and #49 respectively to search those fields via JS.

Note: This JS is designed and tested with the structure for Coral3 dialog fields and might need some modifications for other field inheritance.

Next, what we can do is the we can hide this tabId field for each addition as the tabID is now getting auto-populated and will only be used to maintain the components added to each tab. There is no use of this ID to authors. Hence, to hide this field we can include a CSS to the same Clientlib i.e. "autopace.tabs". Below is the CSS file which I have used:

1
2
3
input.tab-id {
	display:none !important;
}


This file will help us hide the field whenever authors will try to author the tab component. This is how it will look if someone is trying to author it:



Once we save this dialog, we can keep switching to preview/edit mode and start adding the components to each tab. All the components added to each tab will be added under a separate node in CRXDE with the node name as the tabID which is generated via our JS:



This way we can create a custom Tab component. But, there is one drawback of this approach, say we need to add 3 tabs say Tab1, Tab2 and Tab3. We added them and authored the content inside each one of them. So the other 3 nodes with the tabId corresponding to each tab will be created. Now let us suppose we need to delete a tab say Tab2 later. In this case we can go to the dialog box and can delete Tab2 easily and the component will also render just fine. But the node created for storing Tab2 components with tabId will not be deleted from CRXDE. It will result in creating stale nodes in CRXDE. 

Although, this is an edge case scenario and is of not much harm. But if it is very important to fix it based on the usage of this component, we can write a custom service to read all the tabId from the tabs available and can check if any extra node is available in CRXDE. The service can be used to delete the extra nodes (if any).

Contact Form

Name

Email *

Message *

Latest Post

Memory Management in JAVA

One of the most important features of Java is its memory management. Since Java uses automatic memory management, it becomes superior to tho...

Popular Posts