Saturday, April 19, 2014

Creating dynamic JSF Components

You can often read a question how to create dynamic JSF 2 components. That means a programmatically combining already existing components to a dynamic one - just like a puzzle. There are some advices on the internet which don't work reliable in all cases. In this article for instance, the author tried to use PostAddToViewEvent, but had some troubles, and ended up then with an implementation which doesn't consider component's changes on postback. I would like to show a simple and reliable way for creating dynamic JSF 2 components.

The main question is at which time, better to say JSF phase, the components should be created? I suggest to use PreRenderComponentEvent and add all components there. In this blog post, we will create a component called DynamicButtons. DynamicButtons consists of multiple dynamic buttons. Every dynamic button is an abstraction. It is either a PrimeFaces command button or menu item. It depends on component's configuration how a dynamic button should be rendered - as command button or as menu item. In short words, DynamicButtons is a component which shows X command buttons and a menu button with items if more than X dynamic buttons are available. Here is a picture.


The component's tag is d:dynamicButtons. Every single dynamic button is condigured by the tag d:dynamicButton which has the same attributes as p:commandButton and p:menuitem. Sure, not all attributes of p:commandButton and p:menuitem are equal - some attributes in the d:dynamicButton are not availbale for items in the menu button (e.g. widgetVar). The XHTML snippet to the picture above demonstrates the usage of some attributes.
<p:growl showSummary="true" showDetail="false" autoUpdate="true"/>
<p:messages id="msgs" showSummary="true" showDetail="false"/>

<p:notificationBar position="top" effect="slide" widgetVar="bar">
    <h:outputText value="..."/>
</p:notificationBar>

<d:dynamicButtons id="dynaButtons" labelMenuButton="More Actions" iconPosMenuButton="right"
                  positionMenuButton="#{dynamicButtonsController.posMenuButton}">
    <d:dynamicButton value="Show notification" type="button" onclick="PF('bar').show()"/>
    <d:dynamicButton value="Hide notification" type="button" onclick="PF('bar').hide()"/>
    <d:dynamicButton value="Create" icon="ui-icon-plus" process="@this"
                        action="#{dynamicButtonsController.someAction('Create')}"/>
    <d:dynamicButton value="Edit" icon="ui-icon-pencil" process="@this"
                        action="#{dynamicButtonsController.someAction('Edit')}"/>
    <d:dynamicButton value="Delete" icon="ui-icon-trash" process="@this"
                        action="#{dynamicButtonsController.someAction('Delete')}"/>
    <d:dynamicButton value="Save" icon="ui-icon-disk" process="@this"
                        action="#{dynamicButtonsController.someAction('Save')}"/>
    <d:dynamicButton value="Log Work" icon="ui-icon-script" global="false" process="@this" update="msgs"
                        ignoreAutoUpdate="true" actionListener="#{dynamicButtonsController.logWork}"/>
    <d:dynamicButton value="Attach File" icon="ui-icon-document" global="false"/>
    <d:dynamicButton value="Move" icon="ui-icon-arrowthick-1-e" global="false"/>
    <d:dynamicButton value="Clone" icon="ui-icon-copy" disabled="true"/>
    <d:dynamicButton value="Comment" icon="ui-icon-comment" title="This is the comment action"/>
    <d:dynamicButton value="Homepage" icon="ui-icon-home" title="Link to the homepage" ajax="false"
                        action="/views/home?faces-redirect=true"/>
</d:dynamicButtons>
The position of the menu button is configurable by the attribute positionMenuButton. positionMenuButton defines the start index of buttons rendered in a menu button as items vertically. A valid position begins with 1. Negative or 0 value means that no menu button is rendered (all buttons are rendered horizontally). The current position can be controlled by a bean.
@Named
@ViewScoped
public class DynamicButtonsController implements Serializable {
    
    private int posMenuButton = 7;

    public void someAction(String button) {
        FacesContext ctx = FacesContext.getCurrentInstance();
        FacesMessage message = new FacesMessage("Action of the button '" + button + "' has been invoked");
        message.setSeverity(FacesMessage.SEVERITY_INFO);
        ctx.addMessage(null, message);
    }
    
