myfaces-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From we...@apache.org
Subject [myfaces-tobago] 02/09: Tobago-1999: suggest
Date Thu, 09 Jul 2020 08:59:26 GMT
This is an automated email from the ASF dual-hosted git repository.

weber pushed a commit to branch TOBAGO-1999_Select2
in repository https://gitbox.apache.org/repos/asf/myfaces-tobago.git

commit cfc7b604f53e27c75a5f64ed860f8eab67b8d690
Author: Volker Weber <v.weber@inexso.de>
AuthorDate: Fri Jan 10 13:54:24 2020 +0100

    Tobago-1999: suggest
---
 .../component/AbstractUISelectManyBox.java         |  35 ++-
 .../component/AbstractUISelectOneChoice.java       |  35 ++-
 .../internal/component/AbstractUISuggest.java      | 101 +++++++++
 .../internal/component/UISelect2Component.java     |  20 ++
 .../internal/taglib/declaration/Select2.java       |   1 +
 .../internal/util/UISelect2ComponentUtil.java      | 130 +++++++++++
 .../tobago/model/AutoSuggestExtensionItem.java     |   4 +-
 .../tobago/model/UICustomItemContainer.java        |  27 +++
 .../tobago/renderkit/html/DataAttributes.java      |   4 +
 .../myfaces/tobago}/util/SelectItemUtils.java      |   3 +-
 .../tobago/example/demo/Select2Controller.java     | 198 +++++++++++++++-
 .../content/25-select/00-select2/select2.xhtml     |  54 ++++-
 .../standard/tag/SelectManyBoxRenderer.java        |  24 +-
 .../standard/tag/SelectOneChoiceRenderer.java      |  23 +-
 .../standard/standard/tag/SuggestRenderer.java     |  95 +++++---
 .../renderkit/html/util/HtmlRendererUtils.java     |  14 +-
 .../tobago/renderkit/util/SelectItemUtils.java     | 248 +--------------------
 .../standard/standard/script/tobago-select2.js     | 141 +++++++++++-
 18 files changed, 835 insertions(+), 322 deletions(-)

diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISelectManyBox.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISelectManyBox.java
index 56c657d..f6ecc6b 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISelectManyBox.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISelectManyBox.java
@@ -6,19 +6,42 @@
 package org.apache.myfaces.tobago.internal.component;
 
 import org.apache.myfaces.tobago.internal.component.AbstractUISelectOneChoice.Select2Keys;
+import org.apache.myfaces.tobago.internal.util.UISelect2ComponentUtil;
+import org.apache.myfaces.tobago.util.ComponentUtils;
 
+import javax.faces.component.StateHelper;
 import javax.faces.context.FacesContext;
-import java.util.ArrayList;
+import java.io.IOException;
 import java.util.HashSet;
 import java.util.Set;
 
