Any ideas on how to create a menu with a SideNav (like the one created on Vaadin Start) where a Badge on the right keeps updating at a fixed rate or asynchronously when an event happens?
For example, a message count that when you receive a new email changes from 1 to 2, or a real time user session monitor where the number keeps updating to display the logged in users count.
I tried to update it from a Thread in the MainLayout class but I keep getting exceptions that the session scope doesn’t exist when I try to use UI.access() method to update the contents of all badges.
While thinking about this it would make sense encapsulate this to a BadgeSideNavItem.
You can add any component as icon as PrefixComponent.
Create a Div that has an icon and a Badge as children, then set the Div as PrefixComponent.
Keep a reference to the the Badge and update its text with Span.setText within the Thread. And as you mention you need to wrap setText with UI.access(). This updates only this single component.
I would do it like this:
BadgeSideNavItem.class
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import com.knoell.webclient.view.LoginView;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.icon.VaadinIcon;
import com.vaadin.flow.component.sidenav.SideNavItem;
public class BadgeSideNavItem extends SideNavItem {
private final Span badge;
// implement other super constructors
public BadgeSideNavItem(String label, Class<? extends Component> view, Component icon) {
super(label, view);
this.badge = createBadge();
var div = new Div(icon, this.badge);
setPrefixComponent(div);
}
private Span createBadge() {
var span = new Span();
span.getElement().getThemeList().add("badge contrast");
// TODO update sizing and styles that it is smaller
return span;
}
public void setBadgeText(String text) {
this.badge.setText(text);
this.badge.setVisible(text != null);
}
}
MainLayout
private BadgeSideNavItem badgeSideNavItem;
private Integer messageCount;
public void MainView() {
badgeSideNavItem = new BadgeSideNavItem("Login", LoginView.class, VaadinIcon.AIRPLANE.create());
final var ui = UI.getCurrent();
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {
ui.access(() -> {
badgeSideNavItem.setBadgeText((messageCount++).toString());
});
}, 5, 0, TimeUnit.SECONDS);
}
Awesome solution! I was fighting with the Thread class and didn’t remember about Executors. Definitely it’s much cleaner and avoids the busy-wait issues.
Before writing the question, I already got to a very close solution as you proposed, but now I think I’m starting to realize what was doing wrong.
On the constructor of my class BadgeSideNavItem (MenuItemInfo in my case), I pass a Supplier<Integer> countProvider parameter optionally. So, then, on the thread I stream all the menusItems, filter only those with a non-null count provider and then get from the supplier a new value.
And here is where all the mess starts, with Spring complaining that scope 'authenticatedUser' is not defined in the Thread.
The issue, is I have a AuthenticatedUser class that holds the logged in user metadata after login, and on the supplier I had something like () -> counterService.countActiveCases(user.getCustomer()). As the user.getCustomer was not final, it is aparently resolved when calling Supplier::get() on the render() method. So inside the thread, the user entity doesn’t exist or is properly scoped.
After creating as final the bean before creating the supplier lambda, it works perfeclty.