Recently, we required an authoring experience where an author can re-order a list of fixed tabs to their desired order. The only Granite widget that allows drag-n-drop ordering is multifield, but we don’t need a multifield. We just wanted the ability to order a fixed list.
An Existing solution
ACS Commons has this Draggable Lists Widget, but I did not want to include ACS commons just for that widget. Additionally, I looked at the implementation for that widget, and it seemed to be complex granted it might be solving a different problem. It also used Coral UI 2; we are on AEM 6.4, where Coral UI 3 is the latest Coral version.
A Simple Solution
If you look at Coral UI 3 documentation, the only component that allows drag-n-drop ordering is the Coral.Table component. This means we can create a Granite widget that uses that UI to render a list!
Here is the widget JSP impl: (Java 8)
for the purpose of this post, I’ve created /apps/widgets/orderedList/orderedList.jsp
Here, I render each item in the list as a hidden field with the same “name”.
The order of the hidden fields in the HTML is what determines the saved order in the JCR content node/prop.
This also means that we dont need ANY custom JS to make this work! just the OOTB CUI3 Table component!
Renders a List of items for ordering purposes only
%><%@ include file="/libs/granite/ui/global.jsp" %><%
%><%@ page session="false"
import="java.util.Iterator,
com.adobe.granite.ui.components.AttrBuilder,
com.adobe.granite.ui.components.Tag,
com.adobe.granite.ui.components.Config,
org.apache.sling.api.resource.ValueMap,
java.util.stream.StreamSupport,
java.util.stream.Collectors,
.. granite:servercomponent:: widgets/orderedList
Renders a List of items for ordering purposes only
It has the following content structure:
- sling:resourceType = "widgets/orderedList"
if(!cmp.getRenderCondition(resource, false).check()){
Tag tag = cmp.consumeTag();
AttrBuilder attrs = tag.getAttrs();
attrs.addClass("coral-Well");
cmp.populateCommonAttrs(attrs);
Config cfg = cmp.getConfig();
String title = cfg.get("title", String.class);
String name = cfg.get("name", String.class);
// the easy way to do this...
? name.replace(".", "").replace("/", "")
String tableId = "order-table-" + cleanName;
String hiddenInputId = "order-input-" + cleanName;
String[] values = cmp.getValue().getContentValue(name, newString[0]);
List<String> valuesList = Arrays.asList(values);
Iterator<Resource> itemsIterator = cmp.getItemDataSource().iterator();
// server-side ordering of values based on already saved order
List<ValueMap> items = StreamSupport.stream(
Spliterators.spliteratorUnknownSize(itemsIterator, Spliterator.ORDERED),
Comparator.comparing(item ->{
ValueMap vm = ((Resource) item).getValueMap();
String val = vm.get("value", "");
return valuesList != null ? valuesList.indexOf(val) : 0;
).map(Resource::getValueMap)
.collect(Collectors.toList());
<div <%= attrs.build() %>>
<table is="coral-table" orderable id="<%=tableId%>">
<col is="coral-table-column">
<col is="coral-table-column" fixedwidth>
<thead is="coral-table-head">
<tr is="coral-table-row">
<th is="coral-table-headercell"><%=title%></th>
<th is="coral-table-headercell"></th>
<tbody is="coral-table-body">
for(ValueMap vm : items){
<tr is="coral-table-row">
<td is="coral-table-cell">
<input type="hidden" name="<%=name%>" value="<%=vm.get("value", "")%>"/>
<td is="coral-table-cell">
<button is="coral-button" type="button" variant="minimal" icon="dragHandle" coral-table-roworder></button>
<%--
Renders a List of items for ordering purposes only
--%><%
%><%@ include file="/libs/granite/ui/global.jsp" %><%
%><%@ page session="false"
import="java.util.Iterator,
com.adobe.granite.ui.components.AttrBuilder,
com.adobe.granite.ui.components.Tag,
com.adobe.granite.ui.components.Config,
org.apache.sling.api.resource.ValueMap,
java.util.stream.StreamSupport,
java.util.stream.Collectors,
java.util.*" %>
<%--###
Ordered List
====
// TODO
.. granite:servercomponent:: widgets/orderedList
Renders a List of items for ordering purposes only
It has the following content structure:
+ myList
- sling:resourceType = "widgets/orderedList"
- name = "./myList"
- title = "My List"
+ items
+ item1
- text = "Item 1"
- value = "item1"
+ item 2
- text = "Item 2"
- value = "item2"
###--%>
<%
if (!cmp.getRenderCondition(resource, false).check()) {
return;
}
Tag tag = cmp.consumeTag();
AttrBuilder attrs = tag.getAttrs();
attrs.addClass("coral-Well");
cmp.populateCommonAttrs(attrs);
Config cfg = cmp.getConfig();
String title = cfg.get("title", String.class);
String name = cfg.get("name", String.class);
// the easy way to do this...
String cleanName =
name != null
? name.replace(".", "").replace("/", "")
: "";
String tableId = "order-table-" + cleanName;
String hiddenInputId = "order-input-" + cleanName;
String[] values = cmp.getValue().getContentValue(name, new String[0]);
List<String> valuesList = Arrays.asList(values);
Iterator<Resource> itemsIterator = cmp.getItemDataSource().iterator();
// server-side ordering of values based on already saved order
List<ValueMap> items = StreamSupport.stream(
Spliterators.spliteratorUnknownSize(itemsIterator, Spliterator.ORDERED),
false
).sorted(
Comparator.comparing(item -> {
ValueMap vm = ((Resource) item).getValueMap();
String val = vm.get("value", "");
return valuesList != null ? valuesList.indexOf(val) : 0;
})
).map(Resource::getValueMap)
.collect(Collectors.toList());
%>
<div <%= attrs.build() %>>
<table is="coral-table" orderable id="<%=tableId%>">
<colgroup>
<col is="coral-table-column">
<col is="coral-table-column" fixedwidth>
</colgroup>
<thead is="coral-table-head">
<tr is="coral-table-row">
<th is="coral-table-headercell"><%=title%></th>
<th is="coral-table-headercell"></th>
</tr>
</thead>
<tbody is="coral-table-body">
<%
for (ValueMap vm : items) {
%>
<tr is="coral-table-row">
<td is="coral-table-cell">
<%=vm.get("text", "") %>
<input type="hidden" name="<%=name%>" value="<%=vm.get("value", "")%>"/>
</td>
<td is="coral-table-cell">
<button is="coral-button" type="button" variant="minimal" icon="dragHandle" coral-table-roworder></button>
</td>
</tr>
<%
}
%>
</tbody>
</table>
</div>
<%--
Renders a List of items for ordering purposes only
--%><%
%><%@ include file="/libs/granite/ui/global.jsp" %><%
%><%@ page session="false"
import="java.util.Iterator,
com.adobe.granite.ui.components.AttrBuilder,
com.adobe.granite.ui.components.Tag,
com.adobe.granite.ui.components.Config,
org.apache.sling.api.resource.ValueMap,
java.util.stream.StreamSupport,
java.util.stream.Collectors,
java.util.*" %>
<%--###
Ordered List
====
// TODO
.. granite:servercomponent:: widgets/orderedList
Renders a List of items for ordering purposes only
It has the following content structure:
+ myList
- sling:resourceType = "widgets/orderedList"
- name = "./myList"
- title = "My List"
+ items
+ item1
- text = "Item 1"
- value = "item1"
+ item 2
- text = "Item 2"
- value = "item2"
###--%>
<%
if (!cmp.getRenderCondition(resource, false).check()) {
return;
}
Tag tag = cmp.consumeTag();
AttrBuilder attrs = tag.getAttrs();
attrs.addClass("coral-Well");
cmp.populateCommonAttrs(attrs);
Config cfg = cmp.getConfig();
String title = cfg.get("title", String.class);
String name = cfg.get("name", String.class);
// the easy way to do this...
String cleanName =
name != null
? name.replace(".", "").replace("/", "")
: "";
String tableId = "order-table-" + cleanName;
String hiddenInputId = "order-input-" + cleanName;
String[] values = cmp.getValue().getContentValue(name, new String[0]);
List<String> valuesList = Arrays.asList(values);
Iterator<Resource> itemsIterator = cmp.getItemDataSource().iterator();
// server-side ordering of values based on already saved order
List<ValueMap> items = StreamSupport.stream(
Spliterators.spliteratorUnknownSize(itemsIterator, Spliterator.ORDERED),
false
).sorted(
Comparator.comparing(item -> {
ValueMap vm = ((Resource) item).getValueMap();
String val = vm.get("value", "");
return valuesList != null ? valuesList.indexOf(val) : 0;
})
).map(Resource::getValueMap)
.collect(Collectors.toList());
%>
<div <%= attrs.build() %>>
<table is="coral-table" orderable id="<%=tableId%>">
<colgroup>
<col is="coral-table-column">
<col is="coral-table-column" fixedwidth>
</colgroup>
<thead is="coral-table-head">
<tr is="coral-table-row">
<th is="coral-table-headercell"><%=title%></th>
<th is="coral-table-headercell"></th>
</tr>
</thead>
<tbody is="coral-table-body">
<%
for (ValueMap vm : items) {
%>
<tr is="coral-table-row">
<td is="coral-table-cell">
<%=vm.get("text", "") %>
<input type="hidden" name="<%=name%>" value="<%=vm.get("value", "")%>"/>
</td>
<td is="coral-table-cell">
<button is="coral-button" type="button" variant="minimal" icon="dragHandle" coral-table-roworder></button>
</td>
</tr>
<%
}
%>
</tbody>
</table>
</div>
Here is an example dialog XML that uses this widget:
<?xml version="1.0" encoding="UTF-8"?>
<jcr:rootxmlns:sling="http://sling.apache.org/jcr/sling/1.0"
xmlns:cq="http://www.day.com/jcr/cq/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0"
xmlns:nt="http://www.jcp.org/jcr/nt/1.0"jcr:primaryType="nt:unstructured"jcr:title="Product Tabs"sling:resourceType="cq/gui/components/authoring/dialog">
<contentjcr:primaryType="nt:unstructured"sling:resourceType="granite/ui/components/coral/foundation/container">
<itemsjcr:primaryType="nt:unstructured">
<tabsjcr:primaryType="nt:unstructured"sling:resourceType="granite/ui/components/coral/foundation/tabs"maximized="{Boolean}true">
<itemsjcr:primaryType="nt:unstructured">
<generaljcr:primaryType="nt:unstructured"jcr:title="Properties"sling:resourceType="granite/ui/components/coral/foundation/container"margin="{Boolean}true">
<itemsjcr:primaryType="nt:unstructured">
<tabOrderjcr:primaryType="nt:unstructured"
sling:resourceType = "widgets/orderedList"
<itemsjcr:primaryType="nt:unstructured">
jcr:primaryType="nt:unstructured"
jcr:primaryType="nt:unstructured"
jcr:primaryType="nt:unstructured"
jcr:primaryType="nt:unstructured"
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
xmlns:cq="http://www.day.com/jcr/cq/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0"
xmlns:nt="http://www.jcp.org/jcr/nt/1.0" jcr:primaryType="nt:unstructured" jcr:title="Product Tabs" sling:resourceType="cq/gui/components/authoring/dialog">
<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">
<general jcr:primaryType="nt:unstructured" jcr:title="Properties" sling:resourceType="granite/ui/components/coral/foundation/container" margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<tabOrder jcr:primaryType="nt:unstructured"
sling:resourceType = "widgets/orderedList"
name="./tabOrder"
title="Tabs Order">
<items jcr:primaryType="nt:unstructured">
<item1
jcr:primaryType="nt:unstructured"
text="Overview"
value="overview"/>
<item2
jcr:primaryType="nt:unstructured"
text="Features"
value="features"/>
<item3
jcr:primaryType="nt:unstructured"
text="Spec"
value="spec"/>
<item4
jcr:primaryType="nt:unstructured"
text="Additiona info"
value="info"/>
</items>
</tabOrder>
</items>
</general>
</items>
</tabs>
</items>
</content>
</jcr:root>
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
xmlns:cq="http://www.day.com/jcr/cq/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0"
xmlns:nt="http://www.jcp.org/jcr/nt/1.0" jcr:primaryType="nt:unstructured" jcr:title="Product Tabs" sling:resourceType="cq/gui/components/authoring/dialog">
<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">
<general jcr:primaryType="nt:unstructured" jcr:title="Properties" sling:resourceType="granite/ui/components/coral/foundation/container" margin="{Boolean}true">
<items jcr:primaryType="nt:unstructured">
<tabOrder jcr:primaryType="nt:unstructured"
sling:resourceType = "widgets/orderedList"
name="./tabOrder"
title="Tabs Order">
<items jcr:primaryType="nt:unstructured">
<item1
jcr:primaryType="nt:unstructured"
text="Overview"
value="overview"/>
<item2
jcr:primaryType="nt:unstructured"
text="Features"
value="features"/>
<item3
jcr:primaryType="nt:unstructured"
text="Spec"
value="spec"/>
<item4
jcr:primaryType="nt:unstructured"
text="Additiona info"
value="info"/>
</items>
</tabOrder>
</items>
</general>
</items>
</tabs>
</items>
</content>
</jcr:root>
And here is a demo of the dialog:

This widget stores the values as a multi-value property with the correct order:

Now you can use that order to sort your tabs in your component, or use it for another purpose entirely!