-public abstract class AbstractUISelectManyBox extends AbstractUISelectMany {
+public abstract class AbstractUISelectManyBox extends AbstractUISelectMany implements UISelect2Component {
+
+
+
+  public AbstractUISuggest getSuggest() {
+    return ComponentUtils.findDescendant(this, AbstractUISuggest.class);
+  }
 
   @Override
-  protected void validateValue(FacesContext context, Object convertedValue) {
-    if (!isAllowCustom()) {
-      super.validateValue(context, convertedValue);
-    }
+  protected void validateValue(FacesContext facesContext, Object convertedValue) {
+  UISelect2ComponentUtil.ensureCustomItemsContainer(facesContext, this);
+    super.validateValue(facesContext, UISelect2ComponentUtil.ensureCustomValues(facesContext, this, convertedValue));
+  }
+
+  @Override
+  public Object getValue() {
+    return UISelect2ComponentUtil.ensureCustomValues(FacesContext.getCurrentInstance(), this, super.getValue());
+  }
+
+  @Override
+  public void encodeChildren(FacesContext facesContext) throws IOException {
+    UISelect2ComponentUtil.ensureCustomItemsContainer(facesContext, this);
+    super.encodeChildren(facesContext);
+  }
+
+  public StateHelper getComponentStateHelper() {
+    return getStateHelper();
   }
 
   public boolean isAllowClear() {
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISelectOneChoice.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISelectOneChoice.java
index 8555b95..771d2e0 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISelectOneChoice.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISelectOneChoice.java
@@ -19,7 +19,19 @@
 
 package org.apache.myfaces.tobago.internal.component;
 
-public abstract class AbstractUISelectOneChoice extends AbstractUISelectOneBase {
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.myfaces.tobago.internal.util.UISelect2ComponentUtil;
+import org.apache.myfaces.tobago.util.ComponentUtils;
+
+import javax.faces.component.StateHelper;
+import javax.faces.context.FacesContext;
+import java.io.IOException;
+
+public abstract class AbstractUISelectOneChoice extends AbstractUISelectOneBase implements UISelect2Component {
+
+  private static final Logger LOG = LoggerFactory.getLogger(AbstractUISelectOneChoice.class);
 
   enum Select2Keys {
     allowClear,
@@ -36,9 +48,30 @@ public abstract class AbstractUISelectOneChoice extends AbstractUISelectOneBase
     tokenSeparators
   }
 
+  public AbstractUISuggest getSuggest() {
+    return ComponentUtils.findDescendant(this, AbstractUISuggest.class);
+  }
+
+  @Override
+  protected void validateValue(FacesContext facesContext, Object value) {
+    UISelect2ComponentUtil.ensureCustomItemsContainer(facesContext, this);
+    super.validateValue(facesContext, UISelect2ComponentUtil.ensureCustomValue(facesContext, this, value));
+  }
 
+  @Override
+  public Object getValue() {
+    return UISelect2ComponentUtil.ensureCustomValue(FacesContext.getCurrentInstance(), this, super.getValue());
+  }
 
+  @Override
+  public void encodeChildren(FacesContext facesContext) throws IOException {
+    UISelect2ComponentUtil.ensureCustomItemsContainer(facesContext, this);
+    super.encodeChildren(facesContext);
+  }
 
+  public StateHelper getComponentStateHelper() {
+    return getStateHelper();
+  }
 
   public boolean isAllowClear() {
     Boolean allowClear = (Boolean) getStateHelper().eval(Select2Keys.allowClear);
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISuggest.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISuggest.java
index 548dda8..2197709 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISuggest.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/AbstractUISuggest.java
@@ -19,26 +19,127 @@
 
 package org.apache.myfaces.tobago.internal.component;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import org.apache.myfaces.tobago.component.InputSuggest2;
 import org.apache.myfaces.tobago.component.SupportsMarkup;
+import org.apache.myfaces.tobago.component.UISelectItems;
+import org.apache.myfaces.tobago.component.UISelectManyBox;
+import org.apache.myfaces.tobago.component.UISelectOneChoice;
+import org.apache.myfaces.tobago.model.AutoSuggestItem;
+import org.apache.myfaces.tobago.model.AutoSuggestItems;
 import org.apache.myfaces.tobago.model.SuggestFilter;
 
+import javax.el.MethodExpression;
 import javax.faces.component.UIComponentBase;
+import javax.faces.component.UIInput;
+import javax.faces.context.FacesContext;
+import javax.faces.convert.Converter;
+import javax.faces.model.SelectItem;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 public abstract class AbstractUISuggest
     extends UIComponentBase implements SupportsMarkup, InputSuggest2 {
 
+  private static final Logger LOG = LoggerFactory.getLogger(AbstractUISuggest.class);
+
   public static final String COMPONENT_TYPE = "org.apache.myfaces.tobago.Suggest";
   public static final String COMPONENT_FAMILY = "org.apache.myfaces.tobago.Suggest";
 
+  private static final String ITEM_MAP_KEY
+      = "org.apache.myfaces.tobago.internal.component.AbstractUISuggest.ITEM_MAP_KEY";
+
   @Override
   public String getFamily() {
     return COMPONENT_FAMILY;
   }
 
+  public AutoSuggestItems getSuggestItems(FacesContext facesContext) {
+    final MethodExpression suggestMethodExpression = getSuggestMethodExpression();
+
+    UIInput in;
+    if (isSelect2()) {
+      String search = FacesContext.getCurrentInstance().getExternalContext().getRequestParameterMap()
+          .get(getClientId());
+      LOG.trace(" search = \"{}\"", search);
+      in = new UIInput();
+      in.setSubmittedValue(search);
+    } else {
+      in = (UIInput) getParent();
+    }
+    AutoSuggestItems suggestItems
+        = createAutoSuggestItems(suggestMethodExpression.invoke(facesContext.getELContext(), new Object[]{in}));
+    if (!suggestItems.getItems().isEmpty()) {
+      //noinspection unchecked
+      Map<String, AutoSuggestItem> itemMap = (Map<String, AutoSuggestItem>) getStateHelper().get(ITEM_MAP_KEY);
+      if (itemMap == null) {
+        itemMap = new HashMap<String, AutoSuggestItem>();
+        getStateHelper().put(ITEM_MAP_KEY, itemMap);
+      }
+      for (AutoSuggestItem item : suggestItems.getItems()) {
+        if (!itemMap.containsKey(item.getValue())) {
+          itemMap.put(item.getValue(), item);
+        }
+      }
+    }
+    return suggestItems;
+  }
+
+  public boolean isSelect2() {
+    return getParent() instanceof UISelectManyBox || getParent() instanceof UISelectOneChoice;
+  }
+
+  private AutoSuggestItems createAutoSuggestItems(final Object object) {
+    if (object instanceof AutoSuggestItems) {
+      return (AutoSuggestItems) object;
+    }
+    final AutoSuggestItems autoSuggestItems = new AutoSuggestItems();
+    if (object instanceof List && !((List) object).isEmpty()) {
+      if (((List) object).get(0) instanceof AutoSuggestItem) {
+        //noinspection unchecked
+        autoSuggestItems.setItems((List<AutoSuggestItem>) object);
+      } else if (((List) object).get(0) instanceof String) {
+        final List<AutoSuggestItem> items = new ArrayList<AutoSuggestItem>(((List) object).size());
+        for (int i = 0; i < ((List) object).size(); i++) {
+          final AutoSuggestItem item = new AutoSuggestItem();
+          item.setLabel((String) ((List) object).get(i));
+          item.setValue((String) ((List) object).get(i));
+          items.add(item);
+        }
+        autoSuggestItems.setItems(items);
+      } else {
+        throw new ClassCastException("Can't create AutoSuggestItems from '" + object + "'. "
+            + "Elements needs to be " + String.class.getName() + " or " + AutoSuggestItem.class.getName());
+      }
+    } else {
+      autoSuggestItems.setItems(Collections.<AutoSuggestItem>emptyList());
+    }
+    return autoSuggestItems;
+  }
+
+  public SelectItem getSelectItem(FacesContext facesContext, Object value, Converter converter) {
+    //noinspection unchecked
+    Map<String, AutoSuggestItem> itemMap = (Map<String, AutoSuggestItem>) getStateHelper().get(ITEM_MAP_KEY);
+    if (itemMap != null) {
+      String id = converter != null ? converter.getAsString(facesContext, getParent(), value) : (String) value;
+      AutoSuggestItem autoSuggestItem = itemMap.get(id);
+      if (autoSuggestItem != null) {
+        return new SelectItem(value, autoSuggestItem.getLabel());
+      }
+    }
+    return null;
+  }
+
   public abstract void setDelay(Integer delay);
 
   public abstract void setMinimumCharacters(Integer minimumCharacters);
 
   public abstract void setFilter(SuggestFilter filter);
+
+  public abstract Integer getMinimumCharacters();
 }
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/UISelect2Component.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/UISelect2Component.java
new file mode 100644
index 0000000..f7d456a
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/component/UISelect2Component.java
@@ -0,0 +1,20 @@
+package org.apache.myfaces.tobago.internal.component;
+
+import javax.faces.component.StateHelper;
+import javax.faces.component.UIComponent;
+import javax.faces.convert.Converter;
+import java.util.HashSet;
+import java.util.Map;
+
+public interface UISelect2Component {
+
+  AbstractUISuggest getSuggest();
+
+  boolean isAllowCustom();
+
+  Converter getConverter();
+
+  StateHelper getComponentStateHelper();
+
+
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/declaration/Select2.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/declaration/Select2.java
index b5506f6..e682f5d 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/declaration/Select2.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/declaration/Select2.java
@@ -2,6 +2,7 @@ package org.apache.myfaces.tobago.internal.taglib.declaration;
 
 import org.apache.myfaces.tobago.apt.annotation.TagAttribute;
 import org.apache.myfaces.tobago.apt.annotation.UIComponentTagAttribute;
+import org.apache.myfaces.tobago.internal.component.AbstractUISuggest;
 
 public interface Select2 {
 
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/util/UISelect2ComponentUtil.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/util/UISelect2ComponentUtil.java
new file mode 100644
index 0000000..539f559
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/util/UISelect2ComponentUtil.java
@@ -0,0 +1,130 @@
+package org.apache.myfaces.tobago.internal.util;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.myfaces.tobago.internal.component.AbstractUISelectManyBox;
+import org.apache.myfaces.tobago.internal.component.AbstractUISuggest;
+import org.apache.myfaces.tobago.internal.component.UISelect2Component;
+import org.apache.myfaces.tobago.model.SelectItem;
+import org.apache.myfaces.tobago.model.UICustomItemContainer;
+import org.apache.myfaces.tobago.util.SelectItemUtils;
+
+import javax.faces.component.UIComponent;
+import javax.faces.context.FacesContext;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
+
+public class UISelect2ComponentUtil {
+
+  private static final Logger LOG = LoggerFactory.getLogger(UISelect2ComponentUtil.class);
+
+  private static final String VALID_CUSTOMITEM_MAP
+      = "org.apache.myfaces.tobago.internal.util.UISelect2ComponentUtil.valid_CustomItem_Map";
+
+  private UISelect2ComponentUtil() {
+  }
+
+  public static Object ensureCustomValue(FacesContext facesContext, UISelect2Component component, Object value) {
+    if (value != null && !"".equals(value)) {
+      AbstractUISuggest suggest = component.getSuggest();
+      if (component.isAllowCustom() || suggest != null) {
+        ensureCustomValue(facesContext, component, value, suggest);
+      }
+    }
+    return value;
+  }
+
+  public static Object ensureCustomValues(FacesContext facesContext, UISelect2Component component, Object values) {
+    if (values != null) {
+      AbstractUISuggest suggest = component.getSuggest();
+      if (component.isAllowCustom() || suggest != null) {
+        if (values instanceof Object[]) {
+          for (Object value : (Object[]) values) {
+            ensureCustomValue(facesContext, component, value, suggest);
+          }
+        } else if (values instanceof Collection) {
+          for (Object value : (Collection) values) {
+            ensureCustomValue(facesContext, component, value, suggest);
+          }
+        }
+      }
+    }
+    return values;
+  }
+
+  public static void ensureCustomValue(
+      FacesContext facesContext, UISelect2Component component, Object value, AbstractUISuggest suggest) {
+
+    if (!isInItemlist(facesContext, component, value)) {
+      javax.faces.model.SelectItem item = null;
+      if (suggest != null) {
+        item = suggest.getSelectItem(facesContext, value, component.getConverter());
+      }
+      if (item == null && component.isAllowCustom()) {
+        item = new SelectItem(value);
+      }
+      if (item != null) {
+        LOG.trace("ADD item = \"{}\"", item.getValue());
+        getValidCustomItemMap(component).add(item);
+      }
+    }
+  }
+
+  private static boolean isInItemlist(FacesContext facesContext, UISelect2Component component, Object value) {
+    LOG.trace("check for value = \"{}\"", value);
+    Iterable<javax.faces.model.SelectItem> items
+        = SelectItemUtils.getItemIterator(facesContext, (UIComponent) component);
+    for (javax.faces.model.SelectItem item : items) {
+      LOG.trace("check item value = \"{}\"", item.getValue());
+      if (item.getValue() != null && item.getValue().equals(value)) {
+        LOG.trace("TRUE");
+        return true;
+      }
+    }
+    LOG.trace("false");
+    return false;
+  }
+
+
+  public static Set<javax.faces.model.SelectItem> getValidCustomItemMap(UISelect2Component component) {
+    Object o = component.getComponentStateHelper().get(VALID_CUSTOMITEM_MAP);
+    //noinspection unchecked
+    HashSet<javax.faces.model.SelectItem> set = (HashSet<javax.faces.model.SelectItem>) o;
+    if (set == null) {
+      set = new HashSet<javax.faces.model.SelectItem>();
+      component.getComponentStateHelper().put(VALID_CUSTOMITEM_MAP, set);
+    }
+    return set;
+  }
+
+  public static void ensureCustomItemsContainer(FacesContext facesContext, UISelect2Component component) {
+    Set<javax.faces.model.SelectItem> validCustomItemMap = getValidCustomItemMap(component);
+    boolean done = false;
+    for (UIComponent child : ((UIComponent) component).getChildren()) {
+      if (child instanceof UICustomItemContainer) {
+        ((UICustomItemContainer) child).setValue(validCustomItemMap);
+        done = true;
+        break;
+      }
+    }
+    if (!done) {
+      ((UIComponent) component).getChildren().add(new UICustomItemContainer(validCustomItemMap));
+    }
+
+    if (component instanceof AbstractUISelectManyBox) {
+      // remove duplicates
+      Set<javax.faces.model.SelectItem> set = new HashSet<javax.faces.model.SelectItem>(validCustomItemMap);
+      validCustomItemMap.clear();
+      for (javax.faces.model.SelectItem selectItem : set) {
+        LOG.trace("cleanup check = \"{}\"", selectItem.getValue());
+        if (!isInItemlist(facesContext, component, selectItem.getValue())) {
+          LOG.trace("cleanup readd = \"{}\"", selectItem.getValue());
+          validCustomItemMap.add(selectItem);
+        }
+      }
+    }
+  }
+
+}
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/model/AutoSuggestExtensionItem.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/model/AutoSuggestExtensionItem.java
index 3b22356..c6ef5a8 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/model/AutoSuggestExtensionItem.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/model/AutoSuggestExtensionItem.java
@@ -20,7 +20,9 @@
 package org.apache.myfaces.tobago.model;
 
 
-public class AutoSuggestExtensionItem {
+import java.io.Serializable;
+
+public class AutoSuggestExtensionItem implements Serializable {
   private String id;
   private String value;
 
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/model/UICustomItemContainer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/model/UICustomItemContainer.java
new file mode 100644
index 0000000..813d193
--- /dev/null
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/model/UICustomItemContainer.java
@@ -0,0 +1,27 @@
+package org.apache.myfaces.tobago.model;
+
+import javax.faces.component.UISelectItems;
+import java.util.Set;
+
+public class UICustomItemContainer extends UISelectItems {
+
+  public UICustomItemContainer() {
+  }
+
+  public UICustomItemContainer(Set<javax.faces.model.SelectItem> validCustomItems) {
+    setValue(validCustomItems);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    return true;
+  }
+
+  @Override
+  public int hashCode() {
+    return getClass().hashCode();
+  }
+
+}
\ No newline at end of file
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java
index e4a48e4..061b686 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/html/DataAttributes.java
@@ -228,12 +228,16 @@ public final class DataAttributes {
 
   public static final String SUGGEST_DELAY = "data-tobago-suggest-delay";
 
+  public static final String SUGGEST_ID = "data-tobago-suggest-id";
+
   public static final String SUGGEST_ITEM_FOR = "data-tobago-suggest-item-for";
 
   public static final String SUGGEST_MAX_ITEMS = "data-tobago-suggest-max-items";
 
   public static final String SUGGEST_MIN_CHARS = "data-tobago-suggest-min-chars";
 
+  public static final String SUGGEST_RESPONSE_DATA = "data-tobago-suggest-response-data";
+
   public static final String SUGGEST_TOTAL_COUNT = "data-tobago-suggest-total-count";
 
   public static final String SUGGEST_UPDATE = "data-tobago-suggest-update";
diff --git a/tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/util/SelectItemUtils.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/util/SelectItemUtils.java
similarity index 99%
copy from tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/util/SelectItemUtils.java
copy to tobago-core/src/main/java/org/apache/myfaces/tobago/util/SelectItemUtils.java
index cdd4ea9..874a18a 100644
--- a/tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/util/SelectItemUtils.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/util/SelectItemUtils.java
@@ -16,8 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
-package org.apache.myfaces.tobago.renderkit.util;
+package org.apache.myfaces.tobago.util;
 
 import org.apache.myfaces.tobago.component.Attributes;
 import org.apache.myfaces.tobago.component.SupportsMarkup;
diff --git a/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/Select2Controller.java b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/Select2Controller.java
index 1e81080..6be927c 100644
--- a/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/Select2Controller.java
+++ b/tobago-example/tobago-example-demo/src/main/java/org/apache/myfaces/tobago/example/demo/Select2Controller.java
@@ -1,27 +1,207 @@
 package org.apache.myfaces.tobago.example.demo;
 
-import org.apache.myfaces.tobago.renderkit.html.JsonUtils;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.myfaces.tobago.model.AutoSuggestItem;
+import org.apache.myfaces.tobago.model.SelectItem;
 
 import javax.enterprise.context.SessionScoped;
+import javax.faces.component.UIComponent;
+import javax.faces.component.UIInput;
+import javax.faces.context.FacesContext;
+import javax.faces.convert.Converter;
+import javax.faces.convert.ConverterException;
 import javax.inject.Named;
 import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
 
 @SessionScoped
 @Named
 public class Select2Controller implements Serializable {
 
-  private String one2;
+  private static final Logger LOG = LoggerFactory.getLogger(Select2Controller.class);
+
+  private LocaleConverter localeConverter = new LocaleConverter();
+
+  private String one2Value;
+
+  private String one3Value;
+
+  private Locale one4Locale;
+
+  private List<Locale> many8Locales;
+
+  private List<String> many7Countries;
+
+  private List<SelectItem> items;
+
+  public Select2Controller() {
+    items = new ArrayList<SelectItem>();
+    items.add(new SelectItem("letter", "Letter"));
+    items.add(new SelectItem("phone", "Phone"));
+    items.add(new SelectItem("eMail", "eMail"));
+    items.add(new SelectItem("fax", "Fax"));
+  }
+
+  public List<SelectItem> getItems() {
+    return items;
+  }
+
+  public List<SelectItem> getLocaleItems() {
+    return items;
+  }
+
+  public Converter getLocaleConverter() {
+    return localeConverter;
+  }
+
+  public List<AutoSuggestItem> suggestLocale(final UIInput input) {
+    return localeConverter.getSuggestLocale((String) input.getSubmittedValue());
+  }
+
+  public String getOne2Value() {
+    return one2Value;
+  }
+
+  public void setOne2Value(String one2Value) {
+    this.one2Value = one2Value;
+  }
+
+  public String getOne3Value() {
+    return one3Value;
+  }
+
+  public void setOne3Value(String one3Value) {
+    this.one3Value = one3Value;
+  }
+
+  public Locale getOne4Locale() {
+    LOG.warn("get one4Locale = \"{}\"", one4Locale);
+    return one4Locale;
+  }
+
+  public void setOne4Locale(Locale one4Locale) {
+    LOG.warn("set one4Locale = \"{}\"", one4Locale);
+    this.one4Locale = one4Locale;
+  }
+
+  public SelectItem[] getOne4LocaleItem() {
+    if (one4Locale != null) {
+      Locale displayLocale = FacesContext.getCurrentInstance().getViewRoot().getLocale();
+      return new SelectItem[] {new SelectItem(one4Locale, one4Locale.getDisplayName(displayLocale))};
+    } else {
+      return new SelectItem[0];
+    }
+  }
+
+
+  public List<String> getMany7Countries() {
+    return many7Countries;
+  }
+
+  public void setMany7Countries(List<String> many7Countries) {
+    this.many7Countries = many7Countries;
+  }
+
+  public List<SelectItem> getMany7CountryItems() {
+    if (many7Countries != null && !many7Countries.isEmpty()) {
+      List<SelectItem> items = new ArrayList<SelectItem>();
+      for (String locale : many7Countries) {
+        items.add(new SelectItem(locale));
+      }
+      return items;
+    } else {
+      return Collections.emptyList();
+    }
+  }
 
-  public String getOne2() {
-    return one2;
+  public List<Locale> getMany8Locales() {
+    LOG.warn("get many8Locales = \"{}\"", many8Locales);
+    return many8Locales;
   }
 
-  public void setOne2(String one2) {
-    this.one2 = one2;
+  public void setMany8Locales(List<Locale> many8Locales) {
+    LOG.warn("set many8Locales = \"{}\"", many8Locales);
+    this.many8Locales = many8Locales;
   }
 
-  public String getCaseSensitiveMatcher() {
-    return "{\"matcher\": \"Tobago.Select2.caseSensitiveMatcher\"}";
-//    JsonUtils.encode()
+  public List<SelectItem> getMany8LocaleItems() {
+    if (many8Locales != null && !many8Locales.isEmpty()) {
+      Locale displayLocale = FacesContext.getCurrentInstance().getViewRoot().getLocale();
+      List<SelectItem> items = new ArrayList<SelectItem>();
+      for (Locale locale : many8Locales) {
+        items.add(new SelectItem(locale, locale.getDisplayName(displayLocale)));
+      }
+      return items;
+    } else {
+      return Collections.emptyList();
+    }
+  }
+
+  private class LocaleConverter implements Converter {
+
+    private Map<String, Locale> localeMap;
+
+    public LocaleConverter() {
+      localeMap = new HashMap<String, Locale>();
+      for (final Locale locale : Locale.getAvailableLocales()) {
+        localeMap.put(locale.toString(), locale);
+      }
+    }
+
+    public Object getAsObject(FacesContext facesContext, UIComponent uiComponent, String value)
+        throws ConverterException {
+      if (value != null) {
+        Locale locale = localeMap.get(value);
+        if (locale != null) {
+          return locale;
+        } else {
+          Locale displayLocale = facesContext.getViewRoot().getLocale();
+          for (Locale mapLocale : localeMap.values()) {
+            if (mapLocale.getDisplayName(displayLocale).equals(value)) {
+              return mapLocale;
+            }
+          }
+          throw new ConverterException("Could not convert \"" + value + "\" to Locale");
+        }
+      } else {
+        return null;
+      }
+    }
+
+    public String getAsString(FacesContext facesContext, UIComponent uiComponent, Object o) throws ConverterException {
+      if (o != null) {
+        return ((Locale) o).toString();
+      } else {
+        return null;
+      }
+    }
+
+    public List<AutoSuggestItem> getSuggestLocale(String prefix) {
+      LOG.info("Creating items for prefix: '" + prefix + "'");
+      Locale displayLocale = FacesContext.getCurrentInstance().getViewRoot().getLocale();
+      final List<AutoSuggestItem> result = new ArrayList<AutoSuggestItem>();
+      for (final Locale locale : localeMap.values()) {
+        if (StringUtils.startsWithIgnoreCase(locale.getDisplayName(displayLocale), prefix)) {
+          AutoSuggestItem suggestItem = new AutoSuggestItem();
+          suggestItem.setValue(locale.toString());
+          suggestItem.setLabel(locale.getDisplayName(displayLocale));
+          result.add(suggestItem);
+        }
+        if (result.size() > 100) { // this value should be greater than the value of the input control
+          break;
+        }
+      }
+      return result;
+    }
+
+
   }
 }
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/content/25-select/00-select2/select2.xhtml b/tobago-example/tobago-example-demo/src/main/webapp/content/25-select/00-select2/select2.xhtml
index 4e1edeb..d5febd3 100644
--- a/tobago-example/tobago-example-demo/src/main/webapp/content/25-select/00-select2/select2.xhtml
+++ b/tobago-example/tobago-example-demo/src/main/webapp/content/25-select/00-select2/select2.xhtml
@@ -67,7 +67,7 @@
         <tc:selectOneChoice id="one_2"
                             minimumResultsForSearch="10"
                             placeholder="Please select message type"
-                            value="#{select2Controller.one2}">
+                            value="#{select2Controller.one2Value}">
 
           <tc:selectItem itemLabel="Letter" itemValue="letter"/>
           <tc:selectItem itemLabel="Phone" itemValue="phone"/>
@@ -80,12 +80,18 @@
                             allowCustom="true"
                             placeholder="Custom input allowed"
                             allowClear="true"
-                            value="">
+                            value="#{select2Controller.one3Value}">
 
-          <tc:selectItem itemLabel="Letter" itemValue="letter"/>
-          <tc:selectItem itemLabel="Phone" itemValue="phone"/>
-          <tc:selectItem itemLabel="eMail" itemValue="eMail"/>
-          <tc:selectItem itemLabel="Fax" itemValue="fax"/>
+          <tc:selectItems value="#{select2Controller.items}"/>
+        </tc:selectOneChoice>
+
+        <tc:label value="Suggest locale" for="one_4"/>
+        <tc:selectOneChoice id="one_4"
+                            placeholder="Please select a locale"
+                            converter="#{select2Controller.localeConverter}"
+                            value="#{select2Controller.one4Locale}">
+          <tc:selectItems value="#{select2Controller.one4LocaleItem}"/>
+          <tc:suggest suggestMethod="#{select2Controller.suggestLocale}" minimumCharacters="2" />
         </tc:selectOneChoice>
 
       </tc:panel>
@@ -189,6 +195,42 @@
         </tc:selectManyBox>
 
       </tc:panel>
+      <tc:panel>
+        <f:facet name="layout">
+          <tc:gridLayout columns="400px;1*" rows="45px"/>
+        </f:facet>
+
+        <tc:label value="Suggest countries" for="many_7"/>
+        <tc:selectManyBox id="many_7"
+                          placeholder="Select countries"
+                          allowCustom="true"
+                          tokenSeparators=","
+                          value="#{select2Controller.many7Countries}">
+
+          <tc:selectItems value="#{select2Controller.many7CountryItems}"/>
+          <tc:suggest suggestMethod="#{countries.prefixed}" minimumCharacters="2" />
+          <tc:dataAttribute name="tobago-select2-extend" value='{"resultsAdapter": "suppressMessages"}'/>
+
+
+        </tc:selectManyBox>
+
+      </tc:panel>
+
+      <tc:panel>
+        <f:facet name="layout">
+          <tc:gridLayout columns="400px;1*" rows="45px"/>
+        </f:facet>
+
+        <tc:label value="Suggest locales" for="many_8"/>
+        <tc:selectManyBox id="many_8"
+                          placeholder="Select locales"
+                          converter="#{select2Controller.localeConverter}"
+                          value="#{select2Controller.many8Locales}">
+          <tc:selectItems value="#{select2Controller.many8LocaleItems}"/>
+          <tc:suggest suggestMethod="#{select2Controller.suggestLocale}" minimumCharacters="2" />
+        </tc:selectManyBox>
+
+      </tc:panel>
     </tc:panel>
 
 
diff --git a/tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/html/standard/standard/tag/SelectManyBoxRenderer.java b/tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/html/standard/standard/tag/SelectManyBoxRenderer.java
index 4010fa6..3c202e8 100644
--- a/tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/html/standard/standard/tag/SelectManyBoxRenderer.java
+++ b/tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/html/standard/standard/tag/SelectManyBoxRenderer.java
@@ -19,13 +19,17 @@
 
 package org.apache.myfaces.tobago.renderkit.html.standard.standard.tag;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import org.apache.myfaces.tobago.component.UISelectManyBox;
-import org.apache.myfaces.tobago.component.UISelectManyListbox;
+import org.apache.myfaces.tobago.internal.component.AbstractUISuggest;
 import org.apache.myfaces.tobago.internal.util.FacesContextUtils;
 import org.apache.myfaces.tobago.layout.Measure;
 import org.apache.myfaces.tobago.renderkit.SelectManyRendererBase;
 import org.apache.myfaces.tobago.renderkit.css.Classes;
 import org.apache.myfaces.tobago.renderkit.css.Style;
+import org.apache.myfaces.tobago.renderkit.html.DataAttributes;
 import org.apache.myfaces.tobago.renderkit.html.HtmlAttributes;
 import org.apache.myfaces.tobago.renderkit.html.HtmlElements;
 import org.apache.myfaces.tobago.renderkit.html.Select2Options;
@@ -39,9 +43,6 @@ import javax.faces.context.FacesContext;
 import javax.faces.model.SelectItem;
 import java.io.IOException;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 public class SelectManyBoxRenderer extends SelectManyRendererBase {
 
   private static final Logger LOG = LoggerFactory.getLogger(SelectManyBoxRenderer.class);
@@ -70,15 +71,21 @@ public class SelectManyBoxRenderer extends SelectManyRendererBase {
 
     final UISelectManyBox select = (UISelectManyBox) component;
     final TobagoResponseWriter writer = HtmlRendererUtils.getTobagoResponseWriter(facesContext);
+    AbstractUISuggest suggest = (AbstractUISuggest) select.getSuggest();
 
     final String id = select.getClientId(facesContext);
     final Iterable<SelectItem> items = SelectItemUtils.getItemIterator(facesContext, select);
     final boolean readonly = select.isReadonly();
-    final boolean disabled = (!items.iterator().hasNext() && !select.isAllowCustom())
-        || select.isDisabled() || readonly;
+    final boolean disabled = !(suggest != null || select.isAllowCustom() || items.iterator().hasNext())
+        || select.isDisabled()
+        || select.isReadonly();
     final Style style = new Style(facesContext, select);
 
-    ComponentUtils.putDataAttribute(select, "tobago-select2", Select2Options.of(select).toJson());
+    Select2Options select2Options = Select2Options.of(select);
+    if (suggest != null) {
+      select2Options.setMinimumInputLength(suggest.getMinimumCharacters());
+    }
+    ComponentUtils.putDataAttribute(select, "tobago-select2", select2Options.toJson());
 
     final String title = HtmlRendererUtils.getTitleFromTipAndMessages(facesContext, select);
     writer.startElement(HtmlElements.DIV, select);
@@ -93,6 +100,9 @@ public class SelectManyBoxRenderer extends SelectManyRendererBase {
     writer.writeAttribute(HtmlAttributes.DISABLED, disabled);
     writer.writeAttribute(HtmlAttributes.READONLY, readonly);
     writer.writeAttribute(HtmlAttributes.REQUIRED, select.isRequired());
+    if (suggest != null) {
+      writer.writeAttribute(DataAttributes.SUGGEST_ID, suggest.getClientId(facesContext), false);
+    }
     HtmlRendererUtils.renderFocus(id, select.isFocus(), ComponentUtils.isError(select), facesContext, writer);
     final Integer tabIndex = select.getTabIndex();
     if (tabIndex != null) {
diff --git a/tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/html/standard/standard/tag/SelectOneChoiceRenderer.java b/tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/html/standard/standard/tag/SelectOneChoiceRenderer.java
index 8c4d5d8..c1963b3 100644
--- a/tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/html/standard/standard/tag/SelectOneChoiceRenderer.java
+++ b/tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/html/standard/standard/tag/SelectOneChoiceRenderer.java
@@ -19,23 +19,24 @@
 
 package org.apache.myfaces.tobago.renderkit.html.standard.standard.tag;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import org.apache.myfaces.tobago.component.UISelectOneChoice;
-import org.apache.myfaces.tobago.internal.util.FacesContextUtils;
+import org.apache.myfaces.tobago.internal.component.AbstractUISuggest;
 import org.apache.myfaces.tobago.layout.Measure;
-import org.apache.myfaces.tobago.renderkit.html.Select2Options;
 import org.apache.myfaces.tobago.renderkit.HtmlUtils;
 import org.apache.myfaces.tobago.renderkit.SelectOneRendererBase;
 import org.apache.myfaces.tobago.renderkit.css.Classes;
 import org.apache.myfaces.tobago.renderkit.css.Style;
+import org.apache.myfaces.tobago.renderkit.html.DataAttributes;
 import org.apache.myfaces.tobago.renderkit.html.HtmlAttributes;
 import org.apache.myfaces.tobago.renderkit.html.HtmlElements;
-import org.apache.myfaces.tobago.renderkit.html.JsonUtils;
+import org.apache.myfaces.tobago.renderkit.html.Select2Options;
 import org.apache.myfaces.tobago.renderkit.html.util.HtmlRendererUtils;
 import org.apache.myfaces.tobago.renderkit.util.SelectItemUtils;
 import org.apache.myfaces.tobago.util.ComponentUtils;
 import org.apache.myfaces.tobago.webapp.TobagoResponseWriter;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import javax.faces.component.UIComponent;
 import javax.faces.context.FacesContext;
@@ -68,14 +69,20 @@ public class SelectOneChoiceRenderer extends SelectOneRendererBase {
 
     final UISelectOneChoice select = (UISelectOneChoice) component;
     final TobagoResponseWriter writer = HtmlRendererUtils.getTobagoResponseWriter(facesContext);
+    AbstractUISuggest suggest = (AbstractUISuggest) select.getSuggest();
 
     final String id = select.getClientId(facesContext);
     final Iterable<SelectItem> items = SelectItemUtils.getItemIterator(facesContext, select);
     final String title = HtmlRendererUtils.getTitleFromTipAndMessages(facesContext, select);
-    final boolean disabled = !items.iterator().hasNext() || select.isDisabled() || select.isReadonly();
+    final boolean disabled = !(suggest != null || select.isAllowCustom() || items.iterator().hasNext())
+        || select.isDisabled()
+        || select.isReadonly();
     final Style style = new Style(facesContext, select);
     final Select2Options select2Options = Select2Options.of(select);
     final boolean renderAsSelect2 = select2Options.hasAnyOption();
+    if (suggest != null) {
+      select2Options.setMinimumInputLength(suggest.getMinimumCharacters());
+    }
 
     if (renderAsSelect2) {
       String json = select2Options.toJson();
@@ -107,6 +114,10 @@ public class SelectOneChoiceRenderer extends SelectOneRendererBase {
     if (onchange != null) {
       writer.writeAttribute(HtmlAttributes.ONCHANGE, onchange, true);
     }
+    if (suggest != null) {
+      writer.writeAttribute(DataAttributes.SUGGEST_ID, suggest.getClientId(facesContext), false);
+    }
+
     HtmlRendererUtils.renderCommandFacet(select, facesContext , writer);
     HtmlRendererUtils.renderFocus(id, select.isFocus(), ComponentUtils.isError(select), facesContext, writer);
     if (renderAsSelect2 && select.getPlaceholder() != null && select.getPlaceholder().length() > 0) {
diff --git a/tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/html/standard/standard/tag/SuggestRenderer.java b/tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/html/standard/standard/tag/SuggestRenderer.java
index ebc0c16..230655f 100644
--- a/tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/html/standard/standard/tag/SuggestRenderer.java
+++ b/tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/html/standard/standard/tag/SuggestRenderer.java
@@ -19,7 +19,12 @@
 
 package org.apache.myfaces.tobago.renderkit.html.standard.standard.tag;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import org.apache.myfaces.tobago.component.UIIn;
+import org.apache.myfaces.tobago.component.UISelectManyBox;
+import org.apache.myfaces.tobago.component.UISelectOneChoice;
 import org.apache.myfaces.tobago.component.UISuggest;
 import org.apache.myfaces.tobago.context.ResourceManagerUtils;
 import org.apache.myfaces.tobago.model.AutoSuggestItem;
@@ -29,11 +34,13 @@ import org.apache.myfaces.tobago.renderkit.css.Classes;
 import org.apache.myfaces.tobago.renderkit.html.DataAttributes;
 import org.apache.myfaces.tobago.renderkit.html.HtmlAttributes;
 import org.apache.myfaces.tobago.renderkit.html.HtmlElements;
+import org.apache.myfaces.tobago.renderkit.html.HtmlInputTypes;
 import org.apache.myfaces.tobago.renderkit.html.util.HtmlRendererUtils;
 import org.apache.myfaces.tobago.webapp.TobagoResponseWriter;
 
 import javax.el.MethodExpression;
 import javax.faces.component.UIComponent;
+import javax.faces.component.UIInput;
 import javax.faces.context.FacesContext;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -42,22 +49,33 @@ import java.util.List;
 
 public class SuggestRenderer extends InputRendererBase {
 
+  private static final Logger LOG = LoggerFactory.getLogger(SuggestRenderer.class);
+
   @Override
   public void encodeEnd(final FacesContext facesContext, final UIComponent component) throws IOException {
 
     final UISuggest suggest = (UISuggest) component;
     final TobagoResponseWriter writer = HtmlRendererUtils.getTobagoResponseWriter(facesContext);
-    final String id  = suggest.getClientId(facesContext);
-    final UIIn in = (UIIn) suggest.getParent();
-    String inClientId = in.getClientId(facesContext);
-    final MethodExpression suggestMethodExpression = suggest.getSuggestMethodExpression();
-    final AutoSuggestItems items
-        = createAutoSuggestItems(suggestMethodExpression.invoke(facesContext.getELContext(), new Object[]{in}));
     // todo: declare unused/unsupported stuff deprecated
 
+    if (suggest.getParent() instanceof UIIn) {
+      writeInSuggestElements(facesContext, writer, suggest);
+    } else if (suggest.isSelect2()) {
+      writeSelect2SuggestElements(facesContext, writer, suggest);
+    } else {
+    }
+  }
+
+  public void writeInSuggestElements(FacesContext facesContext, TobagoResponseWriter writer, UISuggest suggest)
+      throws IOException {
+    final UIIn in = (UIIn) suggest.getParent();
+    final String inClientId = in.getClientId(facesContext);
+
+    final AutoSuggestItems items = suggest.getSuggestItems(facesContext);
+
     writer.startElement(HtmlElements.DIV, null);
     writer.writeClassAttribute(Classes.create(suggest));
-    writer.writeIdAttribute(id);
+    writer.writeIdAttribute(suggest.getClientId(facesContext));
     writer.writeAttribute(DataAttributes.FOR, inClientId, false);
     writer.writeAttribute(DataAttributes.SUGGEST_MIN_CHARS, suggest.getMinimumCharacters());
     writer.writeAttribute(DataAttributes.SUGGEST_DELAY, suggest.getDelay());
@@ -107,32 +125,47 @@ public class SuggestRenderer extends InputRendererBase {
     writer.endElement(HtmlElements.DIV);
   }
 
-  private AutoSuggestItems createAutoSuggestItems(final Object object) {
-    if (object instanceof AutoSuggestItems) {
-      return (AutoSuggestItems) object;
+  public void writeSelect2SuggestElements(
+      FacesContext facesContext, TobagoResponseWriter writer, UISuggest suggest)
+      throws IOException {
+    final UIComponent selectManyBox = (UIComponent) suggest.getParent();
+    final String inClientId = selectManyBox.getClientId(facesContext);
+    String id = suggest.getClientId(facesContext);
+
+    final AutoSuggestItems items = suggest.getSuggestItems(facesContext);
+
+    writer.startElement(HtmlElements.INPUT, null);
+    writer.writeClassAttribute(Classes.create(suggest));
+    writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.HIDDEN, false);
+    writer.writeNameAttribute(id);
+    writer.writeIdAttribute(id);
+    writer.writeAttribute(DataAttributes.FOR, inClientId, false);
+    writer.writeAttribute(DataAttributes.SUGGEST_MIN_CHARS, suggest.getMinimumCharacters());
+    writer.writeAttribute(DataAttributes.SUGGEST_DELAY, suggest.getDelay());
+    writer.writeAttribute(DataAttributes.SUGGEST_MAX_ITEMS, suggest.getMaximumItems());
+    writer.writeAttribute(DataAttributes.SUGGEST_UPDATE, Boolean.toString(suggest.isUpdate()), false);
+    int totalCount = suggest.getTotalCount();
+    if (totalCount == -1) {
+      totalCount = items.getItems().size();
     }
-    final AutoSuggestItems autoSuggestItems = new AutoSuggestItems();
-    if (object instanceof List && !((List) object).isEmpty()) {
-      if (((List) object).get(0) instanceof AutoSuggestItem) {
-        //noinspection unchecked
-        autoSuggestItems.setItems((List<AutoSuggestItem>) object);
-      } else if (((List) object).get(0) instanceof String) {
-        final List<AutoSuggestItem> items = new ArrayList<AutoSuggestItem>(((List) object).size());
-        for (int i = 0; i < ((List) object).size(); i++) {
-          final AutoSuggestItem item = new AutoSuggestItem();
-          item.setLabel((String) ((List) object).get(i));
-          item.setValue((String) ((List) object).get(i));
-          items.add(item);
-        }
-        autoSuggestItems.setItems(items);
-      } else {
-        throw new ClassCastException("Can't create AutoSuggestItems from '" + object + "'. "
-            + "Elements needs to be " + String.class.getName() + " or " + AutoSuggestItem.class.getName());
-      }
-    } else {
-      autoSuggestItems.setItems(Collections.<AutoSuggestItem>emptyList());
+    writer.writeAttribute(DataAttributes.SUGGEST_TOTAL_COUNT, totalCount);
+
+    StringBuilder builder = new StringBuilder("{\"results\":[");
+    for (final AutoSuggestItem item : items.getItems()) {
+      builder.append("{");
+
+      builder.append("\"id\":\"").append(item.getValue()).append("\",");
+      builder.append("\"text\":\"").append(item.getLabel()).append("\"");
+
+      builder.append("},");
+    }
+    if (builder.toString().endsWith(",")) {
+      builder.setLength(builder.length() - 1);
     }
-    return autoSuggestItems;
+    builder.append("]}");
+    writer.writeAttribute(DataAttributes.SUGGEST_RESPONSE_DATA, builder.toString(), true);
+
+    writer.endElement(HtmlElements.INPUT);
   }
 
 }
diff --git a/tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/html/util/HtmlRendererUtils.java b/tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/html/util/HtmlRendererUtils.java
index 74d835d..38025c3 100644
--- a/tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/html/util/HtmlRendererUtils.java
+++ b/tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/html/util/HtmlRendererUtils.java
@@ -949,11 +949,15 @@ public final class HtmlRendererUtils {
     for (final Map.Entry<Object, Object> entry : dataAttributes.entrySet()) {
       final Object mapKey = entry.getKey();
       final String name = mapKey instanceof ValueExpression
-          ? ((ValueExpression) mapKey).getValue(elContext).toString() : mapKey.toString();
-      final Object mapValue = entry.getValue();
-      final String value = mapValue instanceof ValueExpression
-          ? ((ValueExpression) mapValue).getValue(elContext).toString() : mapValue.toString();
-      writer.writeAttribute("data-" + name, value, true);
+          ? ((ValueExpression) mapKey).getValue(elContext).toString()
+          : mapKey.toString();
+      Object mapValue = entry.getValue();
+      mapValue = mapValue instanceof ValueExpression ? ((ValueExpression) mapValue).getValue(elContext) : mapValue;
+      if (mapValue == null) {
+        throw new NullPointerException("Data attribute value of " + name + " is null on component "
+            + component.getClass().getName() + " [" + component.getClientId(context) + "]");
+      }
+      writer.writeAttribute("data-" + name, mapValue.toString(), true);
     }
   }
 }
diff --git a/tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/util/SelectItemUtils.java b/tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/util/SelectItemUtils.java
index cdd4ea9..fd3fd46 100644
--- a/tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/util/SelectItemUtils.java
+++ b/tobago-theme/tobago-theme-standard/src/main/java/org/apache/myfaces/tobago/renderkit/util/SelectItemUtils.java
@@ -19,24 +19,10 @@
 
 package org.apache.myfaces.tobago.renderkit.util;
 
-import org.apache.myfaces.tobago.component.Attributes;
-import org.apache.myfaces.tobago.component.SupportsMarkup;
-import org.apache.myfaces.tobago.context.Markup;
-
-import javax.el.ValueExpression;
 import javax.faces.component.UIComponent;
-import javax.faces.component.UISelectItem;
-import javax.faces.component.UISelectItems;
 import javax.faces.context.FacesContext;
 import javax.faces.model.SelectItem;
-import java.lang.reflect.Array;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
-import java.util.NoSuchElementException;
 
 /**
  * Based on code from MyFaces core.
@@ -47,21 +33,7 @@ public class SelectItemUtils {
    * Creates a list of SelectItems to use for rendering.
    */
   public static Iterable<SelectItem> getItemIterator(final FacesContext facesContext, final UIComponent selector) {
-    if (selector.getChildCount() == 0) {
-      return Collections.emptyList();
-    } else {
-      return new Iterable<SelectItem>() {
-
-        private SelectItemsIterator iterator;
-
-        public Iterator<SelectItem> iterator() {
-          if (iterator == null) {
-            iterator = new SelectItemsIterator(facesContext, selector);
-          }
-          return iterator;
-        }
-      };
-    }
+    return org.apache.myfaces.tobago.util.SelectItemUtils.getItemIterator(facesContext, selector);
   }
 
   /**
@@ -70,222 +42,6 @@ public class SelectItemUtils {
    * Otherwise please use {@link #getItemIterator(javax.faces.context.FacesContext, javax.faces.component.UIComponent)}
    */
   public static List<SelectItem> getItemList(final FacesContext facesContext, final UIComponent selector) {
-    if (selector.getChildCount() == 0) {
-      return Collections.emptyList();
-    } else {
-      final Iterable<SelectItem> iterator = getItemIterator(facesContext, selector);
-      final List<SelectItem> result = new ArrayList<SelectItem>();
-      for (SelectItem selectItem : iterator) {
-        result.add(selectItem);
-      }
-      return result;
-    }
-  }
-
-  private static class SelectItemsIterator implements Iterator<SelectItem> {
-
-    private final FacesContext facesContext;
-    private final Iterator<UIComponent> children;
-    private Iterator<?> nestedItems;
-    private SelectItem nextItem;
-    private UISelectItems currentUISelectItems;
-
-    private SelectItemsIterator(final FacesContext facesContext, final UIComponent selector) {
-      this.children = selector.getChildren().iterator();
-      this.facesContext = facesContext;
-    }
-
-    @SuppressWarnings("unchecked")
-    public boolean hasNext() {
-      if (nextItem != null) {
-        return true;
-      }
-      if (nestedItems != null) {
-        if (nestedItems.hasNext()) {
-          return true;
-        }
-        nestedItems = null;
-      }
-
-      UIComponent child = null;
-      while (children.hasNext()) {
-        final UIComponent c = children.next();
-        // When there is other components nested that does
-        // not extends from UISelectItem or UISelectItems
-        // the behavior for this iterator is just skip this
-        // element(s) until an element that extends from these
-        // classes are found. If there is no more elements
-        // that conform this condition, just return false.
-        if (c instanceof UISelectItem || c instanceof UISelectItems) {
-          child = c;
-          break;
-        }
-      }
-      if (child == null) {
-        return false;
-      }
-
-      if (child instanceof UISelectItem) {
-        final UISelectItem uiSelectItem = (UISelectItem) child;
-        Object item = uiSelectItem.getValue();
-        if (item == null) {
-          // no value attribute --> create the SelectItem out of the other attributes
-          final Object itemValue = uiSelectItem.getItemValue();
-          String label = uiSelectItem.getItemLabel();
-          final String description = uiSelectItem.getItemDescription();
-          final boolean disabled = uiSelectItem.isItemDisabled();
-//          boolean escape = uiSelectItem.isItemEscaped();
-//          boolean noSelectionOption = uiSelectItem.isNoSelectionOption();
-          if (label == null) {
-            label = itemValue.toString();
-          }
-          String image = null;
-          Markup markup = null;
-          if (uiSelectItem instanceof org.apache.myfaces.tobago.component.UISelectItem) {
-            org.apache.myfaces.tobago.component.UISelectItem tobagoSelectItem
-                = (org.apache.myfaces.tobago.component.UISelectItem) uiSelectItem;
-            image = tobagoSelectItem.getItemImage();
-            markup = tobagoSelectItem.getCurrentMarkup();
-          }
-          item = new org.apache.myfaces.tobago.model.SelectItem(itemValue, label, description, disabled, image, markup);
-        } else if (!(item instanceof SelectItem)) {
-          ValueExpression expression = uiSelectItem.getValueExpression("value");
-          throw new IllegalArgumentException("ValueExpression '"
-              + (expression == null ? null : expression.getExpressionString()) + "' of UISelectItem : "
-              + child + " does not reference an Object of type SelectItem");
-        }
-        nextItem = (SelectItem) item;
-        return true;
-      } else { // UISelectItems
-        currentUISelectItems = ((UISelectItems) child);
-        final Object value = currentUISelectItems.getValue();
-
-        if (value instanceof SelectItem) {
-          nextItem = (SelectItem) value;
-          return true;
-        } else if (value != null && value.getClass().isArray()) {
-          // value is any kind of array (primitive or non-primitive)
-          // --> we have to use class Array to get the values
-          final int length = Array.getLength(value);
-          final Collection<Object> items = new ArrayList<Object>(length);
-          for (int i = 0; i < length; i++) {
-            items.add(Array.get(value, i));
-          }
-          nestedItems = items.iterator();
-          return hasNext();
-        } else if (value instanceof Iterable) {
-          // value is Iterable --> Collection, DataModel,...
-          nestedItems = ((Iterable<?>) value).iterator();
-          return hasNext();
-        } else if (value instanceof Map) {
-          final Map<Object, Object> map = ((Map<Object, Object>) value);
-          final Collection<SelectItem> items = new ArrayList<SelectItem>(map.size());
-          for (Map.Entry<Object, Object> entry : map.entrySet()) {
-            items.add(new org.apache.myfaces.tobago.model.SelectItem(entry.getValue(), entry.getKey().toString()));
-          }
-          nestedItems = items.iterator();
-          return hasNext();
-        }
-      }
-      return false;
-    }
-
-    public SelectItem next() {
-      if (!hasNext()) {
-        throw new NoSuchElementException();
-      }
-      if (nextItem != null) {
-        final SelectItem value = nextItem;
-        nextItem = null;
-        return value;
-      }
-      if (nestedItems != null) {
-        Object item = nestedItems.next();
-
-        if (!(item instanceof SelectItem)) {
-          // check new params of SelectItems (since 2.0): itemValue, itemLabel, itemDescription,...
-          // Note that according to the spec UISelectItems does not provide Getter and Setter
-          // methods for this values, so we have to use the attribute map
-          final Map<String, Object> attributeMap = currentUISelectItems.getAttributes();
-
-          // write the current item into the request map under the key listed in var, if available
-          boolean wroteRequestMapVarValue = false;
-          Object oldRequestMapVarValue = null;
-          final String var = (String) attributeMap.get(Attributes.VAR);
-          if (var != null && !"".equals(var)) {
-            // save the current value of the key listed in var from the request map
-            oldRequestMapVarValue = facesContext.getExternalContext().getRequestMap().put(var, item);
-            wroteRequestMapVarValue = true;
-          }
-
-          // check the itemValue attribute
-          Object itemValue = attributeMap.get(Attributes.ITEM_VALUE);
-          if (itemValue == null) {
-            // the itemValue attribute was not provided
-            // --> use the current item as the itemValue
-            itemValue = item;
-          }
-
-          // Spec: When iterating over the select items, toString()
-          // must be called on the string rendered attribute values
-          Object itemLabel = attributeMap.get(Attributes.ITEM_LABEL);
-          if (itemLabel == null) {
-            itemLabel = itemValue.toString();
-          } else {
-            itemLabel = itemLabel.toString();
-          }
-          Object itemDescription = attributeMap.get(Attributes.ITEM_DESCRIPTION);
-          if (itemDescription != null) {
-            itemDescription = itemDescription.toString();
-          }
-          final Boolean itemDisabled = getBooleanAttribute(currentUISelectItems, Attributes.ITEM_DISABLED, false);
-          final String itemImage = (String) attributeMap.get(Attributes.ITEM_IMAGE);
-          final Markup markup;
-          if (currentUISelectItems instanceof SupportsMarkup) {
-            markup = ((SupportsMarkup) currentUISelectItems).getCurrentMarkup();
-          } else {
-            markup = Markup.NULL;
-          }
-// TBD: should this be possible?
-//        Boolean itemLabelEscaped = getBooleanAttribute(currentUISelectItems, ITEM_LABEL_ESCAPED_PROP, true);
-// TBD ?
-//        Object noSelectionValue = attributeMap.get(NO_SELECTION_VALUE_PROP);
-          item = new org.apache.myfaces.tobago.model.SelectItem(
-              itemValue, (String) itemLabel, (String) itemDescription, itemDisabled, itemImage, markup);
-
-          // remove the value with the key from var from the request map, if previously written
-          if (wroteRequestMapVarValue) {
-            // If there was a previous value stored with the key from var in the request map, restore it
-            if (oldRequestMapVarValue != null) {
-              facesContext.getExternalContext().getRequestMap().put(var, oldRequestMapVarValue);
-            } else {
-              facesContext.getExternalContext().getRequestMap().remove(var);
-            }
-          }
-        }
-        return (SelectItem) item;
-      }
-      throw new NoSuchElementException();
-    }
-
-    public void remove() {
-      throw new UnsupportedOperationException();
-    }
-
-    private boolean getBooleanAttribute(
-        final UIComponent component, final String attrName, final boolean defaultValue) {
-      final Object value = component.getAttributes().get(attrName);
-      if (value == null) {
-        return defaultValue;
-      } else if (value instanceof Boolean) {
-        return (Boolean) value;
-      } else {
-        // If the value is a String, parse the boolean.
-        // This makes the following code work: <tag attribute="true" />,
-        // otherwise you would have to write <tag attribute="#{true}" />.
-        return Boolean.valueOf(value.toString());
-      }
-    }
+    return org.apache.myfaces.tobago.util.SelectItemUtils.getItemList(facesContext, selector);
   }
-
 }
diff --git a/tobago-theme/tobago-theme-standard/src/main/resources/org/apache/myfaces/tobago/renderkit/html/standard/standard/script/tobago-select2.js b/tobago-theme/tobago-theme-standard/src/main/resources/org/apache/myfaces/tobago/renderkit/html/standard/standard/script/tobago-select2.js
index f9f4883..86b7c95 100644
--- a/tobago-theme/tobago-theme-standard/src/main/resources/org/apache/myfaces/tobago/renderkit/html/standard/standard/script/tobago-select2.js
+++ b/tobago-theme/tobago-theme-standard/src/main/resources/org/apache/myfaces/tobago/renderkit/html/standard/standard/script/tobago-select2.js
@@ -25,10 +25,34 @@ Tobago.Select2 = {
         .each( function () {
           var element = jQuery(this);
 
-          var select2Options = jQuery.extend({}, element.data("tobago-select2"), Tobago.Select2.getExtensions(element));
+          var select2Options = jQuery.extend({}, element.data("tobago-select2"));
+
+          var suggestId = element.data("tobago-suggest-id");
+          if (typeof suggestId === "string") {
+            var suggest = jQuery(Tobago.Utils.escapeClientId(suggestId));
+            if (suggest.length) {
+              select2Options.ajax = {
+                suggestId: suggestId,
+                url: "http://localhost/just/a/dummy/url",
+                transport: Tobago.Select2.transport
+              };
+              var delay = suggest.data("tobago-suggest-delay");
+              if (delay) {
+                select2Options.ajax.delay = delay;
+              }
+            } else {
+              console.error("Suggest2 ajax problem: could not find element with id " + suggestId);
+              suggestId = undefined;
+            }
+          }
 
           if (element.hasClass("tobago-selectManyBox")) {
             select2Options.containerCss = {height: element.data("tobago-style").height};
+            if (suggestId) {
+              select2Options.dropdownCssClass = undefined; // it makes no sense to hide the ajax response
+              select2Options.createTag = Tobago.Select2.createResponseTag;
+              select2Options.templateResult = Tobago.Select2.suggestTemplateResult;
+            }
           }
           console.info("Select2.init" + element.attr("id") + " with data: " // @DEV_ONLY
               + JSON.stringify(select2Options));                            // @DEV_ONLY
@@ -40,6 +64,7 @@ Tobago.Select2 = {
             console.info("select2Options.tokenizer: " + typeof eval(select2Options.tokenizer));
             select2Options.tokenizer = eval(select2Options.tokenizer)
           }
+
           var commands = element.data("tobago-commands");
 
           if (commands) {
@@ -70,17 +95,109 @@ Tobago.Select2 = {
             }
 
           }
+          select2Options = jQuery.extend(select2Options, Tobago.Select2.getExtensions(element));
+          console.info("Select2 select2Options " + element.attr("id") + " with data: " // @DEV_ONLY
+              + JSON.stringify(select2Options));                                       // @DEV_ONLY
           element.select2(select2Options);
         });
   },
 
+  createResponseTag: function (params) {
+
+    var term = jQuery.trim(params.term);
+
+    if (term === '') {
+      return null;
+    }
+
+    return {
+      id: term,
+      text: term,
+      hide: "query" === params._type
+    }
+  },
+
+  suggestTemplateResult: function (result) {
+    if (result.loading || result.hide) {
+      return null;
+    }
+    return result.text;
+  },
+
+  transport: function (params, success, failure) {
+
+    if (params.data && params.data.q !== undefined) {
+      console.debug("SELECT2 tobago params.data.q : " + params.data.q); // @DEV_ONLY
+      jQuery(Tobago.Utils.escapeClientId(params.suggestId)).val(params.data.q);
+    } else {
+      console.debug("SELECT2 tobago undef "); // @DEV_ONLY
+    }
+
+    Tobago.reloadComponent(null, params.suggestId, params.suggestId, {
+      createOverlay: false,
+      suggestId : params.suggestId,
+      afterDoUpdateSuccess: function (requestOptions) {
+        var data = jQuery(Tobago.Utils.escapeClientId(requestOptions.suggestId)).data("tobago-suggest-response-data");
+        console.debug("SELECT2 tobago SUCCESS"); // @DEV_ONLY
+        var currentValues = jQuery(Tobago.Utils.escapeClientId(params.suggestId))
+            .next().children('.tobago-selectManyBox').val();
+        if (currentValues !== undefined) {
+          // remove already selected items in select many box
+          data.results = Tobago.Select2.removeSelected(data.results, currentValues);
+        }
+        success(data);
+      },
+      afterDoUpdateError: function () {
+        console.debug("SELECT2 tobago ERROR"); // @DEV_ONLY
+        failure();
+      }
+
+    });
+
+    return {
+      abort: function () {
+        console.debug("SELECT2 tobago ABORT transport");  // @DEV_ONLY
+      }
+    }
+  },
+
+  removeSelected: function (results, currentValue) {
+    if (currentValue.length === 0) {
+      return results;
+    }
+    var newData = [];
+    for (var i = 0; i < results.length; i++) {
+      var item = results[i];
+      if (!currentValue.includes(item.id)) {
+        newData.push(item);
+      }
+    }
+    return newData;
+  },
+
   getExtensions: function (element) {
     var extend = element.data("tobago-select2-extend");
     if (extend !== undefined) {
       for (var extName in extend) {
         if (extend.hasOwnProperty(extName)
             && typeof extend[extName] === "string") {
-          extend[extName] = Tobago.Select2.registry[extend[extName]];
+          var extensionName = extend[extName];
+          var value = Tobago.Select2.registry[extensionName];
+          if (value) {
+            extend[extName] = value;
+          } else {
+            try {
+              value = jQuery(extensionName);
+            } catch (e) {}
+            if (!(value && value.length) && extend[extName].charAt(0) !== '#') {
+              try {
+                value = jQuery(Tobago.Utils.escapeClientId(extensionName));
+              } catch (e) {}
+            }
+            if (value && value.length) {
+              extend[extName] = value;
+            }
+          }
         }
       }
     }
@@ -141,5 +258,25 @@ Tobago.Select2.register('caseSensitiveMatcher', function (params, data) {
   return null;
 });
 
+Tobago.Select2.register('suppressMessages', (function() {
+
+  var suppressMessages = (function () {
+
+    function SuppressMessage (decorated, $element, options, dataAdapter) {
+      decorated.call(this, $element, options, dataAdapter);
+    }
+
+    SuppressMessage.prototype.displayMessage = function(decorated, params) {
+    };
+
+    return SuppressMessage;
+  })();
+
+  var Utils = jQuery.fn.select2.amd.require('select2/utils');
+  var resultAdapter = jQuery.fn.select2.amd.require('select2/results');
+  return Utils.Decorate(resultAdapter, suppressMessages);
+
+})());
+
 Tobago.registerListener(Tobago.Select2.init, Tobago.Phase.DOCUMENT_READY);
 Tobago.registerListener(Tobago.Select2.init, Tobago.Phase.AFTER_UPDATE);


Mime
View raw message