/* * Copyright 2017 Matti Tahvonen. * * 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 org.vaadin.viritin.form; import com.vaadin.data.BeanValidationBinder; import com.vaadin.data.Binder; import com.vaadin.data.StatusChangeEvent; import com.vaadin.ui.AbstractComponentContainer; import com.vaadin.ui.AbstractField; import com.vaadin.ui.AbstractTextField; import com.vaadin.ui.Button; import com.vaadin.ui.Component; import com.vaadin.ui.CustomComponent; import com.vaadin.ui.HorizontalLayout; import com.vaadin.ui.UI; import com.vaadin.ui.Window; import java.io.Serializable; import org.vaadin.viritin.button.DeleteButton; import org.vaadin.viritin.button.MButton; import org.vaadin.viritin.button.PrimaryButton; import org.vaadin.viritin.layouts.MHorizontalLayout; /** * * @author mstahv */ public abstract class AbstractForm<T> extends CustomComponent { private boolean settingBean; public interface SavedHandler<T> extends Serializable { void onSave(T entity); } public interface ResetHandler<T> extends Serializable { void onReset(T entity); } public interface DeleteHandler<T> extends Serializable { void onDelete(T entity); } private T entity; private SavedHandler<T> savedHandler; private ResetHandler<T> resetHandler; private DeleteHandler<T> deleteHandler; private String modalWindowTitle = "Edit entry"; private String saveCaption = "Save"; private String deleteCaption = "Delete"; private String cancelCaption = "Cancel"; private Window popup; private Binder<T> binder; private boolean hasChanges = false; public AbstractForm(Class<T> entityType) { addAttachListener(new AttachListener() { private static final long serialVersionUID = 3193438171004932112L; @Override public void attach(AttachEvent event) { lazyInit(); } }); binder = new BeanValidationBinder<>(entityType); binder.addValueChangeListener(e -> { // binder.hasChanges is not really usefull so track it manually if (!settingBean) { hasChanges = true; } }); binder.addStatusChangeListener(e -> { // TODO optimize this // TODO see if explicitly calling writeBean would write also invalid // values -> would make functionality more logical and easier for // users to do validation and error reporting // Eh, value change listener is called after status change listener, so // ensure flag is on... if (!settingBean) { hasChanges = true; } adjustResetButtonState(); adjustSaveButtonState(); }); } /** * Sets the object to be edited by this form. This method binds all fields * from this form to given objects. * <p> * If your form needs to manually configure something based on the state of * the edited object, you can override this method to do that either before * the object is bound to fields or to do something after the bean binding. * * @param entity the object to be edited by this form */ public void setEntity(T entity) { this.entity = entity; this.settingBean = true; lazyInit(); if (entity != null) { binder.setBean(entity); hasChanges = false; setVisible(true); } else { binder.setBean(null); hasChanges = false; setVisible(false); } settingBean = false; } /** * @return true if bean has been changed since last setEntity call. */ public boolean hasChanges() { return hasChanges; } public void setSavedHandler(SavedHandler<T> savedHandler) { this.savedHandler = savedHandler; getSaveButton().setVisible(this.savedHandler != null); } public void setResetHandler(ResetHandler<T> resetHandler) { this.resetHandler = resetHandler; getResetButton().setVisible(this.resetHandler != null); } public void setDeleteHandler(DeleteHandler<T> deleteHandler) { this.deleteHandler = deleteHandler; getDeleteButton().setVisible(this.deleteHandler != null); } public ResetHandler<T> getResetHandler() { return resetHandler; } public SavedHandler<T> getSavedHandler() { return savedHandler; } public DeleteHandler<T> getDeleteHandler() { return deleteHandler; } public String getSaveCaption() { return saveCaption; } public void setSaveCaption(String saveCaption) { this.saveCaption = saveCaption; } public String getModalWindowTitle() { return modalWindowTitle; } public void setModalWindowTitle(String modalWindowTitle) { this.modalWindowTitle = modalWindowTitle; } public String getDeleteCaption() { return deleteCaption; } public void setDeleteCaption(String deleteCaption) { this.deleteCaption = deleteCaption; } public String getCancelCaption() { return cancelCaption; } public void setCancelCaption(String cancelCaption) { this.cancelCaption = cancelCaption; } public Binder<T> getBinder() { return binder; } public void setBinder(Binder<T> binder) { this.binder = binder; } protected void lazyInit() { if (getCompositionRoot() == null) { setCompositionRoot(createContent()); bind(); } } /** * By default just does simple name based binding. Override this method to * customize the binding. */ protected void bind() { binder.bindInstanceFields(this); } /** * 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: * <pre><code> * 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() * ); * } * } * </code></pre> * * @return the content of the form * */ protected abstract Component createContent(); protected void adjustSaveButtonState() { if (isBound()) { boolean valid = binder.isValid(); getSaveButton().setEnabled(hasChanges() && valid); } } public Button getSaveButton() { if (saveButton == null) { setSaveButton(createSaveButton()); } return saveButton; } protected Button createSaveButton() { return new PrimaryButton(getSaveCaption()) .withVisible(false); } private Button saveButton; protected boolean isBound() { return binder != null && binder.getBean() != null; } protected Button createResetButton() { return new MButton(getCancelCaption()) .withVisible(false); } private Button resetButton; public Button getResetButton() { if (resetButton == null) { setResetButton(createResetButton()); } return resetButton; } public void setResetButton(Button resetButton) { this.resetButton = resetButton; this.resetButton.addClickListener(new Button.ClickListener() { private static final long serialVersionUID = -19755976436277487L; @Override public void buttonClick(Button.ClickEvent event) { reset(event); } }); } 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() { private static final long serialVersionUID = -2693734056915561664L; @Override public void buttonClick(Button.ClickEvent event) { delete(event); } }); } public Button getDeleteButton() { if (deleteButton == null) { setDeleteButton(createDeleteButton()); } return deleteButton; } 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 (isBound()) { boolean modified = hasChanges(); getResetButton().setEnabled(modified || popup != null); } } public void setSaveButton(Button button) { this.saveButton = button; saveButton.addClickListener(new Button.ClickListener() { private static final long serialVersionUID = -2058398434893034442L; @Override public void buttonClick(Button.ClickEvent event) { save(event); } }); } /** * @return the currently edited entity or null if the form is currently * unbound */ public T getEntity() { return entity; } protected void save(Button.ClickEvent e) { savedHandler.onSave(getEntity()); hasChanges = false; adjustSaveButtonState(); adjustResetButtonState(); } protected void reset(Button.ClickEvent e) { resetHandler.onReset(getEntity()); hasChanges = false; adjustSaveButtonState(); adjustResetButtonState(); } protected void delete(Button.ClickEvent e) { deleteHandler.onDelete(getEntity()); hasChanges = false; } /** * @return A default toolbar containing save/cancel/delete buttons */ public HorizontalLayout getToolbar() { return new MHorizontalLayout( getSaveButton(), getResetButton(), getDeleteButton() ); } public Window openInModalPopup() { popup = new Window(getModalWindowTitle(), this); popup.setModal(true); UI.getCurrent().addWindow(popup); focusFirst(); return popup; } /** * 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; for (Component component : cc) { 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 AbstractComponentContainer) { if (findFieldAndFocus(component)) { return true; } } } } return false; } /** * * @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; } public void closePopup() { if(getPopup() != null) { getPopup().close(); } } }