Kevin
Kevin

Reputation: 4128

How to implement a recursive menu with PrimeFaces

I am trying to create a dynamic menu: a menu like those found on Amazon or eBay to browse categories. My first attempt is shown below:

The backing bean:

@ManagedBean
@ViewScoped
public class CategoryBackBean implements ActionListener {
    private MenuModel model;
    private Category category;

    public CategoryBackBean() throws IOException {
        category = Category.createRootCategory();
        createModel();
    }


    private void createModel() throws IOException {
        MenuModel tempModel = new DefaultMenuModel();
        for(Category c : category.getChildCategories()) {
            MenuItem childItem = new MenuItem();
            childItem.setValue(c.getName());
            childItem.addActionListener(this);
            tempModel.addMenuItem(childItem);
        }
        this.model = tempModel;
    }

    public MenuModel getModel() {
        return model;
    }

    @Override
    public void processAction(ActionEvent event) throws AbortProcessingException {
        try {
            MenuItem item = (MenuItem) event.getSource();
            String categoryName = (String) item.getValue();
            for(Category c : category.getChildCategories()) {
                if(c.getName().equals(categoryName)) {
                    category = c;
                    createModel();
                    return;
                }
            }
        } catch (IOException ex) {
            Logger.getLogger(CategoryBackBean.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}

The webpage:

<h:body>
    <h:form>
        <p:menubar model="#{categoryBackBean.model}" />
    </h:form>
</h:body>

For starters, my design doesn't work: the initial menu is created, but when clicking on buttons, the menu is not recreated in sub-categories.

What is the best way to tackle this general problem? I'm not looking for quick hacks to get the above code working- I'm looking for a general design for a recursive menu.

Upvotes: 3

Views: 9321

Answers (4)

BalusC
BalusC

Reputation: 1108642

You need to update the menu after the action is completed. Given that you don't want to hardcode the menu ID, use @parent.

childItem.setUpdate("@parent");

An alternative, if the menu is the sole component in the form, is to just use @form.

Whether this all is the "best" way or not can't be objectively answered. If the code does exactly the job you want in the simplest possible and least intrusive way, then it's acceptable.

Upvotes: 5

Kevin
Kevin

Reputation: 4128

Solution 3. This solution is very simple. The backing bean:

@ManagedBean 
@ViewScoped 
public class CategoryBackBean implements Serializable {
    private Category category = Category.createRootCategory();
        public void setCategory(Category category) {
        this.category = category;
    }     

    public Category getCategory() { 
        return category;
  }
}

The web page:

<h:form>
    <p:menu id="myMenu">
        <c:forEach items="#{categoryBackBean.category.childCategories}" var="subCategory">
            <p:menuitem value="#{subCategory.name}" update="myMenu">
                <f:setPropertyActionListener target="#{categoryBackBean.category}" value="#{subCategory}" />
            </p:menuitem>
        </c:forEach>
    </p:menu>
</h:form>

Notable points:

  • <c:forEach> was used instead of <ui:repeat> as the latter doesn't work inside a Primefaces menu.

  • The update="myMenu" is set in the XHTML, and not in the backing code. This removes the issue with my Solution #2.

  • The design goes goes against a suggested Primefaces design heuristics, as hinted to here, which is that a backing model should be used instead of creating the menu with <ui:repeate> or <c:forEach> (I would argue against this, given the simplicity of the technique <c:forEach>).

Update

Do not use this answer! The <c:forEach/> 'runs' before the component tree is created, and is not re-run when the page is updated, either through Ajax or using a postback. The only reason it worked for me was due to the first category having the most sub-categories, and thus, it would initially create the component tree with enough <p:menuitems /> for any other category.

Upvotes: 0

Kevin
Kevin

Reputation: 4128

Another solution, solution 2. This solution avoids the need for setting an action expression, however, it does introduce a backwards dependency on the XHTML code (in this case <p:menubar ..> must have an id of "menuid").

private void createModel() throws IOException {
    MenuModel tempModel = new DefaultMenuModel();
    for(Category c : childCategories) {
        MenuItem childItem = new MenuItem();
        childItem.setValue(c.getName());
        childItem.addActionListener(this);
        childItem.setUpdate("menuId");     // Magic new line.
        tempModel.addMenuItem(childItem);
    }
    this.model = tempModel;
}

This works by making every menu item cause the whole menubar to be updated via Ajax. If there was some way to get the menu's id without hard-coding...

Upvotes: 0

Kevin
Kevin

Reputation: 4128

Solution 1.

I think for the actionListener to be notified, there must first be an action event. So I tried adding this ugly code (surely there is a better way?) to add an action event to each childItem:

private void createModel() throws IOException {
    FacesContext facesCtx = FacesContext.getCurrentInstance();
    ELContext elCtx = facesCtx.getELContext();
    ExpressionFactory expFact = facesCtx.getApplication().getExpressionFactory();

    MenuModel tempModel = new DefaultMenuModel();
    for(Category c : childCategories) {
        MenuItem childItem = new MenuItem();
        childItem.setValue(c.getName());
        childItem.addActionListener(this);
        childItem.setActionExpression(expFact.createMethodExpression(elCtx, "#{categoryBackBean.doSomething()}", void.class, new Class[0]));
        tempModel.addMenuItem(childItem);
    }
    this.model = tempModel;
}

Where doSomething() is just a dummy method in order to get the action listener called.

This led to this issue with creating session after a committed response, which required this hack:

@PostConstruct
public void sessionStart() {
    FacesContext.getCurrentInstance().getExternalContext().getSession(true);
}

After all of which, I needed to disable Ajax on each element so that the form would be submitted:

childItem.setAjax(false);

So in summary, a hack chain, but it now functions (without ajax).

Upvotes: 0

Related Questions