diff options
author | Teemu Suo-Anttila <tsuoanttila@users.noreply.github.com> | 2017-09-27 11:40:17 +0300 |
---|---|---|
committer | Henri Sara <henri.sara@gmail.com> | 2017-09-27 11:40:17 +0300 |
commit | 367c7751a6ff9234fd47bc5a48e6ef9a4117a7a2 (patch) | |
tree | 5b3849bafb37b49c3dcc8616e064f60bd081dff0 /server/src/main | |
parent | 69776b1d08d40bcdd89b9cc5b050e8db793ec06b (diff) | |
download | vaadin-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')
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. |