cancel
Showing results for 
Search instead for 
Did you mean: 

Multiple Input Fields generator

jzulu2000
Champ in-the-making
Champ in-the-making
I needed alfresco to render a single property of type d:text in multiple input fields and I didn't see an easy way of doing this;

For example, I had the Person property in the model, of type d:text and I had to ask for the id, first and last name. Visually, te component had to look like…


<type name="gc:RFC">
      <property name="gc:person">
            <type>d:text</type>
            <mandatory>true</mandatory>
      </property>

——————————————
|         | Id:         _______________   |
|         |                               |
| Person: | First Name: _______________   |
|         |                               |
|         | Last Name:  _______________   |
——————————————



instead of


<type name="gc:RFC">
      <property name="gc:personID">
            <type>d:text</type>
            <mandatory>true</mandatory>
      </property>
      <property name="gc:personFirstName">
            <type>d:text</type>
            <mandatory>true</mandatory>
      </property>
      <property name="gc:personLastName">
            <type>d:text</type>
            <mandatory>true</mandatory>
      </property>

—————————————
| Person Id:         _______________   |
|                                      |
| Person First Name: _______________   |
|                                      |
| Person Last Name:  _______________   |
—————————————


the only way of doing this was creating acustom generator and configure the show-property in the property-sheet to use the custom generator; the problem with this is that not only the person had to be renerer like this; so, we had to create several custom generators, one for each entity.

A better solution was to create a configurable component so we can tell the component type, renderer and label for every input field, so we can combine input text, text areas, lists, etc. This component saves the fields in a single property using the "°" separator, and can be used in properties with multiple = true; in that case, renders a table with columns for every value. This can be used in multi value read-only properties too; in that case, only paints the table with no delete action;
1 REPLY 1

jzulu2000
Champ in-the-making
Champ in-the-making
I had to develop this component; I hope it will be usefull for you all.


The classes created were:

1. MultipleFieldsGenerator that generates the inputs depending on the managed bean's configuration and if in multi value property, configures the UIMultipleFieldsMultiValueEditor that holds the multiple values and makes a correct broadcast of the sent value.

2. UIMultipleFieldsMultiValueEditor is used when property is multiple. Takes all inputs values and concatenates its values using the "°" separator.

3. MultipleFieldsMultiValueRenderer inherits functionality from MultiValueFieldRenderer but renders the existing items in multiple columns. If property is in read only mode doesn't render the delete action; neither the inputs.


4. OPTIONAL: PatchedMultiValueFieldRenderer: based on original MultiValueFieldRenderer but was made to convert correctly the values to String; it uses converters instead of toString; see http://issues.alfresco.com/browse/AWC-1804; It allow us to define table header with multiple columns too. see http://issues.alfresco.com/browse/AWC-1803; if alfresco team accepts those contributions (AWC-1803 and AWC-1803) this class is no longer pertinent, so MultipleFieldsMultiValueRenderer can extend directly from alfresco's MultiValueFieldRenderer.


MultipleFieldsGenerator:

package co.com.arkimia.alfresco.web.bean.generator;

import java.util.List;

import javax.faces.component.UIComponent;
import javax.faces.component.UIOutput;
import javax.faces.context.FacesContext;
import javax.faces.el.ValueBinding;

import org.alfresco.service.cmr.dictionary.PropertyDefinition;
import org.alfresco.web.app.servlet.FacesHelper;
import org.alfresco.web.bean.generator.TextFieldGenerator;
import org.alfresco.web.ui.common.ComponentConstants;
import org.alfresco.web.ui.repo.RepoConstants;
import org.alfresco.web.ui.repo.component.property.PropertySheetItem;
import org.alfresco.web.ui.repo.component.property.UIPropertySheet;

/**
* Multiple fields generator for a single field in model; users can define
* components with its own label, component type, and renderer type.
* Defaults for componentType, renderType and label are javax.faces.Input,
* javax.faces.Text and "" respectively.
* This component supports verticar or horizontal layout.
* @author Juan David Zuluaga Arboleda. Arkimia S.A.
*
*/
public class MultipleFieldsGenerator extends TextFieldGenerator {
   public static final String DEFAULT_LABEL = "";
   public static final String DEFAULT_COMPONENT_TYPE = ComponentConstants.JAVAX_FACES_INPUT;
   public static final String DEFAULT_RENDER_TYPE = ComponentConstants.JAVAX_FACES_TEXT;
   
   private boolean verticalLayout = true;
   private List<String> labels;
   private List<String> componentTypes;
   private List<String> renderTypes;
   
   /**
    * Returns the number of fields to be renderer
    * @return
    */
   private int getNumberOfComponents() {
      int components = 0;
      if(labels != null) {
         components = Math.max(components, labels.size());
      }
      if(componentTypes != null) {
         components = Math.max(components, componentTypes.size());
      }
      if(renderTypes!= null) {
         components = Math.max(components, renderTypes.size());
      }
      return components;
   }

   /**
    * Returns the grid number of columns, depending of layout type and number
    * of components.
    * @return
    */
   private int getColumns() {
      if(verticalLayout) {
         return 2;
      }

      return getNumberOfComponents() * 2;
   }
   
   public List<String> getLabels() {
      return labels;
   }

   public void setLabels(List<String> labels) {
      this.labels = labels;
   }

   public List<String> getComponentTypes() {
      return componentTypes;
   }

   public void setComponentTypes(List<String> componentTypes) {
      this.componentTypes = componentTypes;
   }
   
   
   public List<String> getRenderTypes() {
      return renderTypes;
   }

   public void setRenderTypes(List<String> renderTypes) {
      this.renderTypes = renderTypes;
   }

   public boolean isVerticalLayout() {
      return verticalLayout;
   }

   public void setVerticalLayout(boolean verticalLayout) {
      this.verticalLayout = verticalLayout;
   }

   /**
    * Returns the label of the pos component.
    * @param pos the label position
    * @return the label, or empty string if not label defined for position.
    */
   private String getLabel(int pos) {
      return labels == null || labels.size() - 1 < pos
         ? DEFAULT_LABEL: labels.get(pos);
   }

   /**
    * Returns the component Type of the pos component.
    * @param pos the component Type position
    * @return the component Type, or javax.faces.Input if not component Type defined
    * for position.
    */
   private String getComponentType(int pos) {
      return componentTypes == null || componentTypes.size() - 1 < pos
         ? DEFAULT_COMPONENT_TYPE: componentTypes.get(pos);
   }

   /**
    * Returns the render type of the pos component.
    * @param pos the render type position
    * @return the render type, or javax.faces.Text if not render type defined for
    * position.
    */
   private String getRenderType(int pos) {
      return renderTypes == null || renderTypes.size() - 1 < pos
         ? DEFAULT_RENDER_TYPE: renderTypes.get(pos);
   }

   @Override
   public UIComponent generate(FacesContext context, String id) {
      UIComponent input;
      UIOutput output;
      UIComponent panel = context.getApplication().createComponent(
            "javax.faces.HtmlPanelGrid");
      panel.getAttributes().put("columns", getColumns());
      panel.setRendererType(ComponentConstants.JAVAX_FACES_GRID);

      int numberOfComponents = getNumberOfComponents();
      for (int i = 0; i < numberOfComponents; i++) {
         output = (UIOutput)context.getApplication().createComponent(
               ComponentConstants.JAVAX_FACES_OUTPUT);
         output.setValue(getLabel(i));

         input = context.getApplication().createComponent(
               getComponentType(i));
         input.setRendererType(getRenderType(i));
         FacesHelper.setupComponentId(context, input, id + "_" + i);
         
         panel.getChildren().add(output);
         panel.getChildren().add(input);
         
      }
      return panel;
   }

   @Override
   protected UIComponent setupMultiValuePropertyIfNecessary(
         FacesContext context, UIPropertySheet propertySheet,
         PropertySheetItem property, PropertyDefinition propertyDef,
         UIComponent component) {
         UIComponent multiValueComponent = component;
        
         if (propertySheet.inEditMode() &&
             propertyDef != null && propertyDef.isProtected() == false &&
             propertyDef.isMultiValued())
         {
            // if the property is multi-valued create a multi value editor wrapper component
            String id = "multi_" + property.getName();
            multiValueComponent = context.getApplication().createComponent(
                  "co.com.arkimia.alfresco.faces.MultiValueEditor");
           
            multiValueComponent.getAttributes().put("disabled", Boolean.valueOf(property.isReadOnly()));
           
            FacesHelper.setupComponentId(context, multiValueComponent, id);
              
            // set the renderer depending on whether the item is a 'field' or a 'selector'
            if (getControlType() == ControlType.FIELD)
            {
               multiValueComponent.setRendererType(RepoConstants.ALFRESCO_FACES_FIELD_RENDERER);
            }
            else
            {
               multiValueComponent.setRendererType(RepoConstants.ALFRESCO_FACES_SELECTOR_RENDERER);
              
               // set the value binding for the wrapped component and the lastItemAdded attribute of
               // the multi select component, needs to point somewhere that can hold any object, it
               // will store the item last added by the user.
               String expr = "#{MultiValueEditorBean.lastItemsAdded['" +
                     property.getName() + "']}";
               ValueBinding vb = context.getApplication().createValueBinding(expr);
               multiValueComponent.setValueBinding("lastItemAdded", vb);
               component.setValueBinding("value", vb);
            }
           
            // add the original component as a child of the wrapper
            multiValueComponent.getChildren().add(component);
         }

      return multiValueComponent;
   }

   /**
    * Removes all component childrens if component is read only.
    */
   protected void setupProperty(FacesContext context,
         UIPropertySheet propertySheet, PropertySheetItem item,
         PropertyDefinition propertyDef, UIComponent component) {
      super.setupProperty(context, propertySheet, item, propertyDef,
            component);
      if (item.isReadOnly()) {
         UIComponent panel = (UIComponent) component.getChildren().get(0);
         panel.getChildren().removeAll(panel.getChildren());
      }
   }
}


UIMultipleFieldsMultiValueEditor:

package co.com.arkimia.alfresco.web.ui.repo.component;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.component.UIPanel;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.FacesEvent;
import javax.faces.render.Renderer;

import org.alfresco.web.ui.repo.RepoConstants;
import org.alfresco.web.ui.repo.component.UIMultiValueEditor;

/**
* Takes the information sent in the multiple fields and joins that info in a
* single string
* @author Juan David Zuluaga Arboleda. Arkimia S.A.
*
*/
public class UIMultipleFieldsMultiValueEditor extends UIMultiValueEditor {

   public static final String ITEM_SEPARATOR = "°";

   public UIMultipleFieldsMultiValueEditor() {
      super();
   }

   @Override
   public String getFamily() {
      return "co.com.arkimia.alfresco.faces.MultiValueEditor";
   }

   /**
    * Toma los valores de los inputs que existen en el panel hijo de este
    * componente.
    */
   public void broadcast(FacesEvent event) throws AbortProcessingException {

      if (event instanceof MultiValueEditorEvent) {
         MultiValueEditorEvent assocEvent = (MultiValueEditorEvent) event;

         if(assocEvent.Action == ACTION_ADD) {
            List items = (List) getValue();
            if (items == null) {
               items = new ArrayList();
               setSubmittedValue(items);
            }

            StringBuffer addedItem = new StringBuffer();

            if (getRendererType().equals(
                  (RepoConstants.ALFRESCO_FACES_FIELD_RENDERER))) {
               FacesContext context = FacesContext.getCurrentInstance();
               UIInput input;
               UIComponent component;
               UIPanel panel = (UIPanel) this.getChildren().get(0);
               for (Iterator iterator = panel.getChildren().iterator();
                     iterator.hasNext();) {
                  component = (UIComponent)iterator.next();
                  if(component instanceof UIInput) {
                     input = (UIInput) component;
                     if(input.getSubmittedValue() != null){
                        Renderer r = context.getRenderKit().getRenderer(
                              component.getFamily(),
                              component.getRendererType());
                        Object value = r.getConvertedValue(context,
                              component, input.getSubmittedValue());
                        Converter c = context.getApplication()
                           .createConverter(value.getClass());
                        String localValue;
                        if (c != null) {
                           localValue =  c.getAsString(context,
                                 component, value);
                        } else {
                           localValue = value.toString();
                        }
                        addedItem.append(localValue);
                     }
                     if(iterator.hasNext()){
                        addedItem.append(ITEM_SEPARATOR);
                     }
                     input.setSubmittedValue(null);
                  }
               }
               items.add(addedItem.toString());
               return;
            }
         }
      }

      super.broadcast(event);
   }

}


MultipleFieldsMultiValueRenderer:

package co.com.arkimia.alfresco.web.ui.repo.renderer;

import java.io.IOException;
import java.util.Iterator;
import java.util.StringTokenizer;

import javax.faces.component.UIComponent;
import javax.faces.component.UIInput;
import javax.faces.component.UIOutput;
import javax.faces.component.UIPanel;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;

import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.web.app.Application;
import org.alfresco.web.ui.repo.component.UIMultiValueEditor;

import co.com.arkimia.alfresco.web.ui.repo.component.UIMultipleFieldsMultiValueEditor;
/**
* Renders the multiple fields component.
* @author Juan David Zuluaga Arboleda. Arkimia S.A.
*/
public class MultipleFieldsMultiValueRenderer extends PatchedMultiValueFieldRenderer {

