I have this composite component:
inputMask.xhtml
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:composite="http://xmlns.jcp.org/jsf/composite"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<composite:interface>
<composite:attribute name="value" />
<composite:attribute name="mask" type="java.lang.String" required="true" />
<composite:attribute name="converterId" type="java.lang.String" default="br.edu.ufca.eventos.visao.inputmask.inputMask" />
</composite:interface>
<composite:implementation>
<h:outputScript library="script" name="inputmask.js" target="head" />
<h:inputText id="mascara">
<c:if test="#{cc.getValueExpression('value') != null}">
<f:attribute name="value" value="#{cc.attrs.value}" />
</c:if>
<f:converter converterId="#{cc.attrs.converterId}" />
<f:attribute name="mask" value="#{cc.attrs.mask}" />
</h:inputText>
<h:outputScript target="body">
defineMask("#{cc.clientId}", "#{cc.attrs.mask}");
</h:outputScript>
</composite:implementation>
</html>
In my last question:
Error trying to add composite component programmatically ("no tag was defined for name")
I was getting this error:
javax.faces.view.facelets.TagException: //C:/wildfly-10/standalone/tmp/eventos.ear.visao.war/mojarra7308315477323852505.tmp #2,127 <j:inputMask.xhtml> Tag Library supports namespace: http://xmlns.jcp.org/jsf/composite/componente, but no tag was defined for name: inputMask.xhtml
when trying to add the above composite component programmatically with this code:
Map<String, String> attributes = new HashMap<>();
attributes.put("mask", "999.999");
Components.includeCompositeComponent(Components.getCurrentForm(), "componente", "inputMask.xhtml", "a123", attributes);
but I managed to solve this problem this way:
The implementation of the method Components#includeCompositeComponent from OmniFaces 2.4 (the version I was using) is this:
public static UIComponent includeCompositeComponent(UIComponent parent, String libraryName, String tagName, String id, Map<String, String> attributes) {
String taglibURI = "http://xmlns.jcp.org/jsf/composite/" + libraryName;
Map<String, Object> attrs = (attributes == null) ? null : new HashMap<String, Object>(attributes);
FacesContext context = FacesContext.getCurrentInstance();
UIComponent composite = context.getApplication().getViewHandler()
.getViewDeclarationLanguage(context, context.getViewRoot().getViewId())
.createComponent(context, taglibURI, tagName, attrs);
composite.setId(id);
parent.getChildren().add(composite);
return composite;
}
So I decided to give a try to the code from an earlier version of OmniFaces (with some change adding the attributes parameter from me) of this method:
public static UIComponent includeCompositeComponent(UIComponent parent, String libraryName, String resourceName, String id, Map<String, String> attributes) {
// Prepare.
FacesContext context = FacesContext.getCurrentInstance();
Application application = context.getApplication();
FaceletContext faceletContext = (FaceletContext) context.getAttributes().get(FaceletContext.FACELET_CONTEXT_KEY);
// This basically creates <ui:component> based on <composite:interface>.
Resource resource = application.getResourceHandler().createResource(resourceName, libraryName);
UIComponent composite = application.createComponent(context, resource);
composite.setId(id); // Mandatory for the case composite is part of UIForm! Otherwise JSF can't find inputs.
// This basically creates <composite:implementation>.
UIComponent implementation = application.createComponent(UIPanel.COMPONENT_TYPE);
implementation.setRendererType("javax.faces.Group");
composite.getFacets().put(UIComponent.COMPOSITE_FACET_NAME, implementation);
if (!attributes.isEmpty()) {
ExpressionFactory factory = application.getExpressionFactory();
ELContext ctx = context.getELContext();
for (Map.Entry<String, String> entry : attributes.entrySet()) {
ValueExpression expr = factory.createValueExpression(ctx, entry.getValue(), Object.class);
composite.setValueExpression(entry.getKey(), expr);
}
}
// Now include the composite component file in the given parent.
parent.getChildren().add(composite);
parent.pushComponentToEL(context, composite); // This makes #{cc} available.
try {
faceletContext.includeFacelet(implementation, resource.getURL());
} catch (IOException e) {
throw new FacesException(e);
} finally {
parent.popComponentFromEL(context);
}
return composite;
}
And finally the error was gone. The composite component was dynamically added to the page.
But another problem appeared.
The action in a button to add the component is more or less like this:
if (Components.findComponent("form:a123") == null)
{
Map<String, String> attributes = new HashMap<>();
attributes.put("value", "#{bean.cpf}");
attributes.put("mask", "999.999.999-99");
includeCompositeComponent(Components.getCurrentForm(), "componente", "inputMask.xhtml", "a123", attributes);
}
As you can see, the composite component is only added once.
When the component is first added, the script code that is in the component:
<h:outputScript target="body">
defineMask("#{cc.clientId}", "#{cc.attrs.mask}");
</h:outputScript>
is added to the page. I can see it when I visualize the html source code in the browser. But on postbacks, this script code is not rendered anymore. It's not in the genereted html page. The <h:outputScript> with target="head" is rendered everytime, as expected, but not this one.
From my point of view, maybe there's still someting missing in the assembling of the composite component code in the method above to fix the script code even on postbacks on the page. I really don't know. It's just a guess.
Do you know what's going on or what's missing?
---- UPDATE 1 ----
I think that I really found the source of the problem. It seems that it's a bug in JSF related with scripts in composite components included programatically.
Here's what I found:
I noticed that the correct code from OmniFaces to include my composite component is this:
Components.includeCompositeComponent(Components.getCurrentForm(), "componente", "inputMask", "a123", attributes);
The correct is "inputMask", not "inputMask.xhtml". But as I told you before, when I use this code I get this error instead:
Caused by: javax.faces.FacesException: Cannot remove the same component twice: form:a123:j_idt2
So I suspected that the component with the id form:a123:j_idt2 was one of the h:outputScript present in the composite component. So I changed the composite component code to this:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:composite="http://xmlns.jcp.org/jsf/composite"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:c="http://xmlns.jcp.org/jsp/jstl/core">
<composite:interface componentType="inputMask">
<composite:attribute name="value" />
<composite:attribute name="mask" type="java.lang.String" required="true" />
<composite:attribute name="converterId" type="java.lang.String" default="br.edu.ufca.eventos.visao.inputmask.inputMask" />
</composite:interface>
<composite:implementation>
<h:inputText id="mascara">
<c:if test="#{cc.getValueExpression('value') != null}">
<f:attribute name="value" value="#{cc.attrs.value}" />
</c:if>
<f:converter converterId="#{cc.attrs.converterId}" />
<f:attribute name="mask" value="#{cc.attrs.mask}" />
</h:inputText>
<script type="text/javascript">
defineMask("#{cc.clientId}", "#{cc.attrs.mask}");
</script>
</composite:implementation>
</html>
Removing all references to the h:outputScript tag. (Of course, I placed the inputmask.js script outside the composite component for the component to continue to work).
And now when I run the code, the component is finally added to the page without errors. But, as I said before with the code from an earlier version of OmniFaces, the script is still not rendered in postbacks. JSF only renders it when the component is added, loosing it on postbacks. I know this is not an expected behaviour.
So, I ask you: do you know how I can solve this script problem? Or at least any workaround I can use in this case?
Thank you in advance.
---- UPDATE 2 ----
I found a workaround for it. I did this in a backing component for the composite component and it worked, the script is always rendered:
#Override
public void encodeEnd(FacesContext context) throws IOException
{
super.encodeEnd(context);
ResponseWriter writer = context.getResponseWriter();
writer.startElement("script", this);
writer.writeText(String.format("defineMask('%s', '%s');",
getClientId(), getAttributes().get("mask")), null);
writer.endElement("script");
}
but it's kind of ugly and seems unnecessary. Again, if the component is not included programmatically, I don't need the backing component. It seems like a bug in JSF. Could some of you test and confirm this? I mean, test if a composite component with script in it added programmatically loses its script on postback.
P.S.: I'm using OmniFaces 2.4 and Mojarra 2.2.13.
The solution (workaround) is to remove all script from the composite component and create a backing component for it to do precisely what JSF was supposed to do:
package br.edu.company.project.view.inputmask;
import java.io.IOException;
import java.util.Map;
import javax.faces.component.FacesComponent;
import javax.faces.component.NamingContainer;
import javax.faces.component.UIInput;
import javax.faces.component.UINamingContainer;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import org.omnifaces.util.FacesLocal;
#FacesComponent("inputMask")
public class InputMask extends UIInput implements NamingContainer
{
private static final String SCRIPT_FILE_WRITTEN =
"br.edu.company.project.SCRIPT_FILE_WRITTEN";
#Override
public String getFamily()
{
return UINamingContainer.COMPONENT_FAMILY;
}
#Override
public void encodeBegin(FacesContext context) throws IOException
{
writeScriptFileIfNotWrittenYet(context);
super.encodeBegin(context);
}
#Override
public void encodeEnd(FacesContext context) throws IOException
{
super.encodeEnd(context);
writeMaskDefinition(context);
}
private void writeScriptFileIfNotWrittenYet(FacesContext context) throws IOException
{
if (FacesLocal.getRequestMap(context).putIfAbsent(
SCRIPT_FILE_WRITTEN, true) == null)
{
writeScript(context, w -> w.writeAttribute(
"src", "resources/script/inputmask.js", null));
}
}
private void writeMaskDefinition(FacesContext context) throws IOException
{
writeScript(context, w -> w.writeText(String.format(
"defineMask('%s', '%s');", getClientId(),
getAttributes().get("mask")), null));
}
private void writeScript(FacesContext context, WriteAction writeAction)
throws IOException
{
ResponseWriter writer = context.getResponseWriter();
writer.startElement("script", this);
writer.writeAttribute("type", "text/javascript", null);
writeAction.execute(writer);
writer.endElement("script");
}
#FunctionalInterface
private static interface WriteAction
{
void execute(ResponseWriter writer) throws IOException;
}
}
Again, you don't need this if your composite component won't be included programmatically. In this case, JSF works as expected and you don't need the backing component.
If someone have the time, I think it would be nice to file a bug report to the Mojarra team.
Related
I have a custom component that implements UIInput and that needs to save some state info for later reuse in postback requests. Used standalone it works fine, but inside an <ui:repeat> the postback finds the saved state of the latest rendered row of data. The log output of an action call is
INFORMATION: myData is "third foo"
INFORMATION: myData is "third foo"
INFORMATION: myData is "third foo"
INFORMATION: ok action
where I would expect
INFORMATION: myData is "first foo"
INFORMATION: myData is "second foo"
INFORMATION: myData is "third foo"
INFORMATION: ok action
I understand that myComponent is a single instance inside of ui:repeat. So what is the best way to save component state so it is restored correctly for each row in the dataset?
My XHTML form:
<h:form>
<ui:repeat var="s" value="#{myController.data}">
<my:myComponent data="#{s}"/>
</ui:repeat>
<h:commandButton action="#{myController.okAction}" value="ok">
<f:ajax execute="#form" render="#form"/>
</h:commandButton>
</h:form>
My Bean:
#Named
#ViewScoped
public class MyController implements Serializable {
private static final long serialVersionUID = -2916212210553809L;
private static final Logger LOG = Logger.getLogger(MyController.class.getName());
public List<String> getData() {
return Arrays.asList("first","second","third");
}
public void okAction() {
LOG.info("ok action");
}
}
Composite component XHTML code:
<ui:component xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:cc="http://xmlns.jcp.org/jsf/composite">
<cc:interface componentType="myComponent">
<cc:attribute name="data"/>
</cc:interface>
<cc:implementation>
<h:panelGrid columns="2">
<h:outputLabel value="cc.attrs.data"/>
<h:outputText value="#{cc.attrs.data}"/>
<h:outputLabel value="cc.myData"/>
<h:outputText value="#{cc.myData}"/>
</h:panelGrid>
</cc:implementation>
</ui:component>
Composite Component backing class:
#FacesComponent
public class MyComponent extends UIInput implements NamingContainer {
private static final Logger LOG=Logger.getLogger(MyComponent.class.getName());
public String calculateData() {
return String.format("%s foo", this.getAttributes().get("data") );
}
public String getMyData() {
return (String)getStateHelper().get("MYDATA");
}
public void setMyData( String data ) {
getStateHelper().put("MYDATA", data);
}
#Override
public String getFamily() {
return UINamingContainer.COMPONENT_FAMILY;
}
#Override
public void encodeBegin(FacesContext context) throws IOException {
this.setMyData( calculateData() );
super.encodeBegin(context);
}
#Override
public void processDecodes(FacesContext context) {
super.processDecodes(context);
LOG.log(Level.INFO, "myData {0}", getMyData() );
}
}
Just tried reproducing your issue and yes, now I get what you're after all. You just wanted to use the JSF component state as some sort of view scope for the calculated variables. I can understand that. The observed behavior is indeed unexpected.
In a nutshell, this is explained in this blog of Leonardo Uribe (MyFaces committer): JSF component state per row for datatables.
The reason behind this behavior is tags like h:dataTable or ui:repeat only save properties related with EditableValueHolder interface (value, submittedValue, localValueSet, valid). So, a common hack found to make it work correctly is extend your component from UIInput or use EditableValueHolder interface, and store the state you want to preserve per row inside "value" field.
[...]
Since JSF 2.1, UIData implementation has a new property called rowStatePreserved. Right now this property does not appear on facelets taglib documentation for h:dataTable, but on the javadoc for UIData there is. So the fix is very simple, just add rowStatePreserved="true" in your h:dataTable tag:
In the end, you have basically 3 options:
Use UIInput#value instead of something custom like MYDATA
As instructed by the abovementioned blog, just replace getMyData() and setMyData() by the existing getValue() and setValue() methods from UIInput. Your composite component already extends from it.
#Override
public void encodeBegin(FacesContext context) throws IOException {
this.setValue(calculateData()); // setValue instead of setMyData
super.encodeBegin(context);
}
#Override
public void processDecodes(FacesContext context) {
super.processDecodes(context);
LOG.log(Level.INFO, "myData {0}", getValue() ); // getValue instead of getMyData
}
And equivalently in the XHTML implementation (by the way, the <h:outputText> is unnecessary here):
<h:outputText value="#{cc.value}" /> <!-- cc.value instead of cc.myData -->
However, this didn't really work when I tried it on Mojarra 2.3.14. It turns out that Mojarra's implementation of the <ui:repeat> indeed restores the EditableValueHolder state during restore view (yay!), but then completely clears out it during decode (huh?), turning this a bit useless. I'm frankly not sure why it is doing that. I have also found in Mojarra's UIRepeat source code that it doesn't do that when it's nested in another UIData or UIRepeat. So the following little trick of putting it in another UIRepeat attempting to iterate over an empty string made it work:
<ui:repeat value="#{''}">
<ui:repeat value="#{myController.data}" var="s">
<my:myComponent data="#{s}" />
</ui:repeat>
</ui:repeat>
Remarkably is that nothing of this all worked in MyFaces 2.3.6. I haven't debugged it any further.
Replace <ui:repeat> by <h:dataTable rowStatePreserved="true">
As hinted in the abovementioned blog, this is indeed documented in UIData javadoc. Just replace <ui:repeat> by <h:dataTable> and explicitly set its rowStatePreserved attribute to true. You can just keep using your MYDATA attribute in the state.
<h:dataTable value="#{myController.data}" var="s" rowStatePreserved="true">
<h:column><my:myComponent data="#{s}" /></h:column>
</h:dataTable>
This worked for me in both Mojarra 2.3.14 and MyFaces 2.3.6.
This is unfortunately not supported on UIRepeat. So you'll have to live with a potentially unnecessary HTML <table> markup generated by the <h:dataTable>. It was during JSF 2.3 work however discussed once to add the functionality to UIRepeat, but unfortunately nothing was done before JSF 2.3 release.
Include getClientId() in state key
As suggested by Selaron in your question's comments, store the client ID along as key in the state.
public String getMyData() {
return (String) getStateHelper().get("MYDATA." + getClientId());
}
public void setMyData(String data) {
getStateHelper().put("MYDATA." + getClientId(), data);
}
Whilst it's a relatively trivial change, it's awkward. This does not infer portability at all. You'd have to hesitate and think twice every time you implement a new (composite) component property which should be saved in JSF state. You'd really expect JSF to automatically take care of this.
Displaying a BLOB image using <p:graphicImage> as follows.
<p:graphicImage value="#{categoryBean.image}">
<f:param name="id" value="7"/>
</p:graphicImage>
Where CategoryBean has been defined as follows.
#Named
#ApplicationScoped
public class CategoryBean {
#Inject
private CategoryService service;
public CategoryBean() {}
public StreamedContent getImage() throws IOException {
FacesContext context = FacesContext.getCurrentInstance();
if (context.getCurrentPhaseId() == PhaseId.RENDER_RESPONSE) {
return new DefaultStreamedContent();
} else {
String id = context.getExternalContext().getRequestParameterMap().get("id");
byte[] bytes = Utils.isNumber(id) ? service.findImageById(Long.parseLong(id)) : null;
return bytes == null ? null : new DefaultStreamedContent(new ByteArrayInputStream(bytes));
}
}
}
Regarding the above approach, the following custom tag should work flawlessly but it fails to display the image on <p:graphicImage> with no error / exception.
<my:image bean="#{categoryBean}" property="image" paramName="id" paramValue="7"/>
The tag file is located under /WEB-INF/tags/image.xhtml.
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:p="http://primefaces.org/ui"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:f="http://xmlns.jcp.org/jsf/core">
<p:graphicImage value="#{bean[property]}">
<f:param name="#{paramName}" value="#{paramValue}"/>
</p:graphicImage>
</ui:composition>
The generated <img> tag seems to look fine :
<img id="form:j_idt4"
src="/ContextPath/javax.faces.resource/dynamiccontent.properties.xhtml?ln=primefaces&v=5.3&pfdrid=IA0%2F7ZuBnGS%2BSzeb%2BHyPOTo4Pxp4hjI6&pfdrt=sc&id=7&pfdrid_c=true"
alt=""/>
It only returns a HTTP 404 error.
Is there any flaw in the definition of the custom tag given?
It's caused by the way how PrimeFaces <p:graphicImage> identifies image requests. Basically, it converts the exact value expression #{bean[property]} to string, encrypts it and then passes it as pfdrid value. When the webbrowser needs to download the image by a brand new HTTP request, that value expression is decrypted and evaluated in the "current" EL context. However, during that moment, there's nowhere a #{bean} nor #{property} available anywhere in the EL context because there's no means of a JSF view with tagfiles and all. Only request, session and application scoped beans are available in EL context.
There's nothing to do against this other than reporting an issue at PrimeFaces guys.
As to alternate solutions, OmniFaces <o:graphicImage> does a better job in this by inspecting the target bean/method during render response already instead of during streaming the image. It immediately inspects #{bean[property]}, discovers that it actually represents #{categoryBean.image}, and then succeeds. Just to be sure I tested it in a tagfile like you have and it works fine for me whereas the PF one indeed fails as described.
<o:graphicImage value="#{bean[property](paramValue)}" />
public byte[] getImage(Long id) throws IOException {
return service.findImageById(id);
}
We are migrating a JSF 2.1 application, from JBoss AS 7.2 to Wildfly and thus JSF 2.2. The problem We're having is the following: We have a compositecomponent that is included in a #ViewScoped bean. The component has to retain its value through multiple requests, so a Request Scoped bean is not a solution.
The exception we're getting is a multiple component id one. After the request JSF starts to render the component for the second time, and fails.
I made a simple demo for this:
MyViewBean.java
#ViewScoped
#Named
public class MyViewBean implements Serializable {
private Component component;
public Component getComponent() {
return component;
}
public void setComponent(Component component) {
this.component = component;
}
public String increment(){
component.setCounter(component.getCounter()+1);
return "";
}
}
Component.java
#FacesComponent(value = "composite")
public class Component extends UINamingContainer {
private Integer counter = 0;
public Integer getCounter() {
return counter;
}
public void setCounter(Integer counter) {
this.counter = counter;
}
}
compositeTest.xhtml
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:h="http://xmlns.jcp.org/jsf/html"
template="/WEB-INF/templates/default.xhtml"
xmlns:pelda="http://xmlns.jcp.org/jsf/composite/component">
<ui:define name="content">
<h1>Composite component Test!</h1>
<h:form>
<pelda:composite binding="#{myViewBean.component}" />
<h:commandButton action="#{myViewBean.increment()}" value="Push me!"/>
</h:form>
</ui:define>
</ui:composition>
composite.xhtml
<cc:interface componentType="composite">
</cc:interface>
<cc:implementation>
<h:outputText id="id_hello" value="Helloka" />
<h:outputText id="id_counter" value="#{cc.counter}" />
</cc:implementation>
</html>
How to achieve that the counter can be incremented (with #RequestScoped bean it resets) and won't fail with idUniqueness error? We're using Mojarra 2.2.8 (Default in wildfly), also tried with Mojarra 2.2.12 (the latest as per writing this).
Thanks in advance!
UIComponent instances are inherently request scoped. You should never reference UIComponent instances beyond the request scope. Carefully read How does the 'binding' attribute work in JSF? When and how should it be used? for an elaborate explanation on that.
You only want to save its state in the JSF state via the inherited getStateHelper() method. This acts basically as the view scope.
#FacesComponent(value = "composite")
public class Component extends UINamingContainer {
public Integer getCounter() {
return (Integer) getStateHelper().eval("counter", 0);
}
public void setCounter(Integer counter) {
getStateHelper().put("counter", counter);
}
}
Don't forget to get rid of the binding attribute in the view.
See also:
How to save state when extending UIComponentBase
I'm writing my custom table composite component with Mojarra JSF. I'm also trying to bind that composite to a backing component. The aim is to be able to specify the number of elements the table has in a composite attribute, later on the bound backing component will autogenerate the elements itself before view gets rendered. I've this sample code:
Main page:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:comp="http://java.sun.com/jsf/composite/comp">
<h:head />
<body>
<h:form>
<comp:myTable itemNumber="2" />
</h:form>
</body>
</html>
myTable.xhtml:
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:composite="http://java.sun.com/jsf/composite"
xmlns:h="http://java.sun.com/jsf/html">
<h:body>
<composite:interface componentType="components.myTable">
<composite:attribute name="itemNumber"
type="java.lang.Integer" required="true" />
</composite:interface>
<composite:implementation>
<h:dataTable value="#{cc.values}" var="value">
<h:column headerText="column">
#{value}
<h:commandButton value="Action" action="#{cc.action}" />
</h:column>
</h:dataTable>
</composite:implementation>
</h:body>
</html>
MyTable.java:
#FacesComponent("components.myTable")
public class MyTable extends UINamingContainer {
private List<String> values = new ArrayList<String>();
public void action() {
System.out.println("Called");
}
#Override
public void encodeBegin(FacesContext context) throws IOException {
// Initialize the list according to the element number
Integer num = (Integer) getAttributes().get("itemNumber");
for (int i = 0; i < num; i++) {
values.add("item" + i);
}
super.encodeBegin(context);
}
public List<String> getValues() {
return values;
}
}
The issue is table gets rendered properly (in this case with two items), but action method doesn't get called when pressing the button on the lines.
If I follow the wiki page for composite components, I can get it work in that way, but having to initialize the List each time getValues() is called, introducing logic into the getter method :-(.
Any idea about that? It seems to be a trouble related with overriding encodeBegin method. I also tried initializing it on markInitialState, but attributes are not yet available there...
Tested with Mojarra 2.1.27 + Tomcat 6-7 & Mojarra 2.2.5 + Tomcat 7
As to the cause, UIComponent instances are inherently request scoped. The postback effectively creates a brand new instance with properties like values reinitialized to default. In your implementation, it is only filled during encodeXxx(), which is invoked long after decode() wherein the action event needs to be queued and thus too late.
You'd better fill it during the initialization of the component. If you want a #PostConstruct-like hook for UIComponent instances, then the postAddToView event is a good candidate. This is invoked directly after the component instance is added to the component tree.
<cc:implementation>
<f:event type="postAddToView" listener="#{cc.init}" />
...
</cc:implementation>
with
private List<String> values;
public void init() {
values = new ArrayList<String>();
Integer num = (Integer) getAttributes().get("value");
for (int i = 0; i < num; i++) {
values.add("item" + i);
}
}
(and remove the encodeBegin() method if it isn't doing anything useful anymore)
An alternative would be lazy initialization in getValues() method.
A simpler solution would be to store and retrieve values as part of the components state. Storing can happen during encodeBegin, and retrieving could directly happen within the getter:
#FacesComponent("components.myTable")
public class TestTable extends UINamingContainer {
public void action() {
System.out.println("Called");
}
#Override
public void encodeBegin(FacesContext context) throws IOException {
// Initialize the list according to the element number
List<String> values = new ArrayList<>();
Integer num = (Integer) getAttributes().get("itemNumber");
for (int i = 0; i < num; i++) {
values.add("item" + i);
}
getStateHelper().put("values",values);
super.encodeBegin(context);
}
public List<String> getValues() {
return (List<String>)getStateHelper().get("values");
}
}
To avoid repeating the logic in getValues(), there could be additional parsing required in more complex cases, there should be a way to process and cache the attributes right after they become available, although I am not sure when and how at this point.
Either way - this seemed to be the simplest way to solve this problem.
all
I've been working on a composite component for a date range. Essentially, my composite component uses two Richfaces 4.3 calendar components underneath to capture the individual date values, generate a date range (a pair of LocalDate objects). I found this blog entry which was the basis for my custom component that combines the two submitted values on the calendar into one pair value.
Everything seems to work fine and the values are getting updated. However, I'm trying to figure out how to propagate the change event to the using xhtml page for a partial render of another component, and I've been unsuccessful. I've tried everything I could think of, but I think I'm missing something.
The page:
<rich:panel>
<f:facet name="header">Calendar Date Range Component</f:facet>
<h:outputText id="out1" binding="#{calendarDateRangeTestBean.component1}"
value="#{calendarDateRangeTestBean.dateRange}" converter="localDatePairConverter" /><br/>
<h:outputText id="out2" value="#{calendarDateRangeTestBean.dateRange}" converter="localDatePairConverter" /><b>NOT WORKING</b>
<yxp:calendarDateRange id="calendarDateRange" value="#{calendarDateRangeTestBean.dateRange}"
dataModel="#{calendarDateRangeTestBean}"
valueChangeListener="#{calendarDateRangeTestBean.processValueChange}">
<f:ajax execute="#all" listener="#{calendarDateRangeTestBean.processBehaviorEvent}"/>
<!-- This doesn't seem to work???? -->
<f:ajax execute="#all" render="out2" />
</yxp:calendarDateRange>
</rich:panel>
My test managed bean:
#ViewScoped
#ManagedBean
public class CalendarDateRangeTestBean extends AbstractCalendarDateRangeDataModel implements
ValueChangeListener, Serializable {
private static Logger logger = LoggerFactory.getLogger(CalendarDateRangeTestBean.class);
private Pair<LocalDate> dateRange = Pair.of(LocalDate.now(), LocalDate.now().plusDays(7));
private UIComponent component1;
public UIComponent getComponent1() {
return component1;
}
public LocalDateRange getDateRange() {
return dateRange;
}
public void processBehaviorEvent(final javax.faces.event.AjaxBehaviorEvent event) {
logger.info("processing event " + event + ": " + event.getBehavior());
final FacesContext context = FacesContext.getCurrentInstance();
logger.info("Setting render to " + component1.getClientId(context));
// This seems to cause a rerender of the first component
context.getPartialViewContext().getRenderIds().add(component1.getClientId(context));
}
#Override
public void processValueChange(final ValueChangeEvent event) throws AbortProcessingException {
logger.info(this.toString() + ": processing value change event " + event + ": ["
+ event.getOldValue() + ":" + event.getNewValue() + "]");
}
public void setComponent1(final UIComponent component1) {
this.component1 = component1;
}
public void setDateRange(final Pair<LocalDate> dateRange) {
logger.info("Setting date range to " + dateRange);
this.dateRange = dateRange;
}
}
My composite component:
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:a4j="http://richfaces.org/a4j"
xmlns:rich="http://richfaces.org/rich"
xmlns:composite="http://java.sun.com/jsf/composite">
<!-- Methods exposed on rich:component are available in the __proto__ object. -->
<composite:interface componentType="com.yieldex.platform.ui.CalendarDateRange">
<composite:attribute name="value" required="true" type="demo.Pair"/>
<composite:attribute name="dataModel" required="false" type="demo.Pair" />
<composite:clientBehavior name="change" event="change" targets="startCalendar endCalendar" default="true"/>
</composite:interface>
<composite:implementation>
<h:outputStylesheet library="yieldex/platform" name="css/yieldex-platform.css" target="head" />
<div id="#{cc.clientId}" class="yxp-calendar-date-range">
<rich:calendar id="startCalendar"
binding="#{cc.startCalendar}"
styleClass="yxp-start-date-range"
converter="localDateConverter" mode="ajax"
dataModel="#{not empty cc.attrs.dataModel ? cc.attrs.dataModel.startCalendarDataModel : standardCalendarDateRangeDataModel.startCalendarDataModel}"
monthLabels="#{dateRangeMessages.monthNames}"
weekDayLabelsShort="#{dateRangeMessages.weeksShort}"
monthLabelsShort="#{dateRangeMessages.monthNames}" popup="false"
showInput="false" showFooter="false" showWeeksBar="false"
showWeekDaysBar="true" showApplyButton="false"
buttonIcon="#{resource['yieldex/platform:img/1x1-transparent.png']}"
buttonDisabledIcon="#{resource['yieldex/platform:img/1x1-transparent.png']}">
<f:facet name="weekDays"></f:facet>
<f:ajax immediate="true" execute="#all" render="#this endCalendar"/>
</rich:calendar>
<rich:calendar id="endCalendar"
binding="#{cc.endCalendar}"
styleClass="yxp-end-date-range"
converter="localDateConverter" mode="ajax"
dataModel="#{not empty cc.attrs.dataModel ? cc.attrs.dataModel.endCalendarDataModel : standardCalendarDateRangeDataModel.endCalendarDataModel}"
monthLabels="#{dateRangeMessages.monthNames}"
weekDayLabelsShort="#{dateRangeMessages.weeksShort}"
monthLabelsShort="#{dateRangeMessages.monthNames}" popup="false"
showInput="false" showFooter="false" showWeeksBar="false"
showWeekDaysBar="true" showApplyButton="false"
buttonIcon="#{resource['yieldex/platform:img/1x1-transparent.png']}"
buttonDisabledIcon="#{resource['yieldex/platform:img/1x1-transparent.png']}">
<f:facet name="weekDays"></f:facet>
<f:ajax immediate="true" execute="#all" render="startCalendar #this"/>
</rich:calendar>
</div>
</composite:implementation>
</ui:composition>
My backing component:
#FacesComponent("com.yieldex.platform.ui.CalendarDateRange")
public class YXCalendarDateRange extends UIInput implements NamingContainer {
private UICalendar startCalendarComponent;
private UICalendar endCalendarComponent;
#Override
public void encodeBegin(final FacesContext context) throws IOException {
final Pair<LocalDate> value = (Pair<LocalDate>) this.getValue();
if (value == null) {
startCalendarComponent.setValue(null);
endCalendarComponent.setValue(null);
} else {
startCalendarComponent.setValue(value.getStart());
endCalendarComponent.setValue(value.getEnd());
}
super.encodeBegin(context);
}
#Override
protected Object getConvertedValue(final FacesContext context, final Object submittedValue) {
final LocalDate startDate = (LocalDate) startCalendarComponent.getConverter().getAsObject(context,
startCalendarComponent, (String) this.startCalendarComponent.getSubmittedValue());
final LocalDate endDate = (LocalDate) endCalendarComponent.getConverter().getAsObject(context,
endCalendarComponent, (String) this.endCalendarComponent.getSubmittedValue());
if (startDate == null || endDate == null) {
return null;
} else {
if (startDate.isAfter(endDate)) {
final FacesMessage message = new FacesMessage();
message.setSeverity(FacesMessage.SEVERITY_ERROR);
message.setSummary("start date cannot be after end date");
message.setDetail("start date cannot be after end date");
throw new ConverterException(message);
}
return Pair.of(startDate, endDate);
}
}
public UICalendar getEndCalendar() {
return this.endCalendarComponent;
}
#Override
public String getFamily() {
return UINamingContainer.COMPONENT_FAMILY;
}
public UICalendar getStartCalendar() {
return this.startCalendarComponent;
}
#Override
public Object getSubmittedValue() {
return this;
}
public void setEndCalendar(final UICalendar endCalendarComponent) {
this.endCalendarComponent = endCalendarComponent;
}
public void setStartCalendar(final UICalendar startCalendarComponent) {
this.startCalendarComponent = startCalendarComponent;
}
}
What I see is that the valueChangedEvent is coming though. I also see my processBehaviorEvent being called, and the first outputText being rerendered as I'm calling that programmatically. But the second one doesn't seem to get rerendered. I am trying to figure out if this is a bug in Mojarra 2.1.25 or is there something fundamentally wrong with my approach. Any suggestions would be greatly appreciated.
Any client ID in <f:ajax render> is evaluated relative to the parent naming container of the component it has been attached to. In this construct, the <f:ajax> ends up being attached inside the composite component, which is by itself a naming container. However, there's no component with ID out2 inside the composite, which is the problem.
To solve it, specify the absolute client ID. For example, when it's inside a <h:form id="formId"> element:
<f:ajax execute="#all" render=":formId:out2" />
If it's more complicated, binding the component to the view and refer to its client ID dynamically:
<h:outputText id="out2" binding="#{out2}" ... />
...
<f:ajax execute="#all" render=":#{out2.clientId}" />
See also:
How to find out client ID of component for ajax update/render? Cannot find component with expression "foo" referenced from "bar"