diff options
7 files changed, 677 insertions, 1 deletions
diff --git a/client/src/main/java/com/vaadin/client/ui/composite/CompositeConnector.java b/client/src/main/java/com/vaadin/client/ui/composite/CompositeConnector.java new file mode 100644 index 0000000000..ad5e019b6b --- /dev/null +++ b/client/src/main/java/com/vaadin/client/ui/composite/CompositeConnector.java @@ -0,0 +1,90 @@ +/* + * Copyright 2000-2016 Vaadin 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 com.vaadin.client.ui.composite; + +import com.google.gwt.user.client.ui.Label; +import com.google.gwt.user.client.ui.Widget; +import com.vaadin.client.ComponentConnector; +import com.vaadin.client.ConnectorHierarchyChangeEvent; +import com.vaadin.client.HasComponentsConnector; +import com.vaadin.client.ui.AbstractHasComponentsConnector; +import com.vaadin.shared.AbstractComponentState; +import com.vaadin.shared.ui.Connect; +import com.vaadin.shared.ui.Connect.LoadStyle; +import com.vaadin.ui.Composite; + +@Connect(value = Composite.class, loadStyle = LoadStyle.EAGER) +public class CompositeConnector extends AbstractHasComponentsConnector { + + private ComponentConnector childConnector; + + @Override + protected Widget createWidget() { + throw new UnsupportedOperationException( + "Composite has no widget of its own"); + } + + private boolean hasChildConnector() { + return getChildConnector() != null; + } + + private ComponentConnector getChildConnector() { + // Must store the child connector to have it available when removing the + // connector + if (childConnector == null && !getChildren().isEmpty()) { + childConnector = (ComponentConnector) getChildren().get(0); + } + return childConnector; + } + + @Override + public Widget getWidget() { + if (!hasChildConnector()) { + // This happens in doInit for instance when setConnectorId is called + return new Label("This widget should not end up anywhere ever"); + } else { + return getChildConnector().getWidget(); + } + } + + @Override + public HasComponentsConnector getParent() { + return (HasComponentsConnector) super.getParent(); + } + + @Override + public void updateCaption(ComponentConnector component) { + // Parent might assume that the connector is always a child connector, + // therefore passing "this" instead of the child connector. The child + // caption will be returned as getState() returns the child's state. + getParent().updateCaption(this); + } + + @Override + public AbstractComponentState getState() { + if (!hasChildConnector()) { + return new AbstractComponentState(); + } else { + return getChildConnector().getState(); + } + } + + @Override + public void onConnectorHierarchyChange( + ConnectorHierarchyChangeEvent event) { + // Handled in getChildConnector + } +} diff --git a/server/src/main/java/com/vaadin/ui/ComponentRootSetter.java b/server/src/main/java/com/vaadin/ui/ComponentRootSetter.java new file mode 100644 index 0000000000..1bc3565de8 --- /dev/null +++ b/server/src/main/java/com/vaadin/ui/ComponentRootSetter.java @@ -0,0 +1,54 @@ +/* + * Copyright 2000-2016 Vaadin 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 com.vaadin.ui; + +import java.io.Serializable; + +/** + * Internal utility class. + * + * @since + * @author Vaadin Ltd + */ +public class ComponentRootSetter implements Serializable { + + private ComponentRootSetter() { + // Util methods only + } + + /** + * Sets the composition root for the given custom component or composite. + * <p> + * For internal use only. + * + * @param customComponent + * the custom component or composite + * @param component + * the component to assign as composition root + */ + public static void setRoot(Component customComponent, Component component) { + if (customComponent instanceof CustomComponent) { + ((CustomComponent) customComponent).setCompositionRoot(component); + } else if (customComponent instanceof Composite) { + ((Composite) customComponent).setCompositionRoot(component); + } else { + throw new IllegalArgumentException( + "Parameter is of an unsupported type: " + + customComponent.getClass().getName()); + } + } + +} diff --git a/server/src/main/java/com/vaadin/ui/Composite.java b/server/src/main/java/com/vaadin/ui/Composite.java new file mode 100644 index 0000000000..10bcff0510 --- /dev/null +++ b/server/src/main/java/com/vaadin/ui/Composite.java @@ -0,0 +1,339 @@ +/* + * Copyright 2000-2016 Vaadin 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 com.vaadin.ui; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Objects; +import java.util.Optional; + +import com.vaadin.server.ErrorMessage; +import com.vaadin.server.Resource; +import com.vaadin.shared.composite.CompositeState; +import com.vaadin.shared.ui.ContentMode; + +/** + * Composite allows creating new UI components by composition of existing + * server-side components. + * <p> + * A composite is created by extending the Composite class and setting the + * composition root component using {@link #setCompositionRoot(Component)}. + * </p> + * <p> + * The composition root itself can contain more components. The advantage of + * wrapping it in a composite is that the details of the composition root, such + * as its public API, are hidden from the users of the composite. + * </p> + * <p> + * A composite itself does not contribute to the DOM in any way (contrary to + * {@link CustomComponent} which adds a {@code <div>} to the DOM. + * </p> + * + * @author Vaadin Ltd. + * @since + */ +public class Composite extends AbstractComponent implements HasComponents { + + private static final String COMPOSITE_HAS_NO_DOM_OR_WIDGET = "A composite has no DOM or widget"; + /** + * The contained component. + */ + private Component root = null; + + /** + * Constructs a new empty composite. + * <p> + * Use {@link #setCompositionRoot(Component)} to define the contents of the + * composite. + */ + public Composite() { + } + + /** + * Constructs a new composite containing the given component. + * + * @param compositionRoot + * the root of the composition component tree. It must not be + * null. + */ + public Composite(AbstractComponent compositionRoot) { + this(); + Objects.requireNonNull(compositionRoot); + setCompositionRoot(compositionRoot); + } + + /** + * Returns the composition root. + * + * @return the Component Composition root. + */ + protected Component getCompositionRoot() { + return root; + } + + /** + * Sets the component contained in the composite. + * <p> + * You must set the composition root to a non-null value before the + * component can be used. It cannot be changed. + * </p> + * + * @param compositionRoot + * the root of the composition component tree. + */ + protected void setCompositionRoot(Component compositionRoot) { + if (root != null) { + throw new IllegalStateException( + "Composition root cannot be changed"); + } + if (compositionRoot == null) { + throw new IllegalArgumentException( + "Composition root cannot be null"); + } + + // set new component + if (compositionRoot.getParent() != null) { + // If the component already has a parent, try to remove it + AbstractSingleComponentContainer.removeFromParent(compositionRoot); + } + + compositionRoot.setParent(this); + root = compositionRoot; + markAsDirty(); + } + + /* Basic component features ------------------------------------------ */ + + @Override + public Iterator<Component> iterator() { + if (getCompositionRoot() != null) { + return Collections.singletonList(getCompositionRoot()).iterator(); + } else { + return Collections.<Component> emptyList().iterator(); + } + } + + /** + * Gets the number of contained components. + * + * @return the number of contained components (zero or one) + */ + public int getComponentCount() { + return (getCompositionRoot() != null ? 1 : 0); + } + + @Override + protected CompositeState getState() { + return (CompositeState) super.getState(); + } + + @Override + protected CompositeState getState(boolean markAsDirty) { + return (CompositeState) super.getState(markAsDirty); + } + + @Override + public void beforeClientResponse(boolean initial) { + super.beforeClientResponse(initial); + if (getComponentCount() != 1) { + throw new IllegalStateException( + "A composite must always have a composition root"); + } + } + + @Override + public String getStyleName() { + throw new UnsupportedOperationException(COMPOSITE_HAS_NO_DOM_OR_WIDGET); + } + + @Override + public void setStyleName(String style) { + throw new UnsupportedOperationException(COMPOSITE_HAS_NO_DOM_OR_WIDGET); + } + + @Override + public void setStyleName(String style, boolean add) { + throw new UnsupportedOperationException(COMPOSITE_HAS_NO_DOM_OR_WIDGET); + } + + @Override + public void addStyleName(String style) { + throw new UnsupportedOperationException(COMPOSITE_HAS_NO_DOM_OR_WIDGET); + } + + @Override + public void removeStyleName(String style) { + throw new UnsupportedOperationException(COMPOSITE_HAS_NO_DOM_OR_WIDGET); + } + + @Override + public String getPrimaryStyleName() { + throw new UnsupportedOperationException(COMPOSITE_HAS_NO_DOM_OR_WIDGET); + } + + @Override + public void setPrimaryStyleName(String style) { + throw new UnsupportedOperationException(COMPOSITE_HAS_NO_DOM_OR_WIDGET); + } + + private Component getRootOrThrow() { + return Optional.ofNullable(getCompositionRoot()) + .orElseThrow(() -> new IllegalStateException( + "Composition root has not been set")); + } + + @Override + public float getWidth() { + return getRootOrThrow().getWidth(); + } + + @Override + public float getHeight() { + return getRootOrThrow().getHeight(); + } + + @Override + public Unit getWidthUnits() { + return getRootOrThrow().getWidthUnits(); + } + + @Override + public Unit getHeightUnits() { + return getRootOrThrow().getHeightUnits(); + } + + @Override + public void setHeight(String height) { + getRootOrThrow().setHeight(height); + } + + @Override + public void setWidth(float width, Unit unit) { + getRootOrThrow().setWidth(width, unit); + } + + @Override + public void setHeight(float height, Unit unit) { + getRootOrThrow().setHeight(height, unit); + } + + @Override + public void setWidth(String width) { + getRootOrThrow().setWidth(width); + } + + @Override + public void setSizeFull() { + getRootOrThrow().setSizeFull(); + } + + @Override + public void setSizeUndefined() { + getRootOrThrow().setSizeUndefined(); + } + + @Override + public void setWidthUndefined() { + getRootOrThrow().setWidthUndefined(); + } + + @Override + public void setHeightUndefined() { + getRootOrThrow().setHeightUndefined(); + } + + @Override + public void setId(String id) { + throw new UnsupportedOperationException(COMPOSITE_HAS_NO_DOM_OR_WIDGET); + } + + @Override + public String getId() { + throw new UnsupportedOperationException(COMPOSITE_HAS_NO_DOM_OR_WIDGET); + } + + @Override + public void setDebugId(String id) { + throw new UnsupportedOperationException(COMPOSITE_HAS_NO_DOM_OR_WIDGET); + } + + @Override + public String getDebugId() { + throw new UnsupportedOperationException(COMPOSITE_HAS_NO_DOM_OR_WIDGET); + } + + @Override + public String getCaption() { + throw new UnsupportedOperationException(COMPOSITE_HAS_NO_DOM_OR_WIDGET); + } + + @Override + public void setCaption(String caption) { + throw new UnsupportedOperationException(COMPOSITE_HAS_NO_DOM_OR_WIDGET); + } + + @Override + public void setCaptionAsHtml(boolean captionAsHtml) { + throw new UnsupportedOperationException(COMPOSITE_HAS_NO_DOM_OR_WIDGET); + } + + @Override + public boolean isCaptionAsHtml() { + throw new UnsupportedOperationException(COMPOSITE_HAS_NO_DOM_OR_WIDGET); + } + + @Override + public Resource getIcon() { + throw new UnsupportedOperationException(COMPOSITE_HAS_NO_DOM_OR_WIDGET); + } + + @Override + public void setIcon(Resource icon) { + throw new UnsupportedOperationException(COMPOSITE_HAS_NO_DOM_OR_WIDGET); + } + + @Override + public String getDescription() { + throw new UnsupportedOperationException(COMPOSITE_HAS_NO_DOM_OR_WIDGET); + } + + @Override + public void setDescription(String description) { + throw new UnsupportedOperationException(COMPOSITE_HAS_NO_DOM_OR_WIDGET); + } + + @Override + public void setDescription(String description, ContentMode mode) { + throw new UnsupportedOperationException(COMPOSITE_HAS_NO_DOM_OR_WIDGET); + } + + @Override + public ErrorMessage getErrorMessage() { + return null; + } + + @Override + public ErrorMessage getComponentError() { + throw new UnsupportedOperationException(COMPOSITE_HAS_NO_DOM_OR_WIDGET); + } + + @Override + public void setComponentError(ErrorMessage componentError) { + throw new UnsupportedOperationException(COMPOSITE_HAS_NO_DOM_OR_WIDGET); + } + +} diff --git a/server/src/test/java/com/vaadin/tests/server/component/StateGetDoesNotMarkDirtyTest.java b/server/src/test/java/com/vaadin/tests/server/component/StateGetDoesNotMarkDirtyTest.java index 3b4c2413f9..29dd6638fa 100644 --- a/server/src/test/java/com/vaadin/tests/server/component/StateGetDoesNotMarkDirtyTest.java +++ b/server/src/test/java/com/vaadin/tests/server/component/StateGetDoesNotMarkDirtyTest.java @@ -17,6 +17,8 @@ import org.mockito.Mockito; import com.vaadin.server.VaadinSession; import com.vaadin.tests.VaadinClasses; import com.vaadin.ui.Component; +import com.vaadin.ui.ComponentRootSetter; +import com.vaadin.ui.Composite; import com.vaadin.ui.ConnectorTracker; import com.vaadin.ui.Label; import com.vaadin.ui.UI; @@ -75,7 +77,16 @@ public class StateGetDoesNotMarkDirtyTest { } // just to make sure we can invoke it method.setAccessible(true); - method.invoke(newInstance); + try { + method.invoke(newInstance); + } catch (InvocationTargetException e) { + if (e.getCause() instanceof UnsupportedOperationException) { + // Overridden getter which is not supposed to be + // called + } else { + throw e; + } + } } } catch (Exception e) { System.err.println("problem with method " + clazz.getName() @@ -116,6 +127,9 @@ public class StateGetDoesNotMarkDirtyTest { if (component instanceof UI) { return component; } + if (component instanceof Composite) { + ComponentRootSetter.setRoot(component, new Label()); + } emulateAttach(component); return component; } catch (NoSuchMethodException e) { diff --git a/shared/src/main/java/com/vaadin/shared/composite/CompositeState.java b/shared/src/main/java/com/vaadin/shared/composite/CompositeState.java new file mode 100644 index 0000000000..1f552390df --- /dev/null +++ b/shared/src/main/java/com/vaadin/shared/composite/CompositeState.java @@ -0,0 +1,27 @@ +/* + * Copyright 2000-2016 Vaadin 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 com.vaadin.shared.composite; + +import com.vaadin.shared.AbstractComponentState; + +/** + * Shared state for Composite. + * + * @author Vaadin Ltd + * @since + */ +public class CompositeState extends AbstractComponentState { +} diff --git a/uitest/src/main/java/com/vaadin/tests/components/composite/CompositeChainUI.java b/uitest/src/main/java/com/vaadin/tests/components/composite/CompositeChainUI.java new file mode 100644 index 0000000000..481f7c958f --- /dev/null +++ b/uitest/src/main/java/com/vaadin/tests/components/composite/CompositeChainUI.java @@ -0,0 +1,97 @@ +/* + * Copyright 2000-2016 Vaadin 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 com.vaadin.tests.components.composite; + +import java.util.Iterator; + +import com.vaadin.annotations.Widgetset; +import com.vaadin.server.VaadinRequest; +import com.vaadin.tests.components.AbstractTestUIWithLog; +import com.vaadin.ui.Button; +import com.vaadin.ui.Component; +import com.vaadin.ui.Composite; +import com.vaadin.ui.HasComponents; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Label; +import com.vaadin.ui.VerticalLayout; + +@Widgetset("com.vaadin.DefaultWidgetSet") +public class CompositeChainUI extends AbstractTestUIWithLog { + + private Label innermostComponent; + private Composite innerComposite; + private Composite outerComposite; + private VerticalLayout container; + private HorizontalLayout layout; + + @Override + protected void setup(VaadinRequest request) { + + createComposite(); + layout = new HorizontalLayout(outerComposite); + container = new VerticalLayout(layout); + addComponent(container); + + Button updateCaption = new Button("Update caption"); + updateCaption.addClickListener(e -> { + innermostComponent + .setCaption(innermostComponent.getCaption() + " - updated"); + }); + addComponent(updateCaption); + Button replaceWithAnotherComposite = new Button( + "Replace with another Composite", e -> { + Composite oldOuter = outerComposite; + createComposite(); + layout.replaceComponent(oldOuter, outerComposite); + }); + addComponent(replaceWithAnotherComposite); + logHierarchy(); + } + + private void createComposite() { + innermostComponent = new Label("Label text"); + innermostComponent.setCaption("Label caption"); + innermostComponent.setId("innermost"); + + innerComposite = new Composite(innermostComponent); + outerComposite = new Composite(innerComposite); + } + + private void logHierarchy() { + String msg = "Hierarchy: "; + if (container != null) { + msg += getHierarchy(container); + } + log(msg); + } + + private static String getHierarchy(Component component) { + String msg = component.getClass().getSimpleName(); + if (component instanceof HasComponents) { + + Iterator<Component> iterator = ((HasComponents) component) + .iterator(); + if (iterator.hasNext()) { + Component content = iterator.next(); + if (content != null) { + msg += " -> " + getHierarchy(content); + } + } + } + return msg; + } + +} diff --git a/uitest/src/test/java/com/vaadin/tests/components/composite/CompositeChainUITest.java b/uitest/src/test/java/com/vaadin/tests/components/composite/CompositeChainUITest.java new file mode 100644 index 0000000000..228d20f058 --- /dev/null +++ b/uitest/src/test/java/com/vaadin/tests/components/composite/CompositeChainUITest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2000-2016 Vaadin 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 com.vaadin.tests.components.composite; + +import org.junit.Assert; +import org.junit.Test; +import org.openqa.selenium.WebElement; + +import com.vaadin.testbench.By; +import com.vaadin.testbench.elements.ButtonElement; +import com.vaadin.testbench.elements.LabelElement; +import com.vaadin.tests.tb3.SingleBrowserTest; + +public class CompositeChainUITest extends SingleBrowserTest { + + @Test + public void compositeRenderedAndUpdatedCorrectly() { + openTestURL(); + LabelElement label = $(LabelElement.class).id("innermost"); + WebElement labelGrandParent = label.findElement(By.xpath("../..")); + + Assert.assertEquals("v-slot", labelGrandParent.getAttribute("class")); + Assert.assertEquals("Label caption", label.getCaption()); + + $(ButtonElement.class).caption("Update caption").first().click(); + Assert.assertEquals("Label caption - updated", label.getCaption()); + + } + + @Test + public void compositeRemovedCorrectly() { + openTestURL("debug"); + LabelElement label = $(LabelElement.class).id("innermost"); + $(ButtonElement.class).caption("Update caption").first().click(); + Assert.assertEquals("Label caption - updated", label.getCaption()); + $(ButtonElement.class).caption("Replace with another Composite").first() + .click(); + label = $(LabelElement.class).id("innermost"); + Assert.assertEquals("Label caption", label.getCaption()); + assertNoErrorNotifications(); + } +} |