Verticon
Verticon

Reputation: 2513

Why does 'p:ajax update' fail to update a p:tree?

I want to produce the result that whenever a tree node is expanded any previously expanded sibling node is collapsed.

Below are my page and my bean. The onNodeExpanded method is invoked when a node is expanded; the System.out statements produce the expected results. However, the node upon which setExpanded(false) is executed remains expanded on the page.

I added a commandButton so that I could force an update of the tree. Pressing it results in the page properly displaying the node that was previously collapsed by the ajax event listener.

The Page:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
    xmlns:h="http://xmlns.jcp.org/jsf/html"
    xmlns:f="http://xmlns.jcp.org/jsf/core"
    xmlns:p="http://primefaces.org/ui">

    <h:head>
        <title>PrimeFaces Tree: Ajax Update Problem</title>
    </h:head>
    <h:body> 
        <h3>PrimeFaces Tree: Ajax Update Problem</h3>

        <h:form>
            <p:tree id="tree" value="#{problemController.root}" var="node" dynamic="true" orientation="horizontal">
                <p:treeNode>
                    <h:outputText value="#{node}" />
                </p:treeNode>

                <p:ajax event="expand" listener="#{problemController.onNodeExpanded}" update="tree" />
            </p:tree>

            <h:commandButton value="Update" immediate="true">
                <f:ajax render="tree" />
            </h:commandButton>
        </h:form>
    </h:body>
</html>

The Bean:

import java.io.Serializable;

import javax.annotation.PostConstruct;
import javax.faces.view.ViewScoped;
import javax.inject.Named;

import org.primefaces.event.NodeExpandEvent;
import org.primefaces.model.DefaultTreeNode;
import org.primefaces.model.TreeNode;

@Named
@ViewScoped
public class ProblemController implements Serializable {

    private static final long serialVersionUID = 1L;
    private TreeNode root;

    @PostConstruct
    public void init() {
        root = new DefaultTreeNode("Root", null);
        TreeNode node0 = new DefaultTreeNode("Node 0", root);
        TreeNode node1 = new DefaultTreeNode("Node 1", root);

        TreeNode node00 = new DefaultTreeNode("Node 0.0", node0);
        TreeNode node01 = new DefaultTreeNode("Node 0.1", node0);

        TreeNode node10 = new DefaultTreeNode("Node 1.0", node1);

        node1.getChildren().add(new DefaultTreeNode("Node 1.1"));
        node00.getChildren().add(new DefaultTreeNode("Node 0.0.0"));
        node00.getChildren().add(new DefaultTreeNode("Node 0.0.1"));
        node01.getChildren().add(new DefaultTreeNode("Node 0.1.0"));
        node10.getChildren().add(new DefaultTreeNode("Node 1.0.0"));
        root.getChildren().add(new DefaultTreeNode("Node 2"));
    }

    public TreeNode getRoot() {
        return root;
    }

    public void onNodeExpanded(NodeExpandEvent event) {
        TreeNode expandedNode = event.getTreeNode();
        System.out.printf("Expanded %s\n", expandedNode);

        TreeNode parent = expandedNode.getParent();
        if (parent != null) {
            for (TreeNode sibling : parent.getChildren()) {
                if (sibling.isExpanded() && sibling != expandedNode) {
                    sibling.setExpanded(false);
                    System.out.printf("Collapsed %s\n", sibling);
                }
            }
        }
    }
}

Java 1.8 PrimeFaces 6.1 Mojarra 2.3 Weld 3.0

Update 1

I have updated the page with the code that @Kukeltje provided. Unfortunately, the siblings of expanded nodes are not being collapsed. I added a statement to the closeOthers function that logs to the console: the data ("root", or "0", or "0_1", etc.) of any node that I expand is displayed (as expected).

<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
    xmlns:h="http://xmlns.jcp.org/jsf/html"
    xmlns:f="http://xmlns.jcp.org/jsf/core"
    xmlns:p="http://primefaces.org/ui">

<h:head>
    <title>PrimeFaces Tree</title>
