Skip to content
Draft
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 @@ -23,6 +23,8 @@
import java.util.function.Consumer;

import com.vaadin.flow.data.provider.DataChangeEvent.DataRefreshEvent;
import com.vaadin.flow.data.provider.DataChangeEvent.ItemAddedEvent;
import com.vaadin.flow.data.provider.DataChangeEvent.ItemRemovedEvent;
import com.vaadin.flow.function.SerializableConsumer;
import com.vaadin.flow.shared.Registration;

Expand Down Expand Up @@ -82,6 +84,16 @@ public void refreshItem(T item) {
fireEvent(new DataRefreshEvent<>(this, item));
}

@Override
public void notifyItemAdded(T item) {
fireEvent(new ItemAddedEvent<>(this, item));
}

@Override
public void notifyItemRemoved(T item) {
fireEvent(new ItemRemovedEvent<>(this, item));
}

/**
* Registers a new listener with the specified activation method to listen
* events generated by this component. If the activation method does not
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ public AbstractListDataView<T> addItem(T item) {
final ListDataProvider<T> dataProvider = getDataProvider();
if (!contains(item)) {
dataProvider.getItems().add(item);
dataProvider.refreshAll();
dataProvider.notifyItemAdded(item);
}
return this;
}
Expand Down Expand Up @@ -269,8 +269,11 @@ public AbstractListDataView<T> addItemsBefore(Collection<T> items,
@Override
public AbstractListDataView<T> removeItem(T item) {
final ListDataProvider<T> dataProvider = getDataProvider();
removeItemIfPresent(item, dataProvider);
dataProvider.refreshAll();
if (removeItemIfPresent(item, dataProvider)) {
dataProvider.notifyItemRemoved(item);
} else {
dataProvider.refreshAll();
}
return this;
}

Expand Down Expand Up @@ -315,8 +318,10 @@ protected void validateItemIndex(int itemIndex) {
}
}

private void removeItemIfPresent(T item, ListDataProvider<T> dataProvider) {
dataProvider.getItems().removeIf(nextItem -> equals(item, nextItem));
private boolean removeItemIfPresent(T item,
ListDataProvider<T> dataProvider) {
return dataProvider.getItems()
.removeIf(nextItem -> equals(item, nextItem));
}

private void addItemOnTarget(T item, T target,
Expand Down Expand Up @@ -348,7 +353,7 @@ private void addItemOnTarget(T item, T target,
removeItemIfPresent(item, dataProvider);
itemList.add(insertItemsIndexProvider
.apply(getItemIndex(target, itemList.stream())), item);
dataProvider.refreshAll();
dataProvider.notifyItemAdded(item);
}

private void addItemCollectionOnTarget(Collection<T> items, T target,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,87 @@ public boolean isRefreshChildren() {
}
}

/**
* An event fired when a single item has been added to a
* {@code DataProvider}.
* <p>
* Listeners that don't need fine-grained handling can simply treat this as
* any other {@link DataChangeEvent}. {@link DataCommunicator} uses this
* event to perform a more efficient update than a full refresh while still
* picking up the new total item count and the position of the new item with
* respect to current sorting and filtering.
*
* @param <T>
* the data type
*/
public static class ItemAddedEvent<T> extends DataChangeEvent<T> {

private final T item;

/**
* Creates a new event originating from the given data provider.
*
* @param source
* the data provider, not <code>null</code>
* @param item
* the added item, not <code>null</code>
*/
public ItemAddedEvent(DataProvider<T, ?> source, T item) {
super(source);
Objects.requireNonNull(item, "Added item can't be null");
this.item = item;
}

/**
* Gets the added item.
*
* @return the added item, never <code>null</code>
*/
public T getItem() {
return item;
}
}

/**
* An event fired when a single item has been removed from a
* {@code DataProvider}.
* <p>
* Listeners that don't need fine-grained handling can simply treat this as
* any other {@link DataChangeEvent}. {@link DataCommunicator} uses this
* event to perform a more efficient update than a full refresh while still
* picking up the new total item count.
*
* @param <T>
* the data type
*/
public static class ItemRemovedEvent<T> extends DataChangeEvent<T> {

private final T item;

/**
* Creates a new event originating from the given data provider.
*
* @param source
* the data provider, not <code>null</code>
* @param item
* the removed item, not <code>null</code>
*/
public ItemRemovedEvent(DataProvider<T, ?> source, T item) {
super(source);
Objects.requireNonNull(item, "Removed item can't be null");
this.item = item;
}

/**
* Gets the removed item.
*
* @return the removed item, never <code>null</code>
*/
public T getItem() {
return item;
}
}

/**
* Creates a new {@code DataChangeEvent} event originating from the given
* data provider.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@
import com.vaadin.flow.component.UI;
import com.vaadin.flow.data.provider.ArrayUpdater.Update;
import com.vaadin.flow.data.provider.DataChangeEvent.DataRefreshEvent;
import com.vaadin.flow.data.provider.DataChangeEvent.ItemAddedEvent;
import com.vaadin.flow.data.provider.DataChangeEvent.ItemRemovedEvent;
import com.vaadin.flow.dom.Element;
import com.vaadin.flow.function.SerializableComparator;
import com.vaadin.flow.function.SerializableConsumer;
Expand Down Expand Up @@ -1131,6 +1133,10 @@ private void handleAttach() {
.addDataProviderListener(event -> {
if (event instanceof DataRefreshEvent) {
handleDataRefreshEvent((DataRefreshEvent<T>) event);
} else if (event instanceof ItemAddedEvent) {
handleItemAddedEvent((ItemAddedEvent<T>) event);
} else if (event instanceof ItemRemovedEvent) {
handleItemRemovedEvent((ItemRemovedEvent<T>) event);
} else {
reset();
}
Expand All @@ -1144,6 +1150,40 @@ protected void handleDataRefreshEvent(DataRefreshEvent<T> event) {
refresh(event.getItem());
}

/**
* Handles a notification that a single item has been added to the
* underlying data. The default implementation triggers a viewport refresh
* and a size re-fetch without discarding per-item state for unrelated
* items, which is significantly cheaper than {@link #reset()} when only one
* item changes.
*
* @param event
* the added-item event, not {@code null}
*/
protected void handleItemAddedEvent(ItemAddedEvent<T> event) {
sizeReset = true;
refreshViewport();
}

