You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by bo...@apache.org on 2023/11/14 09:22:28 UTC

(myfaces-tobago) branch main updated: feat(select): Support for noSelectionOptionAttribute of selectItem

This is an automated email from the ASF dual-hosted git repository.

bommel pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/myfaces-tobago.git


The following commit(s) were added to refs/heads/main by this push:
     new 93eb26d694 feat(select): Support for noSelectionOptionAttribute of selectItem
93eb26d694 is described below

commit 93eb26d694cfe19866bb34fb192372befa6d106e
Author: Bernd Bohmann <bo...@apache.org>
AuthorDate: Tue Nov 14 10:22:22 2023 +0100

    feat(select): Support for noSelectionOptionAttribute of selectItem
    
    issue: TOBAGO-2257
---
 .../renderer/SelectManyCheckboxRenderer.java       | 18 ++++---
 .../renderkit/renderer/SelectManyListRenderer.java |  4 ++
 .../renderkit/renderer/SelectManyRendererBase.java |  3 ++
 .../renderkit/renderer/SelectOneListRenderer.java  |  5 ++
 .../renderkit/renderer/SelectOneRadioRenderer.java | 18 ++++---
 .../taglib/component/SelectItemTagDeclaration.java |  9 ++++
 .../tobago/internal/util/SelectItemUtils.java      |  5 +-
 .../apache/myfaces/tobago/model/SelectItem.java    |  8 +++
 .../myfaces/tobago/renderkit/RendererBase.java     |  5 +-
 .../SelectManyCheckboxRendererUnitTest.java        | 58 ++++++++++++++++++++++
 .../renderer/SelectOneChoiceRendererUnitTest.java  | 49 ++++++++++++++++++
 ...ctManyCheckboxNoSelectionOptionNotRendered.html | 29 +++++++++++
 ...electManyCheckboxNoSelectionOptionRendered.html | 33 ++++++++++++
 ...electOneChoiceNoSelectionOptionNotRendered.html | 23 +++++++++
 .../selectOneChoiceNoSelectionOptionRendered.html  | 25 ++++++++++
 .../main/webapp/content/030-select/Select.xhtml    |  5 +-
 16 files changed, 279 insertions(+), 18 deletions(-)

diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyCheckboxRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyCheckboxRenderer.java
index d646c6a81a..c736af990f 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyCheckboxRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyCheckboxRenderer.java
@@ -75,6 +75,17 @@ public class SelectManyCheckboxRenderer<T extends AbstractUISelectManyCheckbox>
     final int[] renderRange = getRenderRangeList(component, reference);
     for (final SelectItem item : SelectItemUtils.getItemIterator(facesContext, component)) {
       if (renderRange == null || ArrayUtils.contains(renderRange, i)) {
+        final String formattedValue = getFormattedValue(facesContext, component, item.getValue());
+        final boolean checked;
+        if (submittedValues == null) {
+          checked = ArrayUtils.contains(values, item.getValue());
+        } else {
+          checked = ArrayUtils.contains(submittedValues, formattedValue);
+        }
+        if (item.isNoSelectionOption() && required && values != null && values.length > 0 && !checked) {
+          // skip the noSelectionOption if there is another value selected and required
+          continue;
+        }
         final boolean itemDisabled = item.isDisabled() || disabled;
         final String itemId = id + ComponentUtils.SUB_SEPARATOR + i;
         writer.startElement(HtmlElements.DIV);
@@ -87,13 +98,6 @@ public class SelectManyCheckboxRenderer<T extends AbstractUISelectManyCheckbox>
             BootstrapClass.FORM_CHECK_INPUT,
             BootstrapClass.validationColor(ComponentUtils.getMaximumSeverity(component)));
         writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.CHECKBOX);
