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

com.vaadin.flow.component.crud.Crud Maven / Gradle / Ivy

There is a newer version: 24.4.12
Show newest version
/**
 * Copyright 2000-2024 Vaadin Ltd.
 *
 * This program is available under Vaadin Commercial License and Service Terms.
 *
 * See {@literal } for the full
 * license.
 */
package com.vaadin.flow.component.crud;

import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import com.vaadin.flow.component.AttachEvent;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.ComponentEvent;
import com.vaadin.flow.component.ComponentEventListener;
import com.vaadin.flow.component.ComponentUtil;
import com.vaadin.flow.component.DomEvent;
import com.vaadin.flow.component.EventData;
import com.vaadin.flow.component.HasSize;
import com.vaadin.flow.component.HasStyle;
import com.vaadin.flow.component.HasTheme;
import com.vaadin.flow.component.Tag;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.dependency.JsModule;
import com.vaadin.flow.component.dependency.NpmPackage;
import com.vaadin.flow.component.grid.Grid;
import com.vaadin.flow.component.shared.SlotUtils;
import com.vaadin.flow.data.provider.DataProvider;
import com.vaadin.flow.data.renderer.LitRenderer;
import com.vaadin.flow.internal.JsonSerializer;
import com.vaadin.flow.shared.Registration;

import elemental.json.JsonObject;

/**
 * A component for performing CRUD
 * operations on a data backend (e.g entities from a database).
 *
 * @param 
 *            the bean type
 * @author Vaadin Ltd
 */
@Tag("vaadin-crud")
@NpmPackage(value = "@vaadin/polymer-legacy-adapter", version = "24.4.7")
@JsModule("@vaadin/polymer-legacy-adapter/style-modules.js")
@NpmPackage(value = "@vaadin/crud", version = "24.4.7")
@JsModule("@vaadin/crud/src/vaadin-crud.js")
@JsModule("@vaadin/crud/src/vaadin-crud-edit-column.js")
public class Crud extends Component implements HasSize, HasTheme, HasStyle {

    private static final String EDIT_COLUMN_KEY = "vaadin-crud-edit-column";
    private static final String EVENT_PREVENT_DEFAULT_JS = "event.preventDefault()";

    private final Set>> newListeners = new LinkedHashSet<>();
    private final Set>> editListeners = new LinkedHashSet<>();
    private final Set>> saveListeners = new LinkedHashSet<>();
    private final Set>> cancelListeners = new LinkedHashSet<>();
    private final Set>> deleteListeners = new LinkedHashSet<>();

    private Class beanType;
    private Grid grid;
    private CrudEditor editor;
    private E gridActiveItem;
    private boolean toolbarVisible = true;
    private boolean saveBtnDisabledOverridden;

    final private Button saveButton;

    final private Button cancelButton;

    final private Button deleteButton;

    private Component newButton;

    /**
     * Instantiates a new Crud using a custom grid.
     *
     * @param beanType
     *            the class of items
     * @param grid
     *            the grid with which the items listing should be displayed
     * @param editor
     *            the editor for manipulating individual items
     * @see com.vaadin.flow.component.crud.Crud#Crud(Class, CrudEditor)
     *      Crud(Class, CrudEditor)
     */
    public Crud(Class beanType, Grid grid, CrudEditor editor) {
        this();

        setGrid(grid);
        setEditor(editor);
        setBeanType(beanType);
    }

    /**
     * Instantiates a new Crud for the given bean type and uses the supplied
     * editor. Furthermore, it displays the items using the built-in grid.
     *
     * @param beanType
     *            the class of items
     * @param editor
     *            the editor for manipulating individual items
     * @see CrudGrid
     * @see com.vaadin.flow.component.crud.Crud#Crud(Class, Grid, CrudEditor)
     *      Crud(Class, Grid, CrudEditor)
     */
    public Crud(Class beanType, CrudEditor editor) {
        this();

        setEditor(editor);
        setBeanType(beanType);
    }

