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, 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: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!