vaadin / flow

Vaadin Flow is a Java framework binding Vaadin web components to Java. This is part of Vaadin 10+.
Apache License 2.0
616 stars 167 forks source link

Allow changing parent layout upon route autodiscovery #7746

Open mvysny opened 4 years ago

mvysny commented 4 years ago

Vaadin 14.1.x.

In bigger apps it could happen that certain @Routes are reused between different apps, for example user management-related routes would be published in customer's Nexus as a jar artifact, then linked to multiple WAR projects.

The problem is that every WAR app defines its own parent layout specific to that app; the user management-related routes therefore can't use the @Route(value = 'users/add', layout = AppLayout) annotation because they do not have the AppLayout class on the classpath.

Currently the solution is to either use @Route(registerAtStartup = false, then discover the routes manually (or automatically by using customized classpath scanning solution), then register the routes into RouteRegistry; alternatively you can visit the registered routes in the registry and modify their parent layouts. The API is not simple and so this approach can be error-prone.

I'd suggest to be able to set a closure somewhere to Vaadin, and that closure would be called for every route discovered. The closure should then be able to tell a different layout class to use; Vaadin should then automatically compute the parent chain from that layout class.

mvysny commented 4 years ago

The current workaround: The code for walking through the registered routes and rewriting the parent layout:

    private void reconfigureRoutesForAppLayout(Router router) {

        //We have some exceptions that should not show the menu...
        Set<String> rootLayoutOnlyViews = new HashSet<>();
        rootLayoutOnlyViews.add("com.example.LoginRoute")

        RouteRegistry routeRegistry = router.getRegistry()
        List<RouteData> routes = routeRegistry.getRegisteredRoutes().stream().filter({ routeData -> !routeData.getParentLayouts().contains(AppLayout.class) }).filter({ routeData -> !rootLayoutOnlyViews.contains(routeData.getNavigationTarget().getName()) }).collect(Collectors.toList())

        List<Class<? extends RouterLayout>> parentChain = new ArrayList<>()
        parentChain.add(AppLayout.class)
        parentChain.add(RootLayout.class)

        if (routes.size() > 0) {
            log.info("Reconfiguring " + routes.size() + " routes to use AppLayout")
            routes.forEach({ routeData -> replaceRouteWithNewParentChain(routeRegistry, routeData, parentChain) })
        }
    }

    private void replaceRouteWithNewParentChain(RouteRegistry routeRegistry, RouteData routeData, List<Class<? extends RouterLayout>> parentChain) {
        log.info("Reconfiguring: " + routeData.getNavigationTarget().getName())
        routeRegistry.removeRoute(routeData.getNavigationTarget())
        routeRegistry.setRoute(routeData.getUrl(), routeData.getNavigationTarget(), parentChain);
    }
Legioth commented 4 years ago

Yet another workaround approach could be to have generic components in the shared jar file and with minimal subclasses that only introduce the @Route annotation in each war file, e.g.

@Route(value = 'users/add', layout = AppLayout.class)
public class AppUserAddView extends SharedUserAddView {
}