   /**
    * Tokeniza el valor y muestra una columna por cada token del valor.
    */
   protected void renderExistingItem(FacesContext context,
         UIComponent component, ResponseWriter out, NodeService nodeService,
         int index, Object value) throws IOException {
      out.write("<tr>");
      StringTokenizer newValue = new StringTokenizer((String) value,
            UIMultipleFieldsMultiValueEditor.ITEM_SEPARATOR, true);
      int tokens = newValue.countTokens();
      String nextToken;
      
      out.write("<td class='selectedItemsRow");
      if (this.highlightedRow) {
         out.write("Alt");
      }
      out.write("'>");

      for (int i = 0; i < tokens; i++) {
         nextToken = newValue.nextToken();
         if(nextToken.equals(UIMultipleFieldsMultiValueEditor.ITEM_SEPARATOR)) {
            out.write(" </td>");
            out.write("<td class='selectedItemsRow");
            if (this.highlightedRow) {
               out.write("Alt");
            }
            out.write("'>");
         } else {
            out.write(nextToken);
         }
      }

      out.write(" </td>");
      
      Boolean disabled = (Boolean)component.getAttributes().get("disabled");
      if(disabled == null || disabled.equals(Boolean.FALSE)){
         out.write("<td class='selectedItemsRow");
         if (super.highlightedRow) {
            out.write("Alt");
         }
         out.write("'><a href='#' title='");
         out.write(Application.getMessage(context, MSG_REMOVE));
         out.write("' onclick=\"");
         out.write(generateFormSubmit(context, component,
               UIMultiValueEditor.ACTION_REMOVE + UIMultiValueEditor.ACTION_SEPARATOR + index));
         out.write("\"><img src='");
         out.write(context.getExternalContext().getRequestContextPath());
         out.write("/images/icons/delete.gif' border='0' width='13' height='16'/></a>");
      }

      this.highlightedRow = !this.highlightedRow;
   }

   /**
    * if component disabled does not render delete action
    */
   protected void renderPostWrappedComponent(FacesContext context,
         ResponseWriter out, UIMultiValueEditor editor) throws IOException {
      Boolean disabled = (Boolean) editor.getAttributes().get("disabled");
      if (disabled != null && disabled.equals(Boolean.TRUE)) {
         out.write("</td></tr>");
      } else {
         super.renderPostWrappedComponent(context, out, editor);
      }
   }

   protected void renderTableHeader(FacesContext context,
         UIComponent component,ResponseWriter out) throws IOException{
      UIPanel panel = (UIPanel) component.getChildren().get(0);
      Object tmpComp;
      UIOutput campo;
      out.write("<tr>");
      for (Iterator iterator = panel.getChildren().iterator(); iterator.hasNext();) {
         tmpComp = iterator.next();
         if (!(tmpComp instanceof UIInput)) {
            campo = (UIOutput) tmpComp;
            out.write("<td class='selectedItemsHeader'>");
            out.write(campo.getValue().toString());
            out.write("</td>");
         }
         
      }
      out.write("<td></td>");
        out.write("</tr>");
   }
}


Optional: PatchedMultiValueFieldRenderer

package co.com.arkimia.alfresco.web.ui.repo.renderer;

import java.io.IOException;
import java.util.Date;
import java.util.List;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.convert.Converter;

import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.web.app.Application;
import org.alfresco.web.bean.repository.Repository;
import org.alfresco.web.ui.repo.component.UIMultiValueEditor;
import org.alfresco.web.ui.repo.renderer.MultiValueFieldRenderer;
/**
* Allows table header rendering to be dynamic and uses converters to convert an objet to
* String.
*/
public class PatchedMultiValueFieldRenderer extends MultiValueFieldRenderer {
   /**
    * @see javax.faces.render.Renderer#encodeEnd(javax.faces.context.FacesContext,
    *      javax.faces.component.UIComponent)
    */
   public void encodeEnd(FacesContext context, UIComponent component)
         throws IOException {
      if (component.isRendered() == false) {
         return;
      }

      if (component instanceof UIMultiValueEditor) {
         ResponseWriter out = context.getResponseWriter();
         UIMultiValueEditor editor = (UIMultiValueEditor) component;

         // get hold of the node service
         NodeService nodeService = Repository.getServiceRegistry(context)
               .getNodeService();

         // render the area between the component and current items list
         renderPostWrappedComponent(context, out, editor);

         // show the currently selected items
         out.write("<tr><td style='padding-top:8px'>");
         out.write(editor.getSelectedItemsMsg());
         out.write("</td></tr>");

         out.write("<tr><td><table cellspacing='0' cellpadding='2' border='0' class='selectedItems'>");
         renderTableHeader(context, component, out);
         
         List currentItems = (List) editor.getValue();

         if (currentItems != null && currentItems.size() > 0) {
            for (int x = 0; x < currentItems.size(); x++) {
               Object obj = currentItems.get(x);
               if (obj != null) {
                  if (obj instanceof NodeRef) {
                     if (nodeService.exists((NodeRef) obj)) {
                        renderExistingItem(context, component, out,
                              nodeService, x, obj);
                     } else {
                        // remove invalid NodeRefs from the list
                        currentItems.remove(x);
                     }
                  } else {
                     renderExistingItem(context, component, out,
                           nodeService, x, obj);
                  }
               }
            }
         } else {
            out.write("<tr><td class='selectedItemsRow'>");
            out.write(editor.getNoSelectedItemsMsg());
            out.write("</td></tr>");
         }

         // close tables
         out.write("</table></td></tr></table>\n");

         // output a hidden field containing the current value
         out.write("<input type='hidden' id='");
         out.write(component.getClientId(context));
         out.write("_current_value");
         out.write("' name='");
         out.write(component.getClientId(context));
         out.write("_current_value");
         out.write("' value='");
         if (currentItems != null && currentItems.size() > 0) {
            out.write(currentItems.toString());
         }
         out.write("' />");
      }
   }

