Skip to content

Commit

Permalink
feat: Improving UX of Query parameters (#14034)
Browse files Browse the repository at this point in the history
made it easier to use both query and url parameters
shorthand to create URL parameters from single key-value pair

Fixes #14013
  • Loading branch information
mstahv committed Jun 29, 2022
1 parent ac7dfc6 commit c7e5ebe
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 0 deletions.
88 changes: 88 additions & 0 deletions flow-server/src/main/java/com/vaadin/flow/component/UI.java
Expand Up @@ -975,6 +975,94 @@ public <T extends Component> Optional<T> navigate(Class<T> navigationTarget,
navigate(navigationTarget, parameters);
}

/**
* Updates this UI to show the view corresponding to the given navigation
* target with the specified parameter. The parameter needs to be the same
* as defined in the route target HasUrlParameter.
* <p>
* Besides the navigation to the {@code location} this method also updates
* the browser location (and page history).
* <p>
* Note! A {@code null} parameter will be handled the same as
* navigate(navigationTarget) and will throw an exception if HasUrlParameter
* is not @OptionalParameter or @WildcardParameter.
* <p>
* If the view change actually happens (e.g. the view itself doesn't cancel
* the navigation), all navigation listeners are notified and a reference of
* the new view is returned for additional configuration.
*
* @param navigationTarget
* navigation target to navigate to
* @param parameter
* route parameter to pass to view
* @param queryParameters
* additional query parameters to pass to view
* @param <T>
* url parameter type
* @param <C>
* navigation target type
* @return the view instance, if navigation actually happened
* @throws IllegalArgumentException
* if a {@code null} parameter is given while navigationTarget's
* parameter is not annotated with @OptionalParameter
* or @WildcardParameter.
* @throws NotFoundException
* in case there is no route defined for the given
* navigationTarget matching the parameters.
*/
@SuppressWarnings("unchecked")
public <T, C extends Component & HasUrlParameter<T>> Optional<C> navigate(
Class<? extends C> navigationTarget, T parameter,
QueryParameters queryParameters) {

RouteConfiguration configuration = RouteConfiguration
.forRegistry(getInternals().getRouter().getRegistry());
RouteParameters parameters = HasUrlParameterFormat
.getParameters(parameter);
String url = configuration.getUrl(navigationTarget, parameters);
getInternals().getRouter().navigate(this,
new Location(url, queryParameters),
NavigationTrigger.UI_NAVIGATE);
return (Optional<C>) findCurrentNavigationTarget(navigationTarget);
}

/**
* Updates this UI to show the view corresponding to the given navigation
* target and query parameters.
* <p>
* Besides the navigation to the {@code location} this method also updates
* the browser location (and page history).
* <p>
* If the view change actually happens (e.g. the view itself doesn't cancel
* the navigation), all navigation listeners are notified and a reference of
* the new view is returned for additional configuration.
*
* @param navigationTarget
* navigation target to navigate to
* @param queryParameters
* additional query parameters to pass to view
* @param <T>
* navigation target type
* @return the view instance, if navigation actually happened
* @throws NotFoundException
* in case there is no route defined for the given
* navigationTarget matching the parameters.
*/
@SuppressWarnings("unchecked")
public <T extends Component> Optional<T> navigate(
Class<? extends T> navigationTarget,
QueryParameters queryParameters) {

RouteConfiguration configuration = RouteConfiguration
.forRegistry(getInternals().getRouter().getRegistry());
String url = configuration.getUrl(navigationTarget,
RouteParameters.empty());
getInternals().getRouter().navigate(this,
new Location(url, queryParameters),
NavigationTrigger.UI_NAVIGATE);
return (Optional<T>) findCurrentNavigationTarget(navigationTarget);
}

/**
* Updates this UI to show the view corresponding to the given location. The
* location must be a relative path without any ".." segments.
Expand Down
Expand Up @@ -102,6 +102,19 @@ private static Map<String, List<String>> toFullParameters(
entry -> Collections.singletonList(entry.getValue())));
}

/**
* Creates parameters from given key-value pair.
*
* @param key
* the name of the parameter
* @param value
* the value
* @return query parameters information
*/
public static QueryParameters of(String key, String value) {
return simple(Collections.singletonMap(key, value));
}

/**
* Creates parameters from a query string.
* <p>
Expand Down Expand Up @@ -216,4 +229,28 @@ private static String decode(String parameter) {
"Unable to decode parameter: " + parameter, e);
}
}

@Override
public String toString() {
return "QueryParameters(" + getQueryString() + ")";
}

@Override
public boolean equals(Object obj) {
if (obj == this)
return true;

if (obj instanceof QueryParameters) {
QueryParameters o = (QueryParameters) obj;
return parameters.equals(o.parameters);
} else {
return false;
}
}

@Override
public int hashCode() {
return parameters.hashCode();
}

}
Expand Up @@ -220,4 +220,21 @@ public void parameterWithEmptyValue() {
Collections.singletonMap("foo", Collections.singletonList("")));
Assert.assertEquals("foo", fullParams.getQueryString());
}

@Test
public void toStringValidation() {
String toString = QueryParameters.of("foo", "bar").toString();
Assert.assertEquals("QueryParameters(foo=bar)", toString);
}

@Test
public void equalsAndHashCode() {
QueryParameters qp1 = QueryParameters.of("foo", "bar");
QueryParameters qp2 = QueryParameters.fromString("foo=bar");
QueryParameters qp3 = QueryParameters.fromString("bar=foo");
Assert.assertEquals(qp1, qp2);
Assert.assertNotEquals(qp3, qp2);
Assert.assertEquals(qp1.hashCode(), qp2.hashCode());
}

}
@@ -0,0 +1,70 @@
/*
* Copyright 2000-2022 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.flow.uitest.ui;

import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.NativeButton;
import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.router.BeforeEvent;
import com.vaadin.flow.router.HasUrlParameter;
import com.vaadin.flow.router.OptionalParameter;
import com.vaadin.flow.router.QueryParameters;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.uitest.servlet.ViewTestLayout;

@Route(value = "com.vaadin.flow.uitest.ui.RouteAndQueryParametersView", layout = ViewTestLayout.class)
public class RouteAndQueryParametersView extends Div
implements HasUrlParameter<Integer> {
static final String REQUEST_PARAM_NAME = "testRequestParam";

private final Paragraph paramView;

public RouteAndQueryParametersView() {
paramView = new Paragraph("No input");
paramView.setId("paramView");
add(paramView);

NativeButton nativeButton = new NativeButton("Navigate with both");
nativeButton.setId("both");
nativeButton.addClickListener(e -> {
UI.getCurrent().navigate(RouteAndQueryParametersView.class, 5,
QueryParameters.of("foo", "bar"));
});
add(nativeButton);

NativeButton withQueryParametersOnly = new NativeButton(
"Navigate with qp");
withQueryParametersOnly.setId("qponly");
withQueryParametersOnly.addClickListener(e -> {
UI.getCurrent().navigate(RouteAndQueryParametersView.class,
QueryParameters.of("foo", "bar"));
});
add(withQueryParametersOnly);

}

@Override
public void setParameter(BeforeEvent event,
@OptionalParameter Integer parameter) {
String queryString = event.getLocation().getQueryParameters()
.getQueryString();
paramView.setText("route parameter: " + parameter + ", query string:"
+ queryString);

}
}
@@ -0,0 +1,44 @@
/*
* Copyright 2000-2022 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.flow.uitest.ui;

import org.junit.Assert;
import org.junit.Test;
import org.openqa.selenium.By;

import com.vaadin.flow.testutil.ChromeBrowserTest;

public class RouteAndQueryParametersIT extends ChromeBrowserTest {

@Test
public void testNavigationWithBothRouteAndQueryParameters() {
open();

Assert.assertFalse(getDriver().getPageSource().contains("foo=bar"));

findElement(By.id("both")).click();

Assert.assertTrue(getDriver().getPageSource()
.contains("route parameter: 5, query string:foo=bar"));

findElement(By.id("qponly")).click();

Assert.assertTrue(getDriver().getPageSource()
.contains("route parameter: null, query string:foo=bar"));

}

}

0 comments on commit c7e5ebe

Please sign in to comment.