</h:head>
<h:body> 
    <h3>PrimeFaces Tree</h3>

    <h:form>
        <p:tree id="tree" widgetVar="treeW" value="#{testController.root}" var="node" orientation="horizontal" selectionMode="single">
            <p:treeNode>
                <h:outputText value="#{node}" />
            </p:treeNode>

            <p:ajax event="expand" listener="#{testController.onNodeExpanded}" oncomplete="closeOthers(this, PF('treeW'))"/>
        </p:tree>

        <h:outputScript>

        //<![CDATA[

        function closeOthers(current, widget) {

            var dataRowkey = $.urlParam(decodeURIComponent(current.data),current.source+"_expandNode");
            var selector = "li[data-rowkey='"+dataRowkey+"']";
            console.log("Collapsing siblings of " + selector);
            $(widget.jq).find(selector).siblings().find("span[aria-expanded='true']").siblings(".ui-tree-toggler").trigger("click");

        }


         $.urlParam = function (query, name) {
            var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(query);
            return (results !== null) ? results[1] || 0 : false;
         }

        //]]>

        </h:outputScript>

        <h:commandButton value="Update" immediate="true">
            <f:ajax render="tree" />
        </h:commandButton>

    </h:form>
</h:body>
</html>

Upvotes: 0

Views: 1299

Answers (1)

Kukeltje
Kukeltje

Reputation: 12335

It is not very common to update a full component from within ajax calls inside the component itself. Due to you using dynamic="true" you see less downside effects than without using dynamic="true". The same will happen if you update a container around the p:tree. There are effectively 2 updates on the tree that sort of conflict with eachother, especially when there is some additional hiding of nodes being attempted.

What you effectively seem to be trying to achieve is to have just one node open at a time by using server side manipulation of the tree (which in itself is not wrong). This is related indeed what is attempted in https://github.com/primefaces/primefaces/issues/2277 as well but that issue is too narrowly formulated since it is not about selecting/unselecting or like you experience hiding/unhiding, but the more general issue of not being able to update the full tree component from within an ajax call in the component.

Updating things server-side so a full refresh is still supported should still be done. But instead of doing an update="tree", you can try to use the oncomplete javascript callback to close all other nodes client-side.

Adding the following javascript will do this for you:

<h:outputScript>

//<![CDATA[

function closeOthers(current, widget) {

    var dataRowkey = $.urlParam(decodeURIComponent(current.data),current.source+"_expandNode");


    //For a vertical tree:      
    //var selector = "li[data-rowkey='"+dataRowkey+"']";
    //$(widget.jq).find(selector).siblings().find("span[aria-expanded='true']").siblings(".ui-tree-toggler").trigger("click");


    //For a horizontal tree
    var selector = "td[data-rowkey='"+dataRowkey+"']";

    // The first attempt was to look for aria-expanded=true but we
    // found that sometimes it would be present on a collapsed node (bug?).
    // $(widget.jq).find(selector).parent().parent().parent().siblings().find("td[aria-expanded='true']").find(".ui-tree-toggler").trigger("click");

    // Searching instead for the minus icon did the trick
    $(widget.jq).find(selector).parent().parent().parent().siblings().find(".ui-icon-minus").trigger("click");

}


 $.urlParam = function (query, name) {
    var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(query);
    return (results !== null) ? results[1] || 0 : false;
 }

//]]>
</h:outputScript>

Then

  • Give your widget a widgetVar e.g. widgetVar="treeW"
  • add an oncomplete in the p:ajax like so oncomplete="closeOthers(this, PF('treeW'))"
  • REMOVE dynamic="true" OR ADD cache="false" (bug I found, then it will only work once for each node, https://github.com/primefaces/primefaces/issues/3668)
  • REMOVE update="tree"

So the html ends up like:

<p:tree id="tree" widgetVar="treeW" value="#{problemController.root}" var="node" orientation="horizontal">
    <p:treeNode>
        <h:outputText value="#{node}" />
    </p:treeNode>

    <p:ajax event="expand" listener="#{problemController.onNodeExpanded}" oncomplete="closeOthers(this, PF('treeW'))"/>
</p:tree>

Upvotes: 1

Related Questions