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 2024/01/30 18:53:09 UTC

(myfaces-tobago) branch main updated: feat: Support for PassThroughAttributes in Tobago (#4801)

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 a91f06245d feat: Support for PassThroughAttributes in Tobago (#4801)
a91f06245d is described below

commit a91f06245da2ecafb0bb35fb058e924b2b7e46ad
Author: Bernd Bohmann <bo...@apache.org>
AuthorDate: Tue Jan 30 19:53:03 2024 +0100

    feat: Support for PassThroughAttributes in Tobago (#4801)
    
    issue: TOBAGO-2277
---
 .../internal/renderkit/renderer/ImageRenderer.java |   2 +-
 .../internal/renderkit/renderer/InRenderer.java    |   2 +-
 .../internal/renderkit/renderer/OutRenderer.java   |   6 +-
 .../renderkit/renderer/TextareaRenderer.java       |   2 +-
 .../webapp/DebugResponseWriterWrapper.java         |  12 +-
 .../tobago/internal/webapp/HtmlResponseWriter.java |   1 +
 .../internal/webapp/TobagoResponseWriterBase.java  | 154 ++++++++++++++++-----
 .../webapp/TobagoResponseWriterWrapper.java        |   5 +
 .../tobago/internal/webapp/XmlResponseWriter.java  |   1 +
 .../tobago/webapp/TobagoResponseWriter.java        |   2 +
 .../internal/config/AbstractTobagoTestBase.java    |   4 +
 .../webapp/TobagoResponseWriterUnitTest.java       | 103 +++++++++++++-
 12 files changed, 252 insertions(+), 42 deletions(-)

diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/ImageRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/ImageRenderer.java
index 153b542349..dddc040f53 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/ImageRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/ImageRenderer.java
@@ -66,7 +66,7 @@ public class ImageRenderer<T extends AbstractUIImage> extends RendererBase<T> {
           disabled ? BootstrapClass.DISABLED : null,
           component.getCustomClass());
       writer.writeAttribute(HtmlAttributes.TITLE, title, true);
-      writer.startElement(HtmlElements.IMG);
+      writer.startElement(HtmlElements.IMG, component);
       writer.writeAttribute(HtmlAttributes.SRC, value, true);
       writer.writeAttribute(HtmlAttributes.ALT, alt != null ? alt : "", true);
       writer.endElement(HtmlElements.IMG);
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/InRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/InRenderer.java
index 7069ae7fd0..b4350ae667 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/InRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/InRenderer.java
@@ -98,7 +98,7 @@ public class InRenderer<T extends AbstractUIIn> extends DecorationPositionRender
     }
     encodeGroupAddon(facesContext, writer, before, false);
 
-    writer.startElement(HtmlElements.INPUT);
+    writer.startElement(HtmlElements.INPUT, component);
 
     if (component.getAccessKey() != null) {
       writer.writeAttribute(HtmlAttributes.ACCESSKEY, Character.toString(component.getAccessKey()), false);
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/OutRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/OutRenderer.java
index d7a2c76214..69a76a3398 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/OutRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/OutRenderer.java
@@ -88,7 +88,8 @@ public class OutRenderer<T extends AbstractUIOut> extends DecorationPositionRend
     final TobagoResponseWriter writer = getResponseWriter(facesContext);
     final Markup markup = component.getMarkup() != null ? component.getMarkup() : Markup.NULL;
 
-    writer.startElement(isInside(facesContext, HtmlElements.TOBAGO_IN) ? HtmlElements.TOBAGO_OUT : HtmlElements.SPAN);
+    boolean inside = isInside(facesContext, HtmlElements.TOBAGO_IN);
+    writer.startElement(inside ? HtmlElements.TOBAGO_OUT : HtmlElements.SPAN, component);
     HtmlRendererUtils.writeDataAttributes(facesContext, writer, component);
 
     writer.writeClassAttribute(
@@ -97,8 +98,7 @@ public class OutRenderer<T extends AbstractUIOut> extends DecorationPositionRend
         markup.contains(Markup.LARGE) ? BootstrapClass.FORM_CONTROL_LG : null,
         markup.contains(Markup.SMALL) ? BootstrapClass.FORM_CONTROL_SM : null,
         component.getCustomClass(),
-        isInside(facesContext, HtmlElements.TOBAGO_IN)
-            ? BootstrapClass.INPUT_GROUP_TEXT : BootstrapClass.FORM_CONTROL_PLAINTEXT,
+        inside ? BootstrapClass.INPUT_GROUP_TEXT : BootstrapClass.FORM_CONTROL_PLAINTEXT,
         BootstrapClass.textColor(markup),
         BootstrapClass.fontStyle(markup));
     final String title = HtmlRendererUtils.getTitleFromTipAndMessages(facesContext, component);
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TextareaRenderer.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TextareaRenderer.java
index e62578f96c..f270772c1c 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TextareaRenderer.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/renderkit/renderer/TextareaRenderer.java
@@ -86,7 +86,7 @@ public class TextareaRenderer<T extends AbstractUITextarea> extends DecorationPo
     final Markup markup = component.getMarkup() != null ? component.getMarkup() : Markup.NULL;
     final TobagoResponseWriter writer = getResponseWriter(facesContext);
 
-    writer.startElement(HtmlElements.TEXTAREA);
+    writer.startElement(HtmlElements.TEXTAREA, component);
     writer.writeNameAttribute(clientId);
     writer.writeIdAttribute(fieldId);
     HtmlRendererUtils.writeDataAttributes(facesContext, writer, component);
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/webapp/DebugResponseWriterWrapper.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/webapp/DebugResponseWriterWrapper.java
index b08fb4c4ae..89e55d8110 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/webapp/DebugResponseWriterWrapper.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/webapp/DebugResponseWriterWrapper.java
@@ -181,10 +181,20 @@ public class DebugResponseWriterWrapper extends TobagoResponseWriter {
     usedAttributes.clear();
   }
 
+  @Override
+  public void startElement(final HtmlElements name, UIComponent component) throws IOException {
+    if (LOG.isDebugEnabled()) {
+      LOG.debug("start element: '{}'", name);
+    }
+    stack.push(name);
+    responseWriter.startElement(name, component);
+
+    usedAttributes.clear();
+  }
   @Override
   public void startElement(final HtmlElements name) throws IOException {
     if (LOG.isDebugEnabled()) {
-      LOG.debug("start element: '" + name + "'");
+      LOG.debug("start element: '{}'", name);
     }
     stack.push(name);
     responseWriter.startElement(name);
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/webapp/HtmlResponseWriter.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/webapp/HtmlResponseWriter.java
index 468dd9048f..40d115b7e3 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/webapp/HtmlResponseWriter.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/webapp/HtmlResponseWriter.java
@@ -86,6 +86,7 @@ public class HtmlResponseWriter extends TobagoResponseWriterBase {
 
   @Override
   protected void closeEmptyTag() throws IOException {
+    handlePassThroughAttributes();
     getWriter().write(">");
   }
 
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/webapp/TobagoResponseWriterBase.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/webapp/TobagoResponseWriterBase.java
index ebb0f234e9..6ce3e5932d 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/webapp/TobagoResponseWriterBase.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/webapp/TobagoResponseWriterBase.java
@@ -26,7 +26,10 @@ import org.apache.myfaces.tobago.webapp.TobagoResponseWriter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import jakarta.el.ValueExpression;
 import jakarta.faces.component.UIComponent;
+import jakarta.faces.context.FacesContext;
+import jakarta.faces.render.Renderer;
 
 import java.io.IOException;
 import java.io.Writer;
@@ -34,6 +37,9 @@ import java.lang.invoke.MethodHandles;
 import java.net.URI;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
 
 public abstract class TobagoResponseWriterBase extends TobagoResponseWriter {
 
@@ -48,6 +54,7 @@ public abstract class TobagoResponseWriterBase extends TobagoResponseWriter {
   private int inlineStack = 0;
 
   private UIComponent component;
+  private Map<Integer, String> levelToChangedElementTag = new HashMap<>();
 
   private boolean startStillOpen;
 
@@ -171,11 +178,52 @@ public abstract class TobagoResponseWriterBase extends TobagoResponseWriter {
 
   protected void closeOpenTag() throws IOException {
     if (startStillOpen) {
+      handlePassThroughAttributes();
       writer.write('>');
       startStillOpen = false;
     }
   }
 
+  protected void handlePassThroughAttributes() throws IOException {
+    if (component != null && component.getPassThroughAttributes(false) != null) {
+      for (Map.Entry<String, Object> entry : component.getPassThroughAttributes().entrySet()) {
+        String key = entry.getKey();
+        if (Renderer.PASSTHROUGH_RENDERER_LOCALNAME_KEY.equals(key)) {
+          // skip rendering
+          continue;
+        }
+
+        Object value = entry.getValue();
+        if (value instanceof ValueExpression) {
+          value = ((ValueExpression) value).getValue(FacesContext.getCurrentInstance().getELContext());
+        }
+        // Faces 2.2 In the renderkit javadoc of jsf 2.2 spec says this
+        // (Rendering Pass Through Attributes):
+        // "... The ResponseWriter must ensure that any pass through attributes are
+        // rendered on the outer-most markup element for the component. If there is
+        // a pass through attribute with the same name as a renderer specific
+        // attribute, the pass through attribute takes precedence. Pass through
+        // attributes are rendered as if they were passed to
+        // ResponseWriter.writeURIAttribute(). ..."
+        // Note here it says "as if they were passed", instead say "... attributes are
+        // encoded and rendered as if ...". Black box testing against RI shows that there
+        // is no URI encoding at all in this part, so in this case the best is do the
+        // same here. After all, it is resposibility of the one who set the passthrough
+        // attribute to do the proper encoding in cases when a URI is provided. However,
+        // that does not means the attribute should not be encoded as other attributes.
+        // TODO boolean should be handled in writeAttributeInternal
+        if (value instanceof Boolean) {
+          if (!Boolean.FALSE.equals(value)) {
+            writer.write(' ');
+            writer.write(entry.getKey());
+          }
+        } else {
+          writeAttributeInternal(writer, entry.getKey(), value.toString(), true);
+        }
+      }
+    }
+  }
+
   @Override
   public void startDocument() throws IOException {
     // nothing to do
@@ -199,27 +247,41 @@ public abstract class TobagoResponseWriterBase extends TobagoResponseWriter {
   @Override
   public void startElement(final String name, final UIComponent currentComponent) throws IOException {
     final boolean inline = HtmlElements.isInline(name);
-    if (inline) {
-      inlineStack++;
-    }
     this.component = currentComponent;
-    startElementInternal(writer, name, HtmlElements.isInline(name));
+    startElementInternal(writer, name, inline);
+  }
+
+  public void startElement(final HtmlElements name, final UIComponent currentComponent) throws IOException {
+    this.component = currentComponent;
+    startElement(name);
   }
 
   @Override
   public void startElement(final HtmlElements name) throws IOException {
-    final boolean inline = name.isInline();
-    if (inline) {
-      inlineStack++;
-    }
     startElementInternal(writer, name.getValue(), name.isInline());
-    if (!name.isVoid()) {
-      level++;
-    }
   }
 
   protected void startElementInternal(final Writer sink, final String name, final boolean inline)
       throws IOException {
+    String localElementName = null;
+    boolean localElementInline = inline;
+    if (component != null && component.getPassThroughAttributes(false) != null) {
+      Map<String, Object> passThroughAttributes = component.getPassThroughAttributes(false);
+      Object value = passThroughAttributes.get(Renderer.PASSTHROUGH_RENDERER_LOCALNAME_KEY);
+      if (value != null) {
+        if (value instanceof ValueExpression) {
+          value = ((ValueExpression) value).getValue(FacesContext.getCurrentInstance().getELContext());
+        }
+        localElementName = value.toString().toLowerCase(Locale.ROOT).trim();
+        if (!name.equals(localElementName)) {
+          levelToChangedElementTag.put(level, localElementName);
+          localElementInline = HtmlElements.isInline(localElementName);
+        }
+      }
+    }
+    if (localElementInline) {
+      inlineStack++;
+    }
     if (startStillOpen) {
       sink.write('>');
     }
@@ -230,17 +292,31 @@ public abstract class TobagoResponseWriterBase extends TobagoResponseWriter {
       }
     }
     sink.write('<');
-    sink.write(name);
+    if (localElementName != null) {
+      sink.write(localElementName);
+    } else {
+      sink.write(name);
+    }
     startStillOpen = true;
+    level++;
   }
 
   @Override
   public void endElement(final String name) throws IOException {
-    final boolean inline = HtmlElements.isInline(name);
-    if (HtmlElements.isVoid(name)) {
+    level--;
+    String elementTagToRender = levelToChangedElementTag.get(level);
+    if (elementTagToRender == null) {
+      elementTagToRender = name;
+    }
+    renderNonTobagoEndElement(elementTagToRender);
+  }
+
+  private void renderNonTobagoEndElement(String element) throws IOException {
+    final boolean inline = HtmlElements.isInline(element);
+    if (HtmlElements.isVoid(element)) {
       closeEmptyTag();
     } else {
-      endElementInternal(writer, name, inline);
+      endElementInternal(writer, element, inline);
     }
     startStillOpen = false;
     if (inline) {
@@ -251,19 +327,22 @@ public abstract class TobagoResponseWriterBase extends TobagoResponseWriter {
 
   @Override
   public void endElement(final HtmlElements name) throws IOException {
-    final boolean inline = name.isInline();
-    if (name.isVoid()) {
-      closeEmptyTag();
+    level--;
+    String elementTagToRender = levelToChangedElementTag.remove(level);
+    if (elementTagToRender != null) {
+      renderNonTobagoEndElement(elementTagToRender);
     } else {
-      if (!name.isVoid()) {
-        level--;
+      final boolean inline = name.isInline();
+      if (name.isVoid()) {
+        closeEmptyTag();
+      } else {
+        endElementInternal(writer, name.getValue(), inline);
+      }
+      startStillOpen = false;
+      if (inline) {
+        inlineStack--;
+        assert inlineStack >= 0;
       }
-      endElementInternal(writer, name.getValue(), inline);
-    }
-    startStillOpen = false;
-    if (inline) {
-      inlineStack--;
-      assert inlineStack >= 0;
     }
   }
 
@@ -289,12 +368,7 @@ public abstract class TobagoResponseWriterBase extends TobagoResponseWriter {
       throws IOException {
 
     final String attribute = findValue(value, property);
-    writeAttribute(new MarkupLanguageAttributes() {
-      @Override
-      public String getValue() {
-        return name;
-      }
-    }, attribute, true);
+    writeAttributeInternal(writer, name, attribute, true);
   }
 
   protected final String getCallingClassStackTraceElementString() {
@@ -339,6 +413,7 @@ public abstract class TobagoResponseWriterBase extends TobagoResponseWriter {
 
   protected void endElementInternal(final Writer sink, final String name, final boolean inline) throws IOException {
     if (startStillOpen) {
+      handlePassThroughAttributes();
       sink.write('>');
     }
     if (inline) {
@@ -368,10 +443,23 @@ public abstract class TobagoResponseWriterBase extends TobagoResponseWriter {
       LOG.error(error);
       throw new IllegalStateException(error);
     }
+    // If there is a pass through attribute with the same
+    // name as a renderer specific attribute,
+    // the pass through attribute takes precedence.
+    String attributeName = name.getValue();
+    if (component != null && component.getPassThroughAttributes(false) != null
+        && component.getPassThroughAttributes(false).containsKey(attributeName)) {
+      return;
+    }
+    writeAttributeInternal(sink, attributeName, value, escape);
+  }
 
+  private void writeAttributeInternal(
+      final Writer sink, final String attributeName, final String value, final boolean escape)
+      throws IOException {
     if (value != null) {
       sink.write(' ');
-      sink.write(name.getValue());
+      sink.write(attributeName);
       sink.write("='");
       writerAttributeValue(value, escape);
       sink.write('\'');
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/webapp/TobagoResponseWriterWrapper.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/webapp/TobagoResponseWriterWrapper.java
index 0aeb1739f6..a41aa8ac2a 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/webapp/TobagoResponseWriterWrapper.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/webapp/TobagoResponseWriterWrapper.java
@@ -43,6 +43,11 @@ public class TobagoResponseWriterWrapper extends TobagoResponseWriter {
     responseWriter.startElement(name, component);
   }
 
+  @Override
+  public void startElement(final HtmlElements name, final UIComponent component) throws IOException {
+    responseWriter.startElement(name.getValue(), component);
+  }
+
   @Override
   public void startElement(final HtmlElements name) throws IOException {
     responseWriter.startElement(name.getValue(), null);
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/webapp/XmlResponseWriter.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/webapp/XmlResponseWriter.java
index fab061425f..1fe50f8341 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/webapp/XmlResponseWriter.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/internal/webapp/XmlResponseWriter.java
@@ -66,6 +66,7 @@ public final class XmlResponseWriter extends TobagoResponseWriterBase {
 
   @Override
   protected void closeEmptyTag() throws IOException {
+    handlePassThroughAttributes();
     getWriter().write("/>");
   }
 
diff --git a/tobago-core/src/main/java/org/apache/myfaces/tobago/webapp/TobagoResponseWriter.java b/tobago-core/src/main/java/org/apache/myfaces/tobago/webapp/TobagoResponseWriter.java
index f26cb9d898..7d0bb00453 100644
--- a/tobago-core/src/main/java/org/apache/myfaces/tobago/webapp/TobagoResponseWriter.java
+++ b/tobago-core/src/main/java/org/apache/myfaces/tobago/webapp/TobagoResponseWriter.java
@@ -48,6 +48,8 @@ public abstract class TobagoResponseWriter extends ResponseWriter {
   @Override
   public abstract void startElement(String name, UIComponent component) throws IOException;
 
+  public abstract void startElement(HtmlElements name, UIComponent component) throws IOException;
+
   public abstract void startElement(HtmlElements name) throws IOException;
 
   /**
diff --git a/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/config/AbstractTobagoTestBase.java b/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/config/AbstractTobagoTestBase.java
index a7d89d1fbf..385c9b1132 100644
--- a/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/config/AbstractTobagoTestBase.java
+++ b/tobago-core/src/test/java/org/apache/myfaces/tobago/internal/config/AbstractTobagoTestBase.java
@@ -39,6 +39,7 @@ import org.apache.myfaces.tobago.component.UIColumn;
 import org.apache.myfaces.tobago.component.UIDate;
 import org.apache.myfaces.tobago.component.UIFile;
 import org.apache.myfaces.tobago.component.UIGridLayout;
+import org.apache.myfaces.tobago.component.UIImage;
 import org.apache.myfaces.tobago.component.UIIn;
 import org.apache.myfaces.tobago.component.UILink;
 import org.apache.myfaces.tobago.component.UILinks;
@@ -82,6 +83,7 @@ import org.apache.myfaces.tobago.internal.renderkit.renderer.ColumnRenderer;
 import org.apache.myfaces.tobago.internal.renderkit.renderer.DateRenderer;
 import org.apache.myfaces.tobago.internal.renderkit.renderer.FileRenderer;
 import org.apache.myfaces.tobago.internal.renderkit.renderer.GridLayoutRenderer;
+import org.apache.myfaces.tobago.internal.renderkit.renderer.ImageRenderer;
 import org.apache.myfaces.tobago.internal.renderkit.renderer.InRenderer;
 import org.apache.myfaces.tobago.internal.renderkit.renderer.LinkRenderer;
 import org.apache.myfaces.tobago.internal.renderkit.renderer.LinksRenderer;
@@ -173,6 +175,7 @@ public abstract class AbstractTobagoTestBase extends AbstractJsfTestCase {
     application.addComponent(Tags.column.componentType(), UIColumn.class.getName());
     application.addComponent(Tags.file.componentType(), UIFile.class.getName());
     application.addComponent(Tags.gridLayout.componentType(), UIGridLayout.class.getName());
+    application.addComponent(Tags.image.componentType(), UIImage.class.getName());
     application.addComponent(Tags.in.componentType(), UIIn.class.getName());
     application.addComponent(Tags.link.componentType(), UILink.class.getName());
     application.addComponent(Tags.links.componentType(), UILinks.class.getName());
@@ -223,6 +226,7 @@ public abstract class AbstractTobagoTestBase extends AbstractJsfTestCase {
     renderKit.addRenderer(UIFile.COMPONENT_FAMILY, RendererTypes.FILE, new FileRenderer<UIFile>());
     renderKit.addRenderer(UIGridLayout.COMPONENT_FAMILY, RendererTypes.GRID_LAYOUT,
         new GridLayoutRenderer<UIGridLayout>());
+    renderKit.addRenderer(UIImage.COMPONENT_FAMILY, RendererTypes.IMAGE, new ImageRenderer<UIImage>());
     renderKit.addRenderer(UIIn.COMPONENT_FAMILY, RendererTypes.IN, new InRenderer<UIIn>());
     renderKit.addRenderer(UILink.COMPONENT_FAMILY, RendererTypes.LINK, new LinkRenderer<UILink>());
     renderKit.addRenderer(UILinks.COMPONENT_FAMILY, RendererTypes.LINKS, new LinksRenderer<UILinks>());
diff --git a/tobago-core/src/test/java/org/apache/myfaces/tobago/webapp/TobagoResponseWriterUnitTest.java b/tobago-core/src/test/java/org/apache/myfaces/tobago/webapp/TobagoResponseWriterUnitTest.java
index cf59ed8b96..4c01ddcc2c 100644
--- a/tobago-core/src/test/java/org/apache/myfaces/tobago/webapp/TobagoResponseWriterUnitTest.java
+++ b/tobago-core/src/test/java/org/apache/myfaces/tobago/webapp/TobagoResponseWriterUnitTest.java
@@ -19,11 +19,19 @@
 
 package org.apache.myfaces.tobago.webapp;
 
-import org.apache.myfaces.test.base.junit.AbstractJsfTestCase;
+import jakarta.faces.render.Renderer;
+import org.apache.myfaces.tobago.component.RendererTypes;
+import org.apache.myfaces.tobago.component.Tags;
+import org.apache.myfaces.tobago.component.UIImage;
+import org.apache.myfaces.tobago.component.UIIn;
+import org.apache.myfaces.tobago.component.UIOut;
+import org.apache.myfaces.tobago.component.UITextarea;
+import org.apache.myfaces.tobago.internal.config.AbstractTobagoTestBase;
 import org.apache.myfaces.tobago.internal.webapp.HtmlResponseWriter;
 import org.apache.myfaces.tobago.internal.webapp.XmlResponseWriter;
 import org.apache.myfaces.tobago.renderkit.html.HtmlAttributes;
 import org.apache.myfaces.tobago.renderkit.html.HtmlElements;
+import org.apache.myfaces.tobago.util.ComponentUtils;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
@@ -32,7 +40,7 @@ import java.io.IOException;
 import java.io.StringWriter;
 import java.nio.charset.StandardCharsets;
 
-public class TobagoResponseWriterUnitTest extends AbstractJsfTestCase {
+public class TobagoResponseWriterUnitTest extends AbstractTobagoTestBase {
 
   private StringWriter stringWriter;
   private TobagoResponseWriter writer;
@@ -152,4 +160,95 @@ public class TobagoResponseWriterUnitTest extends AbstractJsfTestCase {
     xmlResponseWriter.writeText("123".toCharArray(), 0, 3);
     Assertions.assertEquals("123", stringWriter.toString());
   }
+
+  @Test
+  public void testPassthrough() throws IOException {
+    final UIIn c = (UIIn) ComponentUtils.createComponent(
+        facesContext, Tags.in.componentType(), RendererTypes.In, "id");
+    c.getPassThroughAttributes().put("step", 1);
+    c.getPassThroughAttributes().put("type", "number");
+    writer.startElement(HtmlElements.INPUT.getValue(), c);
+    writer.writeAttribute(HtmlAttributes.VALUE.getValue(), "100", null);
+    writer.endElement(HtmlElements.INPUT.getValue());
+
+    Assertions.assertEquals("\n<input value='100' step='1' type='number'>",
+        stringWriter.toString());
+  }
+
+  @Test
+  public void testPassthroughChangeElementTag() throws IOException {
+    final UIIn c = (UIIn) ComponentUtils.createComponent(
+        facesContext, Tags.in.componentType(), RendererTypes.In, "id");
+    c.getPassThroughAttributes().put("step", 1);
+    c.getPassThroughAttributes().put(Renderer.PASSTHROUGH_RENDERER_LOCALNAME_KEY, HtmlElements.TEXTAREA.getValue());
+    writer.startElement(HtmlElements.INPUT.getValue(), c);
+    writer.writeAttribute(HtmlAttributes.VALUE.getValue(), "100", null);
+    writer.endElement(HtmlElements.INPUT.getValue());
+
+    Assertions.assertEquals("\n<textarea value='100' step='1'></textarea>",
+        stringWriter.toString());
+  }
+
+  @Test
+  public void testPassthroughInRenderer() throws IOException {
+    final UIIn c = (UIIn) ComponentUtils.createComponent(
+        facesContext, Tags.in.componentType(), RendererTypes.In, "id");
+
+    c.getPassThroughAttributes().put("step", 1);
+    c.getPassThroughAttributes().put("type", "number");
+    c.encodeAll(facesContext);
+    Assertions.assertEquals("\n<tobago-in id='id' class='tobago-auto-spacing'>\n"
+        + " <input name='id' id='id::field' class='form-control' step='1' type='number'>\n"
+        + "</tobago-in>", getLastWritten());
+  }
+
+  @Test
+  public void testPassthroughChangeElementTagInRender() throws IOException {
+    final UIIn c = (UIIn) ComponentUtils.createComponent(
+        facesContext, Tags.in.componentType(), RendererTypes.In, "id");
+
+    c.getPassThroughAttributes().put("test", 1);
+    c.getPassThroughAttributes().put(Renderer.PASSTHROUGH_RENDERER_LOCALNAME_KEY, HtmlElements.TEXTAREA.getValue());
+    c.encodeAll(facesContext);
+    Assertions.assertEquals("\n<tobago-in id='id' class='tobago-auto-spacing'>\n"
+        + " <textarea type='text' name='id' id='id::field' class='form-control' test='1'></textarea>\n"
+        + "</tobago-in>", getLastWritten());
+
+  }
+
+  @Test
+  public void testPassthroughWithTextareaRenderer() throws IOException {
+    final UITextarea c = (UITextarea) ComponentUtils.createComponent(
+        facesContext, Tags.textarea.componentType(), RendererTypes.Textarea, "id");
+
+    c.getPassThroughAttributes().put("spellcheck", true);
+    c.encodeAll(facesContext);
+    Assertions.assertEquals("\n<tobago-textarea id='id' class='tobago-auto-spacing'>\n"
+        + " <textarea name='id' id='id::field' class='form-control' spellcheck></textarea>\n"
+        + "</tobago-textarea>", getLastWritten());
+  }
+
+  @Test
+  public void testPassthroughWithImgRenderer() throws IOException {
+    final UIImage c = (UIImage) ComponentUtils.createComponent(
+        facesContext, Tags.image.componentType(), RendererTypes.Image, "id");
+
+    c.getPassThroughAttributes().put("spellcheck", true);
+    c.encodeAll(facesContext);
+    Assertions.assertEquals("\n<tobago-image id='id'>\n"
+        + " <img alt='' spellcheck>\n"
+        + "</tobago-image>", getLastWritten());
+  }
+
+  @Test
+  public void testPassthroughWithOutRenderer() throws IOException {
+    final UIOut c = (UIOut) ComponentUtils.createComponent(
+        facesContext, Tags.out.componentType(), RendererTypes.Out, "id");
+
+    c.getPassThroughAttributes().put("spellcheck", true);
+    c.encodeAll(facesContext);
+    Assertions.assertEquals("\n<tobago-out id='id' class='tobago-auto-spacing'>"
+        + "<span class='form-control-plaintext' spellcheck></span>"
+        + "</tobago-out>", getLastWritten());
+  }
 }