-
Available since OmniFaces 2.0
The <o:moveComponent>
component is a utility component via which components, facets and behaviors can be moved at runtime to a target component in various ways. This allows for simple programmatic composition of components using a declarative page author centric approach.
The destination of a move operation is specified in terms of a location that's relative to a given target component. The following shows a list of supported destinations:
BEFORE
- Component is moved right before target component, i.e. as a sibling with an index that's 1 position lower.ADD_FIRST
- Component is added as the first child of the target component, any other children will have their index increased by 1.ADD_LAST
- Component is added as the last child of the target component, any other children will stay at their original location.FACET
- Component will be moved to the facet section of the target component under the name denoted by "facet".BEHAVIOR
- A Behavior will be moved to the behavior section of the target component.AFTER
- Component is moved right after target component, i.e. as a sibling with an index that's 1 position higher.
Showing number of search results in PrimeFaces table
The PrimeFaces data table component is notoriously difficult to modify when it comes to the build-in filter/search capabilities. Required components with fixed IDs ("globalFilter") and the fact that filtering is applied in the render stage are among the things that make this surprisingly difficult in practice.
With the help of the o:moveComponent the required modification actions can be packed up in a Facelets tag file for reuse. The following shows an example:
So what's going on here? The demo:globalSearch tag (see second tab in the source code section below) contains 3 instances of the o:moveComponent.
1. Moving a script to reside after the table
The first moveComponent instance has a JavaScript surrounded by a named panel group as its children. These components are moved in the tree
to sit right after the table component. This script takes the number of elements that appear in the so-called filtered collection and updates
another panel group with that number.
The script needs to be placed AFTER the table component, because the number we need is only available
when the table is actually rendered, so it's only available to components (and EL expressions within those) that are rendered after the table
is rendered. This somewhat defeats the purpose of the phases in Faces (an important reason for which is to prevent just this location dependency),
but with the o:moveComponent we can hide this somewhat nasty detail for the tag user.
2. Moving an AJAX event listener into the table
The second moveComponent instance has an AJAX event listener as its child. The event listener makes sure the JavaScript that we defined above is
updated after each filtering operation.
An AJAX event listener is not a component but a behavior. It therefore needs some extra handling. (among others to fool it that it's temporarily
a child of the moveComponent which does not have the events a behavior can be checking for) It's moved to the special behavior section
components have.
3. Moving a component into the facet section of the table
The third moveComponent instance has a placeholder component with the fixed ID 'globalFilter' as its child. This component is used by PrimeFaces
to read the input used by the filtering operation from.
This placeholder component is needed for this example, since we wanted to have the real input component just above the table, not inside the table.
The standard PrimeFaces approach with the facet tightly couples the location of the input component, which we here thus decouple. The real input
component updates the placecholder using its onkeyup
attribute.
<h3>Showing number of search results in PrimeFaces table</h3>
<p>
The PrimeFaces data table component is notoriously difficult to modify when it comes to the build-in filter/search capabilities.
Required components with fixed IDs ("globalFilter") and the fact that filtering is applied in the render stage are among the
things that make this surprisingly difficult in practice.
</p>
<p>
With the help of the o:moveComponent the required modification actions can be packed up in a Facelets tag file for reuse.
The following shows an example:
</p>
<hr/>
<demo:globalSearch forx="myTable"/> <br/><br/>
<h:form id="myForm">
<p:dataTable id="myTable" value="#{myTableBacking.users}" var="user" filteredValue="#{myTableBacking.filteredUsers}" widgetVar="myTableVar">
<p:column filterBy="#{user.firstName}" filterMatchMode="contains">
#{user.firstName}
</p:column>
<p:column filterBy="#{user.lastName}" filterMatchMode="contains">
#{user.lastName}
</p:column>
<p:column filterBy="#{user.age}" filterMatchMode="contains">
#{user.age}
</p:column>
</p:dataTable>
</h:form>
<hr/>
<p>
So what's going on here? The <b>demo:globalSearch</b> tag <i>(see second tab in the source code section below)</i> contains 3 instances
of the o:moveComponent.
</p>
<h4>1. Moving a script to reside after the table</h4>
<p>
The first moveComponent instance has a JavaScript surrounded by a named panel group as its children. These components are moved in the tree
to sit right after the table component. This script takes the number of elements that appear in the so-called filtered collection and updates
another panel group with that number.<br/> <br/>
The script needs to be placed AFTER the table component, because the number we need is only available
when the table is actually rendered, so it's only available to components (and EL expressions within those) that are rendered after the table
is rendered. This somewhat defeats the purpose of the phases in Faces (an important reason for which is to prevent just this location dependency),
but with the o:moveComponent we can hide this somewhat nasty detail for the tag user.
</p>
<h4>2. Moving an AJAX event listener into the table</h4>
<p>
The second moveComponent instance has an AJAX event listener as its child. The event listener makes sure the JavaScript that we defined above is
updated after each filtering operation.<br/> <br/>
An AJAX event listener is not a component but a behavior. It therefore needs some extra handling. <i>(among others to fool it that it's temporarily
a child of the moveComponent which does not have the events a behavior can be checking for)</i> It's moved to the special behavior section
components have.
</p>
<h4>3. Moving a component into the facet section of the table</h4>
<p>
The third moveComponent instance has a placeholder component with the fixed ID 'globalFilter' as its child. This component is used by PrimeFaces
to read the input used by the filtering operation from.<br/> <br/>
This placeholder component is needed for this example, since we wanted to have the real input component just above the table, not inside the table.
The standard PrimeFaces approach with the facet tightly couples the location of the input component, which we here thus decouple. The real input
component updates the placecholder using its <code>onkeyup</code> attribute.
</p>
<ui:composition
xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="jakarta.faces.core"
xmlns:h="jakarta.faces.html"
xmlns:ui="jakarta.faces.facelets"
xmlns:c="jakarta.tags.core"
xmlns:p="http://primefaces.org/ui"
xmlns:o="http://omnifaces.org/ui"
xmlns:of="http://omnifaces.org/functions"
>
<o:resolveComponent name="tableComponent" for="#{forx}" />
<p:inputText id="search" onkeyup="$(document.getElementById('#{tableComponent.clientId}:globalFilter')).val(this.value);PF('#{tableComponent.widgetVar}').filter();" placeholder="Search" />
<h:outputText value="&nbsp;" escape="false"/>
<h:panelGroup id="resultCountPlaceholder" binding="#{resultCountPlaceholder}" />
<o:moveComponent for="#{forx}" destination="AFTER">
<h:panelGroup id="resultCount" binding="#{resultCount}" >
<!-- Mojarra has issues with dynamically changing UIInstructions, so hide in output text -->
<h:outputText value="<script type='text/javascript'>" escape="false" />
<h:panelGroup rendered="#{not empty tableComponent.filteredValue}">
<h:outputText value="document.getElementById('#{resultCountPlaceholder.clientId}').innerHTML = 'Showing #{tableComponent.filteredValue.size()} of #{of:evalAttribute(tableComponent, 'value').size()}';" escape="false" />
</h:panelGroup>
<h:panelGroup rendered="#{empty tableComponent.filteredValue or (tableComponent.filteredValue.size() eq of:evalAttribute(tableComponent, 'value').size())}">
<!-- MyFaces is not able to re-render text and expressions directly put on a Facelet, so wrap in output text as well -->
<h:outputText value="document.getElementById('#{resultCountPlaceholder.clientId}').innerHTML = '';" escape="false"/>
</h:panelGroup>
<h:outputText value="</script>" escape="false" />
</h:panelGroup>
</o:moveComponent>
<o:moveComponent for="#{forx}" destination="BEHAVIOR">
<p:ajax event="filter" update=":#{resultCount.clientId}" />
</o:moveComponent>
<o:moveComponent for="#{forx}" destination="FACET" facet="header">
<h:inputHidden id="globalFilter" />
</o:moveComponent>
</ui:composition>
package org.omnifaces.showcase.components;
import static java.util.Arrays.asList;
import java.io.Serializable;
import java.util.List;
import jakarta.annotation.PostConstruct;
import jakarta.inject.Named;
import org.omnifaces.cdi.ViewScoped;
import org.omnifaces.showcase.model.User;
@Named
@ViewScoped
public class MyTableBacking implements Serializable {
private static final long serialVersionUID = 1L;
private List<User> users;
private List<User> filteredUsers;
@PostConstruct
public void init() {
users = asList(
new User("John", "Peterson", 32),
new User("Linda", "Harrison", 33),
new User("Akira", "Yamada", 18),
new User("Fangni", "Chen", 39)
);
}
public List<User> getUsers() {
return users;
}
public List<User> getFilteredUsers() {
return filteredUsers;
}
public void setFilteredUsers(List<User> filteredUsers) {
this.filteredUsers = filteredUsers;
}
}
package org.omnifaces.showcase.model;
public class User {
private String firstName;
private String lastName;
private int age;
public User(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public int getAge() {
return age;
}
}