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

io.graphenee.vaadin.TRAbstractBaseForm Maven / Gradle / Ivy

/*******************************************************************************
 * Copyright (c) 2016, 2018 Farrukh Ijaz
 *
 * 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.graphenee.vaadin;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import com.vaadin.ui.AbstractComponent;
import com.vaadin.ui.AbstractComponentContainer;
import com.vaadin.ui.AbstractField;
import com.vaadin.ui.AbstractSingleComponentContainer;
import com.vaadin.ui.AbstractTextField;
import com.vaadin.ui.Alignment;
import com.vaadin.ui.Button;
import com.vaadin.ui.Component;
import com.vaadin.ui.CustomComponent;
import com.vaadin.ui.Field;
import com.vaadin.ui.ProgressBar;
import com.vaadin.ui.UI;
import com.vaadin.ui.Window;
import com.vaadin.ui.themes.ValoTheme;
import com.vaadin.util.ReflectTools;

import org.vaadin.viritin.BeanBinder;
import org.vaadin.viritin.MBeanFieldGroup;
import org.vaadin.viritin.MBeanFieldGroup.FieldGroupListener;
import org.vaadin.viritin.button.DeleteButton;
import org.vaadin.viritin.button.MButton;
import org.vaadin.viritin.button.PrimaryButton;
import org.vaadin.viritin.label.MLabel;
import org.vaadin.viritin.label.RichText;
import org.vaadin.viritin.layouts.MHorizontalLayout;
import org.vaadin.viritin.layouts.MVerticalLayout;

import io.graphenee.vaadin.util.VaadinUtils;

/**
 * This form has been fixed to handle situation where the creation of the form
 * depends on the content of entity. Original form was copied from https://github.com/viritin/viritin/wiki/AbstractForm
 * 
 * @author ijazfx
 * @param  - typically a java bean type
 */
public abstract class TRAbstractBaseForm extends CustomComponent implements FieldGroupListener {

	private static final long serialVersionUID = 1L;

	private boolean binding = false;

	private String modalWindowTitle = "Edit entry";
	private String saveCaption = "Save";
	private String deleteCaption = "Delete";
	private String cancelCaption = "Cancel";
	private ProgressBar busyIndicator = new ProgressBar();
	private Set> validationListeners = new HashSet<>();

	public static class ValidityChangedEvent extends Component.Event {

		private static final Method method = ReflectTools.findMethod(ValidityChangedListener.class, "onValidityChanged",
				ValidityChangedEvent.class);

		public ValidityChangedEvent(Component source) {
			super(source);
		}

		@Override
		public TRAbstractBaseForm getComponent() {
			return (TRAbstractBaseForm) super.getComponent();
		}

	}

	public interface ValidityChangedListener extends Serializable {

		public void onValidityChanged(ValidityChangedEvent event);
	}

	public interface ValidationListener extends Serializable {

		public void onValidation(T entity, boolean isValid);

	}

	private Window popup;

	public TRAbstractBaseForm() {
		addAttachListener(new AttachListener() {
			@Override
			public void attach(AttachEvent event) {
				lazyInit();
				// adjustResetButtonState();
			}
		});

		addValidationListener(new ValidationListener() {

			@Override
			public void onValidation(T entity, boolean isValid) {
				TRAbstractBaseForm.this.onValidation(entity, isValid);
			}
		});

	}

	protected void onValidation(T entity, boolean isValid) {
	}

	protected void lazyInit() {
		if (getCompositionRoot() == null) {
			setCompositionRoot(new MVerticalLayout());
			// adjustSaveButtonState();
			// adjustResetButtonState();
		}
	}

	private MBeanFieldGroup fieldGroup;

	/**
	 * The validity checked and cached on last change. Should be pretty much always
	 * up to date due to eager changes. At least after onFieldGroupChange call.
	 */
	boolean isValid = false;

	private RichText beanLevelViolations;

	@Override
	public void onFieldGroupChange(MBeanFieldGroup beanFieldGroup) {
		boolean wasValid = isValid;
		isValid = fieldGroup.isValid();
		notifyValidationListeners(isValid);
		adjustSaveButtonState();
		adjustResetButtonState();
		if (wasValid != isValid) {
			fireValidityChangedEvent();
		}
		updateConstraintViolationsDisplay();
	}

