A Simpler Select for Tapestry

After writing my last post, Howard came up with a better idea. Why to stick to only one implementation of Select ? If we need a simpler Select, why not create a simpler one. So, I came with a simple implementation!!

I have not fully tested it (something for next weekend), but it should work as it is mostly copied from the original Select component.

Usage

The usage is quite similar to the one suggested by Inge.

<select t:type='simpleSelect' t:value='user' t:items='users' t:label='name'
   t:key='id'/>

label can also be an expression like #{name} (#{address}). It uses '#' instead of '$' as '${}' expressions in a template are resolved by Tapestry and result is passed to the component instead of the actual expression.

How it works

@SupportsInformalParameters
@Events({ EventConstants.VALIDATE,
      EventConstants.VALUE_CHANGED + " when 'zone' parameter is 'bound'" })
public class SimpleSelect extends AbstractField {
   public static final String CHANGE_EVENT = "change";

   @Parameter(required = true, allowNull = false)
   private List<?> items;

   @Parameter(defaultPrefix = BindingConstants.LITERAL, allowNull = false)
   private String label;

   @Parameter(defaultPrefix = BindingConstants.LITERAL, allowNull = false)
   private String key;

   @Parameter(autoconnect = true, required = true)
   private Object value;

   @Parameter(value = "auto", defaultPrefix = BindingConstants.LITERAL)
   private BlankOption blankOption;

   @Parameter(defaultPrefix = BindingConstants.LITERAL)
   private String blankLabel;

   @Parameter(defaultPrefix = BindingConstants.VALIDATE)
   private FieldValidator<Object> validate;

   @Parameter(defaultPrefix = BindingConstants.LITERAL)
   private String zone;

   @Inject
   private Request request;

   @Inject
   private ComponentResources resources;

   @Inject
   private ComponentDefaultProvider defaultProvider;

   @Environmental
   private ValidationTracker tracker;

   @Inject
   private PropertyAccess propertyAccess;

   @Inject
   private TypeCoercer typeCoercer;

   @Inject
   private FieldValidationSupport fieldValidationSupport;

   @Inject
   private JavaScriptSupport javaScriptSupport;

   @SuppressWarnings("unused")
   @Mixin
   private RenderDisabled renderDisabled;

   private Map<String, PropertyAdapter> adapterMap;

   private final static Pattern PROPERTY_PATTERN = Pattern.compile("#\\{([\\w.$_]+)\\}");

   @Override
   protected void processSubmission(String elementName) {
      String submittedValue = request.getParameter(elementName);
      tracker.recordInput(this, submittedValue);

      Object selectedValue = toValue(submittedValue);
      putPropertyNameIntoBeanValidationContext("value");

      try {
         fieldValidationSupport.validate(selectedValue, resources, validate);
         value = selectedValue;
      } catch (ValidationException ex) {
         tracker.recordError(this, ex.getMessage());
      }

      removePropertyNameFromBeanValidationContext();
   }

   private Object toValue(String submittedValue) {
      if (InternalUtils.isBlank(submittedValue)) {
         return null;
      } else {
         Object submittedKey = typeCoercer.coerce(submittedValue, getPropertyType(key));

         for (Object item : items) {
            Object itemKey = getPropertyValue(item, key);
            if (itemKey.equals(submittedKey)) {
               return item;
            }
         }
         return null;
      }
   }
   
   Object onChange(@RequestParameter(value = "t:selectvalue", allowBlank = true)
      final String selectValue){
      final Object newValue = toValue(selectValue);
      CaptureResultCallback<Object> callback = new CaptureResultCallback<Object>();
      this.resources.triggerEvent(EventConstants.VALUE_CHANGED, new Object[]
      { newValue }, callback);
      this.value = newValue;
      return callback.getResult();
  }