    /**
     * Instantiates a new Crud with no grid, editor and runtime bean type
     * information. The editor and bean type must be initialized before a Crud
     * is put into full use therefore this constructor only exists for partial
     * initialization in order to support template binding.
     *
     * @see #setEditor(CrudEditor)
     * @see #setBeanType(Class)
     */
    public Crud() {
        setI18n(CrudI18n.createDefault(), false);
        registerHandlers();

        // Prevent web component from creating default buttons
        getElement().setProperty("_noDefaultButtons", true);

        newButton = new Button();
        newButton.getElement().setAttribute("theme", "primary");
        // Ensure the flag is set before the element is added to the slot
        newButton.getElement().setProperty("_isDefault", true);
        SlotUtils.addToSlot(this, "new-button", newButton);

        saveButton = new SaveButton();
        saveButton.addThemeName("primary");
        // Ensure the flag is set before the element is added to the slot
        saveButton.getElement().setProperty("_isDefault", true);
        SlotUtils.addToSlot(this, "save-button", saveButton);

        cancelButton = new Button();
        cancelButton.addThemeName("tertiary");
        // Ensure the flag is set before the element is added to the slot
        cancelButton.getElement().setProperty("_isDefault", true);
        SlotUtils.addToSlot(this, "cancel-button", cancelButton);

        deleteButton = new Button();
        deleteButton.addThemeNames("tertiary", "error");
        // Ensure the flag is set before the element is added to the slot
        deleteButton.getElement().setProperty("_isDefault", true);
        SlotUtils.addToSlot(this, "delete-button", deleteButton);
    }

    private class SaveButton extends Button {
        @Override
        public void onEnabledStateChanged(boolean enabled) {
            super.onEnabledStateChanged(enabled);
            saveBtnDisabledOverridden = true;
            overrideSaveDisabled(enabled);
        }
    }

    @Override
    protected void onAttach(AttachEvent attachEvent) {
        super.onAttach(attachEvent);
        if (saveBtnDisabledOverridden) {
            overrideSaveDisabled(getSaveButton().isEnabled());
        }
        getElement().executeJs("this.__validate = function () {return true;}");
    }

    private void overrideSaveDisabled(boolean enabled) {
        getElement().executeJs("this.__isSaveBtnDisabled = () => {return $0;}",
                !enabled);
    }

    private void registerHandlers() {
        ComponentUtil.addListener(this, NewEvent.class,
                (ComponentEventListener) ((ComponentEventListener>) e -> {
                    try {
                        getEditor().setItem(e.getItem() != null ? e.getItem()
                                : getBeanType().newInstance());
                        clearActiveItem();
                        setClientIsNew(true);
                    } catch (Exception ex) {
                        throw new RuntimeException(
                                "Unable to instantiate new bean", ex);
                    }

                    NewEvent eventWithNewItem = new NewEvent(e.getSource(),
                            e.isFromClient(), getEditor().getItem(), null);
                    newListeners.forEach(listener -> listener
                            .onComponentEvent(eventWithNewItem));
                }));

        ComponentUtil.addListener(this, EditEvent.class,
                (ComponentEventListener) ((ComponentEventListener>) e -> {
                    if (getEditor().getItem() != e.getItem()) {
                        getEditor().setItem(e.getItem(), true);
                        setOpened(true);
                        setClientIsNew(false);
                        if (isEditOnClick() && getGrid() instanceof CrudGrid) {
                            getGrid().select(e.getItem());
                        }
                    }

                    editListeners
                            .forEach(listener -> listener.onComponentEvent(e));
                }));

        ComponentUtil.addListener(this, CancelEvent.class,
                (ComponentEventListener) ((ComponentEventListener>) e -> {
                    cancelListeners
                            .forEach(listener -> listener.onComponentEvent(e));
                    if ((this.gridActiveItem != null && this.getEditor()
                            .getItem() == this.gridActiveItem)
                            || this.gridActiveItem == null) {
                        setOpened(false);
                        getEditor().clear();
                        clearActiveItem();
                    }
                }));

        ComponentUtil.addListener(this, SaveEvent.class,
                (ComponentEventListener) ((ComponentEventListener>) e -> {
                    if (!getEditor().validate()) {
                        return;
                    }

                    getEditor().writeItemChanges();
                    try {
                        saveListeners.forEach(
                                listener -> listener.onComponentEvent(e));
                        setOpened(false);
                        getEditor().clear();
                    } finally {
                        if (getGrid().getDataProvider() != null) {
                            getGrid().getDataProvider().refreshAll();
                        }
                    }
                }));

        ComponentUtil.addListener(this, DeleteEvent.class,
                (ComponentEventListener) ((ComponentEventListener>) e -> {
                    try {
                        deleteListeners.forEach(
                                listener -> listener.onComponentEvent(e));
                        setOpened(false);
                        getEditor().clear();
                    } finally {
                        getGrid().getDataProvider().refreshAll();
                    }
                }));
    }