   protected void renderTableHeader(FacesContext context,
         UIComponent component,ResponseWriter out) throws IOException{
        out.write("<tr><td colspan='2' class='selectedItemsHeader'>");
        out.write(Application.getMessage(context, "name"));
        out.write("</td></tr>");
   }

   protected void renderExistingItem(FacesContext context,
         UIComponent component, ResponseWriter out, NodeService nodeService,
         int index, Object value) throws IOException {
      if (value instanceof NodeRef || value instanceof Date
            || value instanceof Boolean) {
         super.renderExistingItem(context, component, out, nodeService,
               index, value);
      } else {
         String localValue = getStringValue(context, component, value);
         super.renderExistingItem(context, component, out, nodeService,
               index, localValue);
      }
   }

   protected String getStringValue(FacesContext context, UIComponent component, Object value) {
      Converter converter = context.getApplication().createConverter(
            value.getClass());
      String localValue;
      if (converter != null) {
         localValue = converter.getAsString(context, component, value);
      } else {
         localValue = value.toString();
      }
      return localValue;
   }
}




And the faces-config-custom.xml, for a Person Generator and a Department Generator will be something like…



   <managed-bean>
      <description>
         Bean that generates inputs for a Person
      </description>
      <managed-bean-name>PersonGenerator</managed-bean-name>
      <managed-bean-class>
         co.com.arkimia.alfresco.web.bean.generator.MultipleFieldsGenerator
      </managed-bean-class>
      <managed-bean-scope>request</managed-bean-scope>
      <managed-property>
         <property-name>componentTypes</property-name>
         <list-entries>
            <value>javax.faces.Input</value>
            <value>javax.faces.Input</value>
            <value>javax.faces.Input</value>
            <value>javax.faces.Input</value>
         </list-entries>
      </managed-property>
      <managed-property>
         <property-name>renderTypes</property-name>
         <list-entries>
            <value>javax.faces.Text</value>
            <value>javax.faces.Text</value>
            <value>javax.faces.Text</value>
            <value>javax.faces.Textarea</value>
         </list-entries>
      </managed-property>
      <managed-property>
         <property-name>labels</property-name>
         <list-entries>
            <value>Id</value>
            <value>First Name</value>
            <value>Last Name</value>
            <value>Recomendation</value>
         </list-entries>
      </managed-property>
   </managed-bean>

   <managed-bean>
      <description>
         Bean that generates inputs for a Department
      </description>
      <managed-bean-name>DepartmentGenerator</managed-bean-name>
      <managed-bean-class>
         co.com.arkimia.alfresco.web.bean.generator.MultipleFieldsGenerator
      </managed-bean-class>
      <managed-bean-scope>request</managed-bean-scope>
      <managed-property>
         <property-name>componentTypes</property-name>
         <list-entries>
            <value>javax.faces.Input</value>
            <value>javax.faces.Input</value>
            <value>javax.faces.SelectBoolean</value>
         </list-entries>
      </managed-property>
      <managed-property>
         <property-name>renderTypes</property-name>
         <list-entries>
            <value>javax.faces.Text</value>
            <value>javax.faces.Text</value>
            <value>javax.faces.Checkbox</value>
         </list-entries>
      </managed-property>
      <managed-property>
         <property-name>labels</property-name>
         <list-entries>
            <value>Id</value>
            <value>Name</value>
            <value>Enabled</value>
         </list-entries>
      </managed-property>
   </managed-bean>