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
48 changes: 44 additions & 4 deletions flow-server/src/main/java/com/vaadin/flow/dom/Element.java
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,50 @@ public Element setPropertyMap(String name, Map<String, ?> value) {
return setPropertyJson(name, JacksonUtils.mapToJson(value));
}

/**
* Binds a {@link Signal}'s value to a given property and keeps the property
* value synchronized with the signal value while the element is in attached
* state. When the element is in detached state, signal value changes have
* no effect. <code>null</code> signal unbinds existing binding.
* <p>
* Same rules applies for the property name and value from the bound Signal
* as in {@link #setProperty(String, String)}.
* <p>
* While a Signal is bound to a property, any attempt to set property value
* manually throws {@link BindingActiveException}. Same happens when trying
* to bind a new Signal while one is already bound.
* <p>
* Supported data types for the signal are the same as for the various
* {@code setProperty} methods in this class: {@link String},
* {@link Boolean}, {@link Double}, {@link BaseJsonNode}, {@link Object}
* (bean), {@link List} and {@link Map}.
* <p>
* Example of usage:
*
* <pre>
* ValueSignal&lt;String&gt; signal = new ValueSignal&lt;&gt;("");
* Element element = new Element("span");
* getElement().appendChild(element);
* element.bindProperty("mol", signal);
* signal.value("42"); // The element now has property mol="42"
* </pre>
*
* @param name
* the name of the property
* @param signal
* the signal to bind or <code>null</code> to unbind any existing
* binding
* @throws com.vaadin.signals.BindingActiveException
* thrown when there is already an existing binding
* @see #setProperty(String, String)
*/
public Element bindProperty(String name, Signal<?> signal) {
verifySetPropertyName(name);

getStateProvider().bindPropertySignal(this, name, signal);
return this;
}

