summaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorMatti Tahvonen <matti@vaadin.com>2017-01-13 17:07:36 +0200
committerPekka Hyvönen <pekka@vaadin.com>2017-01-13 17:07:36 +0200
commit62c44dd77e47c908361a87332182f2e2465972c0 (patch)
tree2485171f3a1326d0f7bf0a067569e535dd18d626 /server
parentfe536b3efc4ed3d6637fead851c01375af91f762 (diff)
downloadvaadin-framework-62c44dd77e47c908361a87332182f2e2465972c0.tar.gz
vaadin-framework-62c44dd77e47c908361a87332182f2e2465972c0.zip
Support for HTML5 push/replaceState for proper deep linking features (#8116)
* Added support for HTML5 push/replaceState for proper deep linkin features * Automated test script now works at least on chrome * Uses html5 push/popstate to implement uri fragment feature * fire legacy fragment change events also via popstate events rpc calls * send new fragments via pushstate mechanism * formatting * Formatting and adding test and workaround for IE bug * Formatting and depracated UriFragmentListener * Aligned naming in the new API * Ignored IE due to web driver bug Tested a workaround with javascript based window.location.href fetch, but that don’t seem to work stable enough.
Diffstat (limited to 'server')
-rw-r--r--server/src/main/java/com/vaadin/server/Page.java203
-rw-r--r--server/src/main/java/com/vaadin/ui/UI.java15
2 files changed, 205 insertions, 13 deletions
diff --git a/server/src/main/java/com/vaadin/server/Page.java b/server/src/main/java/com/vaadin/server/Page.java
index c96114b3f7..bf55e7ce9f 100644
--- a/server/src/main/java/com/vaadin/server/Page.java
+++ b/server/src/main/java/com/vaadin/server/Page.java
@@ -251,7 +251,9 @@ public class Page implements Serializable {
* changes.
*
* @see Page#addUriFragmentChangedListener(UriFragmentChangedListener)
+ * @deprecated Use {@link PopStateListener} instead
*/
+ @Deprecated
@FunctionalInterface
public interface UriFragmentChangedListener extends Serializable {
/**
@@ -273,6 +275,32 @@ public class Page implements Serializable {
"uriFragmentChanged", UriFragmentChangedEvent.class);
/**
+ * Listener that that gets notified when the URI of the page changes due to
+ * back/forward functionality of the browser.
+ *
+ * @see Page#addPopStateListener(PopStateListener)
+ * @since 8.0
+ */
+ @FunctionalInterface
+ public interface PopStateListener extends Serializable {
+ /**
+ * Event handler method invoked when the URI fragment of the page
+ * changes. Please note that the initial URI fragment has already been
+ * set when a new UI is initialized, so there will not be any initial
+ * event for listeners added during {@link UI#init(VaadinRequest)}.
+ *
+ * @see Page#addUriFragmentChangedListener(UriFragmentChangedListener)
+ *
+ * @param event
+ * the URI fragment changed event
+ */
+ public void uriChanged(PopStateEvent event);
+ }
+
+ private static final Method URI_CHANGED_METHOD = ReflectTools.findMethod(
+ Page.PopStateListener.class, "uriChanged", PopStateEvent.class);
+
+ /**
* Resources to be opened automatically on next repaint. The list is
* automatically cleared when it has been sent to the client.
*/
@@ -328,6 +356,53 @@ public class Page implements Serializable {
}
}
+ /**
+ * Event fired when the URI of a <code>Page</code> changes (aka HTML 5
+ * popstate event) on the client side due to browsers back/forward
+ * functionality.
+ *
+ * @see Page#addPopStateListener(PopStateListener)
+ * @since 8.0
+ */
+ public static class PopStateEvent extends EventObject {
+
+ /**
+ * The new URI as String
+ */
+ private final String uri;
+
+ /**
+ * Creates a new instance of PopstateEvent.
+ *
+ * @param source
+ * the Source of the event.
+ * @param uri
+ * the new uri
+ */
+ public PopStateEvent(Page source, String uri) {
+ super(source);
+ this.uri = uri;
+ }
+
+ /**
+ * Gets the page in which the uri has changed.
+ *
+ * @return the page in which the uri has changed
+ */
+ public Page getPage() {
+ return (Page) getSource();
+ }
+
+ /**
+ * Get the new URI
+ *
+ * @return the new uri
+ */
+ public String getUri() {
+ return uri;
+ }
+ }
+
@FunctionalInterface
private static interface InjectedStyle extends Serializable {
public void paint(int id, PaintTarget target) throws PaintException;
@@ -483,6 +558,9 @@ public class Page implements Serializable {
private String windowName;
+ private String newPushState;
+ private String newReplaceState;
+
public Page(UI uI, PageState state) {
this.uI = uI;
this.state = state;
@@ -516,7 +594,10 @@ public class Page implements Serializable {
* @param listener
* the URI fragment listener to add
* @return a registration object for removing the listener
+ * @deprecated Use {@link Page#addPopStateListener(PopStateListener)}
+ * instead
*/
+ @Deprecated
public Registration addUriFragmentChangedListener(
Page.UriFragmentChangedListener listener) {
return addListener(UriFragmentChangedEvent.class, listener,
@@ -524,6 +605,27 @@ public class Page implements Serializable {
}
/**
+ * Adds a listener that gets notified every time the URI of this page is
+ * changed due to back/forward functionality of the browser.
+ * <p>
+ * Note that one only gets notified when the back/forward button affects
+ * history changes with-in same UI, created by
+ * {@link Page#pushState(String)} or {@link Page#replaceState(String)}
+ * functions.
+ *
+ * @see #getLocation()
+ * @see Registration
+ *
+ * @param listener
+ * the Popstate listener to add
+ * @return a registration object for removing the listener
+ * @since 8.0
+ */
+ public Registration addPopStateListener(Page.PopStateListener listener) {
+ return addListener(PopStateEvent.class, listener, URI_CHANGED_METHOD);
+ }
+
+ /**
* Removes a URI fragment listener that was previously added to this page.
*
* @param listener
@@ -580,6 +682,7 @@ public class Page implements Serializable {
try {
location = new URI(location.getScheme(),
location.getSchemeSpecificPart(), newUriFragment);
+ pushState(location);
} catch (URISyntaxException e) {
// This should not actually happen as the fragment syntax is not
// constrained
@@ -588,7 +691,6 @@ public class Page implements Serializable {
if (fireEvents) {
fireEvent(new UriFragmentChangedEvent(this, newUriFragment));
}
- uI.markAsDirty();
}
private void fireEvent(EventObject event) {
@@ -866,9 +968,14 @@ public class Page implements Serializable {
notifications = null;
}
- if (location != null) {
- target.addAttribute(UIConstants.LOCATION_VARIABLE,
- location.toString());
+ if (newPushState != null) {
+ target.addAttribute(UIConstants.ATTRIBUTE_PUSH_STATE, newPushState);
+ newPushState = null;
+ }
+ if (newReplaceState != null) {
+ target.addAttribute(UIConstants.ATTRIBUTE_REPLACE_STATE,
+ newReplaceState);
+ newReplaceState = null;
}
if (styles != null) {
@@ -932,6 +1039,84 @@ public class Page implements Serializable {
}
/**
+ * Updates the browsers URI without causing actual page change. This method
+ * is useful if you wish implement "deep linking" to your application.
+ * Calling the method also adds a new entry to clients browser history and
+ * you can further use {@link PopStateListener} to track the usage of
+ * back/forward feature in browser.
+ * <p>
+ * Note, the current implementation supports setting only one new uri in one
+ * user interaction.
+ *
+ * @param uri
+ * to be used for pushState operation. The URI is resolved over
+ * the current location. If the given URI is absolute, it must be
+ * of same origin as the current URI or the browser will not
+ * accept the new value.
+ * @since 8.0
+ */
+ public void pushState(String uri) {
+ newPushState = uri;
+ uI.markAsDirty();
+ location = location.resolve(uri);
+ }
+
+ /**
+ * Updates the browsers URI without causing actual page change. This method
+ * is useful if you wish implement "deep linking" to your application.
+ * Calling the method also adds a new entry to clients browser history and
+ * you can further use {@link PopStateListener} to track the usage of
+ * back/forward feature in browser.
+ * <p>
+ * Note, the current implementation supports setting only one new uri in one
+ * user interaction.
+ *
+ * @param uri
+ * the URI to be used for pushState operation. The URI is
+ * resolved over the current location. If the given URI is
+ * absolute, it must be of same origin as the current URI or the
+ * browser will not accept the new value.
+ * @since 8.0
+ */
+ public void pushState(URI uri) {
+ pushState(uri.toString());
+ }
+
+ /**
+ * Updates the browsers URI without causing actual page change in the same
+ * way as {@link #pushState(String)}, but does not add new entry to browsers
+ * history.
+ *
+ * @param uri
+ * the URI to be used for replaceState operation. The URI is
+ * resolved over the current location. If the given URI is
+ * absolute, it must be of same origin as the current URI or the
+ * browser will not accept the new value.
+ * @since 8.0
+ */
+ public void replaceState(String uri) {
+ newReplaceState = uri;
+ uI.markAsDirty();
+ location = location.resolve(uri);
+ }
+
+ /**
+ * Updates the browsers URI without causing actual page change in the same
+ * way as {@link #pushState(URI)}, but does not add new entry to browsers
+ * history.
+ *
+ * @param uri
+ * the URI to be used for replaceState operation. The URI is
+ * resolved over the current location. If the given URI is
+ * absolute, it must be of same origin as the current URI or the
+ * browser will not accept the new value.
+ * @since 8.0
+ */
+ public void replaceState(URI uri) {
+ replaceState(uri.toString());
+ }
+
+ /**
* For internal use only. Used to update the server-side location when the
* client-side location changes.
*
@@ -943,7 +1128,7 @@ public class Page implements Serializable {
*/
@Deprecated
public void updateLocation(String location) {
- updateLocation(location, true);
+ updateLocation(location, true, false);
}
/**
@@ -957,8 +1142,11 @@ public class Page implements Serializable {
* @param fireEvents
* whether to fire {@link UriFragmentChangedEvent} if the URI
* fragment changes
+ * @param firePopstate
+ * whether to fire {@link PopStateEvent}
*/
- public void updateLocation(String location, boolean fireEvents) {
+ public void updateLocation(String location, boolean fireEvents,
+ boolean firePopstate) {
try {
String oldUriFragment = this.location.getFragment();
this.location = new URI(location);
@@ -967,6 +1155,9 @@ public class Page implements Serializable {
&& !SharedUtil.equals(oldUriFragment, newUriFragment)) {
fireEvent(new UriFragmentChangedEvent(this, newUriFragment));
}
+ if (firePopstate) {
+ fireEvent(new PopStateEvent(this, location));
+ }
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
diff --git a/server/src/main/java/com/vaadin/ui/UI.java b/server/src/main/java/com/vaadin/ui/UI.java
index 437a188361..c0dabb37c0 100644
--- a/server/src/main/java/com/vaadin/ui/UI.java
+++ b/server/src/main/java/com/vaadin/ui/UI.java
@@ -193,6 +193,12 @@ public abstract class UI extends AbstractSingleComponentContainer
public void acknowledge() {
// Nothing to do, just need the message to be sent and processed
}
+
+ @Override
+ public void popstate(String uri) {
+ getPage().updateLocation(uri, true, true);
+
+ }
};
private DebugWindowServerRpc debugRpc = new DebugWindowServerRpc() {
@Override
@@ -437,11 +443,6 @@ public abstract class UI extends AbstractSingleComponentContainer
actionManager.handleActions(variables, this);
}
- if (variables.containsKey(UIConstants.LOCATION_VARIABLE)) {
- String location = (String) variables
- .get(UIConstants.LOCATION_VARIABLE);
- getPage().updateLocation(location, true);
- }
}
/*
@@ -789,10 +790,10 @@ public abstract class UI extends AbstractSingleComponentContainer
int newWidth = page.getBrowserWindowWidth();
int newHeight = page.getBrowserWindowHeight();
- page.updateLocation(oldLocation.toString(), false);
+ page.updateLocation(oldLocation.toString(), false, false);
page.updateBrowserWindowSize(oldWidth, oldHeight, false);
- page.updateLocation(newLocation.toString(), true);
+ page.updateLocation(newLocation.toString(), true, false);
page.updateBrowserWindowSize(newWidth, newHeight, true);
}