   void beginRender(MarkupWriter writer) {
      writer.element("select", "id", getClientId(), "name", getControlName());
      putPropertyNameIntoBeanValidationContext("value");
      validate.render(writer);
      removePropertyNameFromBeanValidationContext();

      resources.renderInformalParameters(writer);
      decorateInsideField();
      if (zone != null) {
         Link link = resources.createEventLink(CHANGE_EVENT);
         JSONObject spec = new JSONObject("selectId", getClientId(), 
               "zoneId", zone, "url", link.toURI());
         javaScriptSupport.addInitializerCall("linkSelectToZone", spec);
      }
   }

   void afterRender(MarkupWriter writer) {
      writer.end();
   }

   @BeforeRenderTemplate
   void options(MarkupWriter writer) {
      if (showBlankOption()) {
         writer.element("option", "value", "");
         writer.write(blankLabel);
         writer.end();
      }

      for (Object item : items) {
         writer.element("option", "value", getPropertyValue(item, key));

         if (value != null && getPropertyValue(key).equals(getPropertyValue(item, key))) {
            writer.attributes("selected", "selected");
         }
         writer.write(getLabel(item));
         writer.end();
      }
   }

   private String getLabel(Object item) {
      if (label.contains("#")) {
         if (adapterMap == null) {
            adapterMap = new HashMap<String, PropertyAdapter>();
            Matcher matcher = PROPERTY_PATTERN.matcher(label);
            while (matcher.find()) {
               adapterMap.put(
                     matcher.group(0),
                     propertyAccess.getAdapter(resources.getBoundType("value")).
                     getPropertyAdapter(matcher.group(1)));
            }
         }

         String text = this.label;
         for (String key : adapterMap.keySet()) {
            String value = adapterMap.get(key) == null ? ""
                  : adapterMap.get(key).get(item).toString();
            text = text.replace(key, value);
         }
         return text;
      } else {
         Object propertyValue = getPropertyValue(item, label);
         if(propertyValue == null){
            return "";
         }else {
            return propertyValue.toString();
         }
      }
   }

   private boolean showBlankOption() {
      switch (blankOption) {
         case ALWAYS:
            return true;

         case NEVER:
            return false;

         default:
            return !isRequired();
      }
   }

   String defaultBlankLabel() {
      Messages containerMessages = resources.getContainerMessages();
      String key = resources.getId() + "-blanklabel";
      if (containerMessages.contains(key)) {
         return containerMessages.get(key);
      }
      return null;
   }

   Binding defaultValidate() {
      return defaultProvider.defaultValidatorBinding("value", resources);
   }

   private Object getPropertyValue(String property) {
      return getPropertyValue(value, property);
   }

   private Object getPropertyValue(Object object, String property) {
      if (object == null) {
         return null;
      }
      return propertyAccess.getAdapter(object).getPropertyAdapter(property).
             get(object);
   }

   private Class<?> getPropertyType(String property) {
      return propertyAccess.getAdapter(resources.getBoundType("value")).
             getPropertyAdapter(property).getType();
   }

}

This is mostly the same as the original Select component but instead of using a SelectModel it uses a List and instead of using a ValueEncoder, it relies on the TypeCoercer service for convertion between the client submitted value(String) and the item’s key.

There are two important changes.

  1. Line 85-92 : In order to get the submitted value, we convert the request parameter corresponding to this component to the item’s key using TypeCoercer service and then search through the list for an item with the same key. (Remember the key value has to be unique for each item)
  2. Line 126-144: Here we add a blank option, based on the blankOption parameter and blankLabel parameter. We then add an option tag for each item. The value for each option is obtained after converting its key to String. The content of each option is the resolved label expression.
Advertisements

Tagged: , ,

One thought on “A Simpler Select for Tapestry

  1. cenjames January 11, 2014 at 11:35 PM Reply

    Hi, I am relatively new to the Tapestry world. So I apologize in advance if I am asking a question too obvious or on the wrong forum. I am looking for some working example where I can have dropdowns based on entries from database tables in an editable grid. For example, if I were to have a editable grid for “role assignments”, I would like the “role” and “user” as dropdowns from the “user” and “role” tables in the database. I looked at the “jumpstart” examples for editable grid, but could not find an example with field as dropdowns populated from database tables.

    If you have some working examples that can be shared, it will be greatly appreciated.

    Thanks,

    James

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: