Kirill Savitsky
Kirill Savitsky

Reputation: 11

How to add a custom composite component dynamically in JSF?

I'm trying to add two components dynamically to the page: "selectOneTreeList.xhtml" and "selectManyTreeList.xhtml". When I add them directly in the XHTML, they work correctly. However, when added dynamically, with "selectOneTreeList", nodes can be selected, but if anything is entered in the search field, all nodes disappear. With "selectManyTreeList", the search field issue remains, and it's also not possible to select any elements.

I created a small project to demonstrate the issue: GitHub

But you can see the main code below:

In all cases, when searching in both components and selecting an item in selectManyTreeList, there is one error:

2024-08-07 12:03:07.729 ERROR 417419 --- [io-55557-exec-6] j.e.resource.webcontainer.jsf.context    : javax.faces.FacesException: Unsupported tree node type:default
    at org.primefaces.component.tree.Tree.getUITreeNodeByType(Tree.java:116)
    at org.primefaces.component.tree.TreeRenderer.encodeTreeNode(TreeRenderer.java:663)
    at org.primefaces.component.tree.TreeRenderer.encodeTreeNodeChildren(TreeRenderer.java:806)
    at org.primefaces.component.tree.TreeRenderer.encodeVerticalTree(TreeRenderer.java:405)
    at org.primefaces.component.tree.TreeRenderer.encodeMarkup(TreeRenderer.java:344)
    at org.primefaces.component.tree.TreeRenderer.encodeEnd(TreeRenderer.java:257)
    at javax.faces.component.UIComponentBase.encodeEnd(UIComponentBase.java:949)
    at javax.faces.component.UIComponent.encodeAll(UIComponent.java:1912)
    at com.sun.faces.context.PartialViewContextImpl$PhaseAwareVisitCallback.visit(PartialViewContextImpl.java:638)
    at com.sun.faces.component.visit.PartialVisitContext.invokeVisitCallback(PartialVisitContext.java:183)
    at org.primefaces.component.api.UITree.visitTree(UITree.java:823)
    at javax.faces.component.UIComponent.visitTree(UIComponent.java:1747)
    at javax.faces.component.UIComponent.visitTree(UIComponent.java:1747)
    at javax.faces.component.UIComponent.visitTree(UIComponent.java:1747)
    at javax.faces.component.UIComponent.visitTree(UIComponent.java:1747)
    at javax.faces.component.UIForm.visitTree(UIForm.java:395)
    at javax.faces.component.UIComponent.visitTree(UIComponent.java:1747)
    at javax.faces.component.UIComponent.visitTree(UIComponent.java:1747)
    at com.sun.faces.context.PartialViewContextImpl.processComponents(PartialViewContextImpl.java:423)
    at com.sun.faces.context.PartialViewContextImpl.processPartial(PartialViewContextImpl.java:342)
    at org.primefaces.context.PrimePartialViewContext.processPartial(PrimePartialViewContext.java:65)
    at javax.faces.component.UIViewRoot.encodeChildren(UIViewRoot.java:1124)
    at javax.faces.component.UIComponent.encodeAll(UIComponent.java:1905)
    at com.sun.faces.application.view.FaceletViewHandlingStrategy.renderView(FaceletViewHandlingStrategy.java:465)
    at com.sun.faces.application.view.MultiViewHandler.renderView(MultiViewHandler.java:194)
    at javax.faces.application.ViewHandlerWrapper.renderView(ViewHandlerWrapper.java:151)
    at com.sun.faces.lifecycle.RenderResponsePhase.execute(RenderResponsePhase.java:126)
    at com.sun.faces.lifecycle.Phase.doPhase(Phase.java:100)
    at com.sun.faces.lifecycle.LifecycleImpl.render(LifecycleImpl.java:223)
    at javax.faces.webapp.FacesServlet.service(FacesServlet.java:671)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
    at com.spring.jsf.spring_jsf.filter.CorsFilter.doFilterInternal(CorsFilter.java:26)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:540)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:382)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:895)
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1732)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.lang.Thread.run(Thread.java:748)

Template selectOneTreeList.xhtml:


    <cc:interface componentType="selectOneTreeList">
        <cc:attribute name="value" required="true"
                      shortDescription="Value from NodeDTO"/>
        <cc:attribute name="items" type="java.util.List" required="true"
                      shortDescription="List items, should implements NodeDTO"/>
        <cc:attribute name="disabled" type="boolean" required="false" default="false"/>
        <cc:attribute name="update" type="java.lang.String" required="false"
                      shortDescription="Component(s) id to update"/>
        <cc:attribute name="styleClass" type="java.lang.String" required="false"
                      shortDescription="Style class of the component"/>
    </cc:interface>

    <cc:implementation>

        <div id="#{cc.clientId}" class="select-tree-list #{cc.attrs.disabled ? 'ui-state-disabled' : ''} #{cc.attrs.styleClass}">
            <div jsf:id="label-container" class="p-d-flex p-px-2">
                <div class="p-col p-p-2">
                    <h:outputText id="label"
                                  value="#{cc.selection.data.name}"
                                  styleClass="select-tree-list-label"/>
                </div>
                <div class="p-p-2">
                    <i class="pi pi-chevron-down"/>
                </div>
            </div>

            <p:overlayPanel
                    for="label-container"
                    widgetVar="#{cc.widgetVar}_overlay"
                    styleClass="select-tree-list-overlay"
                    rendered="#{!cc.attrs.disabled}"
                    appendTo="@form">
                <!--@elvariable id="node" type="com.spring.jsf.spring_jsf.dto.NodeDTO"-->
                <p:tree var="node"
                        binding="#{cc.tree}"
                        value="#{cc.rootNode}"
                        filterBy="#{node.name}"
                        filterMatchMode="contains"
                        selectionMode="single">
                    <p:ajax event="select" update="@parent:label #{cc.attrs.update}"
                            oncomplete="PF('#{cc.widgetVar}_overlay').hide()"
                            listener="#{cc.onNodeSelect}"/>
                    <p:treeNode>
                        <h:outputText value="#{node.name}"/>
                    </p:treeNode>
                </p:tree>
            </p:overlayPanel>
        </div>
    </cc:implementation>

Class SelectOneTreeList:

@FacesComponent("selectOneTreeList")
@ResourceDependency(library = "components", name = "select-tree-list.css")
public class SelectOneTreeList extends UIInput implements NamingContainer {

    private Tree tree;

    enum PropertyKeys {
        rootNode,
        items,
        disabled,
        update
    }

    private Map<Object, Object> attrs = new HashMap<>();

    @Override
    public String getFamily() {
        return UINamingContainer.COMPONENT_FAMILY;
    }

    @Override
    public void encodeBegin(FacesContext context) throws IOException {
        TreeNode rootNode = new DefaultTreeNode();
        setRootNode(rootNode);

        List<TreeNode> nodes = NodeUtils.createTreeNodes(rootNode, getItems(), DefaultTreeNode::new);

        TreeNode selected = Optional.ofNullable(getValue())
                .flatMap(v -> NodeUtils.findNode(v, nodes))
                .orElse(null);
        selectNode(selected);
        NodeUtils.expandParents(selected);
        super.encodeBegin(context);
    }

    private void selectNode(TreeNode treeNode) {
        tree.setSelection(treeNode);
        if (treeNode != null) {
            treeNode.setSelected(true);
        }
    }

    @Override
    public Object getSubmittedValue() {
        return NodeUtils.getNodeValue(getSelection())
                .orElse(null);
    }

    @SuppressWarnings("unchecked")
    private Collection<NodeDTO> getItems() {
        return (Collection<NodeDTO>) getStateHelper()
                .eval(PropertyKeys.items, Collections.EMPTY_LIST);
    }

    public TreeNode getSelection() {
        return (TreeNode) tree.getSelection();
    }

    public Tree getTree() {
        return tree;
    }

    public void setTree(Tree tree) {
        this.tree = tree;
    }

    public TreeNode getRootNode() {
        return (TreeNode) getStateHelper().eval(PropertyKeys.rootNode);
    }

    public void setRootNode(TreeNode rootNode) {
        getStateHelper().put(PropertyKeys.rootNode, rootNode);
    }

    public String getWidgetVar() {
        return "widget_" + getClientId().replace(":", "_");
    }

    public void onNodeSelect(NodeSelectEvent event) {
        ValueExpression ve = getValueExpression("value");
        ve.setValue(event.getFacesContext().getELContext(), getSubmittedValue());
    }

    public Map<Object, Object> getAttrs() {
        return attrs;
    }

    public void setAttrs(Map<Object, Object> attrs) {
        this.attrs = attrs;
    }

Template selectManyTreeList.xhtml:


    <cc:interface componentType="selectManyTreeList">
        <cc:attribute name="value" type="java.util.Collection" required="true"
                      shortDescription="Collection of selected NodeDTO's values. Must not be null!"/>
        <cc:attribute name="items" type="java.util.List" required="true"
                      shortDescription="List items, should contains NodeDTO"/>
        <cc:attribute name="disabled" type="boolean" required="false"
                      default="false"/>
        <!--TODO: сделать через cc:clientBehavior-->
        <cc:attribute name="update" type="java.lang.String" required="false"
                      shortDescription="Component(s) id to update"/>
        <cc:attribute name="styleClass" type="java.lang.String" required="false"
                      shortDescription="Style class of the component"/>
        <cc:attribute name="propagateSelectionUp" type="java.lang.Boolean" required="false"
                      default="true" shortDescription="Select children of selected node"/>
        <cc:attribute name="propagateSelectionDown" type="java.lang.Boolean" required="false"
                      default="true" shortDescription="Select parent if all children selected"/>
        <cc:attribute name="highlightBranchChips" type="java.lang.Boolean" required="false"
                      default="false" shortDescription="Highlight chips for nodes, that have child nodes"/>
        <cc:attribute name="onChangeSelection" method-signature="void listener(java.util.Collection)"  required="false"
                      shortDescription="An application select/unselect listener"/>
    </cc:interface>

    <cc:implementation>
        <div id="#{cc.clientId}"
             class="#{cc.attrs.styleClass} select-tree-list #{cc.attrs.disabled ? 'ui-state-disabled':''}">
            <div class="p-px-2 selected-values-area">
                <div jsf:id="selected-values"
                     class="p-col p-p-2 select-tree-list-chips">
                    <ui:repeat value="#{cc.selections}" var="node">
                        <!--@elvariable id="node" type="com.spring.jsf.spring_jsf.dto.NodeDTO"-->
                        <p:chip label="#{node.data.name}"
                                styleClass="#{cc.attrs.highlightBranchChips and node.data.hasChildren ? 'branchChip' : ''}"
                                removable="#{not cc.attrs.disabled}"
                                removeIcon="pi pi-times">
                            <p:ajax event="close"
                                    listener="#{cc.onRemoveNode(node)}"/>
                        </p:chip>
                    </ui:repeat>
                </div>
                <div jsf:id="clickable-area" class="clickable-area"></div>
                <div class="p-p-2 chevron">
                    <i class="pi pi-chevron-down"/>
                </div>
            </div>
            <p:overlayPanel
                    for="clickable-area"
                    styleClass="select-tree-list-overlay"
                    appendTo="@form">
                <!--@elvariable id="node" type="com.spring.jsf.spring_jsf.dto.NodeDTO"-->
                <p:tree var="node"
                        id="tree"
                        widgetVar="#{cc.widgetVar}_tree"
                        binding="#{cc.tree}"
                        value="#{cc.rootNode}"
                        filterBy="#{node.name}"
                        filterMatchMode="contains"
                        selectionMode="checkbox"
                        propagateSelectionUp="#{cc.attrs.propagateSelectionUp}"
                        propagateSelectionDown="#{cc.attrs.propagateSelectionDown}">
                    <p:ajax event="select"
                            update="#{cc.clientId}:selected-values #{cc.clientId}:tree #{cc.attrs.update}"
                            process="#{cc.clientId}"
                            oncomplete="postprocess('#{cc.widgetVar}_tree')"
                            listener="#{cc.onChangeSelection()}"/>
                    <p:ajax event="unselect"
                            update="#{cc.clientId}:selected-values #{cc.clientId}:tree #{cc.attrs.update}"
                            process="#{cc.clientId}"
                            oncomplete="postprocess('#{cc.widgetVar}_tree')"
                            listener="#{cc.onChangeSelection()}"/>
                    <p:ajax event="expand"
                            oncomplete="postprocess('#{cc.widgetVar}_tree')"/>
                    <p:treeNode styleClass="#{node.readOnly ? 'ui-state-disabled' : '' }">
                        <h:outputText value="#{node.name}"/>
                    </p:treeNode>
                </p:tree>
            </p:overlayPanel>
        </div>
    </cc:implementation>

Class SelectManyTreeList:

@FacesComponent("selectManyTreeList")
@ResourceDependencies({
        @ResourceDependency(name = "css/select-tree-list.css", library = "components"),
        @ResourceDependency(name = "js/selectManyTreeList.js", library = "components"),
        @ResourceDependency(
                library = ResourceHandler.JSF_SCRIPT_LIBRARY_NAME,
                name = ResourceHandler.JSF_SCRIPT_RESOURCE_NAME,
                target = "head")
})
public class SelectManyTreeList extends UIInput implements NamingContainer {

