All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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 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 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()); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy