Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Path-based routing #52

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2020 ScraM-Team.
*
* 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 org.teavm.flavour.routing;

import static org.teavm.flavour.routing.Routing.build;
import org.teavm.jso.browser.Location;
import org.teavm.jso.browser.Window;
import org.teavm.jso.dom.events.EventListener;
import org.teavm.jso.dom.events.HashChangeEvent;
import org.teavm.jso.dom.html.HTMLElement;

/**
* Handles routing via URL hashes.
*/
public class HashRoutingStrategy implements RoutingStrategy<HashChangeEvent> {
@Override
public void notifyListeners() {
}

@Override
public void addListener(RoutingListener listenerNew) {
}

@Override
public <T extends Route> T open(Window window, Class<T> routeType) {
return build(routeType, hash -> window.getLocation().setHash(hash));
}

@Override
public <T extends Route> T replace(Window window, Class<T> routeType) {
return build(routeType, hash -> {
window.getHistory().replaceState(null, null, "#" + Window.encodeURI(hash));
});
}

@Override
public String parse(Window window) {
Location location = window.getLocation();
String hash = location.getHash();
if (hash.startsWith("#")) {
hash = hash.substring(1);
}
return hash;
}

@Override
public boolean isBlank(Window window) {
return window.getLocation().getHash().isEmpty() || window.getLocation().getHash().equals("#");
}

@Override
public String makeUri(HTMLElement element, String hash) {
return "#" + hash;
}

@Override
public void addListener(Window window, EventListener<HashChangeEvent> listener) {
window.listenHashChange(listener);
}

@Override
public void open(String path) {
// HashRoutingStrategy doesn't use this method, hrefs are used instead
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* Copyright 2020 ScraM-Team.
*
* 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 org.teavm.flavour.routing;

import java.util.ArrayList;
import java.util.List;
import org.teavm.jso.JSBody;
import org.teavm.jso.browser.Location;
import org.teavm.jso.browser.Window;
import org.teavm.jso.dom.events.Event;
import org.teavm.jso.dom.events.EventListener;
import org.teavm.jso.dom.html.HTMLDocument;
import org.teavm.jso.dom.html.HTMLElement;
import org.teavm.jso.dom.html.HTMLHeadElement;
import org.teavm.jso.dom.xml.NodeList;

/**
* Handles routing via URL path changes.
*/
public class PathRoutingStrategy implements RoutingStrategy<Event> {
private List<RoutingListener> listListeners = new ArrayList<>();

@JSBody(params = {"document"}, script = "return document.baseURI;")
native public static String getBaseUri(HTMLDocument document);

@JSBody(params = {"window", "listener"}, script = "window.onpopstate = listener;")
native public static void listenPopState(Window window, EventListener<Event> listener);

public PathRoutingStrategy() {
setupBaseHref("");
}

public PathRoutingStrategy(String rootPath) {
setupBaseHref(rootPath);
}

public void setupBaseHref(String rootPath) {
final HTMLDocument document = Window.current().getDocument();
HTMLHeadElement head = document.getHead();
NodeList<? extends HTMLElement> baseNodes = head.getElementsByTagName("base");
if (baseNodes.getLength() == 0) {
HTMLElement base = document.createElement("base");
head.appendChild(base);
final Location location = Window.current().getLocation();
base.setAttribute("href", location.getProtocol() + "//" + location.getHost() + "/"
+ rootPath + (rootPath.isEmpty() ? "" : "/"));
}
}

private String trim(String path) {
if (path.startsWith("/")) {
return path.substring(1);
}
else return path;
}

@Override
public void notifyListeners() {
for (RoutingListener listener : listListeners) {
listener.handleLocationChange();
}
}

@Override
public void addListener(RoutingListener listenerNew) {
listListeners.add(listenerNew);
}

@Override
public <T extends Route> T open(Window window, Class<T> routeType) {
return Routing.build(routeType, hash -> {
open(window, getBaseUri(window.getDocument()) + Window.encodeURI(trim(hash)));
});
}

public void open(Window window, String path) {
window.getHistory().pushState(null, null, path);
notifyListeners();
}

@Override
public <T extends Route> T replace(Window window, Class<T> routeType) {
return Routing.build(routeType, hash -> {
window.getHistory().replaceState(null, null,
getBaseUri(window.getDocument()) + Window.encodeURI(trim(hash)));
notifyListeners();
});
}

@Override
public String parse(Window window) {
Location location = window.getLocation();
String baseUri = getBaseUri(window.getDocument());
String fullUrl = location.getFullURL();
String path = fullUrl.substring(baseUri.length());
if (path.startsWith("#")) {
path = path.substring(1);
}
return "/" + path;
}

@Override
public boolean isBlank(Window window) {
String path = parse(window);
return path.isEmpty();
}

@Override
public String makeUri(HTMLElement element, String path) {
String pathTrimmed = path.startsWith("/") ? path.substring(1) : path;
return getBaseUri(element.getOwnerDocument()) + pathTrimmed;
}

@Override
public void addListener(Window window, EventListener<Event> listener) {
listenPopState(window, (EventListener<Event>) listener);
}

@Override
public void open(String path) {
open(Window.current(), path);
}
}
8 changes: 1 addition & 7 deletions routing/src/main/java/org/teavm/flavour/routing/Route.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

import org.teavm.flavour.routing.emit.PathImplementor;
import org.teavm.flavour.routing.emit.RoutingImpl;
import org.teavm.jso.browser.Location;
import org.teavm.jso.browser.Window;

public interface Route {
Expand All @@ -26,12 +25,7 @@ default boolean parse() {
}

default boolean parse(Window window) {
Location location = window.getLocation();
String hash = location.getHash();
if (hash.startsWith("#")) {
hash = hash.substring(1);
}
return parse(hash);
return parse(Routing.parse(window));
}

default boolean parse(String path) {
Expand Down
47 changes: 43 additions & 4 deletions routing/src/main/java/org/teavm/flavour/routing/Routing.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,45 @@
import org.teavm.flavour.routing.emit.PathImplementor;
import org.teavm.flavour.routing.emit.RoutingImpl;
import org.teavm.jso.browser.Window;
import org.teavm.jso.dom.events.Event;
import org.teavm.jso.dom.events.EventListener;
import org.teavm.jso.dom.html.HTMLElement;

public final class Routing {
private static RoutingStrategy routingStrategy = new HashRoutingStrategy();

public static void usePathStrategy() {
routingStrategy = new PathRoutingStrategy();
}

public static void usePathStrategy(String rootPath) {
routingStrategy = new PathRoutingStrategy(rootPath);
}

public static RoutingStrategy getRoutingStrategy() {
return routingStrategy;
}

public static void addListener(RoutingListener listenerNew) {
routingStrategy.addListener(listenerNew);
}

static String parse(Window window) {
return routingStrategy.parse(window);
}

public static boolean isBlank(Window window) {
return routingStrategy.isBlank(window);
}

public static String makeUri(HTMLElement element, String path) {
return routingStrategy.makeUri(element, path);
}

public static void addListener(Window window, EventListener<Event> listener) {
routingStrategy.addListener(window, listener);
}

private Routing() {
}

Expand All @@ -38,17 +75,19 @@ public static <T extends Route> T build(Class<T> routeType, Consumer<String> con
}

public static <T extends Route> T open(Window window, Class<T> routeType) {
return build(routeType, hash -> window.getLocation().setHash(hash));
return (T) routingStrategy.open(window, routeType);
}

public static <T extends Route> T open(Class<T> routeType) {
return open(Window.current(), routeType);
}

public static void open(String path) {
routingStrategy.open(path);
}

static <T extends Route> T replace(Window window, Class<T> routeType) {
return build(routeType, hash -> {
window.getHistory().replaceState(null, null, "#" + Window.encodeURI(hash));
});
return (T) routingStrategy.replace(window, routeType);
}

static <T extends Route> T replace(Class<T> routeType) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2020 ScraM-Team.
*
* 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 org.teavm.flavour.routing;

/**
* Implement this to be ready to receive notifications that the route has changed.
*/
public interface RoutingListener {
void handleLocationChange();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2020 ScraM-Team.
*
* 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 org.teavm.flavour.routing;

import org.teavm.jso.browser.Window;
import org.teavm.jso.dom.events.Event;
import org.teavm.jso.dom.events.EventListener;
import org.teavm.jso.dom.html.HTMLElement;

/**
* Methods required of a pluggable routing implementation.
*/
public interface RoutingStrategy<E extends Event> {
void notifyListeners();

void addListener(RoutingListener listenerNew);

<T extends Route> T open(Window window, Class<T> routeType);

<T extends Route> T replace(Window window, Class<T> routeType);

String parse(Window window);

boolean isBlank(Window window);

String makeUri(HTMLElement element, String path);

void addListener(Window window, EventListener<E> listener);

void open(String path);
}
5 changes: 5 additions & 0 deletions templates/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@
<artifactId>teavm-flavour-expr</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.teavm.flavour</groupId>
<artifactId>teavm-flavour-routing</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.teavm</groupId>
<artifactId>teavm-jso-apis</artifactId>
Expand Down
Loading