All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.vaadin.flow.component.virtuallist.VirtualList Maven / Gradle / Ivy

There is a newer version: 24.5.4
Show newest version
/*
 * Copyright 2000-2024 Vaadin Ltd.
 *
 * 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.
 */
package com.vaadin.flow.component.virtuallist;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import com.vaadin.flow.component.ClientCallable;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Focusable;
import com.vaadin.flow.component.HasSize;
import com.vaadin.flow.component.HasStyle;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.dependency.NpmPackage;
import com.vaadin.flow.component.virtuallist.paging.PagelessDataCommunicator;
import com.vaadin.flow.data.binder.HasDataProvider;
import com.vaadin.flow.data.provider.ArrayUpdater;
import com.vaadin.flow.data.provider.ArrayUpdater.Update;
import com.vaadin.flow.data.provider.CompositeDataGenerator;
import com.vaadin.flow.data.provider.DataCommunicator;
import com.vaadin.flow.data.provider.DataProvider;
import com.vaadin.flow.data.renderer.ComponentRenderer;
import com.vaadin.flow.data.renderer.LitRenderer;
import com.vaadin.flow.data.renderer.Renderer;
import com.vaadin.flow.dom.DisabledUpdateMode;
import com.vaadin.flow.function.ValueProvider;
import com.vaadin.flow.internal.JsonUtils;
import com.vaadin.flow.server.Command;
import com.vaadin.flow.shared.Registration;

import elemental.json.Json;
import elemental.json.JsonValue;