    private Tree tree;

    enum PropertyKeys {
        rootNode,
        items,
        disabled,
        update,
        onChangeSelection,
        propagateSelectionUp,
        propagateSelectionDown,
        highlightBranchChips
    }

    @Override
    public String getFamily() {
        return UINamingContainer.COMPONENT_FAMILY;
    }

    @Override
    public void processUpdates(FacesContext context) {
        processTree();
        super.processUpdates(context);
    }

    @Override
    public void encodeBegin(FacesContext context) throws IOException {
        processTree();
        super.encodeBegin(context);
    }

    @Override
    public void encodeEnd(FacesContext context) throws IOException {
        super.encodeEnd(context);
        this.saveState(getFacesContext());
        tree.saveState(getFacesContext());
    }

    private void processTree() {
        Objects.requireNonNull(getValue(), "Value of selectManyTreeList must not be null!");
        TreeNode rootNode = new CheckboxTreeNode();
        List<TreeNode> nodes = NodeUtils.createTreeNodes(rootNode, getItems(), CheckboxTreeNode::new);
        List<TreeNode> selectedNodes = ((Collection<?>) getValue()).stream()
                .map(v -> NodeUtils.findNode(v, nodes).orElse(null))
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        selectedNodes.forEach(node -> {
            node.setSelected(true);
            NodeUtils.expandParents(node);
        });
        tree.setSelection(selectedNodes.toArray(new TreeNode[0]));
        setRootNode(rootNode);
    }

    @Override
    public Object getValue() {
        Object value = isLocalValueSet() ? getLocalValue() : super.getValue();
        if (value == null) {
            return Collections.emptyList();
        }
        return value;
    }

    @Override
    public Object getSubmittedValue() {
        if (getSelections() == null) {
            return null;
        }
        return Stream.of(getSelections())
                .map(node -> NodeUtils.getNodeValue(node).orElse(null))
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(this::createValueCollection));
    }

    @SuppressWarnings("unchecked")
    private Collection<Object> createValueCollection() {
        try {
            return (Collection<Object>) getValue().getClass().newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    public TreeNode[] getSelections() {
        return (TreeNode[]) tree.getSelection();
    }

    @SuppressWarnings("unchecked")
    private Collection<NodeDTO> getItems() {
        return (Collection<NodeDTO>) getStateHelper()
                .eval(PropertyKeys.items, Collections.EMPTY_LIST);
    }

    public void onChangeSelection() {
        CompositeUtils.invokeMethod(PropertyKeys.onChangeSelection, getAttributes(), getSubmittedValue());
    }

    public Tree getTree() {
        return tree;
    }

    public void setTree(Tree tree) {
        this.tree = tree;
    }

    public TreeNode getRootNode() {
        return (TreeNode) getStateHelper().eval(PropertyKeys.rootNode);
    }

    public void setRootNode(TreeNode rootNode) {
        getStateHelper().put(PropertyKeys.rootNode, rootNode);
    }

    public void onRemoveNode(TreeNode treeNode) {
        String rowKey = treeNode.getRowKey();
        String treeWV = getWidgetVar() + "_tree";
        String elementId = getClientId() + ":tree:" + rowKey;
        String script = String.format("PF('%s').toggleCheckboxNode($(\"[id='%s']\"))", treeWV, elementId);
        PrimeFaces.current().executeScript(script);
    }

    public String getWidgetVar() {
        //Default name для widgetVar в Primefaces
        return "widget_" + getClientId().replace(":", "_");
    }

}

index.xhtml:

<!DOCTYPE html>
<html>
<h:head>
    <f:metadata>
        <f:event type="preRenderView"
                 listener="#{bean.onRenderView()}" />
    </f:metadata>
</h:head>

<f:view>
    <h:body>
        <h:form id="example">
            <h3>STATIC</h3>
            <custom:selectOneTreeList id="selectOneTreeList" items="#{bean.getNodes()}" value="#{bean.selectedValue}" />
            <custom:selectManyTreeList id="selectManyTreeList" items="#{bean.getNodes()}" value="#{bean.selectedValues}"
                                       propagateSelectionDown="false" propagateSelectionUp="false"
                                       highlightBranchChips="true" />
            <h3>DYNAMIC</h3>
        </h:form>
    </h:body>
</f:view>
</html>

Class Bean:

@Model
public class Bean implements Serializable {

    private final List<NodeDTO> nodes;

    private Map<String, Object> selectedValue;

    private List<Map<String, Object>> selectedValues = new ArrayList<>();

    public Bean() {
        final Map<String, Object> one = Collections.singletonMap("One", 1);
        final Map<String, Object> two = Collections.singletonMap("Two", 2);
        final Map<String, Object> three = Collections.singletonMap("Three", 3);
        final NodeDTO parent = new NodeDTO("One", one, null, true);
        final NodeDTO nodeChild1 = new NodeDTO("Two", two, one, true);
        final NodeDTO nodeChild2 = new NodeDTO("Three", three, two, false);
        this.nodes = Arrays.asList(parent, nodeChild1, nodeChild2);
    }

    public void onRenderView() {
        if (isRequestMethodGet()) {
            final UIComponent root = FacesContext.getCurrentInstance().getViewRoot();
            final UIComponent body = root.getChildren().stream().filter(c -> c instanceof HtmlBody).findFirst()
                    .orElseThrow(IllegalArgumentException::new);
            final UIComponent form = body.getChildren().get(0);

            //selectOneTreeList dynamic adding
            final UIComponent selectOneTreeList = includeCompositeComponent(form, "components",
                    "selectOneTreeList.xhtml", "selectOneTreeListDynamic");
            selectOneTreeList.setValueExpression("value", ELUtils.createValueExpression("#{bean.selectedValue}"));
            selectOneTreeList.setValueExpression("items", ELUtils.createValueExpression("#{bean.getNodes()}"));

            //selectManyTreeList dynamic adding
            final UIComponent selectManyTreeList = includeCompositeComponent(form, "components",
                    "selectManyTreeList.xhtml",
                    "selectManyTreeListDynamic");
            selectManyTreeList.setValueExpression("value", ELUtils.createValueExpression("#{bean.selectedValues}"));
            selectManyTreeList.setValueExpression("items", ELUtils.createValueExpression("#{bean.getNodes()}"));
            selectManyTreeList.setValueExpression("propagateSelectionDown", ELUtils.createValueExpression("#{false}"));
            selectManyTreeList.setValueExpression("propagateSelectionUp", ELUtils.createValueExpression("#{false}"));
            selectManyTreeList.setValueExpression("highlightBranchChips", ELUtils.createValueExpression("#{true}"));
        }
    }

    public List<NodeDTO> getNodes() {
        return nodes;
    }

    public static boolean isRequestMethodGet() {
        return Objects
                .equals(((HttpServletRequest) FacesContext.getCurrentInstance().getExternalContext()
                        .getRequest()).getMethod(), RequestMethod.GET.name());
    }

    public static String getExpression(String param) {
        return "#{" + param + "}";
    }

    public Map<String, Object> getSelectedValue() {
        return selectedValue;
    }

    public void setSelectedValue(Map<String, Object> selectedValue) {
        this.selectedValue = selectedValue;
    }

    public List<Map<String, Object>> getSelectedValues() {
        return selectedValues;
    }

    public void setSelectedValues(List<Map<String, Object>> selectedValues) {
        this.selectedValues = selectedValues;
    }

How I'm trying to add component:

public static UIComponent includeCompositeComponent(UIComponent parent, String libraryName, String resourceName,
            String id) {
        FacesContext context = FacesContext.getCurrentInstance();
        Application application = context.getApplication();
        FaceletContext faceletContext = (FaceletContext) context.getAttributes()
                .get(FaceletContext.FACELET_CONTEXT_KEY);

        Resource resource = application.getResourceHandler().createResource(resourceName, libraryName);
        UIComponent composite = application.createComponent(context, resource);

        composite.setId(id);

        UIComponent implementation = application.createComponent(UIPanel.COMPONENT_TYPE);
        implementation.setRendererType("javax.faces.Group");

        composite.getFacets().put(UIComponent.COMPOSITE_FACET_NAME, implementation);

        if (parent != null) {
            composite.pushComponentToEL(context, implementation);
            parent.pushComponentToEL(context, composite);
        }
        try {
            faceletContext.includeFacelet(implementation, resource.getURL());
        } catch (IOException e) {
            throw new FacesException(e);
        } finally {
            if (parent != null) {
                parent.getChildren().add(composite);
            }
        }
        return composite;
    }

I kindly ask for help with the problem or advice on how to solve it.

Upvotes: 1

Views: 71

Answers (0)

Related Questions