/**
* Adds a property change listener which is triggered when the property's
* value is updated on the server side.
Expand Down Expand Up @@ -947,10 +991,6 @@ public String getProperty(String name, String defaultValue) {
Object value = getPropertyRaw(name);
if (value == null || value instanceof NullNode) {
return defaultValue;
} else if (value instanceof JsonNode) {
return ((JsonNode) value).toString();
} else if (value instanceof NullNode) {
return defaultValue;
} else if (value instanceof Number) {
double doubleValue = ((Number) value).doubleValue();
int intValue = (int) doubleValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ DomListenerRegistration addEventListener(StateNode node, String eventType,
* the property value
* @param emitChange
* true to create a change event for the client side
* @throws com.vaadin.signals.BindingActiveException
* thrown when a signal binding exists for the property
*/
void setProperty(StateNode node, String name, Serializable value,
boolean emitChange);
Expand All @@ -255,9 +257,28 @@ void setProperty(StateNode node, String name, Serializable value,
* the node containing the data
* @param name
* the property name, not <code>null</code>
* @throws com.vaadin.signals.BindingActiveException
* thrown when a signal binding exists for the property
*/
void removeProperty(StateNode node, String name);

/**
* Binds the given signal to the given property. <code>null</code> signal
* unbinds existing binding.
*
* @param owner
* the owner element for which the signal is bound, not
* <code>null</code>
* @param name
* the property name, not <code>null</code>
* @param signal
* the signal to bind or <code>null</code> to unbind any existing
* binding
* @throws com.vaadin.signals.BindingActiveException
* thrown when there is already an existing binding
*/
void bindPropertySignal(Element owner, String name, Signal<?> signal);

/**
* Checks if the given property has been set.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ public void setProperty(StateNode node, String name, Serializable value,
throw new UnsupportedOperationException();
}

@Override
public void bindPropertySignal(Element owner, String name,
Signal<?> signal) {
throw new UnsupportedOperationException();
}

@Override
public void removeProperty(StateNode node, String name) {
throw new UnsupportedOperationException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
import com.vaadin.flow.internal.nodefeature.VirtualChildrenList;
import com.vaadin.flow.server.AbstractStreamResource;
import com.vaadin.flow.shared.Registration;
import com.vaadin.signals.BindingActiveException;
import com.vaadin.signals.Signal;

/**
Expand Down Expand Up @@ -281,16 +282,38 @@ public void setProperty(StateNode node, String name, Serializable value,
assert node != null;
assert name != null;

if (getPropertyFeature(node).hasSignal(name)) {
throw new BindingActiveException(
"setProperty is not allowed while a binding for the given property exists.");
}

getPropertyFeature(node).setProperty(name, value, emitChange);
}

@Override
public void bindPropertySignal(Element owner, String name,
Signal<?> signal) {
assert owner != null;
assert name != null;

getPropertyFeature(owner.getNode()).bindSignal(owner, name, signal);
}

@Override
public void removeProperty(StateNode node, String name) {
assert node != null;
assert name != null;

getPropertyFeatureIfInitialized(node)
.ifPresent(feature -> feature.removeProperty(name));
ElementPropertyMap elementPropertyMap = getPropertyFeatureIfInitialized(
node).orElse(null);
if (elementPropertyMap != null) {
if (elementPropertyMap.hasSignal(name)) {
throw new BindingActiveException(
"removeProperty is not allowed while a binding for the given property exists.");
}

elementPropertyMap.removeProperty(name);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ public void setProperty(StateNode node, String name, Serializable value,
throw new UnsupportedOperationException();
}

@Override
public void bindPropertySignal(Element owner, String name,
Signal<?> signal) {
throw new UnsupportedOperationException();
}

@Override
public void removeProperty(StateNode node, String name) {
throw new UnsupportedOperationException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@ public void setProperty(String name, Serializable value,
assert name != null;
assert isValidValueType(value);

put(name, value, emitChange);
if (hasSignal(name)) {
SignalBinding b = (SignalBinding) super.get(name);
put(name, new SignalBinding(b.signal(), b.registration(), value),
emitChange);
} else {
put(name, value, emitChange);
}
}

/**
Expand Down Expand Up @@ -130,4 +136,29 @@ public static boolean isValidValueType(Serializable value) {
|| StateNode.class.isAssignableFrom(type);
}

@Override
protected Serializable get(String key) {
Serializable value = super.get(key);
if (value instanceof SignalBinding) {
return ((SignalBinding) value).value();
} else {
return value;
}
}

public boolean hasSignal(String key) {
return super.get(key) instanceof SignalBinding binding
&& binding.signal() != null && binding.registration() != null;
}

@Override
public void updateFromClient(String key, Serializable value) {
if (hasSignal(key)) {
SignalBinding b = (SignalBinding) super.get(key);
super.updateFromClient(key,
new SignalBinding(b.signal(), b.registration(), value));
} else {
super.updateFromClient(key, value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,18 @@

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tools.jackson.databind.node.BaseJsonNode;

import com.vaadin.flow.dom.Element;
import com.vaadin.flow.dom.ElementEffect;
import com.vaadin.flow.dom.PropertyChangeEvent;
import com.vaadin.flow.dom.PropertyChangeListener;
import com.vaadin.flow.function.SerializablePredicate;
import com.vaadin.flow.internal.JacksonUtils;
import com.vaadin.flow.internal.StateNode;
import com.vaadin.flow.shared.Registration;
import com.vaadin.signals.BindingActiveException;
import com.vaadin.signals.Signal;

/**
* Map for element property values.
Expand Down Expand Up @@ -112,6 +117,64 @@ public void setProperty(String name, Serializable value) {
setProperty(name, value, true);
}

/**
* Binds the given signal to the given property. <code>null</code> signal
* unbinds existing binding.
*
* @param owner
* the element owning the property, not <code>null</code>
* @param name
* the name of the property
* @param signal
* the signal to bind or <code>null</code> to unbind any existing
* binding
*/
public void bindSignal(Element owner, String name, Signal<?> signal) {
SignalBinding previousSignalBinding;
if (super.getProperty(name) instanceof SignalBinding binding) {
previousSignalBinding = binding;
} else {
previousSignalBinding = null;
}
if (signal != null && previousSignalBinding != null
&& previousSignalBinding.signal() != null) {
throw new BindingActiveException();
}
Registration registration = signal != null
? ElementEffect.bind(owner, signal,
(element, value) -> setPropertyFromSignal(name, value))
: null;
if (signal == null && previousSignalBinding != null) {
if (previousSignalBinding.registration() != null) {
previousSignalBinding.registration().remove();
}
put(name, get(name), false);
} else {
put(name, new SignalBinding(signal, registration, get(name)),
false);
}
}

public void setPropertyFromSignal(String name, Object value) {
assert !forbiddenProperties.contains(name)
: "Forbidden property name: " + name;

Serializable valueToSet;
if (value == null) {
valueToSet = JacksonUtils.nullNode();
} else if (value instanceof String || value instanceof Number
|| value instanceof Boolean || value instanceof BaseJsonNode) {
valueToSet = (Serializable) value;
} else if (value instanceof List) {
// List type conversion (return type ArrayNode)
valueToSet = JacksonUtils.listToJson((List<?>) value);
} else {
// Map and Bean/Object types conversion (return type ObjectNode)
valueToSet = JacksonUtils.beanToJson(value);
}
setProperty(name, valueToSet, true);
}

/**
* Adds a property change listener.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ public Stream<Serializable> streamValues() {
}
}

record SignalBinding(Signal<String> signal, Registration registration,
public record SignalBinding(Signal<?> signal, Registration registration,
Serializable value) implements Serializable {
}

Expand Down
Loading
Loading