    public void logWork(ActionEvent event) {
        FacesContext ctx = FacesContext.getCurrentInstance();
        FacesMessage message = new FacesMessage("Action listener for 'Log Work' has been invoked");
        message.setSeverity(FacesMessage.SEVERITY_INFO);
        ctx.addMessage(null, message);
    }

    public int getPosMenuButton() {
        return posMenuButton;
    }

    public void setPosMenuButton(int posMenuButton) {
        this.posMenuButton = posMenuButton;
    }
}
Let's create dynamic buttons. I will only show the code which is important to get the idea. First of all we need a TagHandler behind the d:dynamicButton. It's called DynamicButtonTagHandler. DynamicButtonTagHandler collects values of all attributes defined in the d:dynamicButton and buffers them in the data container object DynamicButtonHolder. The object DynamicButtonHolder is saved in the attributes map of the parent component DynamicButtons (component behind the d:dynamicButtons tag).
public class DynamicButtonTagHandler extends TagHandler {

    private final TagAttribute value;
    private final TagAttribute widgetVar;
    private final TagAttribute rendered;
    private final TagAttribute ajax;
    private final TagAttribute process;
 
    // other attributes
    ...
 
    private final TagAttribute action;
    private final TagAttribute actionListener;

    public DynamicButtonTagHandler(TagConfig config) {
        super(config);

        this.value = this.getAttribute("value");
        this.widgetVar = this.getAttribute("widgetVar");
        this.rendered = this.getAttribute("rendered");
        this.ajax = this.getAttribute("ajax");
        this.process = this.getAttribute("process");
  
        // handle other attributes
        ...
  
        this.action = this.getAttribute("action");
        this.actionListener = this.getAttribute("actionListener");
    }

    @Override
    public void apply(FaceletContext ctx, UIComponent parent) throws IOException {
        if (!ComponentHandler.isNew(parent)) {
            return;
        }

        @SuppressWarnings("unchecked")
        List<DynamicButtonHolder> holders = (List<DynamicButtonHolder>) parent.getAttributes().get(
                DynamicButtons.DYNAMIC_BUTTON_ATTR_HOLDER);
        if (holders == null) {
            holders = new ArrayList<DynamicButtonHolder>();
            parent.getAttributes().put(DynamicButtons.DYNAMIC_BUTTON_ATTR_HOLDER, holders);
        }

        DynamicButtonHolder holder = new DynamicButtonHolder();

        if (value != null) {
            if (value.isLiteral()) {
                holder.setValue(value.getValue());
            } else {
                holder.setValue(value.getValueExpression(ctx, Object.class));
            }
        }

        if (widgetVar != null) {
            if (widgetVar.isLiteral()) {
                holder.setWidgetVar(widgetVar.getValue());
            } else {
                holder.setWidgetVar(widgetVar.getValueExpression(ctx, String.class));
            }
        }

        if (rendered != null) {
            if (rendered.isLiteral()) {
                holder.setRendered(Boolean.valueOf(rendered.getValue()));
            } else {
                holder.setRendered(rendered.getValueExpression(ctx, Boolean.class));
            }
        }

        if (ajax != null) {
            if (ajax.isLiteral()) {
                holder.setAjax(Boolean.valueOf(ajax.getValue()));
            } else {
                holder.setAjax(ajax.getValueExpression(ctx, Boolean.class));
            }
        }

        if (process != null) {
            if (process.isLiteral()) {
                holder.setProcess(process.getValue());
            } else {
                holder.setProcess(process.getValueExpression(ctx, String.class));
            }
        }

        // handle other values
        ...

        if (action != null) {
            holder.setActionExpression(action.getMethodExpression(ctx, String.class, new Class[]{}));
        }

        if (actionListener != null) {
            holder.setActionListener(new MethodExpressionActionListener(actionListener.getMethodExpression(
                    ctx, Void.class, new Class[]{ActionEvent.class})));
        }

        // add data
        holders.add(holder);
    }
}
Data container class DynamicButtonHolder looks simple.
public class DynamicButtonHolder {

    private Object value;
    private Object widgetVar;
    private Object rendered;
    private Object ajax;
    private Object process;
 