/**
 * Virtual List allows you to render a long list of items inside a scrollable
 * container without sacrificing performance. Each item is rendered on the fly
 * as the user scrolls the list. To use the component, you need to assign it a
 * set of data items and a renderer that is used for rendering each individual
 * data item. The height of an item is determined by its content and can change
 * dynamically.
 * 

* This component supports {@link DataProvider}s to load data asynchronously and * {@link Renderer}s to render the markup for each item. *

* * @author Vaadin Ltd. * * @param * the type of the items supported by the list */ @Tag("vaadin-virtual-list") @NpmPackage(value = "@vaadin/polymer-legacy-adapter", version = "24.4.5") @JsModule("@vaadin/polymer-legacy-adapter/style-modules.js") @NpmPackage(value = "@vaadin/virtual-list", version = "24.4.5") @JsModule("@vaadin/virtual-list/src/vaadin-virtual-list.js") @JsModule("./flow-component-renderer.js") @JsModule("./virtualListConnector.js") public class VirtualList extends Component implements HasDataProvider, HasStyle, HasSize, Focusable> { private final class UpdateQueue implements Update { private transient List queue = new ArrayList<>(); private UpdateQueue(int size) { enqueue("$connector.updateSize", size); } @Override public void set(int start, List items) { enqueue("$connector.set", start, items.stream().collect(JsonUtils.asArray())); } @Override public void clear(int start, int length) { enqueue("$connector.clear", start, length); } @Override public void commit(int updateId) { getDataCommunicator().confirmUpdate(updateId); queue.forEach(Runnable::run); queue.clear(); } private void enqueue(String name, Serializable... arguments) { queue.add(() -> getElement().callJsFunction(name, arguments)); } } private final ArrayUpdater arrayUpdater = new ArrayUpdater() { @Override public Update startUpdate(int sizeChange) { return new UpdateQueue(sizeChange); } @Override public void initialize() { initConnector(); } }; private Renderer renderer; private final CompositeDataGenerator dataGenerator = new CompositeDataGenerator<>(); private final List renderingRegistrations = new ArrayList<>(); private transient T placeholderItem; private final DataCommunicator dataCommunicator = new PagelessDataCommunicator<>( dataGenerator, arrayUpdater, data -> getElement().callJsFunction("$connector.updateData", data), getElement().getNode()); /** * Creates an empty list. */ public VirtualList() { setRenderer((ValueProvider) String::valueOf); addAttachListener((e) -> this.setPlaceholderItem(this.placeholderItem)); } private void initConnector() { getUI().orElseThrow(() -> new IllegalStateException( "Connector can only be initialized for an attached VirtualList")) .getPage().executeJs( "window.Vaadin.Flow.virtualListConnector.initLazy($0)", getElement()); } @Override public void setDataProvider(DataProvider dataProvider) { Objects.requireNonNull(dataProvider, "The dataProvider cannot be null"); getDataCommunicator().setDataProvider(dataProvider, null); } /** * Returns the data provider of this list. * * @return the data provider of this list, not {@code null} */ public DataProvider getDataProvider() { // NOSONAR return getDataCommunicator().getDataProvider(); } /** * Returns the data communicator of this list. * * @return the data communicator, not {@code null} */ public DataCommunicator getDataCommunicator() { return dataCommunicator; } /** * Sets a renderer for the items in the list, by using a * {@link ValueProvider}. The String returned by the provider is used to * render each item. * * @param valueProvider * a provider for the label string for each item in the list, not * null */ public void setRenderer(ValueProvider valueProvider) { Objects.requireNonNull(valueProvider, "The valueProvider must not be null"); this.setRenderer(LitRenderer. of("${item.label}") .withProperty("label", valueProvider)); } /** * Sets a renderer for the items in the list. *

* When set, a same renderer is used for the placeholder item. See * {@link #setPlaceholderItem(Object)} for details. * * @param renderer * a renderer for the items in the list, not null */ public void setRenderer(Renderer renderer) { Objects.requireNonNull(renderer, "The renderer must not be null"); renderingRegistrations.forEach(Registration::remove); renderingRegistrations.clear(); var rendering = renderer.render(getElement(), dataCommunicator.getKeyMapper()); rendering.getDataGenerator().ifPresent(renderingDataGenerator -> { Registration renderingDataGeneratorRegistration = dataGenerator .addDataGenerator(renderingDataGenerator); renderingRegistrations.add(renderingDataGeneratorRegistration); }); renderingRegistrations.add(rendering.getRegistration()); this.renderer = renderer; getDataCommunicator().reset(); // Changing the renderer may also affect how the placeholder item is // processed by the data generator. Call setPlaceholderItem to make sure // the sent placeholder item is up to date. this.setPlaceholderItem(this.placeholderItem); } /** * Sets an item to be shown as placeholder in the list while the real data * in being fetched from the server. *

* Setting a placeholder item improves the user experience of the list while * scrolling, since the placeholder uses the same renderer set with * {@link #setRenderer(Renderer)}, maintaining the same height for * placeholders and actual items. *

* When no placeholder item is set (or when set to null), an * empty placeholder element is created. *

* Note: when using {@link ComponentRenderer}s, the component used for the * placeholder is statically stamped in the list. It can not be modified, * nor receives any events. * * @param placeholderItem * the item used as placeholder in the list, while the real data * is being fetched from the server */ public void setPlaceholderItem(T placeholderItem) { this.placeholderItem = placeholderItem; runBeforeClientResponse(() -> { var json = Json.createObject(); if (placeholderItem != null) { // Use the renderer's data generator to create the final // placeholder item which should be sent to the client. In the // case of ComponentRenderer, the generator also creates a // placeholder element which is automatically sent to the client // and the resulting json object will include its nodeid. dataGenerator.generateData(placeholderItem, json); } var appId = UI.getCurrent() != null ? UI.getCurrent().getInternals().getAppId() : ""; getElement().callJsFunction("$connector.setPlaceholderItem", json, appId); }); } /** * Gets the placeholder item of this list, or null if none has * been set. * * @return the placeholder item */ public T getPlaceholderItem() { return placeholderItem; } private void runBeforeClientResponse(Command command) { getElement().getNode() .runWhenAttached(ui -> ui.getInternals().getStateTree() .beforeClientResponse(getElement().getNode(), context -> command.execute())); } @Override public void onEnabledStateChanged(boolean enabled) { super.onEnabledStateChanged(enabled); /* * Rendered component's enabled state needs to be updated via rendering */ setRenderer(renderer); } @ClientCallable(DisabledUpdateMode.ALWAYS) private void setRequestedRange(int start, int length) { getDataCommunicator().setRequestedRange(start, length); } /** * Scrolls to the given row index. Scrolls so that the element is shown at * the start of the visible area whenever possible. *

* If the index parameter exceeds current item set size the grid will scroll * to the end. * * @param rowIndex * zero based index of the item to scroll to in the current view. */ public void scrollToIndex(int rowIndex) { getElement().getNode().runWhenAttached( ui -> ui.beforeClientResponse(this, ctx -> getElement() .executeJs("this.scrollToIndex($0)", rowIndex))); } /** * Scrolls to the first element. */ public void scrollToStart() { scrollToIndex(0); } /** * Scrolls to the last element of the list. */ public void scrollToEnd() { scrollToIndex(Integer.MAX_VALUE); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy