Sans Pareil Technologies, Inc.

Key To Your Business

Multi-Select Control



The default Magnolia multi-select control stores the multiple pieces of information as a node with property names using serial numbers, as a JSON string or a comma separated list value. None of these options are particularly conducive to efficient JCR query by values stored in these selections. We developed a modified version of the multi-select control that would saves the nodes selected (via a tree browser) as a property of type Path array for the University Of Chicago Press. Implementing a custom control generally involves developing a control class, a freemarker template that renders the UI for the control and if appropriate a custom dialog save handler. In our case all the above were necessary, and are documented in the following sections.
Control Class 
We developed a Groovy class that extends DialogBox and adds and over-rides the methods are appropriate. The primary method to over-ride is readValues which returns a list of strings from the array type property. In addition we wrap each item in the array into a JSON type representation. We then iterate over the values in the getJson method and return a JSON data structure which is used to populate the table structure in the UI.

When reading the array type property value, it is important to check to ensure that the property type is an Array. This is because JackRabbit 1.x does not preserve the array type when exporting the node as XML. This issue arises only in the special case where an array has exactly one item. When the parent node is exported and then imported into another location or repository, the property will be restored as a single-valued property and not an array property.
  @Override
  protected List readValues()
  {
    def values = []
    if ( ! storageNode ) return values

    try
    {
      def node = storageNode.getJCRNode()
      if ( ! node.hasProperty( name ) ) return  values

      def prop = node.getProperty( name )
      if ( prop.definition.isMultiple() )
      {
        for ( value in node.getProperty( name ).values )
        {
          values << "value:'${value.string}'"
        }
      }
      else
      {
        values << "value:'${prop.string}'"
      }
    }
    catch ( Throwable t )
    {
      logger.logp WARNING, getClass().name, 'readValues', 'Cannot set values', t
    }

    return values;
  }
Freemarker Template 
The freemarker template that renders the UI for the control is stored under a path that mirrors the control class package structure in the module jar file. We used the same freemarker file as used with the multiselect control in Magnolia. Minor modifications were applied to ensure we invoked the appropriate methods in the control class.

The only major difference between our version and the version from Magnolia is that we decided to place the HTML that renders each control element for each element in the array inline in the template rather than load that in from another template file.
<textarea id="${this.name}InnerHtml" style="display: none;" rows="0" cols="0">
<#assign name= this.name />
PATH: <input onchange="${this.name}DynamicTable.persist();" type="text" id='${r"${prefix}"}' name="${name}" value='${r"${obj.value}"}' class="mgnlDialogControlEdit"/>
${this.chooseButton}
${this.deleteButton}
</textarea>
Save Handler 
A Groovy class that extends UUIDSaveHandler was developed to save the values selected/entered using the multi-select control as a property of type array. We over-ride the processString method to handle the multi-select control, and delegate to the parent class implementation for all other controls in the dialog. We decided to not use the Magnolia MultiValueSaveHandler, since we wished to ensure that the data stored is always of type Path (path to other nodes in the repository).
  @Override
  protected void processString( Content content, String name, int type,
      int encoding, String[] values, String valueStr )
  {
    def control = dialog.getSub( name )

    if ( control instanceof MultiSelect )
    {
      def node = content.getJCRNode()
      if ( node.hasProperty( name ) )
      {
        node.getProperty( name ).remove()
      }

      def paths = []
      def index = 0
      for ( value in values )
      {
        if ( index++ > 0 ) paths << value
      }

      if ( ! paths.isEmpty() )
      {
        String[] array = new String[ paths.size() ]
        paths.toArray( array )
        node.setProperty name, array, PATH
      }

      node.session.save()
    }
    else
    {
      super.processString content, name, type, encoding, values, valueStr
    }
  }