    // other attributes
    ...
 
    private MethodExpression actionExpression;
    private ActionListener actionListener;

    public Object getValue() {
        return value;
    }

    public void setValue(Object value) {
        this.value = value;
    }

    public Object getWidgetVar() {
        return widgetVar;
    }

    public void setWidgetVar(Object widgetVar) {
        this.widgetVar = widgetVar;
    }

    public Object getRendered() {
        return rendered;
    }

    public void setRendered(Object rendered) {
        this.rendered = rendered;
    }

    public Object getAjax() {
        return ajax;
    }

    public void setAjax(Object ajax) {
        this.ajax = ajax;
    }

    public Object getProcess() {
        return process;
    }

    public void setProcess(Object process) {
        this.process = process;
    }

    // setter / getter for other attributes
    ...

    public MethodExpression getActionExpression() {
        return actionExpression;
    }

    public void setActionExpression(MethodExpression actionExpression) {
        this.actionExpression = actionExpression;
    }

    public ActionListener getActionListener() {
        return actionListener;
    }

    public void setActionListener(ActionListener actionListener) {
        this.actionListener = actionListener;
    }
}
The component class DynamicButtons extends HtmlPanelGroup and registers itself as a listener for PreRenderComponentEvent. Command buttons and menu items in the menu button (see positionMenuButton) are added dynamically to the panel group in the method processEvent(). It happens shortly before the rendering phase.
@FacesComponent(value = "examples.component.DynamicButtons")
@ListenerFor(systemEventClass = PreRenderComponentEvent.class)
public class DynamicButtons extends HtmlPanelGroup {

    private static final String OPTIMIZED_PACKAGE = "examples.component.";
    public static final String DYNAMIC_BUTTON_ATTR_HOLDER = "dynamicButtonAttrHolder";

    enum PropertyKeys {
        disabled,
        positionMenuButton,
        labelMenuButton,
        iconPosMenuButton
    }

    public DynamicButtons() {
        super();
    }

    public boolean isDisabled() {
        return (Boolean) getStateHelper().eval(PropertyKeys.disabled, false);
    }

    public void setDisabled(boolean disabled) {
        getStateHelper().put(PropertyKeys.disabled, disabled);
    }

    public Integer getPositionMenuButton() {
        return (Integer) getStateHelper().eval(PropertyKeys.positionMenuButton, 0);
    }

    public void setPositionMenuButton(Integer positionMenuButton) {
        getStateHelper().put(PropertyKeys.positionMenuButton, positionMenuButton);
    }

    public String getLabelMenuButton() {
        return (String) getStateHelper().eval(PropertyKeys.labelMenuButton, null);
    }

    public void setLabelMenuButton(String labelMenuButton) {
        getStateHelper().put(PropertyKeys.labelMenuButton, labelMenuButton);
    }

    public String getIconPosMenuButton() {
        return (String) getStateHelper().eval(PropertyKeys.iconPosMenuButton, "left");
    }

    public void setIconPosMenuButton(String iconPosMenuButton) {
        getStateHelper().put(PropertyKeys.iconPosMenuButton, iconPosMenuButton);
    }

    /**
     * {@inheritDoc}
     */
    public void processEvent(ComponentSystemEvent event) throws AbortProcessingException {
        super.processEvent(event);

        if (!(event instanceof PreRenderComponentEvent)) {
            return;
        }

        // add components to this panel group
        addComponents();
    }