	private void notifyValidationListeners(boolean isValid) {
		validationListeners.forEach(validationListener -> {
			validationListener.onValidation(getEntity(), isValid);
		});
	}

	protected void updateConstraintViolationsDisplay() {
		if (beanLevelViolations != null) {
			Collection errorMessages = getFieldGroup().getBeanLevelValidationErrors();
			if (!errorMessages.isEmpty()) {
				StringBuilder sb = new StringBuilder();
				for (String e : errorMessages) {
					sb.append(e);
					sb.append("
"); } beanLevelViolations.setValue(sb.toString()); beanLevelViolations.setVisible(true); } else { beanLevelViolations.setVisible(false); beanLevelViolations.setValue(""); } } } public Component getConstraintViolationsDisplay() { if (beanLevelViolations == null) { beanLevelViolations = new RichText(); beanLevelViolations.setVisible(false); beanLevelViolations.setStyleName(ValoTheme.LABEL_FAILURE); } return beanLevelViolations; } public boolean isValid() { return isValid; } protected void adjustSaveButtonState() { if (isEagerValidation() && isBound()) { // boolean beanModified = fieldGroup.isBeanModified(); // getSaveButton().setEnabled(beanModified && isValid()); getSaveButton().setEnabled(isValid()); } } protected boolean isBound() { return fieldGroup != null; } protected void adjustResetButtonState() { if (popup != null && popup.getParent() != null) { // Assume cancel button in a form opened to a popup also closes // it, allows closing via cancel button by default getResetButton().setEnabled(true); return; } if (isEagerValidation() && isBound()) { boolean modified = fieldGroup.isBeanModified(); getResetButton().setEnabled(modified || popup != null); } } public void addValidationListener(ValidationListener listener) { validationListeners.add(listener); } public void removeValidationListener(ValidationListener listener) { validationListeners.remove(listener); } public void addValidityChangedListener(ValidityChangedListener listener) { addListener(ValidityChangedEvent.class, listener, ValidityChangedEvent.method); } public void removeValidityChangedListener(ValidityChangedListener listener) { removeListener(ValidityChangedEvent.class, listener, ValidityChangedEvent.method); } private void fireValidityChangedEvent() { fireEvent(new ValidityChangedEvent(this)); } public interface SavedHandler extends Serializable { void onSave(T entity); } public interface ResetHandler extends Serializable { void onReset(T entity); } public interface DeleteHandler extends Serializable { void onDelete(T entity); } private T entity; private SavedHandler savedHandler; private ResetHandler resetHandler; private DeleteHandler deleteHandler; private boolean eagerValidation = true; public boolean isEagerValidation() { return eagerValidation; } /** * In case one is working with "detached entities" enabling eager validation * will highly improve usability. The validity of the form will be updated on * each changes and save/cancel buttons will reflect to the validity and * possible changes. * * @param eagerValidation true if the form should have eager validation */ public void setEagerValidation(boolean eagerValidation) { this.eagerValidation = eagerValidation; } public MBeanFieldGroup setEntity(Class entityClass, T originalEntity) { lazyInit(); this.entity = originalEntity; if (entity != null) { setCompositionRoot(createContent()); if (isBound()) { fieldGroup.unbind(); } binding = true; fieldGroup = bindEntity(entity); binding = false; for (Map.Entry, Collection> e : mValidators.entrySet()) { fieldGroup.addValidator(e.getKey(), e.getValue().toArray(new AbstractComponent[e.getValue().size()])); } for (Map.Entry e : validatorToErrorTarget.entrySet()) { fieldGroup.setValidationErrorTarget(e.getKey(), e.getValue()); } isValid = fieldGroup.isValid(); if (isEagerValidation()) { fieldGroup.withEagerValidation(this); notifyValidationListeners(isValid); adjustSaveButtonState(); adjustResetButtonState(); } fieldGroup.hideInitialEmpyFieldValidationErrors(); setVisible(true); return fieldGroup; } else { setVisible(false); return null; } } @Deprecated public MBeanFieldGroup setEntity(T originalEntity) { if (originalEntity != null) return this.setEntity((Class) originalEntity.getClass(), originalEntity); return this.setEntity(null, null); } protected Component getBindingComponent() { return this; } /** * Creates a field group, configures the fields, binds the entity to those * fields * * @param entity The entity to bind * @return the fieldGroup created */ protected MBeanFieldGroup bindEntity(T entity) { return bindEntityWithComponent(entity, getBindingComponent()); } final protected MBeanFieldGroup bindEntityWithComponent(T entity, Component c) { return bindEntityWithComponentAndNestedProperties(entity, c, getNestedProperties()); } final protected MBeanFieldGroup bindEntityWithComponentAndNestedProperties(T entity, Component c, String... nestedProperties) { preBinding(entity); MBeanFieldGroup beanFieldGroup = BeanBinder.bind(entity, c, nestedProperties); beanFieldGroup.setValidateAllProperties(false); postBinding(entity); return beanFieldGroup; } protected void preBinding(T entity) { } protected void postBinding(T entity) { } private String[] nestedProperties; public String[] getNestedProperties() { return nestedProperties; } public void setNestedProperties(String... nestedProperties) { this.nestedProperties = nestedProperties; } /** * Sets the given object to be a handler for saved,reset,deleted, based on what * it happens to implement. * * @param handler the handler to be set as saved/reset/delete handler */ public void setHandler(Object handler) { if (handler != null) { if (handler instanceof SavedHandler) { setSavedHandler((SavedHandler) handler); } if (handler instanceof ResetHandler) { setResetHandler((ResetHandler) handler); } if (handler instanceof DeleteHandler) { setDeleteHandler((DeleteHandler) handler); } } } public void setSavedHandler(SavedHandler savedHandler) { this.savedHandler = savedHandler; getSaveButton().setVisible(this.savedHandler != null); } public void setResetHandler(ResetHandler resetHandler) { this.resetHandler = resetHandler; getResetButton().setVisible(this.resetHandler != null); } public void setDeleteHandler(DeleteHandler deleteHandler) { this.deleteHandler = deleteHandler; getDeleteButton().setVisible(this.deleteHandler != null); } public ResetHandler getResetHandler() { return resetHandler; } public SavedHandler getSavedHandler() { return savedHandler; } public DeleteHandler getDeleteHandler() { return deleteHandler; } public Window openInModalPopup() { popup = new Window(getModalWindowTitle(), this); popup.setModal(true); UI.getCurrent().addWindow(popup); focusFirst(); // localize popup... localizeRecursively(this); return popup; } /** * @return the last Popup into which the Form was opened with #openInModalPopup * method or null if the form hasn't been use in window */ public Window getPopup() { return popup; } /** * If the form is opened into a popup window using openInModalPopup(), you you * can use this method to close the popup. */ public void closePopup() { if (popup != null) { popup.close(); popup = null; } } /** * @return A default toolbar containing save/cancel/delete buttons */ public MHorizontalLayout getToolbar() { MLabel spacer = new MLabel("").withFullWidth(); busyIndicator.setVisible(false); busyIndicator.setIndeterminate(true); MHorizontalLayout layout = new MHorizontalLayout().withSpacing(true).withDefaultComponentAlignment(Alignment.MIDDLE_RIGHT); addButtonsToFooter(layout); layout.addComponents(spacer, busyIndicator, getSaveButton(), getResetButton(), getDeleteButton()); layout.setExpandRatio(spacer, 1); return layout; } protected void addButtonsToFooter(MHorizontalLayout footer) { } protected Button createCancelButton() { return new MButton(getCancelCaption()).withVisible(false); } private Button resetButton; public Button getResetButton() { if (resetButton == null) { setResetButton(createCancelButton()); } return resetButton; } public void setResetButton(Button resetButton) { this.resetButton = resetButton; this.resetButton.addClickListener(new Button.ClickListener() { @Override public void buttonClick(Button.ClickEvent event) { reset(event); } }); } protected Button createSaveButton() { return new PrimaryButton(getSaveCaption()).withVisible(false); } private Button saveButton; public void setSaveButton(Button saveButton) { this.saveButton = saveButton; saveButton.addClickListener(new Button.ClickListener() { @Override public void buttonClick(Button.ClickEvent event) { try { saveButton.setEnabled(false); busyIndicator.setVisible(true); UI.getCurrent().push(); save(event); } finally { saveButton.setEnabled(true); busyIndicator.setVisible(false); UI.getCurrent().push(); } } }); } public Button getSaveButton() { if (saveButton == null) { setSaveButton(createSaveButton()); } return saveButton; } protected Button createDeleteButton() { return new DeleteButton(getDeleteCaption()).withVisible(false); } private Button deleteButton; public void setDeleteButton(final Button deleteButton) { this.deleteButton = deleteButton; deleteButton.addClickListener(new Button.ClickListener() { @Override public void buttonClick(Button.ClickEvent event) { delete(event); } }); } public Button getDeleteButton() { if (deleteButton == null) { setDeleteButton(createDeleteButton()); } return deleteButton; } protected void save(Button.ClickEvent e) { savedHandler.onSave(getEntity()); getFieldGroup().setBeanModified(false); adjustResetButtonState(); adjustSaveButtonState(); } protected void reset(Button.ClickEvent e) { resetHandler.onReset(getEntity()); getFieldGroup().setBeanModified(false); adjustResetButtonState(); adjustSaveButtonState(); } protected void delete(Button.ClickEvent e) { deleteHandler.onDelete(getEntity()); } /** * Focuses the first field found from the form. It often improves UX to call * this method, or focus another field, when you assign a bean for editing. */ public void focusFirst() { Component compositionRoot = getCompositionRoot(); findFieldAndFocus(compositionRoot); } private boolean findFieldAndFocus(Component compositionRoot) { if (compositionRoot instanceof AbstractComponentContainer) { AbstractComponentContainer cc = (AbstractComponentContainer) compositionRoot; Iterator iterator = cc.iterator(); while (iterator.hasNext()) { Component component = iterator.next(); if (component.isReadOnly()) continue; if (component instanceof AbstractTextField) { AbstractTextField abstractTextField = (AbstractTextField) component; abstractTextField.selectAll(); return true; } if (component instanceof AbstractField) { AbstractField abstractField = (AbstractField) component; abstractField.focus(); return true; } if (component instanceof AbstractSingleComponentContainer) { AbstractSingleComponentContainer container = (AbstractSingleComponentContainer) component; if (findFieldAndFocus(container.getContent())) { return true; } } if (component instanceof AbstractComponentContainer) { if (findFieldAndFocus(component)) { return true; } } } } else if (compositionRoot instanceof AbstractSingleComponentContainer) { AbstractSingleComponentContainer container = (AbstractSingleComponentContainer) compositionRoot; if (findFieldAndFocus(container.getContent())) { return true; } } return false; } Set lockedFields = new HashSet<>(); public void lockFields() { lockFields(getCompositionRoot()); } private void lockFields(Component compositionRoot) { if (compositionRoot instanceof AbstractComponentContainer) { AbstractComponentContainer cc = (AbstractComponentContainer) compositionRoot; Iterator iterator = cc.iterator(); while (iterator.hasNext()) { Component component = iterator.next(); if (component.isReadOnly()) continue; if (component instanceof AbstractTextField) { AbstractTextField abstractTextField = (AbstractTextField) component; if (abstractTextField.isEnabled()) { lockedFields.add(abstractTextField); } } if (component instanceof AbstractField) { AbstractField abstractField = (AbstractField) component; if (abstractField.isEnabled()) { lockedFields.add(abstractField); } } if (component instanceof AbstractSingleComponentContainer) { AbstractSingleComponentContainer container = (AbstractSingleComponentContainer) component; lockFields(container.getContent()); } if (component instanceof AbstractComponentContainer) { lockFields(component); } } } else if (compositionRoot instanceof AbstractSingleComponentContainer) { AbstractSingleComponentContainer container = (AbstractSingleComponentContainer) compositionRoot; lockFields(container.getContent()); } lockedFields.forEach(field -> { field.setReadOnly(true); }); } public void unlockFields() { lockedFields.forEach(field -> { field.setReadOnly(false); }); lockedFields.clear(); } /** * This method should return the actual content of the form, including possible * toolbar. Use setEntity(T entity) to fill in the data. Am example * implementation could look like this: * *
	 * 
	 * public class PersonForm extends AbstractForm<Person> {
	 *
	 *     private TextField firstName = new MTextField("First Name");
	 *     private TextField lastName = new MTextField("Last Name");
	 *
	 *    {@literal @}Override
	 *     protected Component createContent() {
	 *         return new MVerticalLayout(
	 *                 new FormLayout(
	 *                         firstName,
	 *                         lastName
	 *                 ),
	 *                 getToolbar()
	 *         );
	 *     }
	 * }
	 * 
	 * 
* * @return the content of the form */ protected abstract Component createContent(); public MBeanFieldGroup getFieldGroup() { return fieldGroup; } public T getEntity() { return entity; } private final LinkedHashMap, Collection> mValidators = new LinkedHashMap, Collection>(); private final Map validatorToErrorTarget = new LinkedHashMap(); public void setValidationErrorTarget(Class aClass, AbstractComponent errorTarget) { validatorToErrorTarget.put(aClass, errorTarget); if (getFieldGroup() != null) { getFieldGroup().setValidationErrorTarget(aClass, errorTarget); } } /** * EXPERIMENTAL: The cross field validation support is still experimental and * its API is likely to change. * * @param validator a validator that validates the whole bean making cross field * validation much simpler * @param fields the ui fields that this validator affects and on which a * possible error message is shown. * @return this FieldGroup */ public TRAbstractBaseForm addValidator(MBeanFieldGroup.MValidator validator, AbstractComponent... fields) { mValidators.put(validator, Arrays.asList(fields)); if (getFieldGroup() != null) { getFieldGroup().addValidator(validator, fields); } return this; } public TRAbstractBaseForm removeValidator(MBeanFieldGroup.MValidator validator) { Collection remove = mValidators.remove(validator); if (remove != null) { if (getFieldGroup() != null) { getFieldGroup().removeValidator(validator); } } return this; } /** * Removes all MValidators added the MFieldGroup * * @return the instance */ public TRAbstractBaseForm clearValidators() { mValidators.clear(); if (getFieldGroup() != null) { getFieldGroup().clear(); } return this; } public void setRequired(Field... fields) { for (Field field : fields) { field.setRequired(true); } } public String getModalWindowTitle() { return modalWindowTitle; } public void setModalWindowTitle(String modalWindowTitle) { this.modalWindowTitle = modalWindowTitle; } public String getCancelCaption() { return cancelCaption; } public void setCancelCaption(String cancelCaption) { this.cancelCaption = cancelCaption; } public String getSaveCaption() { return saveCaption; } public void setSaveCaption(String saveCaption) { this.saveCaption = saveCaption; if (saveButton != null) saveButton.setCaption(saveCaption); } public String getDeleteCaption() { return deleteCaption; } public void setDeleteCaption(String deleteCaption) { this.deleteCaption = deleteCaption; } public TRAbstractBaseForm withI18NCaption(String saveCaption, String deleteCaption, String cancelCaption) { this.saveCaption = saveCaption; this.deleteCaption = deleteCaption; this.cancelCaption = cancelCaption; return this; } protected boolean isBinding() { return this.binding; } protected String localizedSingularValue(String key) { return VaadinUtils.localizedSingularValue(key); } protected String localizedPluralValue(String key) { return VaadinUtils.localizedSingularValue(key); } protected void localizeRecursively(Component component) { VaadinUtils.localizeRecursively(component); } protected String localizedSingularValue(Locale locale, String key) { return VaadinUtils.localizedSingularValue(key); } protected String localizedPluralValue(Locale locale, String key) { return VaadinUtils.localizedSingularValue(key); } protected void localizeRecursively(Locale locale, Component component) { VaadinUtils.localizeRecursively(component); } protected int browserWidth() { return VaadinUtils.browserWidth(); } protected int browserHeight() { return VaadinUtils.browserHeight(); } protected String safeWidthInPixels(int width) { return VaadinUtils.safeWidth(width) + "px"; } protected String safeHeightInPixels(int height) { return VaadinUtils.safeHeight(height) + "px"; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy