Skip to content

Commit

Permalink
feat: introduce add and remove widget functionality to dashboard (#6572)
Browse files Browse the repository at this point in the history
* feat: introduce add and remove widget functionality to dashboard

* fix: add missing lines and run formatter

* fix: fix compilation error

* chore: run formatter

* test: remove first and last widgets and use clickelementswithjs

* refactor: make remove from parent use removevirtualchild for widgets

* refactor: use append and remove with virtual children and remove from parent before add

* chore: remove unused code

* refactor: merge attach handlers

* refactor: do not try to remove virtual child on detach

* refactor: use widgets list to update detach listeners

* test: add more unit tests for testing node ids on detach

* refactor: use dashboard remove when removing widget from parent

* test: use clickelementwithjs without locating element

* refactor: remove detach listeners

* test: add tests for moving widgets between parents

* test: add unit tests for moving widgets between components

* test: cleanup dashboard widget related unit tests

* refactor: use add remove directly instead of using virtual children

* refactor: simplify removing all widgets by reusing single remove logic

* refactor: override getchildren
  • Loading branch information
ugur-vaadin committed Oct 10, 2024
1 parent 711c038 commit ece91bc
Show file tree
Hide file tree
Showing 7 changed files with 658 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,68 @@
*/
package com.vaadin.flow.component.dashboard.tests;

import java.util.List;

import com.vaadin.flow.component.dashboard.Dashboard;
import com.vaadin.flow.component.dashboard.DashboardWidget;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.NativeButton;
import com.vaadin.flow.router.Route;

/**
* @author Vaadin Ltd
*/
@Route("vaadin-dashboard")
public class DashboardPage extends Div {

public DashboardPage() {
DashboardWidget widget1 = new DashboardWidget();
widget1.setTitle("Widget 1");
widget1.setId("widget-1");

DashboardWidget widget2 = new DashboardWidget();
widget2.setTitle("Widget 2");
widget2.setId("widget-2");

DashboardWidget widget3 = new DashboardWidget();
widget3.setTitle("Widget 3");
widget3.setId("widget-3");

Dashboard dashboard = new Dashboard();
dashboard.add(widget1, widget2, widget3);

NativeButton addWidgetAtIndex1 = new NativeButton(
"Add widget at index 1");
addWidgetAtIndex1.addClickListener(click -> {
DashboardWidget widgetAtIndex1 = new DashboardWidget();
widgetAtIndex1.setTitle("Widget at index 1");
widgetAtIndex1.setId("widget-at-index-1");
dashboard.addWidgetAtIndex(1, widgetAtIndex1);
});
addWidgetAtIndex1.setId("add-widget-at-index-1");

NativeButton removeFirstAndLastWidgets = new NativeButton(
"Remove first and last widgets");
removeFirstAndLastWidgets.addClickListener(click -> {
List<DashboardWidget> currentWidgets = dashboard.getWidgets();
if (currentWidgets.isEmpty()) {
return;
}
int currentWidgetCount = currentWidgets.size();
if (currentWidgetCount == 1) {
dashboard.remove(dashboard.getWidgets().get(0));
} else {
dashboard.remove(dashboard.getWidgets().get(0),
dashboard.getWidgets().get(currentWidgetCount - 1));
}
});
removeFirstAndLastWidgets.setId("remove-first-and-last-widgets");

NativeButton removeAllWidgets = new NativeButton("Remove all widgets");
removeAllWidgets.addClickListener(click -> dashboard.removeAll());
removeAllWidgets.setId("remove-all-widgets");

add(addWidgetAtIndex1, removeFirstAndLastWidgets, removeAllWidgets,
dashboard);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@
*/
package com.vaadin.flow.component.dashboard.tests;

import java.util.Arrays;
import java.util.List;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import com.vaadin.flow.component.dashboard.testbench.DashboardElement;
import com.vaadin.flow.component.dashboard.testbench.DashboardWidgetElement;
import com.vaadin.flow.testutil.TestPath;
import com.vaadin.tests.AbstractComponentIT;

Expand All @@ -16,4 +25,43 @@
*/
@TestPath("vaadin-dashboard")
public class DashboardIT extends AbstractComponentIT {

private DashboardElement dashboardElement;

@Before
public void init() {
open();
dashboardElement = $(DashboardElement.class).waitForFirst();
}

@Test
public void addWidgets_widgetsAreCorrectlyAdded() {
assertWidgetsByTitle("Widget 1", "Widget 2", "Widget 3");
}

@Test
public void addWidgetsAtIndex1_widgetIsAddedIntoTheCorrectPlace() {
clickElementWithJs("add-widget-at-index-1");
assertWidgetsByTitle("Widget 1", "Widget at index 1", "Widget 2",
"Widget 3");
}

@Test
public void removeFirstAndLastWidgets_widgetsAreCorrectlyRemoved() {
clickElementWithJs("remove-first-and-last-widgets");
assertWidgetsByTitle("Widget 2");
}

@Test
public void removeAllWidgets_widgetsAreCorrectlyRemoved() {
clickElementWithJs("remove-all-widgets");
assertWidgetsByTitle();
}

private void assertWidgetsByTitle(String... expectedWidgetTitles) {
List<DashboardWidgetElement> widgets = dashboardElement.getWidgets();
List<String> widgetTitles = widgets.stream()
.map(DashboardWidgetElement::getTitle).toList();
Assert.assertEquals(Arrays.asList(expectedWidgetTitles), widgetTitles);
}
}
5 changes: 5 additions & 0 deletions vaadin-dashboard-flow-parent/vaadin-dashboard-flow/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@
<groupId>com.vaadin</groupId>
<artifactId>flow-html-components</artifactId>
</dependency>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-renderer-flow</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,30 @@
*/
package com.vaadin.flow.component.dashboard;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;

import org.slf4j.LoggerFactory;

import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.Component;
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.dom.Element;
import com.vaadin.flow.dom.ElementDetachEvent;
import com.vaadin.flow.dom.ElementDetachListener;
import com.vaadin.flow.shared.Registration;

import elemental.json.Json;
import elemental.json.JsonArray;
import elemental.json.JsonObject;

/**
* @author Vaadin Ltd
Expand All @@ -20,6 +40,210 @@
@NpmPackage(value = "@vaadin/polymer-legacy-adapter", version = "24.5.0-alpha8")
@JsModule("@vaadin/polymer-legacy-adapter/style-modules.js")
@JsModule("@vaadin/dashboard/src/vaadin-dashboard.js")
@JsModule("./flow-component-renderer.js")
// @NpmPackage(value = "@vaadin/dashboard", version = "24.6.0-alpha0")
public class Dashboard extends Component {

private final List<DashboardWidget> widgets = new ArrayList<>();

private boolean pendingUpdate = false;

/**
* Creates an empty dashboard.
*/
public Dashboard() {
}

/**
* Returns the widgets in the dashboard.
*
* @return The widgets in the dashboard
*/
public List<DashboardWidget> getWidgets() {
return Collections.unmodifiableList(widgets);
}

/**
* Adds the given widgets to the dashboard.
*
* @param widgets
* the widgets to add, not {@code null}
*/
public void add(DashboardWidget... widgets) {
Objects.requireNonNull(widgets, "Widgets to add cannot be null.");
List<DashboardWidget> toAdd = new ArrayList<>(widgets.length);
for (DashboardWidget widget : widgets) {
Objects.requireNonNull(widget, "Widget to add cannot be null.");
toAdd.add(widget);
}
toAdd.forEach(this::doAddWidget);
updateClient();
}

/**
* Adds the given widget as child of this dashboard at the specific index.
* <p>
* In case the specified widget has already been added to another parent, it
* will be removed from there and added to this one.
*
* @param index
* the index, where the widget will be added. The index must be
* non-negative and may not exceed the children count
* @param widget
* the widget to add, not {@code null}
*/
public void addWidgetAtIndex(int index, DashboardWidget widget) {
Objects.requireNonNull(widget, "Widget to add cannot be null.");
if (index < 0) {
throw new IllegalArgumentException(
"Cannot add a widget with a negative index.");
}
// The case when the index is bigger than the children count is handled
// inside the method below
doAddWidget(index, widget);
updateClient();
}

/**
* Removes the given widgets from this dashboard.
*
* @param widgets
* the widgets to remove, not {@code null}
* @throws IllegalArgumentException
* if there is a widget whose non {@code null} parent is not
* this dashboard
*/
public void remove(DashboardWidget... widgets) {
Objects.requireNonNull(widgets, "Widgets to remove cannot be null.");
List<DashboardWidget> toRemove = new ArrayList<>(widgets.length);
for (DashboardWidget widget : widgets) {
Objects.requireNonNull(widget, "Widget to remove cannot be null.");
Element parent = widget.getElement().getParent();
if (parent == null) {
LoggerFactory.getLogger(getClass()).debug(
"Removal of a widget with no parent does nothing.");
continue;
}
if (getElement().equals(parent)) {
toRemove.add(widget);
} else {
throw new IllegalArgumentException("The given widget (" + widget
+ ") is not a child of this dashboard");
}
}
toRemove.forEach(this::doRemoveWidget);
updateClient();
}

/**
* Removes all widgets from this dashboard.
*/
public void removeAll() {
doRemoveAllWidgets();
updateClient();
}

@Override
public Stream<Component> getChildren() {
return getWidgets().stream().map(Component.class::cast);
}

@Override
protected void onAttach(AttachEvent attachEvent) {
super.onAttach(attachEvent);
attachRenderer();
doUpdateClient();
}

private final Map<Element, Registration> childDetachListenerMap = new HashMap<>();

// Must not use lambda here as that would break serialization. See
// https://github.com/vaadin/flow-components/issues/5597
private final ElementDetachListener childDetachListener = new ElementDetachListener() {
@Override
public void onDetach(ElementDetachEvent e) {
var detachedElement = e.getSource();
getWidgets().stream()
.filter(widget -> Objects.equals(detachedElement,
widget.getElement()))
.findAny().ifPresent(detachedWidget -> {
// The child was removed from the dashboard

// Remove the registration for the child detach listener
childDetachListenerMap.get(detachedWidget.getElement())
.remove();
childDetachListenerMap
.remove(detachedWidget.getElement());

widgets.remove(detachedWidget);
updateClient();
});
}
};

private void updateClient() {
if (pendingUpdate) {
return;
}
pendingUpdate = true;
getElement().getNode()
.runWhenAttached(ui -> ui.beforeClientResponse(this, ctx -> {
doUpdateClient();
pendingUpdate = false;
}));
}

private void doUpdateClient() {
widgets.forEach(widget -> {
Element childWidgetElement = widget.getElement();
if (!childDetachListenerMap.containsKey(childWidgetElement)) {
childDetachListenerMap.put(childWidgetElement,
childWidgetElement
.addDetachListener(childDetachListener));
}
});
getElement().setPropertyJson("items", createItemsJsonArray());
}

private void attachRenderer() {
getElement().executeJs(
"Vaadin.FlowComponentHost.patchVirtualContainer(this);");
String appId = UI.getCurrent().getInternals().getAppId();
getElement().executeJs(
"this.renderer = (root, _, model) => Vaadin.FlowComponentHost.setChildNodes($0, [model.item.nodeid], root);",
appId);
}

private JsonArray createItemsJsonArray() {
JsonArray jsonItems = Json.createArray();
for (DashboardWidget widget : widgets) {
JsonObject jsonItem = Json.createObject();
jsonItem.put("nodeid", getWidgetNodeId(widget));
jsonItems.set(jsonItems.length(), jsonItem);
}
return jsonItems;
}

private int getWidgetNodeId(DashboardWidget widget) {
return widget.getElement().getNode().getId();
}

private void doRemoveAllWidgets() {
new ArrayList<>(widgets).forEach(this::doRemoveWidget);
}

private void doRemoveWidget(DashboardWidget widget) {
getElement().removeChild(widget.getElement());
widgets.remove(widget);
}

private void doAddWidget(int index, DashboardWidget widget) {
getElement().appendChild(widget.getElement());
widgets.add(index, widget);
}

private void doAddWidget(DashboardWidget widget) {
getElement().appendChild(widget.getElement());
widgets.add(widget);
}
}
Loading

0 comments on commit ece91bc

Please sign in to comment.