What is wrong about vaadin’s Breadcrumb?

I’m going to tell about my experience with add-on page. First of all, this page contains outdated code examples, but that’s only the start of the list.

The point is that this component provides only the styles for active/inactive buttons. (Maybe I expected too much after using WordPress breadcrumb plugin.) The rest of responsibilities is all yours. You are to recreate Breadcrumb when you need to change the list of the Buttons (actually it’s a Map componentToLi, but it’s still private with no setter). You are to memorize in your code which view/page is active.

Luckily, vaadin provides all the means to cover these gaps. Let’s see the needed harness.
In my code I’ve composed a wrapper class CustomBreadcrumb:

public class CustomBreadcrumb implements NavigationEventListener {
 
private List<BreadCrumbLinkWrapper> links;
    //private Field mapField;
    private NavigationEventEnum currentEvent;
    private Breadcrumb breadcrumb;
    private SessionContainer sessionContainer;
 
    public CustomBreadcrumb(SessionContainer sessionContainer) {
 
        this.sessionContainer = sessionContainer;
        currentEvent = null;
        links = new ArrayList<>();
 
        resetBreadcrumb();
 
        // register in event broadcaster
        if (sessionContainer != null) {
         // is null in unit tests
            sessionContainer.getNavigationEventBroadcaster().register(this);
        }
    }
 
    public Breadcrumb getBreadcrumb() {
        return breadcrumb;
    }
 
    public void setBreadcrumb(Breadcrumb breadcrumb) {
        this.breadcrumb = breadcrumb;
    }
 
    public NavigationEventEnum getCurrentEvent() {
        return currentEvent;
    }
 
    public void setCurrentEvent(NavigationEventEnum currentEvent) {
        this.currentEvent = currentEvent;
    }
 
    public void resetLinks() {
        links = new ArrayList<>();
    }
 
    public List<BreadCrumbLinkWrapper> getLinks() {
        return links;
    }
 
    public void setLinks(List<BreadCrumbLinkWrapper> links) {
        this.links = links;
    }
 
    public List<Button> renderButtons(List<BreadCrumbLinkWrapper> links) {
        List<Button> buttons = new ArrayList<>();
        if (links != null && links.size() > 0) {
            links.forEach(link -> {
                buttons.add(link.getButton());
            });
        }
        return buttons;
    }
 
    // this method generates a new list of buttons
    public List<BreadCrumbLinkWrapper> updateWrappers(BreadCrumbLinkWrapper newLinkWrapper) {
        Integer level = newLinkWrapper
                .getValue()
                .getLevel();
 
        List<BreadCrumbLinkWrapper> newButtons = new ArrayList<>();
 
        // now we are adding Buttons with level higher than the level of the new button
        // level 0 is higher than level 1
        links.forEach(wrapper -> {
            Integer currentLevel = wrapper.getValue().getLevel();
            if (level > currentLevel) {
                newButtons.add(wrapper);
            }
        });
        // adding the new Button itself
        newButtons.add(newLinkWrapper);
        resetBreadcrumb();
        setLinks(newButtons);
        return newButtons;
    }
 
    public void resetBreadcrumb() {
        breadcrumb = new Breadcrumb();
        breadcrumb.addStyleName("customBreadcrumb");
        breadcrumb.setShowAnimationSpeed(Breadcrumb.AnimSpeed.SLOW);
        breadcrumb.setHideAnimationSpeed(Breadcrumb.AnimSpeed.SLOW);
        breadcrumb.setUseDefaultClickBehaviour(false);
    }
 
    private void addButton(Button button) {
        breadcrumb.addLink(button);
    }
 
    // the returned list is to be set into Breadcrumb component
    public void setButtonsFromWrappers() {
        getLinks().forEach(wrapper -> {
            addButton(wrapper.getButton());
        });
    }
 
    @Override
    public void handleNavigationEvent(NavigationEventEnum navigationEvent) {
        sessionContainer
                .getCustomBreadCrumb()
                .setCurrentEvent(navigationEvent);
 
        BaseLayout baseLayout = sessionContainer.getBaseLayout();
 
        if (navigationEvent.equals(NavigationEventEnum.MAIN)) {
            baseLayout.cleanWorkingArea();
            baseLayout.renderMainView();
        } else if (navigationEvent.equals(NavigationEventEnum.NEW_REQUESTS)) {
            baseLayout.cleanWorkingArea();
            baseLayout.renderNewRequestsView();
        } else {
            throw new IllegalStateException("unknown event");
        }
    }
}

SessionContainer looks like:

@SpringComponent
@VaadinSessionScope
public class SessionContainer {
    ...
    private CustomBreadcrumb customBreadCrumb;
 
    @Autowired
    private NavigationEventBroadcaster navigationEventBroadcaster;
 
    @PostConstruct
    private void init() {
        customBreadCrumb = new CustomBreadcrumb(this);
    }
    ...
}

It’s managed by spring and is unique for each session. Now CustomBreadcrumb has access to this container.

NavigationEventEnum, listing only a couple of events:

