summaryrefslogtreecommitdiffstats
path: root/server/src/main
diff options
context:
space:
mode:
authorTeemu Suo-Anttila <tsuoanttila@users.noreply.github.com>2017-09-27 11:40:17 +0300
committerHenri Sara <henri.sara@gmail.com>2017-09-27 11:40:17 +0300
commit367c7751a6ff9234fd47bc5a48e6ef9a4117a7a2 (patch)
tree5b3849bafb37b49c3dcc8616e064f60bd081dff0 /server/src/main
parent69776b1d08d40bcdd89b9cc5b050e8db793ec06b (diff)
downloadvaadin-framework-367c7751a6ff9234fd47bc5a48e6ef9a4117a7a2.tar.gz
vaadin-framework-367c7751a6ff9234fd47bc5a48e6ef9a4117a7a2.zip
Add option to use PushState instead of URI fragments in Navigator (#10042)
* Navigator now by default uses pushState and normal URLs * added documentation for pushState and updated Navigator documentation * improving docs etc, adding one TODO to be solved before merging * pushState/replaceState no work better with changing titles * Making uri fragment navigator work when not using specially mapped UI * Revert to older default, add annotation for selecting * Fix tests, add null checks * Reorder if-clause, fix tests * Revert unnecessary test change * Use correct variable in UI, fix test clean up * Updates to JavaDocs, fix some methods and tests * Add comments, fix test ui, TODO for fallbacks * Navigation documentation, JavaDocs, removed TODOs * Documentation fixes * Improve JavaDocs * Fix link name in documentation * Improve throws declaration in getLocation * Change documentation about the PushState based navigation * Add since tags * Add since tags for UI
Diffstat (limited to 'server/src/main')
-rw-r--r--server/src/main/java/com/vaadin/navigator/Navigator.java117
-rw-r--r--server/src/main/java/com/vaadin/navigator/PushStateNavigation.java47
-rw-r--r--server/src/main/java/com/vaadin/server/Page.java6
-rw-r--r--server/src/main/java/com/vaadin/ui/UI.java81
4 files changed, 245 insertions, 6 deletions
diff --git a/server/src/main/java/com/vaadin/navigator/Navigator.java b/server/src/main/java/com/vaadin/navigator/Navigator.java
index 6b18cd4000..d65bad9db7 100644
--- a/server/src/main/java/com/vaadin/navigator/Navigator.java
+++ b/server/src/main/java/com/vaadin/navigator/Navigator.java
@@ -16,6 +16,7 @@
package com.vaadin.navigator;
import java.io.Serializable;
+import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -27,6 +28,7 @@ import java.util.Objects;
import com.vaadin.navigator.ViewChangeListener.ViewChangeEvent;
import com.vaadin.server.Page;
+import com.vaadin.server.Page.PopStateEvent;
import com.vaadin.shared.Registration;
import com.vaadin.shared.util.SharedUtil;
import com.vaadin.ui.Component;
@@ -82,6 +84,95 @@ public class Navigator implements Serializable {
}
/**
+ * A {@link NavigationStateManager} using path info, HTML5 push state and
+ * {@link PopStateEvent}s to track views and enable listening to view
+ * changes. This manager can be enabled with UI annotation
+ * {@link PushStateNavigation}.
+ * <p>
+ * The part of path after UI's "root path" (UI's path without view
+ * identifier) is used as {@link View}s identifier. The rest of the path
+ * after the view name can be used by the developer for extra parameters for
+ * the View.
+ * <p>
+ * This class is mostly for internal use by Navigator, and is only public
+ * and static to enable testing.
+ *
+ * @since 8.2
+ */
+ public static class PushStateManager implements NavigationStateManager {
+ private Registration popStateListenerRegistration;
+ private UI ui;
+
+ /**
+ * Creates a new PushStateManager.
+ *
+ * @param ui
+ * the UI where the Navigator is attached to
+ */
+ public PushStateManager(UI ui) {
+ this.ui = ui;
+ }
+
+ @Override
+ public void setNavigator(Navigator navigator) {
+ if (popStateListenerRegistration != null) {
+ popStateListenerRegistration.remove();
+ popStateListenerRegistration = null;
+ }
+ if (navigator != null) {
+ popStateListenerRegistration = ui.getPage()
+ .addPopStateListener(e -> {
+ navigator.navigateTo(getState());
+ });
+ }
+ }
+
+ @Override
+ public String getState() {
+ // Get the current URL
+ URI location = ui.getPage().getLocation();
+ String path = location.getPath();
+ if (ui.getUiPathInfo() != null
+ && path.contains(ui.getUiPathInfo())) {
+ // Split the path from after the UI PathInfo
+ path = path.substring(path.indexOf(ui.getUiPathInfo())
+ + ui.getUiPathInfo().length());
+ } else if (path.startsWith(ui.getUiRootPath())) {
+ // Use the whole path after UI RootPath
+ String uiRootPath = ui.getUiRootPath();
+ path = path.substring(uiRootPath.length());
+ } else {
+ throw new IllegalStateException(getClass().getSimpleName()
+ + " is unable to determine the view path from the URL.");
+ }
+
+ if (path.startsWith("/")) {
+ // Strip leading '/'
+ path = path.substring(1);
+ }
+ return path;
+ }
+
+ @Override
+ public void setState(String state) {
+ StringBuilder sb = new StringBuilder(ui.getUiRootPath());
+ if (!ui.getUiRootPath().endsWith("/")) {
+ // make sure there is a '/' between the root path and the
+ // navigation state.
+ sb.append("/");
+ }
+ sb.append(state);
+ URI location = ui.getPage().getLocation();
+ if (location != null) {
+ ui.getPage().pushState(location.resolve(sb.toString()));
+ } else {
+ throw new IllegalStateException(
+ "The Page of the UI does not have a location.");
+ }
+ }
+ }
+
+ /**
* A {@link NavigationStateManager} using hashbang fragments in the Page
* location URI to track views and enable listening to view changes.
* <p>
@@ -92,6 +183,10 @@ public class Navigator implements Serializable {
* <p>
* This class is mostly for internal use by Navigator, and is only public
* and static to enable testing.
+ * <p>
+ * <strong>Note:</strong> Since 8.2 you can use {@link PushStateManager},
+ * which is based on HTML5 History API. To use it, add
+ * {@link PushStateNavigation} annotation to the UI.
*/
public static class UriFragmentManager implements NavigationStateManager {
private final Page page;
@@ -426,7 +521,7 @@ public class Navigator implements Serializable {
* The ViewDisplay used to display the views.
*/
public Navigator(UI ui, ViewDisplay display) {
- this(ui, new UriFragmentManager(ui.getPage()), display);
+ this(ui, null, display);
}
/**
@@ -494,7 +589,7 @@ public class Navigator implements Serializable {
this.ui = ui;
this.ui.setNavigator(this);
if (stateManager == null) {
- stateManager = new UriFragmentManager(ui.getPage());
+ stateManager = createNavigationStateManager(ui);
}
if (stateManager != null && this.stateManager != null
&& stateManager != this.stateManager) {
@@ -506,6 +601,24 @@ public class Navigator implements Serializable {
}
/**
+ * Creates a navigation state manager for given UI. This method should take
+ * into account any navigation related annotations.
+ *
+ * @param ui
+ * the ui
+ * @return the navigation state manager
+ *
+ * @since 8.2
+ */
+ protected NavigationStateManager createNavigationStateManager(UI ui) {
+ if (ui.getClass().getAnnotation(PushStateNavigation.class) != null) {
+ return new PushStateManager(ui);
+ }
+ // Fall back to old default
+ return new UriFragmentManager(ui.getPage());
+ }
+
+ /**
* Navigates to a view and initialize the view with given parameters.
* <p>
* The view string consists of a view name optionally followed by a slash
diff --git a/server/src/main/java/com/vaadin/navigator/PushStateNavigation.java b/server/src/main/java/com/vaadin/navigator/PushStateNavigation.java
new file mode 100644
index 0000000000..f1cab0391b
--- /dev/null
+++ b/server/src/main/java/com/vaadin/navigator/PushStateNavigation.java
@@ -0,0 +1,47 @@
+/*
+ * 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.navigator;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import com.vaadin.server.DeploymentConfiguration;
+import com.vaadin.server.Page.PopStateEvent;
+import com.vaadin.ui.UI;
+
+/**
+ * Annotation for {@link UI}s to enable the PushState navigation mode when
+ * initializing a {@link Navigator} for it. PushState navigation is an
+ * alternative way to handle URLs in the {@link Navigator}. It uses path info,
+ * HTML5 push state and {@link PopStateEvent}s to track views and enable
+ * listening to view changes.
+ * <p>
+ * <strong>Note:</strong> For PushState navigation to work, the
+ * {@link DeploymentConfiguration} parameter
+ * {@link DeploymentConfiguration#isSendUrlsAsParameters() SendUrlAsParameters}
+ * must not be set to {@code false}.
+ *
+ * @since 8.2
+ */
+@Retention(RUNTIME)
+@Target(TYPE)
+@Inherited
+public @interface PushStateNavigation {
+}
diff --git a/server/src/main/java/com/vaadin/server/Page.java b/server/src/main/java/com/vaadin/server/Page.java
index 4c880c2a6b..3e95de0c4b 100644
--- a/server/src/main/java/com/vaadin/server/Page.java
+++ b/server/src/main/java/com/vaadin/server/Page.java
@@ -1035,8 +1035,12 @@ public class Page implements Serializable {
* deployed in due to potential proxies, redirections and similar.
*
* @return The browser location URI.
+ * @throws IllegalStateException
+ * if the
+ * {@link DeploymentConfiguration#isSendUrlsAsParameters()} is
+ * set to {@code false}
*/
- public URI getLocation() {
+ public URI getLocation() throws IllegalStateException {
if (location == null && !uI.getSession().getConfiguration()
.isSendUrlsAsParameters()) {
throw new IllegalStateException("Location is not available as the "
diff --git a/server/src/main/java/com/vaadin/ui/UI.java b/server/src/main/java/com/vaadin/ui/UI.java
index ac4815f92d..5d27c1de74 100644
--- a/server/src/main/java/com/vaadin/ui/UI.java
+++ b/server/src/main/java/com/vaadin/ui/UI.java
@@ -44,6 +44,7 @@ import com.vaadin.event.UIEvents.PollEvent;
import com.vaadin.event.UIEvents.PollListener;
import com.vaadin.event.UIEvents.PollNotifier;
import com.vaadin.navigator.Navigator;
+import com.vaadin.navigator.PushStateNavigation;
import com.vaadin.server.ClientConnector;
import com.vaadin.server.ComponentSizeValidator;
import com.vaadin.server.ComponentSizeValidator.InvalidLayout;
@@ -196,7 +197,6 @@ public abstract class UI extends AbstractSingleComponentContainer
@Override
public void popstate(String uri) {
getPage().updateLocation(uri, true, true);
-
}
};
private DebugWindowServerRpc debugRpc = new DebugWindowServerRpc() {
@@ -261,8 +261,7 @@ public abstract class UI extends AbstractSingleComponentContainer
private WindowOrderRpc windowOrderRpc = new WindowOrderRpc() {
@Override
- public void windowOrderChanged(
- Map<Integer, Connector> windowOrders) {
+ public void windowOrderChanged(Map<Integer, Connector> windowOrders) {
Map<Integer, Window> orders = new LinkedHashMap<>();
for (Entry<Integer, Connector> entry : windowOrders.entrySet()) {
if (entry.getValue() instanceof Window) {
@@ -661,6 +660,10 @@ public abstract class UI extends AbstractSingleComponentContainer
private String embedId;
+ private String uiPathInfo;
+
+ private String uiRootPath;
+
private boolean mobileHtml5DndPolyfillLoaded;
/**
@@ -740,6 +743,36 @@ public abstract class UI extends AbstractSingleComponentContainer
getPage().init(request);
+ String uiPathInfo = (String) request
+ .getAttribute(ApplicationConstants.UI_ROOT_PATH);
+ if (uiPathInfo != null) {
+ setUiPathInfo(uiPathInfo);
+ }
+
+ if (getSession() != null && getSession().getConfiguration() != null
+ && getSession().getConfiguration().isSendUrlsAsParameters()
+ && getPage().getLocation() != null) {
+ // By default the root is the URL from client
+ String uiRootPath = getPage().getLocation().getPath();
+
+ if (uiPathInfo != null && uiRootPath.contains(uiPathInfo)) {
+ // String everything from the URL after uiPathInfo
+ // This will remove the navigation state from the URL
+ uiRootPath = uiRootPath.substring(0,
+ uiRootPath.indexOf(uiPathInfo) + uiPathInfo.length());
+ } else if (request.getPathInfo() != null) {
+ // uiRootPath does not match the uiPathInfo
+ // This can happen for example when embedding a Vaadin UI
+ String pathInfo = request.getPathInfo();
+ if (uiRootPath.endsWith(pathInfo)) {
+ uiRootPath = uiRootPath.substring(0,
+ uiRootPath.length() - pathInfo.length());
+ }
+ }
+ // Store the URL as the UI Root Path
+ setUiRootPath(uiRootPath);
+ }
+
// Call the init overridden by the application developer
init(request);
@@ -750,6 +783,48 @@ public abstract class UI extends AbstractSingleComponentContainer
}
}
+ private void setUiRootPath(String uiRootPath) {
+ this.uiRootPath = uiRootPath;
+ }
+
+ /**
+ * Gets the part of path (from browser's URL) that points to this UI.
+ * Basically the same as the value from {@link Page#getLocation()}, but
+ * without possible view identifiers or path parameters.
+ *
+ * @return the part of path (from browser's URL) that points to this UI,
+ * without possible view identifiers or path parameters
+ *
+ * @since 8.2
+ */
+ public String getUiRootPath() {
+ return uiRootPath;
+ }
+
+ private void setUiPathInfo(String uiPathInfo) {
+ this.uiPathInfo = uiPathInfo;
+ }
+
+ /**
+ * Gets the path info part of the request that is used to detect the UI.
+ * This is defined during UI init by certain {@link UIProvider UIProviders}
+ * that map different UIs to different URIs, like Vaadin Spring. This
+ * information is used by the {@link Navigator} when the {@link UI} is
+ * annotated with {@link PushStateNavigation}.
+ * <p>
+ * For example if the UI is accessed through
+ * {@code http://example.com/MyUI/mainview/parameter=1} the path info would
+ * be {@code /MyUI}.
+ *
+ * @return the path info part of the request; {@code null} if no request
+ * from client has been processed
+ *
+ * @since 8.2
+ */
+ public String getUiPathInfo() {
+ return uiPathInfo;
+ }
+
/**
* Initializes this UI. This method is intended to be overridden by
* subclasses to build the view and configure non-component functionality.