Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
46 changes: 45 additions & 1 deletion signals/src/main/java/com/vaadin/signals/WritableSignal.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@

import java.util.concurrent.atomic.AtomicReference;

import com.vaadin.signals.function.SignalMapper;
import com.vaadin.signals.function.SignalUpdater;
import com.vaadin.signals.function.ValueMerger;
import com.vaadin.signals.impl.MappedWritableSignal;
import com.vaadin.signals.operations.CancelableOperation;
import com.vaadin.signals.operations.SignalOperation;

/**
* A signal to which a new value can be directly written.
*
*
* @param <T>
* the signal value type
*/
Expand Down Expand Up @@ -92,4 +95,45 @@ public interface WritableSignal<T> extends Signal<T> {
default Signal<T> asReadonly() {
return () -> value();
}

/**
* Creates a two-way mapped signal that provides a bidirectional view of
* this signal. Reading the mapped signal applies the getter function to
* extract a child value. Writing to the mapped signal uses the setter
* function to update this signal with a new value derived from the current
* value and the new child value.
* <p>
* This is useful for creating component bindings to properties of complex
* objects. For example, to bind a checkbox to the "done" property of a Todo
* record:
*
* <pre>
* record Todo(String text, boolean done) {
* Todo withDone(boolean done) {
* return new Todo(this.text, done);
* }
* }
*
* WritableSignal&lt;Todo&gt; todoSignal = new ValueSignal&lt;&gt;(
* new Todo("Buy milk", false));
* WritableSignal&lt;Boolean&gt; doneSignal = todoSignal.map(Todo::done,
* Todo::withDone);
*
* checkbox.bindValue(doneSignal); // Two-way binding
* </pre>
*
* @param <C>
* the child (mapped) signal type
* @param getter
* the function to extract the child value from this signal's
* value, not <code>null</code>
* @param merger
* the function to create a new value for this signal given the
* current value and a new child value, not <code>null</code>
* @return a two-way mapped signal, not <code>null</code>
*/
default <C> WritableSignal<C> map(SignalMapper<T, C> getter,
ValueMerger<T, C> merger) {
return new MappedWritableSignal<>(this, getter, merger);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2000-2026 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.signals.function;

import com.vaadin.signals.local.ValueSignal;

/**
* Modifies the parent signal value in place based on a new child value. Used
* for creating two-way computed signals with mutable parent values where
* changes to the mapped signal propagate back to the parent signal.
* <p>
* This interface is used with mutable value patterns where changing the child
* value directly modifies the parent value instance rather than creating a new
* one.
* <p>
* Example usage with a mutable bean:
*
* <pre>
* class Todo {
* private String text;
* private boolean done;
*
* // getters and setters...
* }
*
* ValueSignal&lt;Todo&gt; todoSignal = new ValueSignal&lt;&gt;(
* new Todo("Buy milk", false));
* WritableSignal&lt;Boolean&gt; doneSignal = todoSignal.mapMutable(Todo::isDone,
* Todo::setDone);
*
* doneSignal.value(true); // Calls todoSignal.modify(t -&gt; t.setDone(true))
* </pre>
*
* @param <P>
* the parent signal value type
* @param <C>
* the child (mapped) signal value type
* @see ValueSignal#mapMutable(SignalMapper, SignalModifier)
*/
@FunctionalInterface
public interface SignalModifier<P, C> {
/**
* Modifies the parent value in place with the new child value.
*
* @param parentValue
* the parent signal value to modify, may be <code>null</code>
* @param newChildValue
* the new child value to apply, may be <code>null</code>
*/
void modify(P parentValue, C newChildValue);
}
64 changes: 64 additions & 0 deletions signals/src/main/java/com/vaadin/signals/function/ValueMerger.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2000-2026 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.signals.function;

import com.vaadin.signals.WritableSignal;

/**
* Creates a new outer value by merging a new inner value with the old outer
* value. Used for creating two-way computed signals where changes to the mapped
* signal propagate back to the parent signal.
* <p>
* This interface is used with immutable value patterns where changing the inner
* value requires creating a new outer value instance.
* <p>
* Example usage with a record:
*
* <pre>
* record Todo(String text, boolean done) {
* Todo withDone(boolean done) {
* return new Todo(this.text, done);
* }
* }
*
* WritableSignal&lt;Todo&gt; todoSignal = new ValueSignal&lt;&gt;(
* new Todo("Buy milk", false));
* WritableSignal&lt;Boolean&gt; doneSignal = todoSignal.map(Todo::done,
* Todo::withDone);
*
* doneSignal.value(true); // Updates todoSignal to Todo("Buy milk", true)
* </pre>
*
* @param <O>
* the outer (parent) signal value type
* @param <I>
* the inner (mapped) signal value type
* @see WritableSignal#map(SignalMapper, ValueMerger)
*/
@FunctionalInterface
public interface ValueMerger<O, I> {
/**
* Creates a new outer value by merging the new inner value with the old
* outer value.
*
* @param outerValue
* the current outer signal value, may be <code>null</code>
* @param newInnerValue
* the new inner value to merge, may be <code>null</code>
* @return the new outer value, may be <code>null</code>
*/
O merge(O outerValue, I newInnerValue);
}
112 changes: 112 additions & 0 deletions signals/src/main/java/com/vaadin/signals/impl/MappedModifySignal.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2000-2026 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.signals.impl;

import java.util.Objects;

import com.vaadin.signals.WritableSignal;
import com.vaadin.signals.function.SignalMapper;
import com.vaadin.signals.function.SignalModifier;
import com.vaadin.signals.function.SignalUpdater;
import com.vaadin.signals.local.ValueSignal;
import com.vaadin.signals.operations.CancelableOperation;
import com.vaadin.signals.operations.SignalOperation;

/**
* A writable signal that provides a two-way mapped view of a
* {@link ValueSignal} using in-place modification. Reading the signal applies a
* getter function to extract a child value from the parent. Writing to the
* signal uses a modifier function to update the parent value in place.
* <p>
* This is useful for mutable bean patterns where the parent object's properties
* are modified directly using setters.
*
* @param <P>
* the parent signal value type
* @param <C>
* the child (this signal's) value type
*/
public class MappedModifySignal<P, C> implements WritableSignal<C> {

private final ValueSignal<P> parent;
private final SignalMapper<P, C> getter;
private final SignalModifier<P, C> modifier;

/**
* Creates a new mapped modify signal.
*
* @param parent
* the parent value signal to map, not <code>null</code>
* @param getter
* the function to extract the child value from the parent, not
* <code>null</code>
* @param modifier
* the function to modify the parent value in place with the new
* child value, not <code>null</code>
*/
public MappedModifySignal(ValueSignal<P> parent, SignalMapper<P, C> getter,
SignalModifier<P, C> modifier) {
this.parent = Objects.requireNonNull(parent);
this.getter = Objects.requireNonNull(getter);
this.modifier = Objects.requireNonNull(modifier);
}

@Override
public C value() {
return getter.map(parent.value());
}

@Override
public C peek() {
return getter.map(parent.peek());
}

@Override
public SignalOperation<C> value(C newChildValue) {
C oldChildValue = getter.map(parent.peek());
parent.modify(
parentValue -> modifier.modify(parentValue, newChildValue));
return new SignalOperation<>(
new SignalOperation.Result<>(oldChildValue));
}

@Override
public SignalOperation<Void> replace(C expectedValue, C newValue) {
C currentChildValue = getter.map(parent.peek());
if (Objects.equals(expectedValue, currentChildValue)) {
parent.modify(
parentValue -> modifier.modify(parentValue, newValue));
return new SignalOperation<>(new SignalOperation.Result<>(null));
} else {
return new SignalOperation<>(
new SignalOperation.Error<>("Unexpected child value"));
}
}

@Override
public CancelableOperation<C> update(SignalUpdater<C> childUpdater) {
Objects.requireNonNull(childUpdater);
C currentChildValue = getter.map(parent.peek());
C newChildValue = childUpdater.update(currentChildValue);
parent.modify(
parentValue -> modifier.modify(parentValue, newChildValue));

CancelableOperation<C> operation = new CancelableOperation<>();
operation.result()
.complete(new SignalOperation.Result<>(currentChildValue));
return operation;
}
}
Loading
Loading