-        final String formattedValue = getFormattedValue(facesContext, component, item.getValue());
-        final boolean checked;
-        if (submittedValues == null) {
-          checked = ArrayUtils.contains(values, item.getValue());
-        } else {
-          checked = ArrayUtils.contains(submittedValues, formattedValue);
-        }
         writer.writeAttribute(HtmlAttributes.CHECKED, checked);
         writer.writeNameAttribute(id);
         writer.writeIdAttribute(itemId);
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyListRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyListRenderer.java
index 87b754aa20..e5362f5eed 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyListRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyListRenderer.java
@@ -231,6 +231,10 @@ public class SelectManyListRenderer<T extends AbstractUISelectManyList> extends
     } else {
       contains = ArrayUtils.contains(submittedValues, formattedValue);
     }
+    if (item.isNoSelectionOption() && component.isRequired() && values != null && values.length > 0 && !contains) {
+      // skip the noSelectionOption if there is another value selected and required
+      return;
+    }
     writer.startElement(HtmlElements.TR);
     writer.writeAttribute(DataAttributes.VALUE, formattedValue, true);
     writer.writeClassAttribute(
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyRendererBase.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyRendererBase.java
index 213eef58af..f57d6ac1ce 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyRendererBase.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyRendererBase.java
@@ -406,6 +406,9 @@ public abstract class SelectManyRendererBase<T extends AbstractUISelectManyBase>
     Converter converter = null;
     while (converter == null && iterator.hasNext()) {
       final SelectItem item = iterator.next();
+      if (item.isNoSelectionOption()) {
+        continue;
+      }
       if (item instanceof SelectItemGroup) {
         final Iterator<SelectItem> groupIterator = Arrays.asList(
             ((SelectItemGroup) item).getSelectItems()).iterator();
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneListRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneListRenderer.java
index 9171bb0602..ecf831363a 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneListRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneListRenderer.java
@@ -191,6 +191,7 @@ public class SelectOneListRenderer<T extends AbstractUISelectOneList> extends Se
 
   private void encodeSelectItem(FacesContext facesContext, TobagoResponseWriter writer, T component, SelectItem item,
         Object value, Object submittedValue, boolean disabled, boolean group) throws IOException {
+
     Object itemValue = item.getValue();
     // when using selectItem tag with a literal value: use the converted value
     if (itemValue instanceof String && value != null && !(value instanceof String)) {
@@ -203,6 +204,10 @@ public class SelectOneListRenderer<T extends AbstractUISelectOneList> extends Se
     } else {
       contains = value != null && value.equals(itemValue);
     }
+    if (item.isNoSelectionOption() && component.isRequired() && value != null && !contains) {
+      // skip the noSelectionOption if there is another value selected and required
+      return;
+    }
     writer.startElement(HtmlElements.TR);
     writer.writeAttribute(DataAttributes.VALUE, formattedValue, true);
     writer.writeClassAttribute(
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneRadioRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneRadioRenderer.java
index 0f7dabe23d..e6c2ce75c0 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneRadioRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneRadioRenderer.java
@@ -125,6 +125,17 @@ public class SelectOneRadioRenderer<T extends AbstractUISelectOneRadio> extends
     final int[] renderRange = getRenderRangeList(component, reference);
     for (final SelectItem item : items) {
       if (renderRange == null || ArrayUtils.contains(renderRange, i)) {
+        final String formattedValue = getFormattedValue(facesContext, component, item.getValue());
+        final boolean checked;
+        if (submittedValue == null) {
+          checked = ObjectUtils.equals(item.getValue(), value);
+        } else {
+          checked = ObjectUtils.equals(formattedValue, submittedValue);
+        }
+        if (item.isNoSelectionOption() && component.isRequired() && value != null && !checked) {
+          // skip the noSelectionOption if there is a value available
+          continue;
+        }
         final boolean itemDisabled = item.isDisabled() || disabled;
         final String itemId = id + ComponentUtils.SUB_SEPARATOR + i;
         writer.startElement(HtmlElements.DIV);
@@ -137,13 +148,6 @@ public class SelectOneRadioRenderer<T extends AbstractUISelectOneRadio> extends
             BootstrapClass.FORM_CHECK_INPUT,
             BootstrapClass.validationColor(ComponentUtils.getMaximumSeverity(component)));
         writer.writeAttribute(HtmlAttributes.TYPE, HtmlInputTypes.RADIO);
-        final String formattedValue = getFormattedValue(facesContext, component, item.getValue());
-        final boolean checked;
-        if (submittedValue == null) {
-          checked = ObjectUtils.equals(item.getValue(), value);
-        } else {
-          checked = ObjectUtils.equals(formattedValue, submittedValue);
-        }
         writer.writeAttribute(HtmlAttributes.CHECKED, checked);
         writer.writeNameAttribute(name);
         writer.writeIdAttribute(itemId);
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SelectItemTagDeclaration.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SelectItemTagDeclaration.java
index f34d83871a..1fe77aa116 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SelectItemTagDeclaration.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/taglib/component/SelectItemTagDeclaration.java
@@ -77,4 +77,13 @@ public interface SelectItemTagDeclaration extends HasBinding, HasId, IsVisual, H
       type = "jakarta.faces.model.SelectItem",
       expression = DynamicExpression.VALUE_EXPRESSION_REQUIRED)
   void setValue(String value);
+
+  /**
+   * Flag indicating whether the option created
+   * by this component is a noSelectionOption.
+   */
+  @TagAttribute
+  @UIComponentTagAttribute(type = {"boolean"}, defaultValue = "false")
+  void setNoSelectionOption(String itemDisabled);
+
 }
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/util/SelectItemUtils.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/util/SelectItemUtils.java
index d79100cbb5..5c3179a355 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/util/SelectItemUtils.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/util/SelectItemUtils.java
@@ -144,7 +144,7 @@ public class SelectItemUtils {
           final String description = uiSelectItem.getItemDescription();
           final boolean disabled = uiSelectItem.isItemDisabled();
 //          boolean escape = uiSelectItem.isItemEscaped();
-//          boolean noSelectionOption = uiSelectItem.isNoSelectionOption();
+          boolean noSelectionOption = uiSelectItem.isNoSelectionOption();
           if (label == null && itemValue != null) {
             label = itemValue.toString();
           }
@@ -156,7 +156,8 @@ public class SelectItemUtils {
             image = tobagoSelectItem.getItemImage();
             markup = tobagoSelectItem.getMarkup();
           }
-          item = new org.apache.myfaces.tobago.model.SelectItem(itemValue, label, description, disabled, image, markup);
+          item = new org.apache.myfaces.tobago.model.SelectItem(itemValue, label, description, disabled,
+              true, noSelectionOption, image, markup);
         } else if (!(item instanceof SelectItem)) {
           final ValueExpression expression = uiSelectItem.getValueExpression("value");
           throw new IllegalArgumentException("ValueExpression '"
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/model/SelectItem.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/model/SelectItem.java
index fb3dc37ba9..445c03b798 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/model/SelectItem.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/model/SelectItem.java
@@ -69,6 +69,14 @@ public class SelectItem extends jakarta.faces.model.SelectItem implements Visual
     this.markup = markup;
   }
 
+  public SelectItem(
+      final Object value, final String label, final String tip, final boolean disabled, final boolean escape,
+      final boolean noSelectionOption, final String image, final Markup markup) {
+    super(value, label, tip, disabled, escape, noSelectionOption);
+    this.image = image;
+    this.markup = markup;
+  }
+
   /**
    * Alias name for description.
    */
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/RendererBase.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/RendererBase.java
index 8981fa61a7..4f9ec75316 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/RendererBase.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/renderkit/RendererBase.java
@@ -424,7 +424,6 @@ public abstract class RendererBase<T extends UIComponent> extends Renderer {
             onlySelected, writer, facesContext);
         writer.endElement(HtmlElements.OPTGROUP);
       } else {
-
         Object itemValue = item.getValue();
         // when using selectItem tag with a literal value: use the converted value
         if (itemValue instanceof String && values != null && values.length > 0 && !(values[0] instanceof String)) {
@@ -437,6 +436,10 @@ public abstract class RendererBase<T extends UIComponent> extends Renderer {
         } else {
           contains = ArrayUtils.contains(submittedValues, formattedValue);
         }
+        if (item.isNoSelectionOption() && component.isRequired() && values != null && values.length > 0 && !contains) {
+          // skip the noSelectionOption if there is another value selected and required
+          continue;
+        }
         if (onlySelected != null) {
           if (onlySelected) {
             if (!contains) {
diff --git a/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyCheckboxRendererUnitTest.java b/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyCheckboxRendererUnitTest.java
index f58d656c44..b246616865 100644
--- a/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyCheckboxRendererUnitTest.java
+++ b/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectManyCheckboxRendererUnitTest.java
@@ -75,4 +75,62 @@ public class SelectManyCheckboxRendererUnitTest extends RendererTestBase {
     Assertions.assertEquals(loadHtml("renderer/selectManyCheckbox/selectManyCheckboxFatal.html"),
         formattedResult());
   }
+
+  @Test
+  public void noSelectionOptionRendered() throws IOException {
+    final UISelectManyCheckbox c = (UISelectManyCheckbox) ComponentUtils.createComponent(
+        facesContext, Tags.selectManyCheckbox.componentType(), RendererTypes.SelectManyCheckbox, "id");
+
+    final UISelectItem noSelectionOption = (UISelectItem) ComponentUtils.createComponent(
+        facesContext, Tags.selectItem.componentType(), null, "noSelectionOption");
+    noSelectionOption.setItemLabel("Choose a value..");
+    noSelectionOption.setNoSelectionOption(true);
+    c.getChildren().add(noSelectionOption);
+    final UISelectItem i1 = (UISelectItem) ComponentUtils.createComponent(
+        facesContext, Tags.selectItem.componentType(), null, "i1");
+    i1.setItemLabel("Entry One");
+    i1.setItemValue("Entry One");
+    c.getChildren().add(i1);
+    final UISelectItem i2 = (UISelectItem) ComponentUtils.createComponent(
+        facesContext, Tags.selectItem.componentType(), null, "i2");
+    i2.setItemLabel("Entry Two");
+    i2.setItemValue("Entry Two");
+    c.getChildren().add(i2);
+
+    c.encodeAll(facesContext);
+
+    Assertions.assertEquals(
+        loadHtml("renderer/selectManyCheckbox/selectManyCheckboxNoSelectionOptionRendered.html"),
+        formattedResult());
+  }
+
+  @Test
+  public void noSelectionOptionNotRendered() throws IOException {
+    final UISelectManyCheckbox c = (UISelectManyCheckbox) ComponentUtils.createComponent(
+        facesContext, Tags.selectManyCheckbox.componentType(), RendererTypes.SelectManyCheckbox, "id");
+    c.setValue(new String[] {"Entry One"});
+    c.setRequired(true);
+
+    final UISelectItem noSelectionOption = (UISelectItem) ComponentUtils.createComponent(
+        facesContext, Tags.selectItem.componentType(), null, "noSelectionOption");
+    noSelectionOption.setItemLabel("Choose a value..");
+    noSelectionOption.setNoSelectionOption(true);
+    c.getChildren().add(noSelectionOption);
+    final UISelectItem i1 = (UISelectItem) ComponentUtils.createComponent(
+        facesContext, Tags.selectItem.componentType(), null, "i1");
+    i1.setItemLabel("Entry One");
+    i1.setItemValue("Entry One");
+    c.getChildren().add(i1);
+    final UISelectItem i2 = (UISelectItem) ComponentUtils.createComponent(
+        facesContext, Tags.selectItem.componentType(), null, "i2");
+    i2.setItemLabel("Entry Two");
+    i2.setItemValue("Entry Two");
+    c.getChildren().add(i2);
+
+    c.encodeAll(facesContext);
+
+    Assertions.assertEquals(
+        loadHtml("renderer/selectManyCheckbox/selectManyCheckboxNoSelectionOptionNotRendered.html"),
+        formattedResult());
+  }
 }
diff --git a/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneChoiceRendererUnitTest.java b/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneChoiceRendererUnitTest.java
index ca76fa19ef..5a9d27a1eb 100644
--- a/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneChoiceRendererUnitTest.java
+++ b/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/renderkit/renderer/SelectOneChoiceRendererUnitTest.java
@@ -51,4 +51,53 @@ public class SelectOneChoiceRendererUnitTest extends RendererTestBase {
     Assertions.assertEquals(loadHtml("renderer/selectOneChoice/selectOneChoiceLabel.html"), formattedResult());
   }
 
+  @Test
+  public void noSelectionOptionRendered() throws IOException {
+    final UISelectOneChoice c = (UISelectOneChoice) ComponentUtils.createComponent(
+        facesContext, Tags.selectOneChoice.componentType(), RendererTypes.SelectOneChoice, "id");
+    c.setLabel("label");
+
+    final UISelectItem i1 = (UISelectItem) ComponentUtils.createComponent(
+        facesContext, Tags.selectItem.componentType(), null, "i1");
+    i1.setItemLabel("Choose a value..");
+    i1.setItemValue(null);
+    i1.setNoSelectionOption(true);
+    c.getChildren().add(i1);
+    final UISelectItem i2 = (UISelectItem) ComponentUtils.createComponent(
+        facesContext, Tags.selectItem.componentType(), null, "i2");
+    i2.setItemLabel("Telecaster");
+    c.getChildren().add(i2);
+
+    c.encodeAll(facesContext);
+
+    Assertions.assertEquals(loadHtml("renderer/selectOneChoice/selectOneChoiceNoSelectionOptionRendered.html"),
+        formattedResult());
+  }
+
+  @Test
+  public void noSelectionOptionNotRendered() throws IOException {
+    final UISelectOneChoice c = (UISelectOneChoice) ComponentUtils.createComponent(
+        facesContext, Tags.selectOneChoice.componentType(), RendererTypes.SelectOneChoice, "id");
+    c.setLabel("label");
+    c.setValue("Telecaster");
+    c.setRequired(true);
+
+    final UISelectItem i1 = (UISelectItem) ComponentUtils.createComponent(
+        facesContext, Tags.selectItem.componentType(), null, "i1");
+    i1.setItemLabel("Choose a value..");
+    i1.setItemValue(null);
+    i1.setNoSelectionOption(true);
+    c.getChildren().add(i1);
+    final UISelectItem i2 = (UISelectItem) ComponentUtils.createComponent(
+        facesContext, Tags.selectItem.componentType(), null, "i2");
+    i2.setItemLabel("Telecaster");
+    i2.setItemValue("Telecaster");
+    c.getChildren().add(i2);
+
+    c.encodeAll(facesContext);
+
+    Assertions.assertEquals(loadHtml("renderer/selectOneChoice/selectOneChoiceNoSelectionOptionNotRendered.html"),
+        formattedResult());
+  }
+
 }
diff --git a/tobago-core/src/test/resources/renderer/selectManyCheckbox/selectManyCheckboxNoSelectionOptionNotRendered.html b/tobago-core/src/test/resources/renderer/selectManyCheckbox/selectManyCheckboxNoSelectionOptionNotRendered.html
new file mode 100644
index 0000000000..96a05bee73
--- /dev/null
+++ b/tobago-core/src/test/resources/renderer/selectManyCheckbox/selectManyCheckboxNoSelectionOptionNotRendered.html
@@ -0,0 +1,29 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+-->
+
+<tobago-select-many-checkbox id='id' class='tobago-auto-spacing tobago-required'>
+  <div>
+    <div class='form-check'>
+      <input class='form-check-input' type='checkbox' checked='checked' name='id' id='id::0' value='Entry One' required='required'>
+      <label class='form-check-label' for='id::0'>Entry One</label>
+    </div>
+    <div class='form-check'>
+      <input class='form-check-input' type='checkbox' name='id' id='id::1' value='Entry Two' required='required'>
+      <label class='form-check-label' for='id::1'>Entry Two</label>
+    </div>
+  </div>
+</tobago-select-many-checkbox>
diff --git a/tobago-core/src/test/resources/renderer/selectManyCheckbox/selectManyCheckboxNoSelectionOptionRendered.html b/tobago-core/src/test/resources/renderer/selectManyCheckbox/selectManyCheckboxNoSelectionOptionRendered.html
new file mode 100644
index 0000000000..1602eac779
--- /dev/null
+++ b/tobago-core/src/test/resources/renderer/selectManyCheckbox/selectManyCheckboxNoSelectionOptionRendered.html
@@ -0,0 +1,33 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+-->
+
+<tobago-select-many-checkbox id='id' class='tobago-auto-spacing'>
+  <div>
+    <div class='form-check'>
+      <input class='form-check-input' type='checkbox' name='id' id='id::0' value=''>
+      <label class='form-check-label' for='id::0'>Choose a value..</label>
+    </div>
+    <div class='form-check'>
+      <input class='form-check-input' type='checkbox' name='id' id='id::1' value='Entry One'>
+      <label class='form-check-label' for='id::1'>Entry One</label>
+    </div>
+    <div class='form-check'>
+      <input class='form-check-input' type='checkbox' name='id' id='id::2' value='Entry Two'>
+      <label class='form-check-label' for='id::2'>Entry Two</label>
+    </div>
+  </div>
+</tobago-select-many-checkbox>
diff --git a/tobago-core/src/test/resources/renderer/selectOneChoice/selectOneChoiceNoSelectionOptionNotRendered.html b/tobago-core/src/test/resources/renderer/selectOneChoice/selectOneChoiceNoSelectionOptionNotRendered.html
new file mode 100644
index 0000000000..3224545074
--- /dev/null
+++ b/tobago-core/src/test/resources/renderer/selectOneChoice/selectOneChoiceNoSelectionOptionNotRendered.html
@@ -0,0 +1,23 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+-->
+
+<tobago-select-one-choice id='id' class='tobago-label-container tobago-auto-spacing tobago-required'>
+  <label for='id::field' class='tobago-required col-form-label'>label</label>
+  <select id='id::field' name='id' class='form-select'>
+    <option value='Telecaster' selected='selected'>Telecaster
+    </option></select>
+</tobago-select-one-choice>
diff --git a/tobago-core/src/test/resources/renderer/selectOneChoice/selectOneChoiceNoSelectionOptionRendered.html b/tobago-core/src/test/resources/renderer/selectOneChoice/selectOneChoiceNoSelectionOptionRendered.html
new file mode 100644
index 0000000000..97e68d5acb
--- /dev/null
+++ b/tobago-core/src/test/resources/renderer/selectOneChoice/selectOneChoiceNoSelectionOptionRendered.html
@@ -0,0 +1,25 @@
+<!--
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+-->
+
+<tobago-select-one-choice id='id' class='tobago-label-container tobago-auto-spacing'>
+  <label for='id::field' class='col-form-label'>label</label>
+  <select id='id::field' name='id' class='form-select'>
+    <option value=''>Choose a value..
+    </option>
+    <option value=''>Telecaster
+    </option></select>
+</tobago-select-one-choice>
diff --git a/tobago-example/tobago-example-demo/src/main/webapp/content/030-select/Select.xhtml b/tobago-example/tobago-example-demo/src/main/webapp/content/030-select/Select.xhtml
index fead3f1089..d8906563ce 100644
--- a/tobago-example/tobago-example-demo/src/main/webapp/content/030-select/Select.xhtml
+++ b/tobago-example/tobago-example-demo/src/main/webapp/content/030-select/Select.xhtml
@@ -20,7 +20,8 @@
 <ui:composition template="/main.xhtml"
                 xmlns="http://www.w3.org/1999/xhtml"
                 xmlns:tc="http://myfaces.apache.org/tobago/component"
-                xmlns:ui="jakarta.faces.facelets">
+                xmlns:ui="jakarta.faces.facelets"
+                xmlns:f="jakarta.faces.core">
 
   <p>Tobago provides several ways to select an option.
     Entries are added with <code class="language-markup">&lt;tc:selectItem/></code> or
@@ -40,7 +41,9 @@
   <tc:section label="Dropdown">
     <demo-highlight language="markup">&lt;tc:selectOneChoice label="Dropdown Box"></demo-highlight>
     <tc:selectOneChoice label="Dropdown Box">
+      <f:selectItem itemValue="#{null}" itemLabel="--select--" noSelectionOption="true"/>
       <tc:selectItems value="#{selectController.entries}"/>
+      <f:ajax render="@this" execute="@this"/>
     </tc:selectOneChoice>
   </tc:section>