Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.devbench.uibuilder.components.form.UIBuilderForm Maven / Gradle / Ivy
/*
*
* Copyright © 2018 Webvalto 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 io.devbench.uibuilder.components.form;
import com.vaadin.flow.component.*;
import com.vaadin.flow.component.dependency.HtmlImport;
import com.vaadin.flow.data.binder.HasItems;
import com.vaadin.flow.internal.StateNode;
import com.vaadin.flow.internal.nodefeature.ComponentMapping;
import com.vaadin.flow.internal.nodefeature.ElementPropertyMap;
import com.vaadin.flow.shared.Registration;
import elemental.json.Json;
import elemental.json.JsonArray;
import io.devbench.uibuilder.api.utils.elemental.json.JsonBuilder;
import io.devbench.uibuilder.components.form.event.*;
import io.devbench.uibuilder.components.form.exception.FormCollectionSpecialBindException;
import io.devbench.uibuilder.components.form.exception.FormSpecialBindException;
import io.devbench.uibuilder.components.form.validator.FormValidator;
import io.devbench.uibuilder.components.form.validator.FormValidatorResult;
import io.devbench.uibuilder.components.form.validator.PropertyValidityDescriptor;
import io.devbench.uibuilder.core.controllerbean.statenodemanager.BeanNode;
import io.devbench.uibuilder.core.controllerbean.statenodemanager.BindingNodeSyncError;
import io.devbench.uibuilder.core.controllerbean.statenodemanager.StateNodeManager;
import io.devbench.uibuilder.core.utils.ElementCollector;
import io.devbench.uibuilder.core.utils.HtmlElementAwareComponent;
import io.devbench.uibuilder.core.utils.reflection.ClassMetadata;
import io.devbench.uibuilder.core.utils.reflection.PropertyMetadata;
import org.apache.commons.lang3.StringUtils;
import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Collectors;
@Tag(UIBuilderForm.TAG_NAME)
@HtmlImport("frontend://bower_components/uibuilder-form/src/uibuilder-form.html")
public class UIBuilderForm extends UIBuilderFormBase implements PropertyValidityDescriptorFilterable {
private T formItem;
private ClassMetadata formItemMetadata;
private UIBuilderForm> parentForm;
private FormValidator validator;
private Set> childForms;
private Set bindings;
private StateNodeManager stateNodeManager;
private StateNode formItemStateNode;
private Set> resettableFields;
private Set unreachableFields;
private Map valueChangeRegistrations;
private Predicate propertyValidityDescriptorPredicate;
public UIBuilderForm() {
validator = new FormValidator();
childForms = new HashSet<>();
resettableFields = new HashSet<>();
unreachableFields = new HashSet<>();
valueChangeRegistrations = new HashMap<>();
UIBuilderFormRegistry.register(this);
addListener(FormFieldValueChangeEvent.class, e -> handleFormFieldValueChange());
addListener(FormResetConfirmedEvent.class, e -> handleFormResetConfirmed());
addListener(FormSaveConfirmedEvent.class, e -> handleFormSaveConfirmed());
addListener(FormItemAssignedEvent.class, e -> handleFormItemAssigned());
addListener(FormFieldChangeEvent.class, this::handleFormFieldChange);
addListener(FormResetEvent.class, this::handleFormReset);
addListener(FormSaveEvent.class, this::handleFormSave);
getElement().addEventListener("validate", event -> validate());
}
@Override
public Predicate getPropertyValidityDescriptorPredicate() {
return propertyValidityDescriptorPredicate;
}
@Override
public void setPropertyValidityDescriptorPredicate(Predicate propertyValidityDescriptorPredicate) {
this.propertyValidityDescriptorPredicate = propertyValidityDescriptorPredicate;
}
protected FormValidator getValidator() {
return validator;
}
private void handleFormReset(FormResetEvent event) {
if (!event.isProceedProcessingPresent()) {
handleFormResetConfirmed();
}
}
private void handleFormResetConfirmed() {
resettableFields.forEach(UIBuilderFormResettableField::reset);
}
private void handleFormSave(FormSaveEvent event) {
if (!event.isProceedProcessingPresent()) {
handleFormSaveConfirmed();
}
}
private void handleFormSaveConfirmed() {
resettableFields.forEach(UIBuilderFormResettableField::mark);
}
private void handleFormFieldValueChange() {
if (formItemMetadata != null && formItem != null) {
validate(stateNodeManager.synchronizeProperties());
handleUnreachableProperties();
}
}
private void handleFormFieldChange(FormFieldChangeEvent event) {
this.bindings = event.getBindings();
event.getChildFormIds().forEach(
childFormId -> UIBuilderFormRegistry.getById(childFormId).ifPresent(this::addChildForm)
);
if (formItem != null && stateNodeManager != null) {
initFormItem();
}
}
@SuppressWarnings("unchecked")
private final Consumer super UIBuilderForm> initChildForms = childForm -> {
childForm.updateFormItemByParent();
if (childForm.hasChildren()) {
childForm.getChildForms().forEach(this.initChildForms);
}
};
private void handleFormItemAssigned() {
getChildForms().forEach(initChildForms);
handleUnreachableProperties();
getElement().callFunction("_onFormReady");
}
private void handleUnreachableProperties() {
if (formItemMetadata == null || formItem == null) {
resetFieldsReachableState();
} else {
if (bindings != null) {
bindings.forEach(binding -> {
String itemBind = binding.getItemBind();
formItemMetadata.property(itemBind).ifPresent(propertyMetadata -> setFieldReachable(itemBind, isReachable(propertyMetadata)));
});
}
}
}
private boolean isReachable(PropertyMetadata> propertyMetadata) {
Object instance = propertyMetadata.getInstance();
return instance != null || BeanNode.findBeanNodeInstanceFactory(propertyMetadata.getContainerClass()).isPresent();
}
private void resetFieldsReachableState() {
unreachableFields.clear();
if (bindings != null) {
bindings.stream()
.map(FormFieldBinding::getItemBind)
.forEach(this::callSetFieldReachable);
}
}
private void callSetFieldReachable(String itemBind) {
callSetFieldReachable(itemBind, true);
}
private void callSetFieldReachable(String itemBind, boolean reachable) {
getElement().callFunction("_setFieldReachable", itemBind, reachable);
}
private void setFieldReachable(String itemBind, boolean reachable) {
if (reachable) {
if (unreachableFields.remove(itemBind)) {
callSetFieldReachable(itemBind);
}
} else {
if (unreachableFields.add(itemBind)) {
callSetFieldReachable(itemBind, false);
}
}
}
public boolean validate() {
return validate(Collections.emptyList());
}
private boolean validate(List bindingNodeSyncErrors) {
FormValidatorResult result = validator.validate(this.bindings, formItem);
FormValidatorResult.add(result, bindingNodeSyncErrors.stream().map(PropertyValidityDescriptor::create).collect(Collectors.toList()));
if (propertyValidityDescriptorPredicate != null) {
result.filter(propertyValidityDescriptorPredicate);
}
getElement().setPropertyJson(PROP_VALIDITY_DESCRIPTORS, result.toJson());
boolean valid = result.isValid() & getChildForms().stream().allMatch(UIBuilderFormBase::isValid);
setValid(valid);
bubbleValidation(valid);
return valid;
}
private void bubbleValidation(boolean valid) {
UIBuilderForm parentForm = getParentForm();
if (valid) {
if (parentForm != null) {
ComponentUtil.fireEvent(parentForm, FormFieldValueChangeEvent.nullEvent(this));
}
} else {
while (parentForm != null) {
parentForm.setValid(false);
parentForm = parentForm.getParentForm();
}
}
}
private void updateFormItemByParent() {
updateFormItemByParent(getItemBindValue(), parentForm.formItemMetadata);
}
private void updateFormItemByParent(String itemBindValue, ClassMetadata> parentFormItemMetadata) {
if (itemBindValue != null) {
if (parentFormItemMetadata != null) {
parentFormItemMetadata.property(itemBindValue).ifPresent(parentItemPropertyMetadata -> {
Object instance = parentItemPropertyMetadata.getInstance();
setFormItem(instance != null ? parentItemPropertyMetadata.getValue() : null);
});
} else {
setFormItem(null);
}
}
}
protected ClassMetadata getFormItemClassMetadata() {
return formItemMetadata;
}
public UIBuilderForm getParentForm() {
return parentForm;
}
public void setParentForm(UIBuilderForm> parentForm) {
if (parentForm != this.parentForm && this.parentForm != null) {
this.parentForm.removeChildForm(this, false);
}
this.parentForm = parentForm;
if (parentForm != null && !parentForm.getChildForms().contains(this)) {
parentForm.addChildForm(this, false);
}
}
private void addChildForm(UIBuilderForm> childForm, boolean updateParent) {
childForms.add(childForm);
if (updateParent) {
childForm.setParentForm(this);
}
}
public void addChildForm(UIBuilderForm> childForm) {
addChildForm(childForm, true);
}
private void removeChildForm(UIBuilderForm> childForm, boolean updateParent) {
childForms.remove(childForm);
if (updateParent) {
childForm.setParentForm(null);
}
}
/**
* Returns the child forms as an unmodifiable set. To modify the
* child forms use the following methods:
*
* {@link #addChildForm(UIBuilderForm)}
* {@link #removeChildForm(UIBuilderForm)}
* {@link #setParentForm(UIBuilderForm)}
*
*
* @return the child forms of the current form
*/
public Set getChildForms() {
return Collections.unmodifiableSet(childForms);
}
public void removeChildForm(UIBuilderForm> childForm) {
removeChildForm(childForm, true);
}
public boolean hasChildren() {
return !childForms.isEmpty();
}
private boolean isFormItemBindable() {
return this.formItem != null && bindings != null && !bindings.isEmpty();
}
public T getFormItem() {
if (formItem == null && parentForm != null) {
updateFormItemByParent();
}
return formItem;
}
public void setFormItem(T formItem) {
this.formItem = formItem;
if (isFormItemBindable()) {
initFormItem();
} else {
clearFormItem();
}
getElement().callFunction("_onFormItemAssigned");
}
private void removeValueChangeRegistrations() {
valueChangeRegistrations.values().forEach(Registration::remove);
valueChangeRegistrations.clear();
}
private void removeValueChangeRegistration(HasValue component) {
Registration registration = valueChangeRegistrations.remove(component);
if (registration != null) {
registration.remove();
}
}
private void clearFormItem() {
removeValueChangeRegistrations();
clearSpecial();
childForms.forEach(UIBuilderForm::clearFormItem);
resettableFields.clear();
formItemMetadata = null;
stateNodeManager = null;
formItemStateNode = null;
getElement().getNode().getFeature(ElementPropertyMap.class).setProperty(PROP_FORM_ITEM, formItemStateNode);
}
private void initFormItem() {
resettableFields.clear();
initFormItemMetadata();
stateNodeManager = new StateNodeManager(name -> formItemMetadata);
Set itemBinds = getPropertyBindings();
itemBinds.forEach(stateNodeManager::addBindingPath);
formItemStateNode = (StateNode) stateNodeManager.populatePropertyValues().get(PROP_FORM_ITEM);
stateNodeManager.synchronizeStateNodes();
getElement().getNode().getFeature(ElementPropertyMap.class).setProperty(PROP_FORM_ITEM, formItemStateNode);
registerUpdatableProperties(itemBinds);
withSpecials(this::bindSpecial);
}
private void clearSpecial() {
if (bindings != null) {
withSpecials(this::clearSpecial);
}
}
private void withSpecials(Consumer super FormFieldBinding> onSpecialBinding) {
List specials = Arrays.asList(PROP_ITEMS, PROP_SELECTED);
bindings.stream()
.filter(binding -> StringUtils.isNotBlank(binding.getComponentId()))
.filter(binding -> specials.contains(binding.getValueSourcePropertyName()))
.forEach(onSpecialBinding);
}
private void clearSpecial(FormFieldBinding binding) {
if (formItemMetadata != null) {
findComponentById(binding.getComponentId())
.ifPresent(component -> formItemMetadata.property(binding.getItemBind())
.ifPresent(propertyMetadata -> clearSpecialComponent(component, binding.getValueSourcePropertyName())));
}
}
@SuppressWarnings("unchecked")
private void clearSpecialComponent(Component component, String valueSourcePropertyName) {
if (PROP_SELECTED.equals(valueSourcePropertyName)) {
if (component instanceof HasValue) {
((HasValue) component).setValue(null);
}
} else if (PROP_ITEMS.equals(valueSourcePropertyName)) {
if (component instanceof HasItems) {
((HasItems) component).setItems(Collections.emptyList());
}
}
}
protected Optional findComponentById(String id) {
List foundComponents = new ArrayList<>();
UI.getCurrent().getInternals().getStateTree().getRootNode().visitNodeTree(stateNode -> {
if (stateNode.hasFeature(ComponentMapping.class)) {
ComponentMapping componentMapping = stateNode.getFeature(ComponentMapping.class);
componentMapping.getComponent().ifPresent(foundComponents::add);
}
});
Optional first = foundComponents.stream()
.filter(c -> c.getId().isPresent() && c.getId().get().equals(id))
.findFirst();
if (first.isPresent()) {
return first;
} else {
Optional comp = ElementCollector.getById(id, this);
return comp.map(HtmlElementAwareComponent::getComponent);
}
}
private void bindSpecial(FormFieldBinding binding) {
findComponentById(binding.getComponentId()).ifPresent(
component -> formItemMetadata.property(binding.getItemBind()).ifPresent(
propertyMetadata -> {
if (PROP_SELECTED.equals(binding.getValueSourcePropertyName())) {
bindSpecialSelection(component, propertyMetadata);
} else if (PROP_ITEMS.equals(binding.getValueSourcePropertyName())) {
bindSpecialCollection(component, propertyMetadata);
}
}));
}
private void bindSpecialSelection(Component component, PropertyMetadata> propertyMetadata) {
if (propertyMetadata.getInstance() != null && component instanceof HasValue) {
makeResetable(component, propertyMetadata);
updateValueChangeListener((HasValue) component, propertyMetadata);
}
}
private void makeResetable(Component component, PropertyMetadata> propertyMetadata) {
resettableFields.add(new UIBuilderFormResettableField<>(this, component, propertyMetadata, true));
}
private void updateValueChangeListener(HasValue, ?> hasValue, PropertyMetadata> propertyMetadata) {
removeValueChangeRegistration(hasValue);
hasValue.setValue(propertyMetadata.getValue()); // no need to use the clone, this is a selection field
addValueChangeRegistration(hasValue, propertyMetadata.getName());
}
private void addValueChangeRegistration(HasValue, ?> hasValue, String propertyName) {
Registration registration = hasValue.addValueChangeListener(
event -> updateHasValueField(propertyName, event.getValue()));
valueChangeRegistrations.put(hasValue, registration);
}
protected void updateHasValueField(String propertyName, V value) {
formItemMetadata.setPropertyValue(propertyName, value);
validate();
}
private - void bindSpecialCollection(Component component, PropertyMetadata> propertyMetadata) {
if (propertyMetadata.getInstance() != null) {
Class> propertyType = propertyMetadata.getType();
if (Collection.class.isAssignableFrom(propertyType)) {
Collection
- collection = ensureCollection(propertyMetadata.getValue(), propertyType);
if (component instanceof HasItems) {
bindToHasItemsCollection(collection, component, propertyMetadata);
} else {
throw new FormSpecialBindException("Invalid component, component must implement HasItems: " + component.getClass().getName());
}
} else {
throw new FormSpecialBindException("Invalid property type, required a collection, found: " + propertyType.getName());
}
}
}
@SuppressWarnings("unchecked")
protected static
- Collection
- ensureCollection(Object value, Class> type) {
if (value instanceof Collection) {
if (type.isAssignableFrom(value.getClass())) {
return (Collection
- ) value;
} else {
throw new FormCollectionSpecialBindException("Illegal collection type: " + value.getClass().getName() + " required: " + type.getName());
}
} else {
if (Set.class.isAssignableFrom(type)) {
return new HashSet<>();
} else if (List.class.isAssignableFrom(type)) {
return new ArrayList<>();
} else {
throw new FormCollectionSpecialBindException("Unsupported collection type: " + type.getName());
}
}
}
private
- void bindToHasItemsCollection(Collection
- collection, Component component, PropertyMetadata> propertyMetadata) {
requireCollectionProperty(component, propertyMetadata);
resettableFields.add(new UIBuilderFormResettableField<>(this, component, propertyMetadata, (Serializable) collection));
@SuppressWarnings("unchecked")
HasItems
- hasItems = (HasItems
- ) component;
hasItems.setItems(collection);
}
private void requireCollectionProperty(Component component, PropertyMetadata> propertyMetadata) {
ParameterizedType parameterizedType = propertyMetadata.getParameterizedType();
if (parameterizedType == null || parameterizedType.getActualTypeArguments() == null || parameterizedType.getActualTypeArguments().length != 1) {
throw new FormSpecialBindException(
"Property " + propertyMetadata.getName() + " is not a collection, cannot special bind to " + component.getClass().getName());
}
}
private Set
getPropertyBindings() {
List specials = Arrays.asList(PROP_FORM_ITEM, PROP_ITEMS, PROP_SELECTED);
return bindings.stream()
.filter(b -> !specials.contains(b.getValueSourcePropertyName()))
.map(b -> PROP_FORM_ITEM + "." + b.getItemBind())
.collect(Collectors.toSet());
}
@SuppressWarnings("unchecked")
private void initFormItemMetadata() {
formItemMetadata = ClassMetadata.ofClass((Class) formItem.getClass()).withInstance(formItem);
}
private void registerUpdatableProperties(Collection bindings) {
StateNode node = getElement().getNode();
JsonArray boundPropertyNames = createPropertyNames(bindings);
bindings.forEach(binding -> getElement().addSynchronizedProperty(binding));
ElementPropertyMap.getModel(node).setUpdateFromClientFilter(bindings::contains);
node.runWhenAttached(ui -> ui.getInternals().getStateTree().beforeClientResponse(node,
ctx -> ctx.getUI().getPage().executeJavaScript(
"this.registerUpdatableModelProperties($0, $1)",
getElement(), boundPropertyNames)));
}
private JsonArray createPropertyNames(Collection bindings) {
return bindings.stream().map(Json::create).collect(JsonBuilder.jsonArrayCollector());
}
}