Adobe

Simple AEM Granite Order Widget (Draggable Lists)

Olav Ahrens Rotne 4ennrbj1svk Unsplash

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!

 

Adobe - Content for Everyone
Content for Everyone

Companies that can quickly and consistently meet the demands of consumers are thriving in an era of infinite content. Learn about how to build fluid experiences for your omnichannel customers.

Get the Guide

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:

Order List Demo

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

Ordered List Jcr Content

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

About the Author

Ahmed is an Adobe Technical Lead and expert in the Adobe Experience Cloud.

More from this Author

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Subscribe to the Weekly Blog Digest:

Sign Up
Categories