Skip to content

Commit

Permalink
Add component-level API for customizing DOM event listeners (#4445)
Browse files Browse the repository at this point in the history
* Add component-level API for customizing DOM event listeners

Each component-level event-listener is now mapped to its corresponding
DOM event listener registration.

An overload of ComponentEventBus::addListener is added which takes a
consumer for customizing the DOM event listener. The method still
returns a component-level registration.

This allows creating Component-level API for eg. overriding the debounce
or filter settings defined in the event's @DomEvent annotation.
  • Loading branch information
pekam authored and gilberto-torrezan committed Aug 2, 2018
1 parent 94f1a8e commit f9231f7
Show file tree
Hide file tree
Showing 5 changed files with 295 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;

import com.vaadin.flow.dom.DebouncePhase;
import com.vaadin.flow.dom.DisabledUpdateMode;
Expand Down Expand Up @@ -51,14 +53,23 @@
*/
public class ComponentEventBus implements Serializable {

private static class ComponentEventData implements Serializable {
private Registration domEventRemover = null;
private List<ComponentEventListener<? extends ComponentEvent<?>>> listeners = new ArrayList<>(
1);
/**
* Pairs a component-level listener for its DOM listener registration, if
* the event-type is annotated with {@link DomEvent}.
*/
private static class ListenerWrapper<T extends ComponentEvent<?>>
implements Serializable {
private ComponentEventListener<T> listener;
private DomListenerRegistration domRegistration;

public ListenerWrapper(ComponentEventListener<T> listener) {
this.listener = listener;
}

}

// Package private to enable testing only
HashMap<Class<? extends ComponentEvent<?>>, ComponentEventData> componentEventData = new HashMap<>();
HashMap<Class<? extends ComponentEvent<?>>, ArrayList<ListenerWrapper<?>>> componentEventData = new HashMap<>();

private Component component;

Expand Down Expand Up @@ -86,13 +97,64 @@ public ComponentEventBus(Component component) {
*/
public <T extends ComponentEvent<?>> Registration addListener(
Class<T> eventType, ComponentEventListener<T> listener) {
addDomTriggerIfNeeded(eventType);
return addListenerInternal(eventType, listener, null);
}

/**
* Adds a listener for the given event type, and customizes the
* corresponding DOM event listener with the given consumer. This allows
* overriding eg. the debounce settings defined in the {@link DomEvent}
* annotation.
* <p>
* Note that customizing the DOM event listener works only for event types
* which are annotated with {@link DomEvent}. Use
* {@link #addListener(Class, ComponentEventListener)} for other listeners,
* or if you don't need to customize the DOM listener.
*
* @param <T>
* the event type
* @param eventType
* the event type for which to call the listener, must be
* annotated with {@link DomEvent}
* @param listener
* the listener to call when the event occurs
* @param domListenerConsumer
* a consumer to customize the behavior of the DOM event
* listener, not {@code null}
* @return an object which can be used to remove the event listener
* @throws IllegalArgumentException
* if the event type is not annotated with {@link DomEvent}
*/
public <T extends ComponentEvent<?>> Registration addListener(
Class<T> eventType, ComponentEventListener<T> listener,
Consumer<DomListenerRegistration> domListenerConsumer) {
Objects.requireNonNull(domListenerConsumer,
"DOM listener consumer cannot be null");
return addListenerInternal(eventType, listener, domListenerConsumer);
}

private <T extends ComponentEvent<?>> Registration addListenerInternal(
Class<T> eventType, ComponentEventListener<T> listener,
Consumer<DomListenerRegistration> domListenerConsumer) {

ListenerWrapper<T> wrapper = new ListenerWrapper<>(listener);

boolean isDomEvent = addDomTriggerIfNeeded(eventType, wrapper);

if (domListenerConsumer != null) {
if (!isDomEvent) {
throw new IllegalArgumentException(String.format(
"DomListenerConsumer can be used only for DOM events. The given event type %s is not annotated with %s.",
eventType.getSimpleName(),
DomEvent.class.getSimpleName()));
}
domListenerConsumer.accept(wrapper.domRegistration);
}

List<ComponentEventListener<? extends ComponentEvent<?>>> listeners = componentEventData
.computeIfAbsent(eventType,
t -> new ComponentEventData()).listeners;
listeners.add(listener);
return () -> removeListener(eventType, listener);
componentEventData.computeIfAbsent(eventType, t -> new ArrayList<>())
.add(wrapper);

return () -> removeListener(eventType, wrapper);
}

/**
Expand Down Expand Up @@ -124,53 +186,62 @@ public void fireEvent(ComponentEvent event) {
if (!hasListener(eventType)) {
return;
}
List<ComponentEventListener> listeners = (List) componentEventData
.get(event.getClass()).listeners;
for (ComponentEventListener l : new ArrayList<>(listeners)) {
event.setUnregisterListenerCommand(() -> {
removeListener(eventType, l);
});
l.onComponentEvent(event);
event.setUnregisterListenerCommand(null);

// Copy the list to avoid ConcurrentModificationException
for (ListenerWrapper wrapper : new ArrayList<>(
componentEventData.get(event.getClass()))) {
fireEventForListener(event, wrapper);
}
}

@SuppressWarnings("unchecked")
private <T extends ComponentEvent<?>> void fireEventForListener(T event,
ListenerWrapper<T> wrapper) {
Class<T> eventType = (Class<T>) event.getClass();
event.setUnregisterListenerCommand(() -> {
removeListener(eventType, wrapper);
});
wrapper.listener.onComponentEvent(event);
event.setUnregisterListenerCommand(null);
}

/**
* Adds a DOM listener for the given component event if it is mapped to a
* DOM event and the event is not yet registered.
* DOM event.
*
* @param eventType
* the type of event
* @param wrapper
* the listener that is being registered
* @return {@code true} if a DOM-trigger was added (the event is annotated
* with {@link DomEvent}), {@code false} otherwise.
*/
private void addDomTriggerIfNeeded(
Class<? extends ComponentEvent<?>> eventType) {
boolean alreadyRegistered = hasListener(eventType);
if (alreadyRegistered) {
return;
}

AnnotationReader
private <T extends ComponentEvent<?>> boolean addDomTriggerIfNeeded(
Class<T> eventType, ListenerWrapper<T> wrapper) {
return AnnotationReader
.getAnnotationFor(eventType,
com.vaadin.flow.component.DomEvent.class)
.ifPresent(annotation -> addDomTrigger(eventType, annotation));
.map(annotation -> {
addDomTrigger(eventType, annotation, wrapper);
return true;
}).orElse(false);
}

/**
* Adds a DOM listener of the given type for the given component event and
* annotation.
* <p>
* Assumes that no listener exists.
*
* @param eventType
* the component event type
* @param the
* @param annotation
* annotation with event configuration
* @param wrapper
* the listener that is being registered
*/
private void addDomTrigger(Class<? extends ComponentEvent<?>> eventType,
com.vaadin.flow.component.DomEvent annotation) {
private <T extends ComponentEvent<?>> void addDomTrigger(Class<T> eventType,
com.vaadin.flow.component.DomEvent annotation,
ListenerWrapper<T> wrapper) {
assert eventType != null;
assert !componentEventData.containsKey(eventType)
|| componentEventData.get(eventType).domEventRemover == null;
assert annotation != null;

String domEventType = annotation.value();
Expand All @@ -188,11 +259,15 @@ private void addDomTrigger(Class<? extends ComponentEvent<?>> eventType,

// Register DOM event handler
DomListenerRegistration registration = element.addEventListener(
domEventType, event -> handleDomEvent(eventType, event));
domEventType,
event -> handleDomEvent(eventType, event, wrapper));

wrapper.domRegistration = registration;

registration.setDisabledUpdateMode(mode);

LinkedHashMap<String, Class<?>> eventDataExpressions =
ComponentEventBusUtil.getEventDataExpressions(eventType);
LinkedHashMap<String, Class<?>> eventDataExpressions = ComponentEventBusUtil
.getEventDataExpressions(eventType);
eventDataExpressions.keySet().forEach(registration::addEventData);

if (!"".equals(filter)) {
Expand All @@ -211,9 +286,6 @@ private void addDomTrigger(Class<? extends ComponentEvent<?>> eventType,

registration.debounce(debounceTimeout, phases[0], rest);
}

componentEventData.computeIfAbsent(eventType,
t -> new ComponentEventData()).domEventRemover = registration;
}

/**
Expand All @@ -231,14 +303,14 @@ private List<Object> createEventDataObjects(DomEvent domEvent,
Class<? extends ComponentEvent<?>> eventType) {
List<Object> eventDataObjects = new ArrayList<>();

LinkedHashMap<String, Class<?>> expressions =
ComponentEventBusUtil.getEventDataExpressions(eventType);
LinkedHashMap<String, Class<?>> expressions = ComponentEventBusUtil
.getEventDataExpressions(eventType);
expressions.forEach((expression, type) -> {
JsonValue jsonValue = domEvent.getEventData().get(expression);
if (jsonValue == null) {
jsonValue = Json.createNull();
}
Object value = JsonCodec.decodeAs(jsonValue,type);
Object value = JsonCodec.decodeAs(jsonValue, type);
eventDataObjects.add(value);
});
return eventDataObjects;
Expand All @@ -252,62 +324,33 @@ private List<Object> createEventDataObjects(DomEvent domEvent,
*
* @param eventType
* the component event type
* @param listener
* @param wrapper
* the listener to remove
*/
private <T extends ComponentEvent<?>> void removeListener(
Class<T> eventType, ComponentEventListener<T> listener) {
Class<T> eventType, ListenerWrapper<T> wrapper) {
assert eventType != null;
assert listener != null;
assert wrapper != null;
assert wrapper.listener != null;

ComponentEventData eventData = componentEventData.get(eventType);
ArrayList<ListenerWrapper<?>> eventData = componentEventData
.get(eventType);
if (eventData == null) {
throw new IllegalArgumentException(
"No listener of the given type is registered");
}
List<ComponentEventListener<? extends ComponentEvent<?>>> listeners = eventData.listeners;
assert listeners != null;

if (!listeners.remove(listener)) {
if (!eventData.remove(wrapper)) {
throw new IllegalArgumentException(
"The given listener is not registered");
}
if (listeners.isEmpty()) {
// No more listeners for this event type
AnnotationReader
.getAnnotationFor(eventType,
com.vaadin.flow.component.DomEvent.class)
.ifPresent(annotation -> unregisterDomEvent(eventType,
annotation.value()));

componentEventData.remove(eventType);
if (wrapper.domRegistration != null) {
wrapper.domRegistration.remove();
}
}

/**
* Removes the DOM listener for the given event type.
*
* @param eventType
* the component event type
* @param domEventType
* the DOM event type for the component event type
*/
private void unregisterDomEvent(
Class<? extends ComponentEvent<?>> eventType, String domEventType) {
assert eventType != null;
assert domEventType != null && !domEventType.isEmpty();

Registration domEventRemover = componentEventData
.get(eventType).domEventRemover;

if (domEventRemover != null) {
domEventRemover.remove();
componentEventData.get(eventType).domEventRemover = null;
} else {
throw new IllegalArgumentException(
"No remover found when unregistering event type "
+ eventType.getName() + " from DOM event "
+ domEventType);
if (eventData.isEmpty()) {
componentEventData.remove(eventType);
}
}

Expand All @@ -319,12 +362,14 @@ private void unregisterDomEvent(
* the component event type which should be fired
* @param domEvent
* the DOM event
* @param wrapper
* the component event listener to call when the DOM event is
* fired
*/
private void handleDomEvent(Class<? extends ComponentEvent<?>> eventType,
DomEvent domEvent) {
ComponentEvent<?> e = createEventForDomEvent(eventType, domEvent,
component);
fireEvent(e);
private <T extends ComponentEvent<?>> void handleDomEvent(
Class<T> eventType, DomEvent domEvent, ListenerWrapper<T> wrapper) {
T event = createEventForDomEvent(eventType, domEvent, component);
fireEventForListener(event, wrapper);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import com.vaadin.flow.component.internal.ComponentMetaData.DependencyInfo;
import com.vaadin.flow.component.internal.ComponentMetaData.SynchronizedPropertyInfo;
import com.vaadin.flow.di.Instantiator;
import com.vaadin.flow.dom.DomEvent;
import com.vaadin.flow.dom.DomListenerRegistration;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.function.SerializableTriConsumer;
import com.vaadin.flow.i18n.LocaleChangeEvent;
Expand Down Expand Up @@ -333,6 +335,41 @@ public static <T extends ComponentEvent<?>> Registration addListener(
return component.addListener(eventType, listener);
}

/**
* Adds a listener for an event of the given type to the {@code component},
* and customizes the corresponding DOM event listener with the given
* consumer. This allows overriding eg. the debounce settings defined in the
* {@link DomEvent} annotation.
* <p>
* Note that customizing the DOM event listener works only for event types
* which are annotated with {@link DomEvent}. Use
* {@link #addListener(Component, Class, ComponentEventListener)} for other
* listeners, or if you don't need to customize the DOM listener.
*
* @param <T>
* the event type
* @param component
* the component to add the {@code listener}
* @param eventType
* the event type for which to call the listener, must be
* annotated with {@link DomEvent}
* @param listener
* the listener to call when the event occurs, not {@code null}
* @param domListenerConsumer
* a consumer to customize the behavior of the DOM event
* listener, not {@code null}
* @return a handle that can be used for removing the listener
* @throws IllegalArgumentException
* if the event type is not annotated with {@link DomEvent}
*/
public <T extends ComponentEvent<?>> Registration addListener(
Component component, Class<T> eventType,
ComponentEventListener<T> listener,
Consumer<DomListenerRegistration> domListenerConsumer) {
return component.getEventBus().addListener(eventType, listener,
domListenerConsumer);
}

/**
* Dispatches the event to all listeners registered for the event type.
*
Expand Down
Loading

0 comments on commit f9231f7

Please sign in to comment.