public enum NavigationEventEnum {
 
    MAIN(0),
    NEW_REQUESTS(1),
    IN_PROCESS_REQUESTS(1),
    ANALYSE_FIRST_STEP(2);
 
    private Integer level;
 
    NavigationEventEnum(Integer level) {
        this.level = level;
    }
 
    public Integer getLevel() {
        return level;
    }
 
    public void setLevel(Integer level) {
        this.level = level;
    }
}

Each event/view has its level. Level=0 is assigned to the main one, for the others it get increased. This parameter helps render hierarchy of buttons, say MAIN > IN_PROCESS_REQUESTS > ANALYSE_FIRST_STEP.

NavigationEventListener is an interface whose instances are informed about navigation events (part of observer pattern):

public interface NavigationEventListener {
 
    void handleNavigationEvent(NavigationEventEnum navigationEvent);
 
}
@SpringComponent
@VaadinSessionScope
public class NavigationEventBroadcaster {
 
    private static final Logger LOG = Logger.getLogger(NavigationEventBroadcaster.class);
 
    private Set<NavigationEventListener> listeners;
 
    public NavigationEventBroadcaster() {
        listeners = new HashSet<>();
    }
 
    public void register(NavigationEventListener listener) {
        if (Objects.nonNull(listener)) {
            listeners.add(listener);
        }
    }
 
    public void unregister(NavigationEventListener listener) {
        if (!CollectionUtils.isEmpty(listeners)) {
            listeners.remove(listener);
        }
    }
 
    public void notify(NavigationEventEnum navigationEvent) {
        if (!CollectionUtils.isEmpty(listeners)) {
            LOG.debug("NavigationEventBroadcaster::notify - catch navigationEvent " + navigationEvent.name());
            listeners.forEach(listener -> {
                listener.handleNavigationEvent(navigationEvent);
            });
        }
    }
 
}

Again, only a couple of enum values are handle, it’s up to you to extend:

public class BreadCrumbLinkFactory {
 
    public static BreadCrumbLinkWrapper construct(SessionContainer sesionContainer, NavigationEventEnum navigationEventEnum, MessageSource messageSource, Locale locale) {
 
        if (navigationEventEnum.equals(NavigationEventEnum.MAIN)) {
            Button mainButton = new Button(messageSource.getMessage("mainView", null, locale));
            BreadCrumbLinkFactory.addStandardClickListener(sesionContainer, mainButton, navigationEventEnum);
            BreadCrumbLinkWrapper mainWrapper = new BreadCrumbLinkWrapper(mainButton, NavigationEventEnum.MAIN);
            return mainWrapper;
        } else if (navigationEventEnum.equals(NavigationEventEnum.NEW_REQUESTS)) {
            Button newRequestsButton = new Button(messageSource.getMessage("newRequestsView", null, locale));
            BreadCrumbLinkFactory.addStandardClickListener(sesionContainer, newRequestsButton, navigationEventEnum);
            BreadCrumbLinkWrapper newRequestsWrapper = new BreadCrumbLinkWrapper(newRequestsButton, NavigationEventEnum.NEW_REQUESTS);
            return newRequestsWrapper;
        } else {
            throw new IllegalStateException("unknown event");
        }
 
    }
 
    private static void addStandardClickListener(SessionContainer sessionContainer, Button button, NavigationEventEnum navigationEventEnum) {
        button.addClickListener(clickEvent -> {
 
            // if the button of this level/view is pressed, we check the actual/current level
            if (navigationEventEnum.equals(sessionContainer.getCustomBreadCrumb().getCurrentEvent())) {
                Notification.show("we are already on this page, no need to regenerate UI");
            } else {
                Notification.show("going to page/view corresponding to " + navigationEventEnum);
                sessionContainer
                        .getNavigationEventBroadcaster()
                        .notify(navigationEventEnum);
            }
        });
    }
}
@SpringComponent
@ViewScope
public class BaseLayout extends CssLayout {
    @Autowired
    private SessionContainer sessionContainer;
 
    ...
 
    public void renderMainView() {
 
        CustomBreadcrumb customBreadcrumb = sessionContainer.getCustomBreadCrumb();
        // cleaning up
        workingArea.removeAllComponents();
        // create a wrapped button
        BreadCrumbLinkWrapper mainLinkWrapper = BreadCrumbLinkFactory.construct(sessionContainer, NavigationEventEnum.MAIN, messageSource, locale);
        // add the wrapper of the new Button into BreadCrumb
        customBreadcrumb.updateWrappers(mainLinkWrapper);
        customBreadcrumb.setButtonsFromWrappers();
        Breadcrumb breadcrumb = customBreadcrumb.getBreadcrumb();
        workingArea.addComponent(breadcrumb);
 
        //main working area default layout
        CreateNewRequestLayout createNewRequestLayout = new CreateNewRequestLayout(messageSource);
        workingArea.addComponent(createNewRequestLayout);
    }
 
}
You can leave a response, or trackback from your own site.

Leave a Reply