Skip to content

Commit

Permalink
#127 - Reuse elements in case route does not change
Browse files Browse the repository at this point in the history
commit 57b18aa
Author: FrankHossfeld <[email protected]>
Date:   Wed Apr 15 08:04:52 2020 +0200

    # 127 - Reuse elements in case route does not change

commit 33038ff
Author: FrankHossfeld <[email protected]>
Date:   Wed Apr 15 08:02:31 2020 +0200

    # 127 - Reuse elements in case route does not change
  • Loading branch information
FrankHossfeld committed Apr 15, 2020
1 parent fc2cdc3 commit 9e98af6
Show file tree
Hide file tree
Showing 339 changed files with 5,571 additions and 5,587 deletions.
73 changes: 42 additions & 31 deletions etc/wiki/14 Controller & Component.md
Original file line number Diff line number Diff line change
Expand Up @@ -729,54 +729,65 @@ There are two kind of scopes:
* Scope.GLOBAL to cache the composite as a singleton (share between components / sites)
* Scope.LOCAL to cache the composite only for this component / site (default)

## Controller/Component resilience
## Reusing Controllers & Components

Consider a *Pet Shop* application consisting of a list of sold pets. The customer is presented with a list of pets being sold. Also, the customer is invited to click on a pet from the list to get more information about the animal. The shop routes to a *details page* for this purpose. The details page has also a browsing capability allowing the user to browse the data from the list without actually leaving the details page. Simply clicking on a *next* or a *previous* button or let's say by swiping on the screen, i.e. trigger a *next* or a *previous* action, the user is presented with the details of the next or previous pet.
In case you have a controller/component pair (using a route f.e. like this: `/application/person/detail/:id`) and you want to page through the person data, which means, the only thing that changes is the `id`, you can do this by using the same route and only changing the id. Nalu will always create a new controller/component pair in case of routing.

Technically, this is a use case such that Nalu has to route multiple consecutive times to the same *details page* Controller each time the user triggers a *next* or a *previous* action. So the route does not change, only the parameters. To be specific, in the Pet Shop case, this is the *:id* parameter.
Starting with version 2.1.0, Nalu will offer a new way to deal with this use case. Instead of always creating new components, you can tell Nalu to reuse them. To do so, just use `super.setMode(Mode.REUSE);`. this is the example code of a controller:

A normal Controller would fetch the Pet by the provided *id* and refresh the Component. This process may look as follows:

