You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@myfaces.apache.org by ta...@apache.org on 2026/08/18 18:10:36 UTC
(myfaces) branch main updated: Faces S0ec 1507 - Enhance UIInput events with HTML5 like oninput
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/myfaces.git
The following commit(s) were added to refs/heads/main by this push:
new 6fa5084be Faces S0ec 1507 - Enhance UIInput events with HTML5 like oninput
6fa5084be is described below
commit 6fa5084be4f0cbd57b38e48ccc21363aa3d0720d
Author: tandraschko <ta...@apache.org>
AuthorDate: Tue Aug 18 20:10:45 2026 +0200
Faces S0ec 1507 - Enhance UIInput events with HTML5 like oninput
---
.../jakarta/faces/component/html/HtmlEvents.java | 315 ++++++++++++++++++
.../component/html/_HtmlOutcomeTargetButton.java | 3 +-
.../component/html/_HtmlOutcomeTargetLink.java | 3 +-
.../faces/component/html/_HtmlOutputLink.java | 3 +-
.../java/jakarta/faces/event/BehaviorEvent.java | 22 ++
.../core/api/shared/CommonHtmlAttributes.java | 18 +-
.../myfaces/core/api/shared/CommonHtmlEvents.java | 54 +++-
.../main/resources/META-INF/componentClass20.vm | 26 ++
.../myfaces/renderkit/html/HtmlLabelRenderer.java | 2 +
.../renderkit/html/base/HtmlBodyRendererBase.java | 12 +-
.../html/base/HtmlButtonRendererBase.java | 3 +
.../html/base/HtmlCheckboxRendererBase.java | 8 +-
.../renderkit/html/base/HtmlGroupRendererBase.java | 26 +-
.../renderkit/html/base/HtmlLinkRendererBase.java | 20 +-
.../base/HtmlOutcomeTargetButtonRendererBase.java | 4 +
.../renderkit/html/base/HtmlRadioRendererBase.java | 4 +-
.../myfaces/renderkit/html/base/HtmlRenderer.java | 17 +
.../html/base/HtmlSelectableRendererBase.java | 3 +
.../renderkit/html/util/CommonHtmlEventsUtil.java | 242 ++++++++++++++
.../facelets/component/JsfElementRenderer.java | 2 +
.../view/facelets/tag/faces/core/AjaxHandler.java | 7 +-
.../main/resources/META-INF/componentClass20.vm | 10 +
.../renderkit/html/HtmlFormRendererTest.java | 2 +-
.../renderkit/html/HtmlGridRendererTest.java | 2 +-
.../renderkit/html/HtmlGroupRendererTest.java | 2 +-
.../renderkit/html/HtmlImageRendererTest.java | 2 +-
.../renderkit/html/HtmlTableRendererTest.java | 2 +-
...tionalHtmlEventsClientBehaviorRendererTest.java | 352 +++++++++++++++++++++
.../HtmlBodyClientBehaviorRendererTest.java | 70 +++-
.../test/utils/HtmlCheckAttributesUtil.java | 28 +-
.../faces/html/AdditionalHtmlEventsTestCase.java | 108 +++++++
.../tag/faces/html/additionalHtmlEvents.xml | 36 +++
32 files changed, 1375 insertions(+), 33 deletions(-)
diff --git a/api/src/main/java/jakarta/faces/component/html/HtmlEvents.java b/api/src/main/java/jakarta/faces/component/html/HtmlEvents.java
new file mode 100644
index 000000000..b1a47cb47
--- /dev/null
+++ b/api/src/main/java/jakarta/faces/component/html/HtmlEvents.java
@@ -0,0 +1,315 @@
+/*
+ * 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.
+ */
+package jakarta.faces.component.html;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Supplier;
+
+import jakarta.faces.component.ActionSource;
+import jakarta.faces.component.EditableValueHolder;
+import jakarta.faces.component.behavior.ClientBehaviorHolder;
+import jakarta.faces.context.FacesContext;
+import jakarta.faces.event.BehaviorEvent.FacesComponentEvent;
+
+/**
+ * <p class="changed_added_5_0">
+ * Events supported by HTML elements as per
+ * <a href="https://html.spec.whatwg.org/multipage/webappapis.html#event-handlers-on-elements,-document-objects,-and-window-objects">current spec</a>.
+ * These can be used to supply {@link ClientBehaviorHolder#getEventNames()} and
+ * {@link ClientBehaviorHolder#getDefaultEventName()}.
+ * </p>
+ * <p>
+ * The events are split into two enums: {@link HtmlDocumentElementEvent} lists the events that fire on the element
+ * itself, while {@link HtmlBodyElementEvent} lists the events that are also part of the HTML spec's
+ * <code>GlobalEventHandlers</code> but have special behavior on the HTML <code><body></code> element where they
+ * are forwarded to the <code>Window</code> object instead of firing on the element. Components that represent the HTML
+ * <code><body></code> element should use {@link #getHtmlDocumentElementEventNames(FacesContext)} to avoid
+ * exposing these window-level events as component behavior events, while all other components should use
+ * {@link #getHtmlBodyElementEventNames(FacesContext)} or one of its supersets.
+ * </p>
+ *
+ * @since 5.0
+ */
+public final class HtmlEvents
+{
+
+ /**
+ * Events supported by all HTML document elements that always fire on the element itself.
+ * See {@link HtmlBodyElementEvent} for events that are forwarded to the <code>Window</code> object on the HTML
+ * <code><body></code> element.
+ */
+ public enum HtmlDocumentElementEvent
+ {
+ abort,
+ auxclick,
+ beforeinput,
+ beforematch,
+ beforetoggle,
+ cancel,
+ canplay,
+ canplaythrough,
+ change,
+ click,
+ close,
+ command,
+ contextlost,
+ contextmenu,
+ contextrestored,
+ copy,
+ cuechange,
+ cut,
+ dblclick,
+ drag,
+ dragend,
+ dragenter,
+ dragleave,
+ dragover,
+ dragstart,
+ drop,
+ durationchange,
+ emptied,
+ ended,
+ formdata,
+ input,
+ invalid,
+ keydown,
+ keypress,
+ keyup,
+ loadeddata,
+ loadedmetadata,
+ loadstart,
+ mousedown,
+ mouseenter,
+ mouseleave,
+ mousemove,
+ mouseout,
+ mouseover,
+ mouseup,
+ paste,
+ pause,
+ play,
+ playing,
+ progress,
+ ratechange,
+ reset,
+ securitypolicyviolation,
+ seeked,
+ seeking,
+ select,
+ slotchange,
+ stalled,
+ submit,
+ suspend,
+ timeupdate,
+ toggle,
+ volumechange,
+ waiting,
+ wheel;
+ }
+
+ /**
+ * Events from the HTML spec's <code>GlobalEventHandlers</code> that have special behavior on the HTML body element:
+ * instead of firing on the element itself, they are forwarded to the <code>Window</code> object.
+ * These events are valid on all HTML elements, but are listed separately here because of this distinction.
+ */
+ public enum HtmlBodyElementEvent
+ {
+ blur,
+ error,
+ focus,
+ load,
+ resize,
+ scroll,
+ scrollend;
+ }
+
+ /**
+ * The name of the context-param whose value must represent a space-separated list of additional HTML event names.
+ * All supported HTML event names are defined in the enums {@link HtmlDocumentElementEvent} and
+ * {@link HtmlBodyElementEvent}. Any HTML event name which you wish to add to these enums can be supplied via this
+ * context-param. Duplicates will be automatically filtered, case sensitive.
+ */
+ public static final String ADDITIONAL_HTML_EVENT_NAMES_PARAM_NAME = "jakarta.faces.ADDITIONAL_HTML_EVENT_NAMES";
+
+ private enum CacheKey
+ {
+ ADDITIONAL_HTML_EVENT_NAMES,
+ HTML_DOCUMENT_ELEMENT_EVENT_NAMES,
+ HTML_BODY_ELEMENT_EVENT_NAMES,
+ FACES_ACTION_SOURCE_EVENT_NAMES,
+ FACES_EDITABLE_VALUE_HOLDER_EVENT_NAMES;
+ }
+
+ private HtmlEvents()
+ {
+ throw new AssertionError();
+ }
+
+ /**
+ * @param context The involved faces context.
+ * @return All additional HTML event names specified via {@link #ADDITIONAL_HTML_EVENT_NAMES_PARAM_NAME}.
+ */
+ public static Collection<String> getAdditionalHtmlEventNames(FacesContext context)
+ {
+ return cache(context, CacheKey.ADDITIONAL_HTML_EVENT_NAMES, () -> getContextParam(context));
+ }
+
+ /**
+ * @param context The involved faces context.
+ * @return All supported event names for HTML document elements, including additional HTML event names.
+ */
+ public static Collection<String> getHtmlDocumentElementEventNames(FacesContext context)
+ {
+ return cache(context, CacheKey.HTML_DOCUMENT_ELEMENT_EVENT_NAMES,
+ () -> merge(getAdditionalHtmlEventNames(context), HtmlDocumentElementEvent.values()));
+ }
+
+ /**
+ * @param context The involved faces context.
+ * @return All supported event names for HTML body elements, including HTML document element event names.
+ */
+ public static Collection<String> getHtmlBodyElementEventNames(FacesContext context)
+ {
+ return cache(context, CacheKey.HTML_BODY_ELEMENT_EVENT_NAMES,
+ () -> merge(getHtmlDocumentElementEventNames(context), HtmlBodyElementEvent.values()));
+ }
+
+ /**
+ * @param context The involved faces context.
+ * @return All supported event names for HTML implementations of Faces {@link ActionSource} components, including
+ * HTML body element event names.
+ */
+ public static Collection<String> getFacesActionSourceEventNames(FacesContext context)
+ {
+ return cache(context, CacheKey.FACES_ACTION_SOURCE_EVENT_NAMES,
+ () -> merge(getHtmlBodyElementEventNames(context), FacesComponentEvent.action));
+ }
+
+ /**
+ * @param context The involved faces context.
+ * @return All supported event names for HTML implementations of Faces {@link EditableValueHolder} components,
+ * including HTML body element event names.
+ */
+ public static Collection<String> getFacesEditableValueHolderEventNames(FacesContext context)
+ {
+ return cache(context, CacheKey.FACES_EDITABLE_VALUE_HOLDER_EVENT_NAMES,
+ () -> merge(getHtmlBodyElementEventNames(context), FacesComponentEvent.valueChange));
+ }
+
+ private static Collection<String> getContextParam(FacesContext context)
+ {
+ String param = context.getExternalContext().getInitParameter(ADDITIONAL_HTML_EVENT_NAMES_PARAM_NAME);
+ if (param == null || param.isBlank())
+ {
+ return Collections.emptyList();
+ }
+
+ List<String> eventNames = new ArrayList<>();
+ for (String eventName : param.trim().split("\\s+"))
+ {
+ if (!eventName.isEmpty())
+ {
+ eventNames.add(eventName);
+ }
+ }
+ return Collections.unmodifiableList(eventNames);
+ }
+
+ private static Collection<String> merge(Collection<String> eventNames, Enum<?>... enumValues)
+ {
+ // a TreeSet gives us the sorted + distinct semantics in one go.
+ TreeSet<String> merged = new TreeSet<>(eventNames);
+ for (Enum<?> enumValue : enumValues)
+ {
+ merged.add(enumValue.name());
+ }
+ return new EventNameCollection(merged);
+ }
+
+ /**
+ * Sorted, distinct and unmodifiable like the plain <code>List</code> the spec javadoc suggests, but with
+ * constant time {@link #contains}, which is what {@link jakarta.faces.component.UIComponentBase#addClientBehavior}
+ * and the renderers actually do with this collection.
+ */
+ private static final class EventNameCollection extends java.util.AbstractList<String>
+ implements java.util.RandomAccess
+ {
+ private final String[] sortedEventNames;
+ private final Set<String> lookup;
+
+ private EventNameCollection(TreeSet<String> sortedDistinctEventNames)
+ {
+ this.sortedEventNames = sortedDistinctEventNames.toArray(new String[sortedDistinctEventNames.size()]);
+ this.lookup = new HashSet<>(sortedDistinctEventNames);
+ }
+
+ @Override
+ public String get(int index)
+ {
+ return sortedEventNames[index];
+ }
+
+ @Override
+ public int size()
+ {
+ return sortedEventNames.length;
+ }
+
+ @Override
+ public boolean contains(Object eventName)
+ {
+ return lookup.contains(eventName);
+ }
+ }
+
+ @SuppressWarnings("unchecked") // the event-name cache is stored under an Object-valued application-map entry
+ private static Collection<String> cache(FacesContext context, CacheKey key, Supplier<Collection<String>> supplier)
+ {
+ Map<String, Object> applicationMap = context.getExternalContext().getApplicationMap();
+
+ // NOTE: this may concurrently create more than one cache map, which is harmless as all of them would hold
+ // equal values anyway, and the map itself is concurrent so no entry can ever get lost or corrupted.
+ Map<CacheKey, Collection<String>> cache =
+ (Map<CacheKey, Collection<String>>) applicationMap.get(ADDITIONAL_HTML_EVENT_NAMES_PARAM_NAME);
+ if (cache == null)
+ {
+ cache = new ConcurrentHashMap<>(CacheKey.values().length);
+ applicationMap.put(ADDITIONAL_HTML_EVENT_NAMES_PARAM_NAME, cache);
+ }
+
+ // NOTE: deliberately no computeIfAbsent, the suppliers build on each other and hence recursively populate
+ // this very map, which a ConcurrentHashMap rejects with "Recursive update". A plain get/put is fine here:
+ // the values are immutable and only ever computed from the same input, so a race just recomputes them.
+ Collection<String> eventNames = cache.get(key);
+ if (eventNames == null)
+ {
+ eventNames = supplier.get();
+ cache.put(key, eventNames);
+ }
+ return eventNames;
+ }
+}
diff --git a/api/src/main/java/jakarta/faces/component/html/_HtmlOutcomeTargetButton.java b/api/src/main/java/jakarta/faces/component/html/_HtmlOutcomeTargetButton.java
index a9e9d5894..2093f8073 100644
--- a/api/src/main/java/jakarta/faces/component/html/_HtmlOutcomeTargetButton.java
+++ b/api/src/main/java/jakarta/faces/component/html/_HtmlOutcomeTargetButton.java
@@ -30,7 +30,8 @@
@JSFComponent(name="h:button",
clazz = "jakarta.faces.component.html.HtmlOutcomeTargetButton",template=true,
implementz = "jakarta.faces.component.behavior.ClientBehaviorHolder",
- defaultRendererType = "jakarta.faces.Button")
+ defaultRendererType = "jakarta.faces.Button",
+ defaultEventName = "click")
abstract class _HtmlOutcomeTargetButton extends UIOutcomeTarget implements _FocusBlurProperties,
_EventProperties, _StyleProperties, _UniversalProperties, _AccesskeyProperty,
_TabindexProperty, _AltProperty, _RoleProperty
diff --git a/api/src/main/java/jakarta/faces/component/html/_HtmlOutcomeTargetLink.java b/api/src/main/java/jakarta/faces/component/html/_HtmlOutcomeTargetLink.java
index c874c1cf2..fd3694939 100644
--- a/api/src/main/java/jakarta/faces/component/html/_HtmlOutcomeTargetLink.java
+++ b/api/src/main/java/jakarta/faces/component/html/_HtmlOutcomeTargetLink.java
@@ -29,7 +29,8 @@
@JSFComponent(name="h:link",
clazz = "jakarta.faces.component.html.HtmlOutcomeTargetLink",template=true,
implementz = "jakarta.faces.component.behavior.ClientBehaviorHolder",
- defaultRendererType = "jakarta.faces.Link")
+ defaultRendererType = "jakarta.faces.Link",
+ defaultEventName = "click")
abstract class _HtmlOutcomeTargetLink extends UIOutcomeTarget implements _FocusBlurProperties,
_EventProperties, _StyleProperties, _UniversalProperties, _AccesskeyProperty,
_TabindexProperty, _LinkProperties, _RoleProperty
diff --git a/api/src/main/java/jakarta/faces/component/html/_HtmlOutputLink.java b/api/src/main/java/jakarta/faces/component/html/_HtmlOutputLink.java
index d6dc1eaff..e93ef6124 100644
--- a/api/src/main/java/jakarta/faces/component/html/_HtmlOutputLink.java
+++ b/api/src/main/java/jakarta/faces/component/html/_HtmlOutputLink.java
@@ -33,7 +33,8 @@
clazz = "jakarta.faces.component.html.HtmlOutputLink",template=true,
tagClass = "org.apache.myfaces.taglib.html.HtmlOutputLinkTag",
defaultRendererType = "jakarta.faces.Link",
- implementz = "jakarta.faces.component.behavior.ClientBehaviorHolder")
+ implementz = "jakarta.faces.component.behavior.ClientBehaviorHolder",
+ defaultEventName = "click")
abstract class _HtmlOutputLink extends UIOutput implements _AccesskeyProperty,
_UniversalProperties, _FocusBlurProperties, _EventProperties, _StyleProperties,
_TabindexProperty, _LinkProperties, _RoleProperty
diff --git a/api/src/main/java/jakarta/faces/event/BehaviorEvent.java b/api/src/main/java/jakarta/faces/event/BehaviorEvent.java
index d163e5f49..a3fe45bc7 100644
--- a/api/src/main/java/jakarta/faces/event/BehaviorEvent.java
+++ b/api/src/main/java/jakarta/faces/event/BehaviorEvent.java
@@ -18,6 +18,8 @@
*/
package jakarta.faces.event;
+import jakarta.faces.component.ActionSource;
+import jakarta.faces.component.EditableValueHolder;
import jakarta.faces.component.UIComponent;
import jakarta.faces.component.behavior.Behavior;
import jakarta.faces.context.FacesContext;
@@ -27,6 +29,26 @@
*/
public abstract class BehaviorEvent extends FacesEvent
{
+ /**
+ * <p class="changed_added_5_0">
+ * Behavior events supported by Faces components.
+ * </p>
+ *
+ * @since 5.0
+ */
+ public enum FacesComponentEvent
+ {
+ /**
+ * Default event of {@link ActionSource} components.
+ */
+ action,
+
+ /**
+ * Default event of {@link EditableValueHolder} components.
+ */
+ valueChange;
+ }
+
private Behavior _behavior;
public BehaviorEvent(FacesContext facesContext, UIComponent uiComponent, Behavior behavior)
diff --git a/api/src/main/java/org/apache/myfaces/core/api/shared/CommonHtmlAttributes.java b/api/src/main/java/org/apache/myfaces/core/api/shared/CommonHtmlAttributes.java
index 95d83738d..0ff94741d 100644
--- a/api/src/main/java/org/apache/myfaces/core/api/shared/CommonHtmlAttributes.java
+++ b/api/src/main/java/org/apache/myfaces/core/api/shared/CommonHtmlAttributes.java
@@ -223,19 +223,23 @@ public class CommonHtmlAttributes
public static void markAttribute(UIComponent component, String name)
{
+ // A ValueExpression bound to a behavior event attribute, e.g. oninput="#{bean.script}", is neither a
+ // component property nor part of the component attribute map key set, so it would be invisible at render
+ // time when the renderer has no matching renderer specific attribute. Remember its name here.
+ if (CommonHtmlEvents.isBehaviorEventAttribute(name))
+ {
+ CommonHtmlEvents.markEventAttribute(component, name);
+ }
+
Long propertyConstant = COMMONERTIES_KEY_BY_NAME.get(name);
if (propertyConstant == null)
{
return;
}
- Long commonPropertiesSet = (Long) component.getAttributes().get(ATTRIBUTES_MARKED);
- if (commonPropertiesSet == null)
- {
- commonPropertiesSet = 0L;
- }
- component.getAttributes().put(ATTRIBUTES_MARKED, commonPropertiesSet | propertyConstant);
+
+ markAttribute(component, propertyConstant);
}
-
+
public static void markAttribute(UIComponent component, long propertyConstant)
{
Long commonPropertiesSet = (Long) component.getAttributes().get(ATTRIBUTES_MARKED);
diff --git a/api/src/main/java/org/apache/myfaces/core/api/shared/CommonHtmlEvents.java b/api/src/main/java/org/apache/myfaces/core/api/shared/CommonHtmlEvents.java
index dc33923c4..e093901be 100644
--- a/api/src/main/java/org/apache/myfaces/core/api/shared/CommonHtmlEvents.java
+++ b/api/src/main/java/org/apache/myfaces/core/api/shared/CommonHtmlEvents.java
@@ -19,13 +19,29 @@
package org.apache.myfaces.core.api.shared;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
import jakarta.faces.component.UIComponent;
public class CommonHtmlEvents
{
public static final String EVENTS_MARKED = "oam.EVENTS_MARKED";
-
+
+ /**
+ * Prefix of the HTML attribute which carries the script of a behavior event, e.g. "onclick" for "click".
+ */
+ public static final String BEHAVIOR_EVENT_ATTRIBUTE_PREFIX = "on";
+
+ /**
+ * Component attribute under which the names of all behavior event attributes bound to a
+ * <code>ValueExpression</code>, e.g. <code>oninput="#{bean.script}"</code>, are remembered as a
+ * <code>Set<String></code>. Such an attribute is neither a component property nor part of the component
+ * attribute map key set, so this marker is the only cheap way for a renderer to find it at render time; see
+ * <code>CommonHtmlEventsUtil#renderAdditionalBehaviorEventHandlers</code>.
+ */
+ public static final String EVENT_ATTRIBUTES_MARKED = "oam.EVENT_ATTRIBUTES_MARKED";
+
public static final long ACTION = 0x1L;
public static final long CLICK = 0x2L;
public static final long DBLCLICK = 0x4L;
@@ -130,4 +146,40 @@ public static long getMarkedEvents(UIComponent component)
}
return commonEvents;
}
+
+ /**
+ * @param name The attribute name to check.
+ * @return <code>true</code> if the given attribute name denotes a behavior event attribute, i.e. it starts with
+ * "on" and has at least one more character.
+ */
+ public static boolean isBehaviorEventAttribute(String name)
+ {
+ return name.length() > BEHAVIOR_EVENT_ATTRIBUTE_PREFIX.length()
+ && name.charAt(0) == 'o'
+ && name.charAt(1) == 'n';
+ }
+
+ /**
+ * @param name The behavior event attribute name, as accepted by {@link #isBehaviorEventAttribute(String)}.
+ * @return The event name of the given behavior event attribute name, e.g. "input" for "oninput".
+ */
+ public static String getEventName(String name)
+ {
+ return name.substring(BEHAVIOR_EVENT_ATTRIBUTE_PREFIX.length());
+ }
+
+ /**
+ * Remembers that a <code>ValueExpression</code> has been bound to the given behavior event attribute,
+ * see {@link #EVENT_ATTRIBUTES_MARKED}. Mirrors the copy-on-write pattern of {@link #markEvent} so that
+ * partial state saving picks up the change.
+ */
+ @SuppressWarnings("unchecked") // the marker entry is stored under an Object-valued component attribute
+ public static void markEventAttribute(UIComponent component, String name)
+ {
+ Map<String, Object> attributes = component.getAttributes();
+ Set<String> marked = (Set<String>) attributes.get(EVENT_ATTRIBUTES_MARKED);
+ Set<String> newMarked = marked == null ? new HashSet<>(4) : new HashSet<>(marked);
+ newMarked.add(name);
+ attributes.put(EVENT_ATTRIBUTES_MARKED, newMarked);
+ }
}
diff --git a/api/src/main/resources/META-INF/componentClass20.vm b/api/src/main/resources/META-INF/componentClass20.vm
index 67837c442..82a45a7ed 100644
--- a/api/src/main/resources/META-INF/componentClass20.vm
+++ b/api/src/main/resources/META-INF/componentClass20.vm
@@ -105,6 +105,31 @@ $innersource
#if ($component.implements)
#if ( ($component.implements == "jakarta.faces.component.behavior.ClientBehaviorHolder")
&& (${utils.getClassFromFullClass($component.className)} != "UIWebsocket"))
+## Spec issue 1507: the standard HTML components must expose every HTML event name, so that both
+## <f:ajax event="input"> and a plain oninput="..." attribute work without the component having to
+## declare a matching property. See jakarta.faces.component.html.HtmlEvents.
+#if ($component.className.startsWith("jakarta.faces.component.html"))
+#if (${utils.getClassFromFullClass($component.className)} == "HtmlBody")
+## the <body> element forwards blur/error/focus/load/resize/scroll/scrollend to the Window object,
+## hence they must not be exposed as behavior events of the component itself.
+#set ($htmlEventNamesMethod = "getHtmlDocumentElementEventNames")
+#else
+#if ($component.defaultEventName == "action")
+#set ($htmlEventNamesMethod = "getFacesActionSourceEventNames")
+#else
+#if ($component.defaultEventName == "valueChange")
+#set ($htmlEventNamesMethod = "getFacesEditableValueHolderEventNames")
+#else
+#set ($htmlEventNamesMethod = "getHtmlBodyElementEventNames")
+#end
+#end
+#end
+ @Override
+ public java.util.Collection<String> getEventNames()
+ {
+ return jakarta.faces.component.html.HtmlEvents.${htmlEventNamesMethod}(getFacesContext());
+ }
+#else
static private final java.util.Collection<String> CLIENT_EVENTS_LIST =
java.util.Collections.unmodifiableCollection(
java.util.Arrays.asList(
@@ -123,6 +148,7 @@ $innersource
{
return CLIENT_EVENTS_LIST;
}
+#end
@Override
public void addClientBehavior(String eventName, jakarta.faces.component.behavior.ClientBehavior behavior)
diff --git a/impl/src/main/java/org/apache/myfaces/renderkit/html/HtmlLabelRenderer.java b/impl/src/main/java/org/apache/myfaces/renderkit/html/HtmlLabelRenderer.java
index 8cc44f25f..96d5c0b78 100755
--- a/impl/src/main/java/org/apache/myfaces/renderkit/html/HtmlLabelRenderer.java
+++ b/impl/src/main/java/org/apache/myfaces/renderkit/html/HtmlLabelRenderer.java
@@ -136,6 +136,8 @@ public void encodeBegin(FacesContext facesContext, UIComponent uiComponent) thro
writer, uiComponent, behaviors);
}
}
+ CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, uiComponent, behaviors,
+ CommonHtmlEventsUtil.RENDERER_HANDLED_FOCUS_BLUR_EVENTS);
if (isCommonPropertiesOptimizationEnabled(facesContext))
{
CommonHtmlAttributesUtil.renderLabelPassthroughPropertiesWithoutEvents(writer,
diff --git a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlBodyRendererBase.java b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlBodyRendererBase.java
index 976704ca3..fb30e29c9 100644
--- a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlBodyRendererBase.java
+++ b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlBodyRendererBase.java
@@ -25,6 +25,7 @@
import java.io.IOException;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import jakarta.faces.application.ProjectStage;
import jakarta.faces.component.UIComponent;
@@ -50,10 +51,19 @@ public void decode(FacesContext context, T component)
{
// check for npe
super.decode(context, component);
-
+
ClientBehaviorRendererUtils.decodeClientBehaviors(context, component);
}
+ /**
+ * On top of the common events, this renderer takes care of onload/onunload itself, see encodeBegin.
+ */
+ @Override
+ protected Set<String> getRendererHandledEvents()
+ {
+ return CommonHtmlEventsUtil.RENDERER_HANDLED_BODY_EVENTS;
+ }
+
@Override
public void encodeBegin(FacesContext facesContext, T component) throws IOException
{
diff --git a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlButtonRendererBase.java b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlButtonRendererBase.java
index 01050e31b..85d7ee228 100644
--- a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlButtonRendererBase.java
+++ b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlButtonRendererBase.java
@@ -272,6 +272,9 @@ public void encodeBegin(FacesContext facesContext, T uiComponent) throws IOExcep
}
}
+ CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, uiComponent, behaviors,
+ CommonHtmlEventsUtil.RENDERER_HANDLED_COMMAND_BUTTON_EVENTS);
+
if (isDisabled(facesContext, uiComponent))
{
writer.writeAttribute(HTML.DISABLED_ATTR, HTML.DISABLED_ATTR, ComponentAttrs.DISABLED_ATTR);
diff --git a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlCheckboxRendererBase.java b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlCheckboxRendererBase.java
index c14d1a54f..2130b2e8a 100644
--- a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlCheckboxRendererBase.java
+++ b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlCheckboxRendererBase.java
@@ -428,6 +428,9 @@ else if (renderId)
facesContext, writer, uiComponent, itemId != null ? itemId : clientId, behaviors);
}
}
+ CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(
+ facesContext, writer, uiComponent, behaviors,
+ CommonHtmlEventsUtil.RENDERER_HANDLED_FIELD_EVENTS);
HtmlRendererUtils.renderHTMLAttributes(writer, uiComponent,
HTML.INPUT_PASSTHROUGH_ATTRIBUTES_WITHOUT_DISABLED_AND_EVENTS);
}
@@ -473,7 +476,10 @@ else if (renderId)
facesContext, writer, uiComponent, itemId != null ? itemId : clientId, behaviors);
}
}
- HtmlRendererUtils.renderHTMLAttributes(writer, uiComponent,
+ CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(
+ facesContext, writer, uiComponent, behaviors,
+ CommonHtmlEventsUtil.RENDERER_HANDLED_FIELD_EVENTS);
+ HtmlRendererUtils.renderHTMLAttributes(writer, uiComponent,
HTML.INPUT_PASSTHROUGH_ATTRIBUTES_WITHOUT_DISABLED_AND_STYLE_AND_EVENTS);
}
else
diff --git a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlGroupRendererBase.java b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlGroupRendererBase.java
index 44984786b..0c72279a6 100644
--- a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlGroupRendererBase.java
+++ b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlGroupRendererBase.java
@@ -96,6 +96,11 @@ private boolean needsWrapper(FacesContext context, UIComponent component)
{
return true;
}
+ if (CommonHtmlEventsUtil.hasAdditionalBehaviorEventHandlers(component, behaviors,
+ CommonHtmlEventsUtil.RENDERER_HANDLED_COMMON_EVENTS))
+ {
+ return true;
+ }
Long commonPropertiesMarked = getCommonPropertiesMarked(context, component);
if (commonPropertiesMarked != null)
{
@@ -169,21 +174,40 @@ private void writeWrapperStart(FacesContext context, UIComponent component) thro
HtmlRendererUtils.renderBehaviorizedEventHandlers(context, writer, component, behaviors);
}
}
+
+ CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(context, writer, component, behaviors,
+ CommonHtmlEventsUtil.RENDERER_HANDLED_COMMON_EVENTS);
}
else
{
+ boolean elementStarted = false;
if (commonPropertiesMarked != null && commonPropertiesMarked > 0)
{
writer.startElement(layoutElement, component);
HtmlRendererUtils.writeIdIfNecessary(writer, component, context);
CommonHtmlAttributesUtil.renderCommonPassthroughProperties(
writer, commonPropertiesMarked, component);
+ elementStarted = true;
}
else if (commonPropertiesMarked == null)
{
- HtmlRendererUtils.renderHTMLAttributesWithOptionalStartElement(writer,
+ elementStarted = HtmlRendererUtils.renderHTMLAttributesWithOptionalStartElement(writer,
component, layoutElement, HTML.COMMON_PASSTROUGH_ATTRIBUTES);
}
+
+ // behavior event attributes without matching property, e.g. oninput="...";
+ // when present, needsWrapper() has already forced the wrapper element, so it must be started here
+ if (CommonHtmlEventsUtil.hasAdditionalBehaviorEventHandlers(component, behaviors,
+ CommonHtmlEventsUtil.RENDERER_HANDLED_COMMON_EVENTS))
+ {
+ if (!elementStarted)
+ {
+ writer.startElement(layoutElement, component);
+ HtmlRendererUtils.writeIdIfNecessary(writer, component, context);
+ }
+ CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(context, writer, component, behaviors,
+ CommonHtmlEventsUtil.RENDERER_HANDLED_COMMON_EVENTS);
+ }
}
}
diff --git a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlLinkRendererBase.java b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlLinkRendererBase.java
index 9c7679799..54836c3a7 100644
--- a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlLinkRendererBase.java
+++ b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlLinkRendererBase.java
@@ -428,6 +428,9 @@ protected void renderCommandLinkStart(FacesContext facesContext, UIComponent com
HtmlRendererUtils.renderHTMLAttribute(writer, HTML.STYLE_CLASS_ATTR, HTML.STYLE_CLASS_ATTR, styleClass);
}
+ CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, component, behaviors,
+ CommonHtmlEventsUtil.RENDERER_HANDLED_LINK_EVENTS);
+
// render value as required by Faces 1.1 renderkitdocs
if(value != null)
{
@@ -794,9 +797,12 @@ protected void renderOutputLinkStart(FacesContext facesContext, UIOutput output)
HtmlRendererUtils.renderHTMLAttributes(writer, output, HTML.ANCHOR_PASSTHROUGH_ATTRIBUTES_DISABLED);
}
}
+
+ CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, output, behaviors,
+ CommonHtmlEventsUtil.RENDERER_HANDLED_LINK_EVENTS);
}
else
- {
+ {
//calculate href
String href = RendererUtils.getStringValue(facesContext, output);
@@ -902,6 +908,10 @@ else if (isAnchorInHref)
HtmlRendererUtils.renderHTMLAttributes(writer, output, HTML.ANCHOR_PASSTHROUGH_ATTRIBUTES);
}
}
+
+ CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, output, behaviors,
+ CommonHtmlEventsUtil.RENDERER_HANDLED_LINK_EVENTS);
+
writer.flush();
}
}
@@ -984,6 +994,9 @@ protected void renderOutcomeLinkStart(FacesContext facesContext, UIOutcomeTarget
}
}
+ CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, output, behaviors,
+ CommonHtmlEventsUtil.RENDERER_HANDLED_LINK_EVENTS);
+
Object value = output.getValue();
if(value != null)
@@ -1054,11 +1067,14 @@ protected void renderOutcomeLinkStart(FacesContext facesContext, UIOutcomeTarget
}
else
{
- HtmlRendererUtils.renderHTMLAttributes(writer, output,
+ HtmlRendererUtils.renderHTMLAttributes(writer, output,
HTML.ANCHOR_PASSTHROUGH_ATTRIBUTES);
}
}
+ CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, output, behaviors,
+ CommonHtmlEventsUtil.RENDERER_HANDLED_LINK_EVENTS);
+
writer.flush();
}
}
diff --git a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlOutcomeTargetButtonRendererBase.java b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlOutcomeTargetButtonRendererBase.java
index 1499f3006..1add4a6cf 100644
--- a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlOutcomeTargetButtonRendererBase.java
+++ b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlOutcomeTargetButtonRendererBase.java
@@ -20,6 +20,7 @@
import org.apache.myfaces.renderkit.html.util.HtmlRendererUtils;
import org.apache.myfaces.renderkit.html.util.CommonHtmlAttributesUtil;
+import org.apache.myfaces.renderkit.html.util.CommonHtmlEventsUtil;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@@ -177,6 +178,9 @@ public void encodeBegin(FacesContext facesContext, T component) throws IOExcepti
HTML.ALT_ATTR, HTML.ALT_ATTR);
}
+ CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, component, behaviors,
+ CommonHtmlEventsUtil.RENDERER_HANDLED_FOCUS_BLUR_EVENTS);
+
writer.flush();
}
diff --git a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlRadioRendererBase.java b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlRadioRendererBase.java
index c68188748..78f91f1c7 100644
--- a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlRadioRendererBase.java
+++ b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlRadioRendererBase.java
@@ -530,7 +530,9 @@ else if (renderId)
facesContext, writer, uiComponent, itemId != null ? itemId : clientId, behaviors);
}
}
- HtmlRendererUtils.renderHTMLAttributes(writer, uiComponent,
+ CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, uiComponent, behaviors,
+ CommonHtmlEventsUtil.RENDERER_HANDLED_FIELD_EVENTS);
+ HtmlRendererUtils.renderHTMLAttributes(writer, uiComponent,
HTML.INPUT_PASSTHROUGH_ATTRIBUTES_WITHOUT_DISABLED_AND_STYLE_AND_EVENTS);
}
else
diff --git a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlRenderer.java b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlRenderer.java
index 310c59638..0047415af 100644
--- a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlRenderer.java
+++ b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlRenderer.java
@@ -30,6 +30,7 @@
import java.io.IOException;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import org.apache.myfaces.renderkit.html.util.CommonHtmlAttributesUtil;
import org.apache.myfaces.renderkit.html.util.CommonHtmlEventsUtil;
import org.apache.myfaces.renderkit.html.util.HTML;
@@ -205,6 +206,19 @@ protected void renderEventHandlers(FacesContext facesContext, ResponseWriter wri
HtmlRendererUtils.renderBehaviorizedEventHandlers(facesContext, writer, component, behaviors);
}
}
+
+ CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, component, behaviors,
+ getRendererHandledEvents());
+ }
+
+ /**
+ * The behavior events {@link #renderEventHandlers} takes care of itself; every other supported event
+ * is rendered generically afterwards. Renderers which render further event attributes on their own
+ * (like the body renderer with onload/onunload) must override this accordingly.
+ */
+ protected Set<String> getRendererHandledEvents()
+ {
+ return CommonHtmlEventsUtil.RENDERER_HANDLED_COMMON_EVENTS;
}
/**
@@ -246,5 +260,8 @@ protected void renderFieldEventHandlers(FacesContext facesContext, ResponseWrite
facesContext, writer, component, behaviors);
}
}
+
+ CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, component, behaviors,
+ CommonHtmlEventsUtil.RENDERER_HANDLED_FIELD_EVENTS);
}
}
diff --git a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlSelectableRendererBase.java b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlSelectableRendererBase.java
index 743c90ea8..1c04e7d9a 100644
--- a/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlSelectableRendererBase.java
+++ b/impl/src/main/java/org/apache/myfaces/renderkit/html/base/HtmlSelectableRendererBase.java
@@ -163,6 +163,9 @@ private void renderSelectEventHandlers(FacesContext facesContext, ResponseWriter
facesContext, writer, uiComponent, behaviors);
}
}
+
+ CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, uiComponent, behaviors,
+ CommonHtmlEventsUtil.RENDERER_HANDLED_SELECTABLE_EVENTS);
}
}
diff --git a/impl/src/main/java/org/apache/myfaces/renderkit/html/util/CommonHtmlEventsUtil.java b/impl/src/main/java/org/apache/myfaces/renderkit/html/util/CommonHtmlEventsUtil.java
index 7b8da20f4..0f5a44ac2 100644
--- a/impl/src/main/java/org/apache/myfaces/renderkit/html/util/CommonHtmlEventsUtil.java
+++ b/impl/src/main/java/org/apache/myfaces/renderkit/html/util/CommonHtmlEventsUtil.java
@@ -20,12 +20,18 @@
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
import jakarta.faces.component.UIComponent;
import jakarta.faces.component.behavior.ClientBehavior;
import jakarta.faces.component.behavior.ClientBehaviorContext;
+import jakarta.faces.component.behavior.ClientBehaviorHolder;
import jakarta.faces.context.FacesContext;
import jakarta.faces.context.PartialResponseWriter;
import jakarta.faces.context.ResponseWriter;
@@ -46,6 +52,242 @@ public static long getMarkedEvents(UIComponent component)
return CommonHtmlEvents.getMarkedEvents(component);
}
+ /**
+ * The event names always rendered by the standard HTML renderers via the various
+ * <code>renderBehaviorizedEventHandlers</code> flavors: the generic mouse, key and click events.
+ * Building block for the per-renderer sets below, which name the events the respective renderer takes care of
+ * itself; every other event has to go through {@link #renderAdditionalBehaviorEventHandlers}.
+ */
+ private static final Set<String> COMMON_DOM_EVENTS = Set.of(
+ ClientBehaviorEvents.CLICK, ClientBehaviorEvents.DBLCLICK,
+ ClientBehaviorEvents.MOUSEDOWN, ClientBehaviorEvents.MOUSEUP, ClientBehaviorEvents.MOUSEOVER,
+ ClientBehaviorEvents.MOUSEMOVE, ClientBehaviorEvents.MOUSEOUT,
+ ClientBehaviorEvents.KEYPRESS, ClientBehaviorEvents.KEYDOWN, ClientBehaviorEvents.KEYUP);
+
+ /**
+ * Events handled by renderers of plain elements: div/span/table/form/img and alike.
+ * See {@link org.apache.myfaces.renderkit.html.base.HtmlRenderer#renderEventHandlers}.
+ */
+ public static final Set<String> RENDERER_HANDLED_COMMON_EVENTS = COMMON_DOM_EVENTS;
+
+ /**
+ * Events handled by renderers of input fields: change, focus, blur, select on top of the common ones,
+ * plus the virtual valueChange which is rendered on the change attribute.
+ * See {@link org.apache.myfaces.renderkit.html.base.HtmlRenderer#renderFieldEventHandlers}.
+ */
+ public static final Set<String> RENDERER_HANDLED_FIELD_EVENTS = merge(COMMON_DOM_EVENTS,
+ ClientBehaviorEvents.CHANGE, ClientBehaviorEvents.FOCUS, ClientBehaviorEvents.BLUR,
+ ClientBehaviorEvents.SELECT, ClientBehaviorEvents.VALUECHANGE);
+
+ /**
+ * Events handled by the listbox/menu renderers: like the field events, but without select
+ * (renderBehaviorizedFieldEventHandlersWithoutOnchangeAndOnselect).
+ */
+ public static final Set<String> RENDERER_HANDLED_SELECTABLE_EVENTS = merge(COMMON_DOM_EVENTS,
+ ClientBehaviorEvents.CHANGE, ClientBehaviorEvents.FOCUS, ClientBehaviorEvents.BLUR,
+ ClientBehaviorEvents.VALUECHANGE);
+
+ /**
+ * Events handled by the label/outcome target button renderers: only focus and blur on top of the common ones.
+ */
+ public static final Set<String> RENDERER_HANDLED_FOCUS_BLUR_EVENTS = merge(COMMON_DOM_EVENTS,
+ ClientBehaviorEvents.FOCUS, ClientBehaviorEvents.BLUR);
+
+ /**
+ * Events handled by the command button renderer: the field events plus the virtual action, which is rendered
+ * on the click attribute.
+ */
+ public static final Set<String> RENDERER_HANDLED_COMMAND_BUTTON_EVENTS = merge(COMMON_DOM_EVENTS,
+ ClientBehaviorEvents.CHANGE, ClientBehaviorEvents.FOCUS, ClientBehaviorEvents.BLUR,
+ ClientBehaviorEvents.SELECT, ClientBehaviorEvents.ACTION);
+
+ /**
+ * Events handled by the link renderers: focus and blur plus the virtual action, which is rendered
+ * on the click attribute.
+ */
+ public static final Set<String> RENDERER_HANDLED_LINK_EVENTS = merge(COMMON_DOM_EVENTS,
+ ClientBehaviorEvents.FOCUS, ClientBehaviorEvents.BLUR, ClientBehaviorEvents.ACTION);
+
+ /**
+ * Events handled by the body renderer: load and unload on top of the common ones.
+ */
+ public static final Set<String> RENDERER_HANDLED_BODY_EVENTS = merge(COMMON_DOM_EVENTS,
+ ClientBehaviorEvents.LOAD, ClientBehaviorEvents.UNLOAD);
+
+ /**
+ * Events handled by the jsf:element renderer: focus, blur, change, select, load and unload
+ * on top of the common ones.
+ */
+ public static final Set<String> RENDERER_HANDLED_PASSTHROUGH_ELEMENT_EVENTS = merge(COMMON_DOM_EVENTS,
+ ClientBehaviorEvents.FOCUS, ClientBehaviorEvents.BLUR, ClientBehaviorEvents.CHANGE,
+ ClientBehaviorEvents.SELECT, ClientBehaviorEvents.LOAD, ClientBehaviorEvents.UNLOAD);
+
+ private static Set<String> merge(Set<String> events, String... moreEvents)
+ {
+ Set<String> merged = new HashSet<>(events);
+ merged.addAll(Arrays.asList(moreEvents));
+ return Collections.unmodifiableSet(merged);
+ }
+
+ /**
+ * Renders every behavior event attribute which is <em>not</em> handled by the calling renderer itself,
+ * as required since Faces 5.0 (spec issue 1507).
+ * <p>
+ * A {@link ClientBehaviorHolder} exposes every HTML event name via {@link ClientBehaviorHolder#getEventNames()},
+ * not only those which happen to have a matching component property. So both
+ * <code><h:inputText oninput="..."/></code> and
+ * <code><h:inputText><f:ajax event="input"/></h:inputText></code> have to end up as an
+ * <code>oninput</code> attribute, chained together when both are present. Each renderer only takes care of the
+ * events for which the component has a matching property, hence this generic pass for all the others.
+ * <p>
+ * This must be invoked <em>after</em> the renderer specific event attributes have been rendered, with
+ * <code>rendererHandledEvents</code> naming exactly the events the calling renderer renders itself
+ * (one of the <code>RENDERER_HANDLED_*</code> constants of this class).
+ *
+ * @param facesContext The involved faces context.
+ * @param writer The involved response writer.
+ * @param component The component being rendered.
+ * @param clientBehaviors The client behaviors of the component, may be <code>null</code> or empty.
+ * @param rendererHandledEvents The events the calling renderer renders itself, to be skipped here.
+ */
+ public static void renderAdditionalBehaviorEventHandlers(FacesContext facesContext, ResponseWriter writer,
+ UIComponent component, Map<String, List<ClientBehavior>> clientBehaviors,
+ Set<String> rendererHandledEvents) throws IOException
+ {
+ if (!(component instanceof ClientBehaviorHolder holder))
+ {
+ return;
+ }
+
+ Set<String> additionalEventNames =
+ collectAdditionalEventNames(component, clientBehaviors, rendererHandledEvents);
+ if (additionalEventNames == null)
+ {
+ return;
+ }
+
+ Map<String, Object> attributes = component.getAttributes();
+ Collection<String> eventNames = holder.getEventNames();
+
+ for (String eventName : additionalEventNames)
+ {
+ // as required by the standard HTML RenderKit, the name after "on" must be a supported event name
+ if (eventNames == null || !eventNames.contains(eventName))
+ {
+ continue;
+ }
+
+ String attributeName = CommonHtmlEvents.BEHAVIOR_EVENT_ATTRIBUTE_PREFIX + eventName;
+ Object attributeValue = attributes.get(attributeName);
+
+ renderBehaviorizedAttribute(facesContext, writer, attributeName, component, null, eventName, null,
+ clientBehaviors, attributeName,
+ attributeValue == null ? null : attributeValue.toString());
+ }
+ }
+
+ /**
+ * @return whether {@link #renderAdditionalBehaviorEventHandlers} would render anything for the given component.
+ * Intended for renderers which have to decide upfront whether an element has to be started at all.
+ */
+ public static boolean hasAdditionalBehaviorEventHandlers(UIComponent component,
+ Map<String, List<ClientBehavior>> clientBehaviors, Set<String> rendererHandledEvents)
+ {
+ if (!(component instanceof ClientBehaviorHolder holder))
+ {
+ return false;
+ }
+
+ Set<String> additionalEventNames =
+ collectAdditionalEventNames(component, clientBehaviors, rendererHandledEvents);
+ if (additionalEventNames == null)
+ {
+ return false;
+ }
+
+ Collection<String> eventNames = holder.getEventNames();
+ if (eventNames == null)
+ {
+ return false;
+ }
+
+ for (String eventName : additionalEventNames)
+ {
+ if (eventNames.contains(eventName))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Collects the names of all behavior events of the given component which the calling renderer does not handle
+ * itself, or <code>null</code> if there are none, which is the common case that has to stay cheap.
+ */
+ @SuppressWarnings("unchecked") // the marker entry is stored under an Object-valued component attribute
+ private static Set<String> collectAdditionalEventNames(UIComponent component,
+ Map<String, List<ClientBehavior>> clientBehaviors, Set<String> rendererHandledEvents)
+ {
+ Set<String> additionalEventNames = null;
+
+ // 1) <f:ajax event="input"> and friends; any behavior whose event is not rendered by the renderer itself.
+ // Usually empty or holds a single renderer handled event, so this loop is a no-op in practice.
+ if (clientBehaviors != null && !clientBehaviors.isEmpty())
+ {
+ for (String eventName : clientBehaviors.keySet())
+ {
+ if (!rendererHandledEvents.contains(eventName))
+ {
+ additionalEventNames = add(additionalEventNames, eventName);
+ }
+ }
+ }
+
+ // 2) + 3) in a SINGLE pass over the component attribute map, which is the only part of this method that
+ // every rendered component has to pay for. That map is the plain state helper map (no property descriptor
+ // lookups, no ValueExpression evaluation) and normally holds no more than the oam.* markers. It gives us
+ // - the literal on* attributes without matching property, e.g. oninput="...", which the facelets
+ // tag handler stores as plain attributes
+ // - the EVENT_ATTRIBUTES_MARKED entry naming the ValueExpression bound ones, e.g. oninput="#{...}",
+ // as those are not part of the attribute map at all
+ for (Map.Entry<String, Object> attribute : component.getAttributes().entrySet())
+ {
+ String name = attribute.getKey();
+ if (CommonHtmlEvents.isBehaviorEventAttribute(name))
+ {
+ String eventName = CommonHtmlEvents.getEventName(name);
+ if (!rendererHandledEvents.contains(eventName))
+ {
+ additionalEventNames = add(additionalEventNames, eventName);
+ }
+ }
+ else if (CommonHtmlEvents.EVENT_ATTRIBUTES_MARKED.equals(name))
+ {
+ for (String markedName : (Set<String>) attribute.getValue())
+ {
+ String eventName = CommonHtmlEvents.getEventName(markedName);
+ if (!rendererHandledEvents.contains(eventName))
+ {
+ additionalEventNames = add(additionalEventNames, eventName);
+ }
+ }
+ }
+ }
+
+ return additionalEventNames;
+ }
+
+ /**
+ * Lazily creates the collector; a sorted set keeps the rendered attribute order deterministic.
+ */
+ private static Set<String> add(Set<String> eventNames, String eventName)
+ {
+ Set<String> result = eventNames == null ? new TreeSet<>() : eventNames;
+ result.add(eventName);
+ return result;
+ }
+
public static boolean renderBehaviorizedAttribute(
FacesContext facesContext, ResponseWriter writer,
String componentProperty, UIComponent component,
diff --git a/impl/src/main/java/org/apache/myfaces/view/facelets/component/JsfElementRenderer.java b/impl/src/main/java/org/apache/myfaces/view/facelets/component/JsfElementRenderer.java
index 5649cc34c..f929bc25d 100644
--- a/impl/src/main/java/org/apache/myfaces/view/facelets/component/JsfElementRenderer.java
+++ b/impl/src/main/java/org/apache/myfaces/view/facelets/component/JsfElementRenderer.java
@@ -118,6 +118,8 @@ public void encodeBegin(FacesContext facesContext, UIComponent component)
ClientBehaviorEvents.LOAD, behaviors, HTML.ONLOAD_ATTR);
HtmlRendererUtils.renderBehaviorizedAttribute(facesContext, writer, HTML.ONUNLOAD_ATTR, component,
ClientBehaviorEvents.UNLOAD, behaviors, HTML.ONUNLOAD_ATTR);
+ CommonHtmlEventsUtil.renderAdditionalBehaviorEventHandlers(facesContext, writer, component, behaviors,
+ CommonHtmlEventsUtil.RENDERER_HANDLED_PASSTHROUGH_ELEMENT_EVENTS);
}
diff --git a/impl/src/main/java/org/apache/myfaces/view/facelets/tag/faces/core/AjaxHandler.java b/impl/src/main/java/org/apache/myfaces/view/facelets/tag/faces/core/AjaxHandler.java
index 8958365d3..49b8ba37d 100644
--- a/impl/src/main/java/org/apache/myfaces/view/facelets/tag/faces/core/AjaxHandler.java
+++ b/impl/src/main/java/org/apache/myfaces/view/facelets/tag/faces/core/AjaxHandler.java
@@ -29,6 +29,7 @@
import jakarta.faces.component.UniqueIdVendor;
import jakarta.faces.component.behavior.AjaxBehavior;
import jakarta.faces.component.behavior.ClientBehaviorHolder;
+import jakarta.faces.component.html.HtmlEvents;
import jakarta.faces.context.FacesContext;
import jakarta.faces.event.AjaxBehaviorEvent;
import jakarta.faces.event.AjaxBehaviorListener;
@@ -357,7 +358,11 @@ else if (!cvh.getEventNames().contains(eventName))
else
{
throw new TagAttributeException(event,
- "event it is not a valid eventName defined for this component");
+ "'" + eventName + "' is not a valid eventName defined for this component."
+ + " Supported event names are " + cvh.getEventNames() + "."
+ + " In case you wish to add new ones, then you can specify them as space-separated value"
+ + " of the context-param with name "
+ + HtmlEvents.ADDITIONAL_HTML_EVENT_NAMES_PARAM_NAME);
}
}
diff --git a/impl/src/main/resources/META-INF/componentClass20.vm b/impl/src/main/resources/META-INF/componentClass20.vm
index 1911a35f2..cdabf120f 100644
--- a/impl/src/main/resources/META-INF/componentClass20.vm
+++ b/impl/src/main/resources/META-INF/componentClass20.vm
@@ -102,6 +102,15 @@ $innersource
## and csv implements
#if ($component.implements)
#if ($component.implements == "jakarta.faces.component.behavior.ClientBehaviorHolder")
+## Spec issue 1507: jsf:element mirrors the standard HTML components and must expose every HTML event name,
+## equivalent to the spec's jakarta.faces.component.html components. See api componentClass20.vm.
+#if (${utils.getClassFromFullClass($component.className)} == "JsfElement")
+ @Override
+ public java.util.Collection<String> getEventNames()
+ {
+ return jakarta.faces.component.html.HtmlEvents.getHtmlBodyElementEventNames(getFacesContext());
+ }
+#else
static private final java.util.Collection<String> CLIENT_EVENTS_LIST =
java.util.Collections.unmodifiableCollection(
java.util.Arrays.asList(
@@ -120,6 +129,7 @@ $innersource
{
return CLIENT_EVENTS_LIST;
}
+#end
@Override
public void addClientBehavior(String eventName, jakarta.faces.component.behavior.ClientBehavior behavior)
diff --git a/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlFormRendererTest.java b/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlFormRendererTest.java
index 193372706..b3e6be5b5 100644
--- a/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlFormRendererTest.java
+++ b/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlFormRendererTest.java
@@ -91,7 +91,7 @@ public void testHtmlPropertyPassTru() throws Exception
@Test
public void testHtmlPropertyPassTruNotRendered() throws Exception
{
- HtmlRenderedAttr[] attrs = HtmlCheckAttributesUtil.generateAttrsNotRenderedForReadOnly();
+ HtmlRenderedAttr[] attrs = HtmlCheckAttributesUtil.generateAttrsNotRenderedForReadOnlyButBehaviorEvents();
try {
HtmlCheckAttributesUtil.checkRenderedAttributes(
diff --git a/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlGridRendererTest.java b/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlGridRendererTest.java
index 7e632ed07..d00c639d7 100644
--- a/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlGridRendererTest.java
+++ b/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlGridRendererTest.java
@@ -125,7 +125,7 @@ public void testHtmlPropertyPassTru() throws Exception
@Test
public void testHtmlPropertyPassTruNotRendered() throws Exception
{
- HtmlRenderedAttr[] attrs = HtmlCheckAttributesUtil.generateAttrsNotRenderedForReadOnly();
+ HtmlRenderedAttr[] attrs = HtmlCheckAttributesUtil.generateAttrsNotRenderedForReadOnlyButBehaviorEvents();
HtmlCheckAttributesUtil.checkRenderedAttributes(
panelGrid, facesContext, writer, attrs);
diff --git a/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlGroupRendererTest.java b/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlGroupRendererTest.java
index e1f79f1e5..d27ea90c1 100644
--- a/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlGroupRendererTest.java
+++ b/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlGroupRendererTest.java
@@ -117,7 +117,7 @@ public void testHtmlPropertyPassTru() throws Exception
@Test
public void testHtmlPropertyPassTruNotRendered() throws Exception
{
- HtmlRenderedAttr[] attrs = HtmlCheckAttributesUtil.generateAttrsNotRenderedForReadOnly();
+ HtmlRenderedAttr[] attrs = HtmlCheckAttributesUtil.generateAttrsNotRenderedForReadOnlyButBehaviorEvents();
HtmlCheckAttributesUtil.checkRenderedAttributes(
panelGroup, facesContext, writer, attrs);
diff --git a/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlImageRendererTest.java b/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlImageRendererTest.java
index c351390f6..8216210f9 100644
--- a/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlImageRendererTest.java
+++ b/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlImageRendererTest.java
@@ -105,7 +105,7 @@ public void testHtmlPropertyPassTru() throws Exception
@Test
public void testHtmlPropertyPassTruNotRendered() throws Exception
{
- HtmlRenderedAttr[] attrs = HtmlCheckAttributesUtil.generateAttrsNotRenderedForReadOnly();
+ HtmlRenderedAttr[] attrs = HtmlCheckAttributesUtil.generateAttrsNotRenderedForReadOnlyButBehaviorEvents();
graphicImage.setId("img1");
graphicImage.setValue("http://www.apache.org/images/feather.gif");
diff --git a/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlTableRendererTest.java b/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlTableRendererTest.java
index fbc2ef9c5..13034f332 100644
--- a/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlTableRendererTest.java
+++ b/impl/src/test/java/org/apache/myfaces/renderkit/html/HtmlTableRendererTest.java
@@ -97,7 +97,7 @@ public void testHtmlPropertyPassTru() throws Exception
@Test
public void testHtmlPropertyPassTruNotRendered() throws Exception
{
- HtmlRenderedAttr[] attrs = HtmlCheckAttributesUtil.generateAttrsNotRenderedForReadOnly();
+ HtmlRenderedAttr[] attrs = HtmlCheckAttributesUtil.generateAttrsNotRenderedForReadOnlyButBehaviorEvents();
HtmlCheckAttributesUtil.checkRenderedAttributes(
dataTable, facesContext, writer, attrs);
diff --git a/impl/src/test/java/org/apache/myfaces/renderkit/html/behavior/AdditionalHtmlEventsClientBehaviorRendererTest.java b/impl/src/test/java/org/apache/myfaces/renderkit/html/behavior/AdditionalHtmlEventsClientBehaviorRendererTest.java
new file mode 100644
index 000000000..8228000d3
--- /dev/null
+++ b/impl/src/test/java/org/apache/myfaces/renderkit/html/behavior/AdditionalHtmlEventsClientBehaviorRendererTest.java
@@ -0,0 +1,352 @@
+/*
+ * 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.
+ */
+package org.apache.myfaces.renderkit.html.behavior;
+
+import jakarta.el.ValueExpression;
+import jakarta.faces.component.UIComponent;
+import jakarta.faces.component.behavior.AjaxBehavior;
+import jakarta.faces.component.behavior.ClientBehaviorHolder;
+import jakarta.faces.component.html.HtmlCommandButton;
+import jakarta.faces.component.html.HtmlEvents;
+import jakarta.faces.component.html.HtmlInputText;
+import jakarta.faces.component.html.HtmlOutputLink;
+import jakarta.faces.component.html.HtmlPanelGroup;
+import jakarta.faces.render.Renderer;
+
+import org.apache.myfaces.view.facelets.component.JsfElement;
+
+import org.apache.myfaces.test.el.MockValueExpression;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Spec issue 1507: every HTML event is a supported behavior event of the standard HTML components, even when the
+ * component has no matching property. So both <code>oninput="..."</code> and
+ * <code><f:ajax event="input"/></code> have to end up as an <code>oninput</code> attribute.
+ * <p>
+ * The inherited test cases cover exactly that for a set of events which have no property on any standard component,
+ * this class adds the cases which are specific to this feature.
+ */
+public class AdditionalHtmlEventsClientBehaviorRendererTest extends AbstractClientBehaviorTestCase
+{
+ private HtmlRenderedClientEventAttr[] attrs = null;
+
+ @Override
+ @BeforeEach
+ public void setUp() throws Exception
+ {
+ super.setUp();
+ attrs = new HtmlRenderedClientEventAttr[]
+ {
+ new HtmlRenderedClientEventAttr("oninput", "input"),
+ new HtmlRenderedClientEventAttr("onbeforeinput", "beforeinput"),
+ new HtmlRenderedClientEventAttr("oncontextmenu", "contextmenu"),
+ new HtmlRenderedClientEventAttr("oncopy", "copy"),
+ new HtmlRenderedClientEventAttr("onpaste", "paste"),
+ new HtmlRenderedClientEventAttr("ondrop", "drop"),
+ new HtmlRenderedClientEventAttr("onwheel", "wheel"),
+ new HtmlRenderedClientEventAttr("ontoggle", "toggle"),
+ new HtmlRenderedClientEventAttr("oninvalid", "invalid")
+ };
+ }
+
+ @Override
+ @AfterEach
+ public void tearDown() throws Exception
+ {
+ super.tearDown();
+ attrs = null;
+ }
+
+ @Override
+ protected UIComponent createComponentToTest()
+ {
+ return new HtmlInputText();
+ }
+
+ @Override
+ protected HtmlRenderedClientEventAttr[] getClientBehaviorHtmlRenderedAttributes()
+ {
+ return attrs;
+ }
+
+ /**
+ * A plain on* attribute without a matching property must be rendered as a pass through attribute.
+ */
+ @Test
+ public void testPlainAttributeWithoutBehaviorIsRendered() throws Exception
+ {
+ HtmlInputText input = new HtmlInputText();
+ input.getAttributes().put("oninput", "alert('input')");
+
+ input.encodeAll(facesContext);
+
+ Assertions.assertTrue(outputWriter.toString().contains(" oninput=\"alert('input')\""),
+ outputWriter.toString());
+ }
+
+ /**
+ * An on* attribute bound to a ValueExpression is neither a property nor part of the component attribute map key
+ * set, so it needs the {@code EVENT_ATTRIBUTES_MARKED} marker to be found at render time.
+ */
+ @Test
+ public void testValueExpressionAttributeWithoutBehaviorIsRendered() throws Exception
+ {
+ ValueExpression expression = new MockValueExpression("#{requestScope.oninputScript}", String.class);
+ expression.setValue(facesContext.getELContext(), "alert(1)");
+
+ HtmlInputText input = new HtmlInputText();
+ input.setValueExpression("oninput", expression);
+
+ input.encodeAll(facesContext);
+
+ Assertions.assertTrue(outputWriter.toString().contains(" oninput=\"alert(1)\""), outputWriter.toString());
+ }
+
+ /**
+ * An attribute which merely starts with "on" but is no known event must not be rendered, as required by the
+ * standard HTML RenderKit: the substring after "on" has to be contained in getEventNames().
+ */
+ @Test
+ public void testUnknownEventAttributeIsNotRendered() throws Exception
+ {
+ HtmlInputText input = new HtmlInputText();
+ input.getAttributes().put("onsomethingweird", "alert('nope')");
+
+ input.encodeAll(facesContext);
+
+ Assertions.assertFalse(outputWriter.toString().contains("onsomethingweird"), outputWriter.toString());
+ }
+
+ /**
+ * The very use case of the spec issue: an f:ajax on an event which no component declares as a property.
+ */
+ @Test
+ public void testAjaxBehaviorOnAdditionalEventIsRendered() throws Exception
+ {
+ HtmlInputText input = new HtmlInputText();
+ input.addClientBehavior("input", new AjaxBehavior());
+
+ input.encodeAll(facesContext);
+
+ String output = outputWriter.toString();
+ Assertions.assertTrue(output.contains(" oninput=\""), output);
+ Assertions.assertTrue(output.contains("myfaces.ab("), output);
+ }
+
+ /**
+ * The renderer specific attributes must keep working exactly as before, next to the additional ones.
+ */
+ @Test
+ public void testRendererSpecificAndAdditionalEventsAreCombined() throws Exception
+ {
+ HtmlInputText input = new HtmlInputText();
+ input.setOnclick("alert('click')");
+ input.getAttributes().put("oninput", "alert('input')");
+
+ input.encodeAll(facesContext);
+
+ String output = outputWriter.toString();
+ Assertions.assertTrue(output.contains(" onclick=\"alert('click')\""), output);
+ Assertions.assertTrue(output.contains(" oninput=\"alert('input')\""), output);
+ }
+
+ /**
+ * Not only inputs; the feature applies to every standard HTML component, which are spread over quite a few
+ * different renderers and rendering paths.
+ */
+ @Test
+ public void testAdditionalEventsOnOtherComponents() throws Exception
+ {
+ assertAdditionalEventRendered(new HtmlCommandButton());
+ assertAdditionalEventRendered(new HtmlOutputLink());
+
+ HtmlPanelGroup group = new HtmlPanelGroup();
+ group.setLayout("block");
+ assertAdditionalEventRendered(group);
+ }
+
+ /**
+ * An event which IS rendered by some renderers (here: focus by the field renderers) but NOT by the renderer at
+ * hand (h:panelGroup only renders the mouse/key/click events itself) must be rendered generically as well.
+ */
+ @Test
+ public void testFieldEventOnNonFieldComponent() throws Exception
+ {
+ HtmlPanelGroup group = new HtmlPanelGroup();
+ group.setLayout("block");
+ group.getAttributes().put("onfocus", "doFocus()");
+ ((ClientBehaviorHolder) group).addClientBehavior("blur", new AjaxBehavior());
+
+ group.encodeAll(facesContext);
+ String output = outputWriter.toString();
+
+ Assertions.assertTrue(output.contains(" onfocus=\"doFocus()\""), output);
+ Assertions.assertTrue(output.contains(" onblur=\""), output);
+ }
+
+ /**
+ * The opposite: an event which the renderer at hand DOES render itself must not be rendered a second time by
+ * the generic pass.
+ */
+ @Test
+ public void testRendererHandledEventIsNotRenderedTwice() throws Exception
+ {
+ HtmlInputText input = new HtmlInputText();
+ input.setOnclick("doClick()");
+ input.addClientBehavior("focus", new AjaxBehavior());
+
+ input.encodeAll(facesContext);
+ String output = outputWriter.toString();
+
+ Assertions.assertEquals(output.indexOf(" onclick=\""), output.lastIndexOf(" onclick=\""), output);
+ Assertions.assertEquals(output.indexOf(" onfocus=\""), output.lastIndexOf(" onfocus=\""), output);
+ }
+
+ private void assertAdditionalEventRendered(UIComponent component) throws Exception
+ {
+ component.getAttributes().put("oninput", "alert('input')");
+ ((ClientBehaviorHolder) component).addClientBehavior("wheel", new AjaxBehavior());
+
+ outputWriter.reset();
+ component.encodeAll(facesContext);
+ String output = outputWriter.toString();
+
+ Assertions.assertTrue(output.contains(" oninput=\"alert('input')\""),
+ component.getClass().getSimpleName() + ": " + output);
+ Assertions.assertTrue(output.contains(" onwheel=\""),
+ component.getClass().getSimpleName() + ": " + output);
+ }
+
+ /**
+ * jsf:element mirrors the standard HTML components and must expose and render every HTML event as well.
+ */
+ @Test
+ public void testAdditionalEventsOnJsfElement() throws Exception
+ {
+ JsfElement element = new JsfElement();
+ element.getPassThroughAttributes().put(Renderer.PASSTHROUGH_RENDERER_LOCALNAME_KEY, "div");
+ element.getAttributes().put("oninput", "doInput()");
+ element.addClientBehavior("wheel", new AjaxBehavior());
+
+ Assertions.assertTrue(element.getEventNames().contains("input"));
+ Assertions.assertTrue(element.getEventNames().contains("scrollend"));
+
+ element.encodeAll(facesContext);
+ String output = outputWriter.toString();
+
+ Assertions.assertTrue(output.contains(" oninput=\"doInput()\""), output);
+ Assertions.assertTrue(output.contains(" onwheel=\""), output);
+ }
+
+ /**
+ * The event names must reflect the component type, as the spec assigns a different set per component category.
+ */
+ @Test
+ public void testEventNamesPerComponentCategory()
+ {
+ // EditableValueHolder
+ Assertions.assertTrue(new HtmlInputText().getEventNames().contains("valueChange"));
+ Assertions.assertFalse(new HtmlInputText().getEventNames().contains("action"));
+
+ // ActionSource
+ Assertions.assertTrue(new HtmlCommandButton().getEventNames().contains("action"));
+ Assertions.assertFalse(new HtmlCommandButton().getEventNames().contains("valueChange"));
+
+ // neither of both
+ Assertions.assertFalse(new HtmlPanelGroup().getEventNames().contains("action"));
+ Assertions.assertFalse(new HtmlPanelGroup().getEventNames().contains("valueChange"));
+
+ // all of them expose the plain HTML events, including the window level ones
+ for (UIComponent component : new UIComponent[]
+ { new HtmlInputText(), new HtmlCommandButton(), new HtmlPanelGroup() })
+ {
+ ClientBehaviorHolder holder = (ClientBehaviorHolder) component;
+ Assertions.assertTrue(holder.getEventNames().contains("input"));
+ Assertions.assertTrue(holder.getEventNames().contains("click"));
+ Assertions.assertTrue(holder.getEventNames().contains("scrollend"));
+ }
+ }
+
+ /**
+ * The default event names as mandated by the spec.
+ */
+ @Test
+ public void testDefaultEventNames()
+ {
+ Assertions.assertEquals("valueChange", new HtmlInputText().getDefaultEventName());
+ Assertions.assertEquals("action", new HtmlCommandButton().getDefaultEventName());
+ Assertions.assertEquals("click", new HtmlOutputLink().getDefaultEventName());
+ Assertions.assertNull(new HtmlPanelGroup().getDefaultEventName());
+ }
+
+ /**
+ * Unknown event names can be added via context-param, and are then usable like any built in one.
+ */
+ @Test
+ public void testAdditionalEventNamesContextParam() throws Exception
+ {
+ servletContext.addInitParameter(HtmlEvents.ADDITIONAL_HTML_EVENT_NAMES_PARAM_NAME, " foo\t bar\nfoo ");
+
+ Assertions.assertTrue(HtmlEvents.getAdditionalHtmlEventNames(facesContext).contains("foo"));
+ Assertions.assertTrue(HtmlEvents.getAdditionalHtmlEventNames(facesContext).contains("bar"));
+
+ HtmlInputText input = new HtmlInputText();
+ Assertions.assertTrue(input.getEventNames().contains("foo"));
+
+ input.getAttributes().put("onfoo", "alert('foo')");
+ input.addClientBehavior("bar", new AjaxBehavior());
+ input.encodeAll(facesContext);
+
+ String output = outputWriter.toString();
+ Assertions.assertTrue(output.contains(" onfoo=\"alert('foo')\""), output);
+ Assertions.assertTrue(output.contains(" onbar=\""), output);
+ }
+
+ /**
+ * Without the context-param there must be no stray empty event name, and the collections must be sorted,
+ * distinct and layered as documented.
+ */
+ @Test
+ public void testEventNameCollections()
+ {
+ Assertions.assertTrue(HtmlEvents.getAdditionalHtmlEventNames(facesContext).isEmpty());
+
+ Assertions.assertTrue(HtmlEvents.getHtmlBodyElementEventNames(facesContext)
+ .containsAll(HtmlEvents.getHtmlDocumentElementEventNames(facesContext)));
+ Assertions.assertTrue(HtmlEvents.getFacesActionSourceEventNames(facesContext)
+ .containsAll(HtmlEvents.getHtmlBodyElementEventNames(facesContext)));
+ Assertions.assertTrue(HtmlEvents.getFacesEditableValueHolderEventNames(facesContext)
+ .containsAll(HtmlEvents.getHtmlBodyElementEventNames(facesContext)));
+
+ // the window level events are exclusive to the body element event names
+ Assertions.assertFalse(HtmlEvents.getHtmlDocumentElementEventNames(facesContext).contains("load"));
+ Assertions.assertTrue(HtmlEvents.getHtmlBodyElementEventNames(facesContext).contains("load"));
+
+ String previous = null;
+ for (String eventName : HtmlEvents.getFacesEditableValueHolderEventNames(facesContext))
+ {
+ Assertions.assertTrue(previous == null || previous.compareTo(eventName) < 0,
+ "not sorted and distinct at " + eventName);
+ previous = eventName;
+ }
+ }
+}
diff --git a/impl/src/test/java/org/apache/myfaces/renderkit/html/behavior/HtmlBodyClientBehaviorRendererTest.java b/impl/src/test/java/org/apache/myfaces/renderkit/html/behavior/HtmlBodyClientBehaviorRendererTest.java
index 14f4c8979..1b58f6a56 100644
--- a/impl/src/test/java/org/apache/myfaces/renderkit/html/behavior/HtmlBodyClientBehaviorRendererTest.java
+++ b/impl/src/test/java/org/apache/myfaces/renderkit/html/behavior/HtmlBodyClientBehaviorRendererTest.java
@@ -22,6 +22,7 @@
import jakarta.faces.component.behavior.AjaxBehavior;
import jakarta.faces.component.behavior.ClientBehaviorHolder;
import jakarta.faces.component.html.HtmlBody;
+import jakarta.faces.component.html.HtmlEvents;
import org.apache.myfaces.renderkit.ClientBehaviorEvents;
import org.apache.myfaces.renderkit.html.util.HTML;
@@ -43,13 +44,11 @@ public class HtmlBodyClientBehaviorRendererTest extends AbstractClientBehaviorTe
public void setUp() throws Exception
{
super.setUp();
- attrs = (HtmlRenderedClientEventAttr[])
- org.apache.myfaces.util.lang.ArrayUtils.concat(
- HtmlClientEventAttributesUtil.generateClientBehaviorEventAttrs(),
- new HtmlRenderedClientEventAttr[]{
- new HtmlRenderedClientEventAttr(HTML.ONLOAD_ATTR, ClientBehaviorEvents.LOAD),
- new HtmlRenderedClientEventAttr(HTML.ONUNLOAD_ATTR, ClientBehaviorEvents.UNLOAD)
- });
+ // NOTE: "load" and "unload" are deliberately absent here. Since Faces 5.0 (spec issue 1507) a component
+ // representing the HTML <body> element exposes HtmlEvents#getHtmlDocumentElementEventNames(), which excludes
+ // the events that the <body> element forwards to the Window object instead of firing on the element itself.
+ // See testWindowEventsAreNoBehaviorEvents() and testWindowEventsCanBeReAddedViaContextParam() below.
+ attrs = HtmlClientEventAttributesUtil.generateClientBehaviorEventAttrs();
}
@Override
@@ -100,4 +99,61 @@ public void testClientBehaviorHolderRendersIdAndName()
}
}
+ /**
+ * Spec issue 1507: the events which the HTML <body> element forwards to the Window object must not be
+ * exposed as behavior events of the component, so <f:ajax event="load"/> is no longer supported on
+ * <h:body>. The plain onload attribute is unaffected, it is still a property of the component.
+ */
+ @Test
+ public void testWindowEventsAreNoBehaviorEvents()
+ {
+ HtmlBody body = new HtmlBody();
+
+ Assertions.assertFalse(body.getEventNames().contains(ClientBehaviorEvents.LOAD));
+ Assertions.assertFalse(body.getEventNames().contains(ClientBehaviorEvents.UNLOAD));
+ Assertions.assertTrue(body.getEventNames().contains(ClientBehaviorEvents.CLICK));
+
+ // still supported as a plain attribute
+ body.setOnload("alert('load')");
+ try
+ {
+ body.encodeAll(facesContext);
+ Assertions.assertTrue(outputWriter.toString().contains(HTML.ONLOAD_ATTR + "=\"alert('load')\""));
+ }
+ catch (Exception e)
+ {
+ Assertions.fail(e.getMessage());
+ }
+ }
+
+ /**
+ * Spec issue 1507: whatever HtmlEvents does not know about can be added via a context-param, which is also the
+ * migration path for applications relying on <f:ajax event="load"/> on <h:body>.
+ */
+ @Test
+ public void testWindowEventsCanBeReAddedViaContextParam()
+ {
+ servletContext.addInitParameter(HtmlEvents.ADDITIONAL_HTML_EVENT_NAMES_PARAM_NAME, "load unload");
+
+ HtmlBody body = new HtmlBody();
+
+ Assertions.assertTrue(body.getEventNames().contains(ClientBehaviorEvents.LOAD));
+ Assertions.assertTrue(body.getEventNames().contains(ClientBehaviorEvents.UNLOAD));
+
+ body.addClientBehavior(ClientBehaviorEvents.LOAD, new AjaxBehavior());
+ try
+ {
+ body.encodeAll(facesContext);
+ String output = outputWriter.toString();
+ Assertions.assertTrue(output.contains(HTML.ONLOAD_ATTR + "=\""), output);
+ // the body renderer renders onload itself; the generic pass must not render it a second time
+ Assertions.assertEquals(output.indexOf(HTML.ONLOAD_ATTR + "=\""),
+ output.lastIndexOf(HTML.ONLOAD_ATTR + "=\""), output);
+ }
+ catch (Exception e)
+ {
+ Assertions.fail(e.getMessage());
+ }
+ }
+
}
diff --git a/impl/src/test/java/org/apache/myfaces/test/utils/HtmlCheckAttributesUtil.java b/impl/src/test/java/org/apache/myfaces/test/utils/HtmlCheckAttributesUtil.java
index a91b339f8..2f3090d00 100644
--- a/impl/src/test/java/org/apache/myfaces/test/utils/HtmlCheckAttributesUtil.java
+++ b/impl/src/test/java/org/apache/myfaces/test/utils/HtmlCheckAttributesUtil.java
@@ -257,22 +257,44 @@ public static HtmlRenderedAttr[] generateBasicAttrs(boolean isOnSelectNeeded)
return attrs;
}
- public static HtmlRenderedAttr[] generateAttrsNotRenderedForReadOnly()
+ public static HtmlRenderedAttr[] generateAttrsNotRenderedForReadOnly()
{
HtmlRenderedAttr[] attrs = {
//_AccesskeyProperty
new HtmlRenderedAttr("accesskey", 0),
//_FocusBlurProperties
- new HtmlRenderedAttr("onfocus", 0),
+ new HtmlRenderedAttr("onfocus", 0),
new HtmlRenderedAttr("onblur", 0),
//_ChangeSelectProperties
- new HtmlRenderedAttr("onchange", 0),
+ new HtmlRenderedAttr("onchange", 0),
new HtmlRenderedAttr("onselect", 0),
//_TabindexProperty
new HtmlRenderedAttr("tabindex", 0)
};
return attrs;
}
+
+ /**
+ * Since Faces 5.0 (spec issue 1507) behavior event attributes pass through generically on every
+ * ClientBehaviorHolder component, even without a matching component property. So on such components only the
+ * non-event attributes stay unrendered.
+ */
+ public static HtmlRenderedAttr[] generateAttrsNotRenderedForReadOnlyButBehaviorEvents()
+ {
+ HtmlRenderedAttr[] attrs = {
+ //_AccesskeyProperty
+ new HtmlRenderedAttr("accesskey", 0),
+ //_FocusBlurProperties
+ new HtmlRenderedAttr("onfocus"),
+ new HtmlRenderedAttr("onblur"),
+ //_ChangeSelectProperties
+ new HtmlRenderedAttr("onchange"),
+ new HtmlRenderedAttr("onselect"),
+ //_TabindexProperty
+ new HtmlRenderedAttr("tabindex", 0)
+ };
+ return attrs;
+ }
public static HtmlRenderedAttr[] generateBasicReadOnlyAttrs() {
HtmlRenderedAttr[] attrs = {
diff --git a/impl/src/test/java/org/apache/myfaces/view/facelets/tag/faces/html/AdditionalHtmlEventsTestCase.java b/impl/src/test/java/org/apache/myfaces/view/facelets/tag/faces/html/AdditionalHtmlEventsTestCase.java
new file mode 100644
index 000000000..bc82588ea
--- /dev/null
+++ b/impl/src/test/java/org/apache/myfaces/view/facelets/tag/faces/html/AdditionalHtmlEventsTestCase.java
@@ -0,0 +1,108 @@
+/*
+ * 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.
+ */
+package org.apache.myfaces.view.facelets.tag.faces.html;
+
+import java.io.StringWriter;
+
+import jakarta.faces.component.UIComponent;
+import jakarta.faces.component.UIInput;
+import jakarta.faces.component.UIOutput;
+import jakarta.faces.component.UIViewRoot;
+import jakarta.faces.component.behavior.AjaxBehavior;
+import jakarta.faces.component.behavior.ClientBehaviorHolder;
+import jakarta.faces.component.html.HtmlInputText;
+
+import org.apache.myfaces.renderkit.html.HtmlAjaxBehaviorRenderer;
+import org.apache.myfaces.renderkit.html.HtmlTextRenderer;
+import org.apache.myfaces.test.mock.MockResponseWriter;
+import org.apache.myfaces.view.facelets.AbstractFaceletTestCase;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Spec issue 1507, end to end: an on* attribute without a matching component property has to survive the facelet
+ * tag handling and end up in the rendered markup, on its own as well as chained with an f:ajax on the same event.
+ */
+public class AdditionalHtmlEventsTestCase extends AbstractFaceletTestCase
+{
+
+ @Override
+ protected void setupComponents() throws Exception
+ {
+ application.addComponent(UIViewRoot.COMPONENT_TYPE, UIViewRoot.class.getName());
+ application.addComponent(UIOutput.COMPONENT_TYPE, UIOutput.class.getName());
+ application.addComponent(HtmlInputText.COMPONENT_TYPE, HtmlInputText.class.getName());
+ application.addBehavior(AjaxBehavior.BEHAVIOR_ID, AjaxBehavior.class.getName());
+ }
+
+ @Override
+ protected void setupConvertersAndValidators() throws Exception
+ {
+ }
+
+ @Override
+ protected void setupRenderers() throws Exception
+ {
+ renderKit.addRenderer(UIInput.COMPONENT_FAMILY, "jakarta.faces.Text", new HtmlTextRenderer());
+ renderKit.addRenderer(UIOutput.COMPONENT_FAMILY, "jakarta.faces.Text", new HtmlTextRenderer());
+ renderKit.addClientBehaviorRenderer(AjaxBehavior.BEHAVIOR_ID, new HtmlAjaxBehaviorRenderer());
+ }
+
+ @Test
+ public void testAdditionalHtmlEvents() throws Exception
+ {
+ externalContext.getRequestMap().put("inputScript", "doEl()");
+
+ UIViewRoot root = facesContext.getViewRoot();
+ vdl.buildView(facesContext, root, "additionalHtmlEvents.xml");
+
+ // a literal on* attribute without matching property
+ String plain = encode(root, "plain");
+ Assertions.assertTrue(plain.contains("oninput=\"doPlain()\""), plain);
+
+ // the same, but bound to a ValueExpression
+ String el = encode(root, "el");
+ Assertions.assertTrue(el.contains("oninput=\"doEl()\""), el);
+
+ // <f:ajax event="input"> on a component which has no oninput property
+ Assertions.assertEquals("[input]",
+ ((ClientBehaviorHolder) root.findComponent("ajax")).getClientBehaviors().keySet().toString());
+ String ajax = encode(root, "ajax");
+ Assertions.assertTrue(ajax.contains("oninput=\""), ajax);
+ Assertions.assertTrue(ajax.contains("myfaces.ab("), ajax);
+
+ // both of them on the same component must be chained
+ String both = encode(root, "both");
+ Assertions.assertTrue(both.contains("faces.util.chain("), both);
+ Assertions.assertTrue(both.contains("doBoth()"), both);
+ Assertions.assertTrue(both.contains("myfaces.ab("), both);
+ }
+
+ private String encode(UIViewRoot root, String id) throws Exception
+ {
+ UIComponent component = root.findComponent(id);
+ Assertions.assertNotNull(component, "component " + id + " not found");
+
+ StringWriter sw = new StringWriter();
+ facesContext.setResponseWriter(new MockResponseWriter(sw));
+ component.encodeAll(facesContext);
+ sw.flush();
+ return sw.toString();
+ }
+}
diff --git a/impl/src/test/resources/org/apache/myfaces/view/facelets/tag/faces/html/additionalHtmlEvents.xml b/impl/src/test/resources/org/apache/myfaces/view/facelets/tag/faces/html/additionalHtmlEvents.xml
new file mode 100644
index 000000000..2d9e64118
--- /dev/null
+++ b/impl/src/test/resources/org/apache/myfaces/view/facelets/tag/faces/html/additionalHtmlEvents.xml
@@ -0,0 +1,36 @@
+<!--
+ Licensed 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.
+-->
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:ui="http://java.sun.com/jsf/facelets"
+ xmlns:h="http://java.sun.com/jsf/html"
+ xmlns:f="http://java.sun.com/jsf/core">
+<body>
+
+<ui:composition>
+ <h:inputText id="plain" oninput="doPlain()" />
+
+ <h:inputText id="el" oninput="#{requestScope.inputScript}" />
+
+ <h:inputText id="ajax">
+ <f:ajax event="input" />
+ </h:inputText>
+
+ <h:inputText id="both" oninput="doBoth()">
+ <f:ajax event="input" />
+ </h:inputText>
+</ui:composition>
+
+</body>
+</html>