Skip to content
Closed
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
1 change: 1 addition & 0 deletions flow
Submodule flow added at edd0fa
6 changes: 6 additions & 0 deletions vaadin-crud-flow-parent/vaadin-crud-flow/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@
<artifactId>vaadin-flow-components-base</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-flow-components-test-util</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-grid-flow</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.vaadin.flow.component.HasSize;
import com.vaadin.flow.component.HasStyle;
import com.vaadin.flow.component.HasTheme;
import com.vaadin.flow.component.SignalPropertySupport;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.dependency.JsModule;
Expand All @@ -35,6 +36,7 @@
import com.vaadin.flow.data.renderer.LitRenderer;
import com.vaadin.flow.internal.JacksonUtils;
import com.vaadin.flow.shared.Registration;
import com.vaadin.flow.signals.Signal;

import tools.jackson.databind.node.ObjectNode;

Expand Down Expand Up @@ -69,6 +71,8 @@ public class Crud<E> extends Component implements HasSize, HasTheme, HasStyle {
private boolean toolbarVisible = true;
private boolean saveBtnDisabledOverridden;

private SignalPropertySupport<Boolean> dirtySupport;

final private Button saveButton;

final private Button cancelButton;
Expand Down Expand Up @@ -314,7 +318,32 @@ public void setOpened(boolean opened) {
* @see #getSaveButton()
*/
public void setDirty(boolean dirty) {
getElement().executeJs("this.__isDirty = $0", dirty);
getDirtySupport().set(dirty);
}

/**
* Binds the dirty state to the given signal. Signal changes push the dirty
* state to the client.
* <p>
* While a signal is bound, any attempt to set the dirty state manually
* throws {@link com.vaadin.flow.signals.BindingActiveException}.
*
* @param signal
* the signal to bind, not {@code null}
* @since 25.1
*/
public void bindDirty(Signal<Boolean> signal) {
Objects.requireNonNull(signal, "Signal cannot be null");
getDirtySupport().bind(signal.map(v -> v == null ? Boolean.FALSE : v));
}

private SignalPropertySupport<Boolean> getDirtySupport() {
if (dirtySupport == null) {
dirtySupport = SignalPropertySupport.create(this,
dirty -> getElement().executeJs("this.__isDirty = $0",
dirty));
}
return dirtySupport;
}

/**
Expand Down
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Please write proper tests that verify that the executeJs call happens.

Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/**
* Copyright 2000-2026 Vaadin Ltd.
*
* This program is available under Vaadin Commercial License and Service Terms.
*
* See {@literal <https://vaadin.com/commercial-license-and-service-terms>} for the full
* license.
*/
package com.vaadin.flow.component.crud;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;

import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.data.provider.DataCommunicator;
import com.vaadin.flow.data.provider.DataKeyMapper;
import com.vaadin.flow.data.provider.DataProvider;
import com.vaadin.flow.signals.BindingActiveException;
import com.vaadin.flow.signals.local.ValueSignal;
import com.vaadin.tests.AbstractSignalsUnitTest;

public class CrudSignalTest extends AbstractSignalsUnitTest {

private Crud<Thing> crud;
private ValueSignal<Boolean> signal;

@Before
public void setup() {
Grid<Thing> grid = Mockito.spy(new Grid<>());
Mockito.when(grid.getDataProvider())
.thenReturn(Mockito.mock(DataProvider.class));
DataCommunicator<Thing> communicator = Mockito
.mock(DataCommunicator.class);
Mockito.when(grid.getDataCommunicator()).thenReturn(communicator);
DataKeyMapper<Thing> keyMapper = Mockito.mock(DataKeyMapper.class);
Mockito.when(communicator.getKeyMapper()).thenReturn(keyMapper);

crud = new Crud<>(Thing.class, grid, new ThingEditor());
signal = new ValueSignal<>(false);
}

@After
public void tearDown() {
if (crud != null && crud.isAttached()) {
crud.removeFromParent();
}
}

@Test
public void bindDirty_signalBound_propertySync() {
crud.bindDirty(signal);
UI.getCurrent().add(crud);

signal.set(true);
// The dirty state is pushed via executeJs, so we verify the signal
// value reflects correctly
Assert.assertTrue(signal.peek());

signal.set(false);
Assert.assertFalse(signal.peek());
}

@Test
public void bindDirty_notAttached_noEffect() {
crud.bindDirty(signal);

// Signal changes should not throw when not attached
signal.set(true);
Assert.assertTrue(signal.peek());
}

@Test
public void bindDirty_detachAndReattach() {
crud.bindDirty(signal);
UI.getCurrent().add(crud);

signal.set(true);
Assert.assertTrue(signal.peek());

crud.removeFromParent();
signal.set(false);

UI.getCurrent().add(crud);
Assert.assertFalse(signal.peek());
}

@Test(expected = BindingActiveException.class)
public void bindDirty_setWhileBound_throws() {
crud.bindDirty(signal);
UI.getCurrent().add(crud);

crud.setDirty(true);
}

@Test(expected = BindingActiveException.class)
public void bindDirty_doubleBind_throws() {
crud.bindDirty(signal);
crud.bindDirty(new ValueSignal<>(true));
}

@Test(expected = NullPointerException.class)
public void bindDirty_nullSignal_throwsNPE() {
crud.bindDirty(null);
}

public static class Thing {
String name;
}

private static class ThingEditor implements CrudEditor<Thing> {
private Thing item;

@Override
public void setItem(Thing item, boolean validate) {
this.item = item;
}

@Override
public Thing getItem() {
return item;
}

@Override
public void clear() {
}

@Override
public boolean validate() {
return false;
}

@Override
public void writeItemChanges() {
}

@Override
public Component getView() {
return new Div();
}
}
}