    private void addComponents() {
        if (!isRendered()) {
            return;
        }

        @SuppressWarnings("unchecked")
        List<DynamicButtonHolder> holders = (List<DynamicButtonHolder>) getAttributes().get(
                DynamicButtons.DYNAMIC_BUTTON_ATTR_HOLDER);
        if (holders == null) {
            return;
        }

        // first remove all children
        this.getChildren().clear();

        final FacesContext fc = FacesContext.getCurrentInstance();
        MenuButton menuButton = null;
        int posMenuButton = getPositionMenuButton();

        for (int i = 0; i < holders.size(); i++) {
            DynamicButtonHolder holder = holders.get(i);

            if (posMenuButton <= 0 || i < posMenuButton - 1) {
                // create single command button
                createCommandButton(fc, holder, i);
            } else {
                if (menuButton == null) {
                    // create menu button
                    menuButton = (MenuButton)fc.getApplication().createComponent(MenuButton.COMPONENT_TYPE);
                    menuButton.setId(this.getId() + "_mbutton");
                    menuButton.setDisabled(isDisabled());
                    menuButton.setIconPos(getIconPosMenuButton());
                    menuButton.setValue(getLabelMenuButton());
                    menuButton.setStyleClass("dynaMenuButton");

                    // add as child to this component
                    this.getChildren().add(menuButton);
                }

                // create menuitem for menu button
                createMenuitem(fc, menuButton, holder, i);
            }
        }
    }

    private void createCommandButton(FacesContext fc, DynamicButtonHolder holder, int i) {
        CommandButton commandButton = (CommandButton)fc.getApplication().createComponent(
                CommandButton.COMPONENT_TYPE);
        commandButton.setId(this.getId() + "_cbutton_" + i);
        commandButton.setStyleClass("dynaCommandButton");

        // add to the children
        this.getChildren().add(commandButton);

        ELContext ec = fc.getELContext();

        Object value = getValue(ec, holder);
        if (value != null) {
            commandButton.setValue(value);
        }

        String widgetVar = getWidgetVar(ec, holder);
        if (StringUtils.isNotBlank(widgetVar)) {
            commandButton.setWidgetVar(widgetVar);
        }

        Boolean rendered = isRendered(ec, holder);
        if (rendered != null) {
            commandButton.setRendered(rendered);
        }

        Boolean ajax = isAjax(ec, holder);
        if (ajax != null) {
            commandButton.setAjax(ajax);
        }

        String process = getProcess(ec, holder);
        if (StringUtils.isNotBlank(process)) {
            commandButton.setProcess(process);
        }

        // handle other attributes
        ...

        MethodExpression me = holder.getActionExpression();
        if (me != null) {
            commandButton.setActionExpression(me);
        }

        ActionListener actionListener = holder.getActionListener();
        if (actionListener != null) {
            commandButton.addActionListener(actionListener);
        }
    }

    private void createMenuitem(FacesContext fc, MenuButton menuButton, DynamicButtonHolder holder, int i) {
        UIMenuItem menuItem = (UIMenuItem)fc.getApplication().createComponent(UIMenuItem.COMPONENT_TYPE);
        menuItem.setId(this.getId() + "_menuitem_" + i);
        menuItem.setStyleClass("dynaMenuitem");

        // add to the children
        menuButton.getChildren().add(menuItem);

        ELContext ec = fc.getELContext();

        Object value = getValue(ec, holder);
        if (value != null) {
            menuItem.setValue(value);
        }

        Boolean rendered = isRendered(ec, holder);
        if (rendered != null) {
            menuItem.setRendered(rendered);
        }

        Boolean ajax = isAjax(ec, holder);
        if (ajax != null) {
            menuItem.setAjax(ajax);
        }

        String process = getProcess(ec, holder);
        if (StringUtils.isNotBlank(process)) {
            menuItem.setProcess(process);
        }

        // handle other attributes
        ...

        MethodExpression me = holder.getActionExpression();
        if (me != null) {
            menuItem.setActionExpression(me);
        }

        ActionListener actionListener = holder.getActionListener();
        if (actionListener != null) {
            menuItem.addActionListener(actionListener);
        }
    }

    private Object getValue(ELContext ec, DynamicButtonHolder holder) {
        Object value;
        Object objValue = holder.getValue();
        if (objValue instanceof ValueExpression) {
            value = ((ValueExpression) objValue).getValue(ec);
        } else {
            value = objValue;
        }

        return value;
    }

    private String getWidgetVar(ELContext ec, DynamicButtonHolder holder) {
        String widgetVar = null;
        Object objWidgetVar = holder.getWidgetVar();
        if (objWidgetVar instanceof ValueExpression) {
            widgetVar = (String) ((ValueExpression) objWidgetVar).getValue(ec);
        } else if (objWidgetVar instanceof String) {
            widgetVar = (String) objWidgetVar;
        }

        return widgetVar;
    }

    private Boolean isRendered(ELContext ec, DynamicButtonHolder holder) {
        Boolean rendered = null;
        Object objRendered = holder.getRendered();
        if (objRendered instanceof ValueExpression) {
            rendered = (Boolean) ((ValueExpression) objRendered).getValue(ec);
        } else if (objRendered instanceof Boolean) {
            rendered = (Boolean) objRendered;
        }

        return rendered;
    }

    private Boolean isAjax(ELContext ec, DynamicButtonHolder holder) {
        Boolean ajax = null;
        Object objAjax = holder.getAjax();
        if (objAjax instanceof ValueExpression) {
            ajax = (Boolean) ((ValueExpression) objAjax).getValue(ec);
        } else if (objAjax instanceof Boolean) {
            ajax = (Boolean) objAjax;
        }

        return ajax;
    }

    private String getProcess(ELContext ec, DynamicButtonHolder holder) {
        String process = null;
        Object objProcess = holder.getProcess();
        if (objProcess instanceof ValueExpression) {
            process = (String) ((ValueExpression) objProcess).getValue(ec);
        } else if (objProcess instanceof String) {
            process = (String) objProcess;
        }

        return process;
    }

    // get other values
    ...

    public void setAttribute(PropertyKeys property, Object value) {
        getStateHelper().put(property, value);

        // some magic code which is not relevant here
        ...
    }
}
DynamicButtons and DynamicButtonTagHandler should be registered in a *.taglib.xml file.
<tag>
    <description>
        <![CDATA[Dynamic buttons.]]>
    </description>
    <tag-name>dynamicButtons</tag-name>
    <component>
        <component-type>examples.component.DynamicButtons</component-type>
        <renderer-type>javax.faces.Group</renderer-type>
    </component>
    <attribute>
        <description>
            <![CDATA[Unique identifier of the component in a NamingContainer.]]>
        </description>
        <name>id</name>
        <required>false</required>
        <type>java.lang.String</type>
    </attribute>
    ...
</tag>

<tag>
    <description>
        <![CDATA[Holder for dynamic button's attributes.]]>
    </description>
    <tag-name>dynamicButton</tag-name>
    <handler-class>examples.taghandler.DynamicButtonTagHandler</handler-class>
    <attribute>
        <description><![CDATA[Label of the component.]]></description>
        <name>value</name>
        <required>false</required>
        <type>java.lang.Object</type>
    </attribute>
    ...
</tag>

8 comments:

  1. Gone through the post with the best process!!

    ReplyDelete
  2. "DynamicButtons and DynamicButtonTagHandler should be registered in a *.taglib.xml file." - that's clear, thank you. I shared your post with my colleagues from custom software development services, I think it might be interesting for them, as well.

    ReplyDelete
  3. Nice Blog, Thank you for sharing this nice information about nice topic on your blog, it is very informatics info thank for this blog
    Big Height

    ReplyDelete
  4. this doesn't seem to work when PARTIAL_STATE_SAVING is turned on. At least with Mojarra

    ReplyDelete
    Replies
    1. I use Mojarra, but I never tried the code with turned out PARTIAL_STATE_SAVING. Can you debug the code? Adding components on PreRenderComponentEvent should work fine. I can not see why it would not be ok with turned out PreRenderComponentEvent.

      Delete
  5. I guess I should clarify "It Doesn't work".. lol I hate when people say that my code "doesn't work".

    There are sometimes I have found that this method will not work. If you needed to call a backing bean from the menu actionListener for instance. This is because dynamic component changes do not survive tag re-execution during render response on postback when PARTICIAL_STATE_SAVING is on.

    There is a Jira to support this. I voted for it, please consider also.
    https://java.net/jira/browse/JAVASERVERFACES-3306

    Also, I will say that I learned a lot from your tutorial so thanks!

    ReplyDelete
  6. Hi Oleg,

    Very nice article... It would be nicer if you could explain a bit more... from point of view of a beginner... :)

    ReplyDelete

Note: Only a member of this blog may post a comment.