Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add component-level API for customizing DOM event listeners #4445

Merged
merged 3 commits into from
Aug 2, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(() -> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MINOR Remove useless curly braces around statement rule

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