How to replace ClickHandler with an Anchor link in AppListMenu.BadgeMenuItem to open views in a new browser tab

Hello!
I am working on a Jmix 2.4.4 project.
Currently, I am dynamically creating menu items (ListMenu.MenuItem) with a click handler (withClickHandler(...)) that handles navigation inside the same browser tab.

Problem:
I would like the menu items to behave like standard HTML <a> links instead of programmatic navigation, so that when a user clicks a menu item, it opens the view in a new browser tab (target="_blank").

In other words, I want to replace the ClickHandler with an Anchor component that navigates via href.

How can I correctly implement this behavior with AppListMenu.BadgeMenuItem?
Is it possible to attach an Anchor component directly to a BadgeMenuItem?
If not, what is the best approach — extending BadgeMenuItem, using a custom Component, or another solution?

private ListMenu.MenuItem badgeMenu(Navigation navigation, Class<? extends View<?>> view, RouteParameters route) {
    return new AppListMenu.BadgeMenuItem(navigation.getCrcId())
            .withBadge(navigationService.getBadge(navigation))
            .withTitle(translateService.translate(
                    navigation,
                    HasName.HasShortName.SHORT_NAME,
                    locale.getLanguage(),
                    navigation.getShortName()
            ))
            .withClassNames(List.of("sub-menu"))
            .withClickHandler(item -> {
                listMenu.getElement().executeJs("""
                        const activeMenus = document.getElementsByClassName("active");
                        for (let i = 0; i < activeMenus.length; i++) {
                            activeMenus[i].classList.remove("active");
                        }
                        document.getElementById($0).classList.add("active");
                        """, navigation.getCrcId());
                viewNavigators.view(origin, view)
                        .withRouteParameters(route)
                        .withQueryParameters(QueryParameters.of("navId", navigation.getCrcId()))
                        .withBackwardNavigation(true)
                        .navigate();
            });
}

Hello!

There is good news: since version 2.2.0, badges are supported in the MenuItem (Improve ListMenu · Issue #2667 · jmix-framework/jmix · GitHub).
You can set a badge as suffix or prefix component (see listMenu :: Jmix Documentation).

Note that ListMenu decides which component will be generated for a specific menu item. If you want to generate custom components for the menu items, you should override one or both the following methods:

  • createMenuRecursively() - the main method for creating components for menu items (MenuBarItem, separator, etc).
  • createMenuItemComponent() - creates component for MenuItem.class and returns RouterLink.

In fact, RouterLink is an anchor element (<a> ), so you can add specific attributes to it, such as target , rel , and so on.

Therefore, if you want users to open views from the menu in a new browser tab, you can add the following code to your custom list menu component:

public class AppListMenu extends JmixListMenu {

    @Override
    protected RouterLink createMenuItemComponent(MenuItem menuItem) {
        RouterLink item = super.createMenuItemComponent(menuItem);

        item.getElement().setAttribute("target", "_blank");

        return item;
    }
}

And registration of the component:

@Bean
public ComponentRegistration appListMenu() {
    return ComponentRegistrationBuilder.create(AppListMenu.class)
            .replaceComponent(JmixListMenu.class)
            .build();
}

However, I think the best option is not to modify the links in the menu. Since RouterLink is an anchor element, users can open a view in a new browser tab using a key combination. For example, on Windows: CTRL + left mouse button . This approach allows users to decide for themselves how a view should be opened.

Thanks Roman Pinyazhin for your response.

public class AppListMenu extends JmixListMenu {

    @Override
    protected RouterLink createMenuItemComponent(ListMenu.MenuItem menuItem) {
        RouterLink routerLink = super.createMenuItemComponent(menuItem);

        if (menuItem instanceof BadgeMenuItem badgeMenuItem) {
            routerLink.getElement().setAttribute("target", "_blank");
            routerLink.getElement().setAttribute("href", badgeMenuItem.getUrl());
            Div div = new Div();
            div.addClassName("menu-item-badge");
            div.setText(badgeMenuItem.getBadge());

            routerLink.add(div);
            routerLink.setId(badgeMenuItem.getId());
        }
        return routerLink;
    }

    @Getter
    public static class BadgeMenuItem extends JmixListMenu.MenuItem {
        protected String badge;
        protected String url;

        public BadgeMenuItem(String id) {
            super(id);
        }

        public BadgeMenuItem withBadge(String badge) {
            this.badge = badge;
            return this;
        }

        public BadgeMenuItem withUrl(String url) {
            this.url = url;
            return this;
        }
    }
 private ListMenu.MenuItem badgeMenu(Navigation navigation, Class<? extends View<?>> view, RouteParameters route) {
        String url = appUrl.concat("/").concat(view.getAnnotation(Route.class).value());
        if (route != null) url = url.replace(":id", route.get("id").isPresent() ? route.get("id").get() : "");

        return new AppListMenu.BadgeMenuItem(navigation.getCrcId())
                .withBadge(navigationService.getBadge(navigation))
                .withUrl(url)
                .withTitle(translateService.translate(
                        navigation,
                        HasName.HasShortName.SHORT_NAME,
                        locale.getLanguage(),
                        navigation.getShortName()
                ))
    }

I generated a URL like this:

String url = appUrl.concat("/").concat(view.getAnnotation(Route.class).value());
if (route != null) {
    url = url.replace(":id", route.get("id").isPresent() ? route.get("id").get() : "");
}

It works for now, but I am concerned about potential issues in the future.
Is this approach safe and reliable long-term, or are there any hidden problems I should be aware of?

Based on your code samples, it will be more convenient to replace BadgeMenuItem.class with standard ViewMenuItem.class. It will set the correct URL and RouteParameter for the RouterLink. You can then set the badge component using withSuffixComponent(). For instance:

private ListMenu.MenuItem badgeMenu(Navigation navigation, Class<? extends View<?>> view, RouteParameters route) {
    Div div = new Div();
    div.setText(navigationService.getBadge(navigation));

    return new ViewMenuItem(navigation.getCrcId())
            .withControllerClass(view)
            .withSuffixComponent(div)
            .withRouteParameters(route)
            .withTitle(translateService.translate(
                    navigation,
                    HasName.HasShortName.SHORT_NAME,
                    locale.getLanguage(),
                    navigation.getShortName()
            ));
}

And in createMenuItemComponent() method you should only update target attribute:

public class AppListMenu extends JmixListMenu {

    @Override
    protected RouterLink createMenuItemComponent(MenuItem menuItem) {
        RouterLink item = super.createMenuItemComponent(menuItem);

        item.getElement().setAttribute("target", "_blank");

        return item;
    }
}
1 Like

Thanks.
I will try it