Search code examples
jsf-2primefacesomnifacesmegamenugraphicimage

Implement <o:graphicImage> instead of icon in <p:menuitem> of <p:megaMenu>


Ok, title is a little confusing but I can not figure out how to put out my problem in single sentence.

So here is the deal: I need to somehow implement graphicImage component from Primefaces (or even better, Omnifaces, since I load images from database and i already use o:graphicImage in application) inside MenuItem component in order to display it in MegaMenu component which is created programmatically using DefaultMenuModel created in backing bean. Also (as I mentioned above), images are loaded from database, so i guess solution with loading those images (icons) via css is not applicable since images are dynamic.

I tried many approaches but (needless to say) I, unfortunately, failed miserably :-( . I do not have any experience with writing full stack Primefaces custom component so i tried with some basic custom renderer for MegaMenu component. For example, i tried to use code from o:graphicImage component (encodeBegin and encodeEnd) and implement it in my CustomMegaMenuRenderer (which extends MegaMenuRenderer) in overridden method encodeMenuItemContent. I used little hack to pass EL expression (like #{imageStreamer.getImage(23)}, where 23 is actual image id) passing this string as value of 'icon' field of MenuItem component in model. Then, in overridden encodeMenuItemContent i check if it is actually el (and not icon name) i do something like this:

writer.startElement("img", null);
writer.writeURIAttribute("src", getSrc(context, expresion), "value");
writeAttributes(writer, this, GraphicImage.ATTRIBUTE_NAMES);
writer.endElement("img");

where getSrc(context, expression) is implemented like:

private String getSrc(FacesContext context, String expression) throws IOException {
    Resource resource;
    ExpressionFactory ef = context.getApplication().getExpressionFactory();
    ValueExpression dynExpression = ef.createValueExpression(context.getELContext(), expression, Object.class);
    resource = GraphicResource.create(context, dynExpression, null);
    return context.getExternalContext().encodeResourceURL(resource.getRequestPath());
}

But unfortunately, getSrc(context, expression) fails to return string content like it does inside normally crated o:graphicImage component.

I also tried some other approach like programmatically instantiating graphicImage component (from Primefaces or Omnifaces) in my rendered and then invoke encodeAll method on the object but that also failed...

GraphicImage gi = (GraphicImage) application.createComponent(GraphicImage.COMPONENT_TYPE);
gi.getAttributes().put("value", expression);
gi.encodeAll(context);

So i concluded that i am missing something really big over there. I tried to find something similar on SO and google, but was unable to succeed. So, here are my real questions here (assuming that my approaches are bad):

  1. Is it event possible to instantiate every primefaces/omnifaces component inside custom renderer and make it work as it would normally work (so it renders itself as expected)? I guess that this would be the easiest way to make my renderer work as desired...
  2. Should i go forward with this "custom renderer" approach or it's useless in this case?
  3. Do i have to make custom MenuModel, MenuItem or MegaMenu class in order to create this functionality explained above?
  4. And finally, what is theoretically easiest way to achieve this and what is "the best" way to achieve this (so it is aligned with primefaces component design, how actually this "should be" done)?

Also to mention (but i guess its out of the scope), i use JSF MOJARRA 2.2.9, Primefaces 5.1, Omnifaces 2.0 and Weld on Tomcat 8.

Thanks in advance.


Solution

  • As far as I understand, you was trying to use something like:

    <p:menuitem ... icon="#{imageStreamer.getImage(23)}" />
    

    This indeed won't work. It would be evaluated immediately when the renderer obtains it. This is also not how <o:graphicImage> works. It postpones the evaluation to the moment the browser actually needs to request the image.

    Your best bet is to pass the image identifier instead:

    <p:menuitem ... icon="23" />
    

    (which can safely be an expression like icon="#{bean.iconId}")

    Then you can render it as follows:

    @Override
    protected void encodeMenuItemContent(FacesContext context, AbstractMenu menu, MenuItem menuitem) throws IOException {
        ResponseWriter writer = context.getResponseWriter();
        String imageId = menuitem.getIcon();
        Object value = menuitem.getValue();
    
        if (imageId != null) {
            ValueExpression expression = Components.createValueExpression("#{imageStreamer.getImage(" + imageId + ")}", Object.class);
            GraphicResource resource = GraphicResource.create(context, expression, null);
            String src = context.getExternalContext().encodeResourceURL(resource.getRequestPath());
            writer.startElement("img", null);
            writer.writeURIAttribute("src", src, null);
            writer.endElement("img");
        }
    
        // ...
    }
    

    (you was pretty close, it's only unfortunate you didn't show how you created expression and how exactly it failed, so that the exact cause could be pinpointed)