/**
* Handles a notification that a single item has been removed from the
* underlying data. The default implementation removes the removed item from
* the {@link KeyMapper} (if active) and triggers a viewport refresh with a
* size re-fetch, without discarding per-item state for unrelated items.
*
* @param event
* the removed-item event, not {@code null}
*/
protected void handleItemRemovedEvent(ItemRemovedEvent<T> event) {
T removed = event.getItem();
if (keyMapper.has(removed)) {
dataGenerator.destroyData(removed);
keyMapper.remove(removed);
}
sizeReset = true;
refreshViewport();
}

private void handleDetach() {
if (future != null) {
future.cancel(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,57 @@ default void refreshItem(T item, boolean refreshChildren) {
*/
void refreshAll();

/**
* Notifies listeners that a single item has been added to the underlying
* data and the listening components should refresh accordingly. This allows
* components to perform an incremental update without rebuilding the
* per-item state for unrelated items, which can be significantly faster
* than {@link #refreshAll()} when only a single item changes.
* <p>
* Where the new item appears in the rendered output is still determined by
* the configured sorting and filtering: the implementation is expected to
* have already added the item to the underlying data structure before
* calling this method.
* <p>
* The default implementation falls back to {@link #refreshAll()} so that
* custom data providers do not need to opt in to the new behavior. Data
* providers that fire {@link DataChangeEvent}s should override this method
* to fire a {@link DataChangeEvent.ItemAddedEvent} instead, which lets
* {@link DataCommunicator} perform an efficient update.
*
* @param item
* the added item; not {@code null}
*/
default void notifyItemAdded(T item) {
Objects.requireNonNull(item, "Added item cannot be null.");
refreshAll();
}

/**
* Notifies listeners that a single item has been removed from the
* underlying data and the listening components should refresh accordingly.
* This allows components to perform an incremental update without
* rebuilding the per-item state for unrelated items, which can be
* significantly faster than {@link #refreshAll()} when only a single item
* changes.
* <p>
* The implementation is expected to have already removed the item from the
* underlying data structure before calling this method.
* <p>
* The default implementation falls back to {@link #refreshAll()} so that
* custom data providers do not need to opt in to the new behavior. Data
* providers that fire {@link DataChangeEvent}s should override this method
* to fire a {@link DataChangeEvent.ItemRemovedEvent} instead, which lets
* {@link DataCommunicator} perform an efficient update.
*
* @param item
* the removed item; not {@code null}
*/
default void notifyItemRemoved(T item) {
Objects.requireNonNull(item, "Removed item cannot be null.");
refreshAll();
}

/**
* Gets an identifier for the given item. This identifier is used by the
* framework to determine equality between two items.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,52 @@ void removeItem_notInList_dataSetNotChanged() {
assertEquals(3, dataView.getItemCount());
}

@Test
void addItem_firesItemAddedEventInsteadOfRefreshAll() {
AtomicReference<DataChangeEvent<String>> received = new AtomicReference<>();
dataProvider.addDataProviderListener(received::set);

dataView.addItem("new");

DataChangeEvent<String> event = received.get();
assertNotNull(event, "Expected an event to be fired");
assertTrue(event instanceof DataChangeEvent.ItemAddedEvent,
"Expected an ItemAddedEvent, got " + event.getClass());
assertEquals("new",
((DataChangeEvent.ItemAddedEvent<String>) event).getItem());
}

@Test
void removeItem_firesItemRemovedEventInsteadOfRefreshAll() {
AtomicReference<DataChangeEvent<String>> received = new AtomicReference<>();
dataProvider.addDataProviderListener(received::set);

dataView.removeItem("middle");

DataChangeEvent<String> event = received.get();
assertNotNull(event, "Expected an event to be fired");
assertTrue(event instanceof DataChangeEvent.ItemRemovedEvent,
"Expected an ItemRemovedEvent, got " + event.getClass());
assertEquals("middle",
((DataChangeEvent.ItemRemovedEvent<String>) event).getItem());
}

@Test
void removeItem_notPresent_fallsBackToRefreshAll() {
AtomicReference<DataChangeEvent<String>> received = new AtomicReference<>();
dataProvider.addDataProviderListener(received::set);

dataView.removeItem("not present");

DataChangeEvent<String> event = received.get();
assertNotNull(event, "Expected an event to be fired");
// When nothing is removed, the data provider has no item to notify
// about, so it falls back to the original refreshAll behavior
assertFalse(event instanceof DataChangeEvent.DataRefreshEvent);
assertFalse(event instanceof DataChangeEvent.ItemRemovedEvent);
assertFalse(event instanceof DataChangeEvent.ItemAddedEvent);
}

@Test
void addItemBefore_itemIsAddedAtExpectedPosition() {
dataView.addItemBefore("newItem", "middle");
Expand Down
Loading
Loading