```Java
@Controller(route = "/petshop/pet/details/:id",
```java
@Controller(route = "/application/person/detail/:id",
selector = "content",
componentInterface = PetShopComponent.class,
component = PetShopComponentImpl.class)
public class PetDetailsController
extends AbstractComponentController<PetShopContext, PetShopComponent, HTMLElement>
implements PetShopComponent.Controller {

private String id;

public PetDetailsController() {
componentInterface = IDetailComponent.class,
component = DetailComponent.class)
public class DetailController
extends AbstractComponentController<Context, IDetailComponent, HTMLElement>
implements IDetailComponent.Controller,
IsComponentCreator<IDetailComponent> {

private long id;

public DetailController() {
}

@Override
public void start() {
super.setMode(Mode.REUSE);
}

@Override
public void activate() {
DomGlobal.window.console.log("activate");
Pet pet = PetService.obtainPet(this.id);
getComponent().initPet(pet);
// ToDo: load data, set up component, etc.
}

@AcceptParameter("id")
public void setId(String s) {
this.id = (s != null) ? s : "";
public void setId(String id)
throws RoutingInterceptionException {
try {
this.id = Long.parseLong(id);
} catch (NumberFormatException e) {
// ToDo: error handling
}
}

}

```

In the *initPet(Pet pet)* method, the Component will likely transform the data in Pet and show it to the user. There are two possible ways of doing that:
To create a controller, that can be reused:

* add `super.setMode(Mode.REUSE);` to the `start`-method of the controller

* use the `activate`-method to load data, set up the compoment, etc.

With `Mode` set to `REUSE`, Nalu will compare the last executed hash with the new one. In case both hashes are not equal, Nalu will work as always, creating everything, etc.

1. Destroy/abandon the already built UI structure (HTMLElements/Widgets/DOM) and build a new one with the new *Pet* data. Then call *initElement()* so that the new UI structure is displayed.
2. Reuse the already built UI structure and replace only the data while leaving the DOM tree in place.
In case the last used hash is equal to the current hash, Nalu will execute the following steps:

Depending on the requirements, the first one is perfectly fine.
* call the `mayStop`-method (so, the routing can be interrupted)

The second one is preferable, if only a part of the refreshed page contains changes. It is plausible to change only that part instead of removing the whole content and rebuilding it.
* call `deactivate`-method to deactivate the controller/composites

Routing to the *PetDetailsController* can behave in one of three ways:
* if the Controller is **not cached**, the router will abandon the Controller and the Component from the previous call. Instead, it will create a new Controller and Component. This means that the DOM tree will be recomputed, new data will be fetched etc.
* if the Controller is **cached**, the router will not recreate the Controller and Component. Their DOM elements will be removed from the selector but not destroyed. This makes it possible to reuse them in case the user returns to the */petshop/pet/details/:id* route. If another route is selected, e.g. */petshop/pet/list* (which may also be cached), the now empty selector will be filled with it's corresponding DOM structure. In the case of *multiple consecutive routing events to the same route*, the same DOM will be re-injected in the selector. We just saved us the rebuilding of both Controller and Component, but there is a slight catch leading to an unexpected user experience issue. Removing an element from the DOM and reinserting it will reset the view of said element (e.g. reset it's scrollbars, caret position, blinking etc). These are all undesirable.
* if the Controller is **cached AND resilient**, then the DOM inside the selector is not removed on consecutive routing events to the same route thus eliminating the removing and inserting of the same element. This allows the corresponding Component to remain in place and refresh only it's data without a reset or glitches diminishing the user experience.
* inject the new parameter values into the controller

So in summary: in the behavioral context of the Router, *resilience* is to the Components and their DOM the same as what *caching* is to the Controllers and their fields.
* call the `activate`-method
30 changes: 15 additions & 15 deletions nalu-plugin-core-web/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,27 +73,27 @@
</properties>

<!--<dependencyManagement>-->
<!--<dependencies>-->
<!--<dependency>-->
<!--<groupId>com.google.gwt</groupId>-->
<!--<artifactId>gwt</artifactId>-->
<!--<version>${gwt.version}</version>-->
<!--<type>pom</type>-->
<!--<scope>import</scope>-->
<!--</dependency>-->
<!--</dependencies>-->
<!--<dependencies>-->
<!--<dependency>-->
<!--<groupId>com.google.gwt</groupId>-->
<!--<artifactId>gwt</artifactId>-->
<!--<version>${gwt.version}</version>-->
<!--<type>pom</type>-->
<!--<scope>import</scope>-->
<!--</dependency>-->
<!--</dependencies>-->
<!--</dependencyManagement>-->

<dependencies>
<!--<dependency>-->
<!--<groupId>com.google.gwt</groupId>-->
<!--<artifactId>gwt-user</artifactId>-->
<!--<scope>provided</scope>-->
<!--<groupId>com.google.gwt</groupId>-->
<!--<artifactId>gwt-user</artifactId>-->
<!--<scope>provided</scope>-->
<!--</dependency>-->
<!--<dependency>-->
<!--<groupId>com.google.gwt</groupId>-->
<!--<artifactId>gwt-dev</artifactId>-->
<!--<scope>provided</scope>-->
<!--<groupId>com.google.gwt</groupId>-->
<!--<artifactId>gwt-dev</artifactId>-->
<!--<scope>provided</scope>-->
<!--</dependency>-->

<dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@

public abstract class AbstractLogger
implements IsLogger {

protected static final String INDENT = "..";

protected String createLog(String message,
int depth) {
if (depth == 0) {
Expand All @@ -40,5 +40,5 @@ protected String createLog(String message,
return "Nalu-Logger -> " + indent;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,7 @@
import java.util.*;

public class NaluPluginCoreWeb {

public static boolean isSuperDevMode() {
return "on".equals(System.getProperty("superdevmode",
"off"));
}


/**
* Log's non-existing selector that was not found inside DOM on the browser's console
*
Expand All @@ -50,27 +45,20 @@ public static void logNonExistingSelector(String selector) {
.logSimple(sb,
0);
}

/**
* Log's the new URL on the browser's console
*
* @param newUrl new url to log
*/
private static void logNewUrl(String newUrl) {
String sb = "Router: new url ->>" + newUrl + "<<";
ClientLogger.get()
.logSimple(sb,
0);

public static boolean isSuperDevMode() {
return "on".equals(System.getProperty("superdevmode",
"off"));
}

@SuppressWarnings("StringSplitter")
public static void getContextPath(ShellConfiguration shellConfiguration) {
if (PropertyFactory.get()
.isUsingHash()) {
return;
}
Location location = Js.uncheckedCast(DomGlobal.location);
String pathName = location.getPathname();
String pathName = location.getPathname();
if (pathName.startsWith("/") && pathName.length() > 1) {
pathName = pathName.substring(1);
}
Expand Down Expand Up @@ -104,12 +92,12 @@ public static void getContextPath(ShellConfiguration shellConfiguration) {
PropertyFactory.get()
.setContextPath("");
}

@SuppressWarnings("StringSplitter")
public static NaluStartModel getNaluStartModel() {
Location location = Js.uncheckedCast(DomGlobal.location);
Location location = Js.uncheckedCast(DomGlobal.location);
Map<String, String> queryParameters = new HashMap<>();
String search = location.getSearch();
String search = location.getSearch();
if (!Objects.isNull(search)) {
if (search.startsWith("?")) {
search = search.substring(1);
Expand All @@ -123,7 +111,7 @@ public static NaluStartModel getNaluStartModel() {
} else if (split.length == 2) {
queryParameters.put(split[0],
split[1]);

}
});
}
Expand Down Expand Up @@ -159,7 +147,7 @@ public static NaluStartModel getNaluStartModel() {
return new NaluStartModel(startRoute,
queryParameters);
}

private static String getHashValue(String hash) {
if (!Objects.isNull(hash)) {
if (hash.startsWith("#")) {
Expand All @@ -172,40 +160,7 @@ private static String getHashValue(String hash) {
}
return null;
}

public static void route(String newRoute,
boolean replace,
RouteChangeHandler handler) {
String newRouteToken;
if (PropertyFactory.get()
.isUsingHash()) {
newRouteToken = newRoute.startsWith("#") ? newRoute : "#" + newRoute;
} else {
newRouteToken = "/";
if (PropertyFactory.get()
.getContextPath()
.length() > 0) {
newRouteToken = newRouteToken +
PropertyFactory.get()
.getContextPath() +
"/";
}
newRouteToken = newRouteToken + newRoute;
}
if (PropertyFactory.get()
.hasHistory()) {
if (replace) {
DomGlobal.window.history.replaceState(newRouteToken,
null,
newRouteToken);
} else {
DomGlobal.window.history.pushState(newRouteToken,
null,
newRouteToken);
}
}
}


public static void addPopStateHandler(RouteChangeHandler handler,
String contextPath) {
DomGlobal.window.onpopstate = e -> {
Expand Down Expand Up @@ -241,18 +196,7 @@ public static void addPopStateHandler(RouteChangeHandler handler,
return null;
};
}

public static void addOnHashChangeHandler(RouteChangeHandler handler) {
DomGlobal.window.onhashchange = e -> {
String newUrl;
Location location = Js.uncheckedCast(DomGlobal.location);
newUrl = location.getHash();
NaluPluginCoreWeb.handleChange(handler,
newUrl);
return null;
};
}


private static void handleChange(RouteChangeHandler handler,
String newUrl) {
if (newUrl.startsWith("#")) {
Expand All @@ -271,5 +215,61 @@ private static void handleChange(RouteChangeHandler handler,
handler.onRouteChange(newUrl);
}
}


public static void route(String newRoute,
boolean replace,
RouteChangeHandler handler) {
String newRouteToken;
if (PropertyFactory.get()
.isUsingHash()) {
newRouteToken = newRoute.startsWith("#") ? newRoute : "#" + newRoute;
} else {
newRouteToken = "/";
if (PropertyFactory.get()
.getContextPath()
.length() > 0) {
newRouteToken = newRouteToken +
PropertyFactory.get()
.getContextPath() +
"/";
}
newRouteToken = newRouteToken + newRoute;
}
if (PropertyFactory.get()
.hasHistory()) {
if (replace) {
DomGlobal.window.history.replaceState(newRouteToken,
null,
newRouteToken);
} else {
DomGlobal.window.history.pushState(newRouteToken,
null,
newRouteToken);
}
}
}

/**
* Log's the new URL on the browser's console
*
* @param newUrl new url to log
*/
private static void logNewUrl(String newUrl) {
String sb = "Router: new url ->>" + newUrl + "<<";
ClientLogger.get()
.logSimple(sb,
0);
}

public static void addOnHashChangeHandler(RouteChangeHandler handler) {
DomGlobal.window.onhashchange = e -> {
String newUrl;
Location location = Js.uncheckedCast(DomGlobal.location);
newUrl = location.getHash();
NaluPluginCoreWeb.handleChange(handler,
newUrl);
return null;
};
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,23 @@
import java.util.Map;

public class NaluStartModel {

private String startRoute;

private Map<String, String> queryParameters;

public NaluStartModel(String startRoute,
Map<String, String> queryParameters) {
this.startRoute = startRoute;
this.startRoute = startRoute;
this.queryParameters = queryParameters;
}

public String getStartRoute() {
return startRoute;
}

public Map<String, String> getQueryParameters() {
return queryParameters;
}

}
Loading

0 comments on commit 9e98af6

Please sign in to comment.