    /**
     * Initiates an item edit from the server-side. This sets the supplied item
     * as the working bean and opens the edit dialog.
     *
     * @param item
     *            the item to be edited
     * @param editMode
     *            the edit mode
     */
    public void edit(E item, EditMode editMode) {
        final CrudEvent event;
        if (editMode == EditMode.NEW_ITEM) {
            event = new NewEvent<>(this, false, item, null);
        } else {
            setDirty(false);
            event = new EditEvent<>(this, false, item);
        }

        setOpened(true);
        ComponentUtil.fireEvent(this, event);
    }

    private void setClientIsNew(boolean isNew) {
        getElement().setProperty("__isNew", isNew);
    }

    /**
     * Opens or closes the editor. In most use cases opening or closing the
     * editor is automatically done by the component and this method does not
     * need to be called.
     *
     * @param opened
     *            true to open or false to close
     */
    public void setOpened(boolean opened) {
        getElement().callJsFunction("set", "editorOpened", opened);
    }

    /**
     * Set the dirty state of the Crud.
     * 

* A dirty Crud has its editor Save button enabled. Ideally a Crud * automatically detects if it is dirty based on interactions with the form * fields within it but in some special cases (e.g with composites) this * might not be automatically detected. For such cases this method could be * used to explicitly set the dirty state of the Crud editor. *

* NOTE: editor Save button will not be automatically enabled in case its * enabled state was changed with {@link Crud#getSaveButton()} * * @param dirty * true if dirty and false if otherwise. * @see #getSaveButton() */ public void setDirty(boolean dirty) { getElement().executeJs("this.set('__isDirty', $0)", dirty); } /** * Gets the runtime bean type information * * @return the bean type */ public Class getBeanType() { if (beanType == null) { throw new IllegalStateException( "The bean type must be initialized before event processing"); } return beanType; } /** * Sets the runtime bean type information. If no grid exists a built-in grid * is created since the bean type information is now known. When injecting a * {@link Crud} with {@literal @}Id this method must be called before the * crud is put into use. * * @param beanType * the bean type */ public void setBeanType(Class beanType) { Objects.requireNonNull(beanType, "Bean type cannot be null"); this.beanType = beanType; if (this.grid == null) { setGrid(new CrudGrid<>(beanType, true, true)); } } /** * Gets the grid * * @return the grid */ public Grid getGrid() { if (grid == null) { throw new IllegalStateException( "The grid must be initialized before event processing"); } return grid; } /** * Sets the grid * * @param grid * the grid */ public void setGrid(Grid grid) { Objects.requireNonNull(grid, "Grid cannot be null"); if (this.grid != null && getElement().equals(this.grid.getElement().getParent())) { this.grid.getElement().removeFromParent(); } this.grid = grid; grid.getElement().setAttribute("slot", "grid"); // It might already have a parent e.g when injected from a template if (grid.getElement().getParent() == null) { getElement().appendChild(grid.getElement()); } } /** * Gets the crud editor. * * @return the crud editor */ public CrudEditor getEditor() { if (editor == null) { throw new IllegalStateException( "The editor must be initialized before event processing"); } return editor; } /** * Sets the editor. When injecting a {@link Crud} with {@literal @}Id this * method must be called before the crud is put into use. * * @param editor * the editor */ public void setEditor(CrudEditor editor) { Objects.requireNonNull(editor, "Editor cannot be null"); if (this.editor != null && this.editor.getView() != null && this.editor .getView().getElement().getParent() == getElement()) { this.editor.getView().getElement().removeFromParent(); } this.editor = editor; // It might already have a parent e.g when injected from a template if (editor.getView() != null && editor.getView().getElement().getParent() == null) { SlotUtils.addToSlot(this, "form", editor.getView()); } } /** * Sets how editor will be presented on desktop screen. *

* The default position is {@link CrudEditorPosition#OVERLAY}. * * @param editorPosition * the editor position, never null * @see CrudEditorPosition */ public void setEditorPosition(CrudEditorPosition editorPosition) { if (editorPosition == null) { throw new IllegalArgumentException( "The 'editorPosition' argument can not be null"); } getElement().setProperty("editorPosition", editorPosition.getEditorPosition()); } /** * Gets the current editor position on the desktop screen. *

* The default position is {@link CrudEditorPosition#OVERLAY}. * * @return the editor position */ public CrudEditorPosition getEditorPosition() { return CrudEditorPosition.toPosition( getElement().getProperty("editorPosition", ""), CrudEditorPosition.OVERLAY); } private Registration gridItemClickRegistration; /** * Sets the option to open item to edit by row click. *

* If enabled, it removes the edit column created by {@link CrudGrid}. * * @param editOnClick * {@code true} to enable it ({@code false}, by default). */ public void setEditOnClick(boolean editOnClick) { getElement().setProperty("editOnClick", editOnClick); Grid grid = getGrid(); if (editOnClick) { if (getGrid() instanceof CrudGrid) { grid.setSelectionMode(Grid.SelectionMode.SINGLE); if (hasEditColumn(grid)) { removeEditColumn(grid); } } gridItemClickRegistration = grid.addItemClickListener( e -> this.gridActiveItem = e.getItem()); } else if (gridItemClickRegistration != null) { clearActiveItem(); gridItemClickRegistration.remove(); if (grid instanceof CrudGrid) { addEditColumn(grid); grid.setSelectionMode(Grid.SelectionMode.NONE); } } } /** * Gets whether click on row to edit item is enabled or not. * * @return {@code true} if enabled, {@code false} otherwise */ public boolean isEditOnClick() { return getElement().getProperty("editOnClick", false); } private void clearActiveItem() { this.gridActiveItem = null; grid.deselectAll(); } /** * Sets the content of the toolbar. * * @param components * the content to be set */ public void setToolbar(Component... components) { SlotUtils.setSlot(this, "toolbar", components); } /** * Sets the internationalized messages to be used by this crud instance. * * @param i18n * the internationalized messages * @see CrudI18n#createDefault() */ public void setI18n(CrudI18n i18n) { setI18n(i18n, true); } private void setI18n(CrudI18n i18n, boolean fireEvent) { getElement().setPropertyJson("i18n", JsonSerializer.toJson(i18n)); if (fireEvent) { ComponentUtil.fireEvent(this.grid, new CrudI18nUpdatedEvent(this, false, i18n)); } } /** * Controls visiblity of toolbar * * @param value */ public void setToolbarVisible(boolean value) { toolbarVisible = value; if (value) { getElement().setProperty("noToolbar", false); } else { getElement().setProperty("noToolbar", true); } } /** * Gets visiblity state of toolbar * * @param * @return true if toolbar is visible false otherwise */ public boolean getToolbarVisible() { return toolbarVisible; } /** * Gets the Crud new item button * * @return the new item button */ public Component getNewButton() { return newButton; } /** * Sets the Crud new item button * * @param button */ public void setNewButton(Component button) { newButton = button; SlotUtils.setSlot(this, "new-button", button); } /** * Gets the Crud editor delete button * * @return the delete button */ public Button getDeleteButton() { return deleteButton; } /** * Gets the Crud save button *

* NOTE: State of the button set with * {@link com.vaadin.flow.component.HasEnabled#setEnabled(boolean)} will * remain even if dirty state of the crud changes * * @return the save button * @see Crud#setDirty(boolean) */ public Button getSaveButton() { return saveButton; } /** * Gets the Crud cancel button * * @return the cancel button */ public Button getCancelButton() { return cancelButton; } /** * Adds theme variants to the component. * * @param variants * theme variants to add */ public void addThemeVariants(CrudVariant... variants) { List variantNames = variantNames(variants); getThemeNames().addAll(variantNames); if (grid instanceof CrudGrid) { ((CrudGrid) grid).addCrudThemeVariants(variantNames); } } /** * Removes theme variants from the component. * * @param variants * theme variants to remove */ public void removeThemeVariants(CrudVariant... variants) { List variantNames = variantNames(variants); getThemeNames().removeAll(variantNames); if (grid instanceof CrudGrid) { ((CrudGrid) grid).removeCrudThemeVariants(variantNames); } } private static List variantNames(CrudVariant... variants) { return Arrays.stream(variants).map(CrudVariant::getVariantName) .collect(Collectors.toList()); } /** * Registers a listener to be notified when the user starts to create a new * item. * * @param listener * a listener to be notified * @return a handle that can be used to unregister the listener */ public Registration addNewListener( ComponentEventListener> listener) { newListeners.add(listener); return () -> newListeners.remove(listener); } /** * Registers a listener to be notified when the user starts to edit an * existing item. * * @param listener * a listener to be notified * @return a handle that can be used to unregister the listener */ public Registration addEditListener( ComponentEventListener> listener) { editListeners.add(listener); return () -> editListeners.remove(listener); } /** * Registers a listener to be notified when the user tries to save a new * item or modifications to an existing item. * * @param listener * a listener to be notified * @return a handle that can be used to unregister the listener */ public Registration addSaveListener( ComponentEventListener> listener) { saveListeners.add(listener); return () -> saveListeners.remove(listener); } /** * Registers a listener to be notified when the user cancels a new item * creation or existing item modification in progress. * * @param listener * a listener to be notified * @return a handle that can be used to unregister the listener */ public Registration addCancelListener( ComponentEventListener> listener) { cancelListeners.add(listener); return () -> cancelListeners.remove(listener); } /** * Registers a listener to be notified when the user tries to delete an * existing item. * * @param listener * a listener to be notified * @return a handle that can be used to unregister the listener */ public Registration addDeleteListener( ComponentEventListener> listener) { deleteListeners.add(listener); return () -> deleteListeners.remove(listener); } /** * Gets the data provider supplying the grid data. * * @return the data provider for the grid */ public DataProvider getDataProvider() { return grid.getDataProvider(); } /** * Sets the data provider for the grid. * * @param provider * the data provider for the grid */ public void setDataProvider(DataProvider provider) { grid.setDataProvider(provider); } /** * A helper method to add an edit column to a grid. Clicking on the edit * cell for a row opens the item for editing in the editor. * * @param grid * the grid in which to add the edit column * @see #addEditColumn(Grid, CrudI18n) * @see #removeEditColumn(Grid) * @see #hasEditColumn(Grid) */ public static void addEditColumn(Grid grid) { addEditColumn(grid, CrudI18n.createDefault()); } /** * A helper method to add an edit column to a grid. Clicking on the edit * cell for a row opens the item for editing in the editor. Additionally, * the i18n object is used for setting the aria-label for the button, * improving accessibility. * * @param grid * the grid in which to add the edit column * @param crudI18n * the i18n object for localizing the accessibility of the edit * column */ public static void addEditColumn(Grid grid, CrudI18n crudI18n) { grid.addColumn(LitRenderer.of(createEditColumnTemplate(crudI18n))) .setKey(EDIT_COLUMN_KEY).setWidth("4em").setFlexGrow(0); } private static String createEditColumnTemplate(CrudI18n crudI18n) { return ""; } /** * Removes the crud edit column from a grid * * @param grid * the grid from which to remove the edit column * @see #addEditColumn(Grid) * @see #hasEditColumn(Grid) */ public static void removeEditColumn(Grid grid) { grid.removeColumnByKey(EDIT_COLUMN_KEY); } /** * Checks if an edit column has been added to the Grid using * {@code Crud.addEditColumn(Grid)} * * @param grid * the grid to check * @return true if an edit column is present or false if otherwise * @see Crud#addEditColumn(Grid) */ public static boolean hasEditColumn(Grid grid) { return grid.getColumnByKey(EDIT_COLUMN_KEY) != null; } /** * The base class for all Crud events. * * @param * the bean type * @see com.vaadin.flow.component.crud.Crud#Crud(Class, CrudEditor) * Crud(Class, CrudEditor) */ static abstract class CrudEvent extends ComponentEvent> { /** * Creates a new event using the given source and indicator whether the * event originated from the client side or the server side. * * @param source * the source component * @param fromClient * true if the event originated from the client */ private CrudEvent(Crud source, boolean fromClient) { super(source, fromClient); } /** * Gets the item being currently edited. * * @return the item being currently edited */ public E getItem() { return getSource().getEditor().getItem(); } } /** * Event fired when the user cancels the creation of a new item or * modifications to an existing item. * * @param * the bean type */ @DomEvent("cancel") public static class CancelEvent extends CrudEvent { /** * Creates a new event using the given source and indicator whether the * event originated from the client side or the server side. * * @param source * the source component * @param fromClient * true if the event originated from the client * @param ignored * an ignored parameter for a side effect */ public CancelEvent(Crud source, boolean fromClient, @EventData(EVENT_PREVENT_DEFAULT_JS) Object ignored) { super(source, fromClient); } } /** * Event fired when the user tries to delete an existing item. * * @param * the bean type */ @DomEvent("delete") public static class DeleteEvent extends CrudEvent { /** * Creates a new event using the given source and indicator whether the * event originated from the client side or the server side. * * @param source * the source component * @param fromClient * true if the event originated from the client * @param ignored * an ignored parameter for a side effect */ public DeleteEvent(Crud source, boolean fromClient, @EventData(EVENT_PREVENT_DEFAULT_JS) Object ignored) { super(source, fromClient); } } /** * Event fired when the user starts to edit an existing item. * * @param * the bean type */ @DomEvent("edit") public static class EditEvent extends CrudEvent { private E item; /** * Creates a new event using the given source and indicator whether the * event originated from the client side or the server side. * * @param source * the source component * @param fromClient * true if the event originated from the client * @param item * the item to be edited, provided in JSON as internally * represented in Grid * @param ignored * an ignored parameter for a side effect */ public EditEvent(Crud source, boolean fromClient, @EventData("event.detail.item") JsonObject item, @EventData(EVENT_PREVENT_DEFAULT_JS) Object ignored) { super(source, fromClient); this.item = source.getGrid().getDataCommunicator().getKeyMapper() .get(item.getString("key")); } private EditEvent(Crud source, boolean fromClient, E item) { super(source, fromClient); this.item = item; } @Override public E getItem() { return item; } } /** * Event fired when the user starts to create a new item. * * @param * the bean type */ @DomEvent("new") public static class NewEvent extends CrudEvent { private E item; /** * Creates a new event using the given source and indicator whether the * event originated from the client side or the server side. * * @param source * the source component * @param fromClient * true if the event originated from the client * @param ignored * an ignored parameter for a side effect */ public NewEvent(Crud source, boolean fromClient, @EventData(EVENT_PREVENT_DEFAULT_JS) Object ignored) { super(source, fromClient); } /** * Private constructor for server-initiated edits * * @param source * the source component * @param fromClient * true if the event originated from the client * @param item * the item to be edited * @param ignored * only present to workaround Java generics erasure (since E * also erases to Object and clashes with the other * constructor) */ private NewEvent(Crud source, boolean fromClient, E item, Object ignored) { super(source, fromClient); this.item = item; } /** * Gets new item being created * * @return a new instance of bean type */ @Override public E getItem() { return item; } } /** * Event fired when the user tries to save a new item or modifications to an * existing item. * * @param * the bean type */ @DomEvent("save") public static class SaveEvent extends CrudEvent { /** * Creates a new event using the given source and indicator whether the * event originated from the client side or the server side. * * @param source * the source component * @param fromClient * true if the event originated from the client * @param ignored * an ignored parameter for a side effect */ public SaveEvent(Crud source, boolean fromClient, @EventData(EVENT_PREVENT_DEFAULT_JS) Object ignored) { super(source, fromClient); } } /** * Determines whether an item presented for editing is to be treated as a * new item or an existing item. * * @see Crud#edit(Object, EditMode) */ public enum EditMode { /** * The item presented for editing should be treated as a new item. */ NEW_ITEM, /** * The item presented for editing should be treated as an existing item. */ EXISTING_ITEM } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy