Reputation: 3102
Got a bit stuck while working on a JSF Custom Component. Would be nice if someone could give me head start.
The idea is to have a component that lets me page through a LocalDate
. Think of a simpler date picker. The rendered HTML has two buttons, one to increment the date by a day and another button to decrement the date by a day. The value itself is stored in a input hidden. Attached is a simple version of the component which doesn't work, but hopefully describes what I am trying to achieve.
The idea is to have say <my:datePager value="#{myBackingBean.someDate}" />
on a page and execute some logic/action once one of either button has been clicked.
For example <my:datePager value="#{myBackingBean.someDate} action="..." />
or <my:datePager value="#{myBackingBean.someDate} previous="..." next="..."/>
No idea what is better.
Here is what I am stuck with: How to call the previous
and next
methods on the component?
This is my JSF 2.3 custom component so far:
@FacesComponent(LocalDatePager.COMPONENT_TYPE)
public class LocalDatePager extends UIInput {
public static final String COMPONENT_TYPE = "LocalDatePager";
public LocalDatePager() {
setConverter(LocalDateConverter.INSTANCE);
}
public void previous() {
LocalDate localDate = (LocalDate) getValue();
localDate = localDate.minusDays(1);
setValue(localDate);
}
public void next() {
LocalDate localDate = (LocalDate) getValue();
localDate = localDate.plusDays(1);
setValue(localDate);
}
@Override
public void decode(FacesContext context) {
super.decode(context);
}
@Override
public void encodeEnd(FacesContext context) throws IOException {
ResponseWriter writer = context.getResponseWriter();
writer.startElement("div", this);
writer.writeAttribute("class", "btn-group", null);
writer.writeAttribute("role", "group", null);
writer.startElement("button", this);
writer.writeAttribute("type", "button", null);
writer.writeAttribute("class", "btn btn-outline-primary", null);
writer.writeText("Previous", null);
writer.endElement("button");
writer.startElement("button", this);
writer.writeAttribute("type", "button", null);
writer.writeAttribute("class", "btn btn-outline-primary", null);
writer.writeText("Next", null);
writer.endElement("button");
writer.endElement("div");
writer.startElement("input", this);
writer.writeAttribute("name", getClientId(context), "clientId");
writer.writeAttribute("type", "hidden", null);
writer.writeAttribute("value", getValue(), "value");
writer.endElement("input");
}
}
From my understanding the component I have is mind is both UIInput
and UICommand
. Do I need to implement ActionEvent
? Fire value change events?
At the moment I do the whole previous and next methods in backing beans and not in a component. This leads to lots of duplicate code and I thought a custom component fits best. I tried to work with Composite Components, but this did not get me far either.
Another approach so far has been client behaviour: execute JavaScript, change the value of the hidden input and submit the form. This felt even worse.
Would be nice if someone could give me a pointer in the right direction.
Upvotes: 2
Views: 226
Reputation: 1108782
UIInput
First you need to render the buttons as normal submit buttons in encodeXxx()
method. Also, you'd rather want to use encodeAll()
instead of encodeEnd()
for this because this appears to be a "leaf" component and you thus don't want to deal with children. Implementing encodeAll()
is then more efficient. Rename your existing encodeEnd()
method to encodeAll()
and adjust the encoding of buttons as below:
writer.startElement("button", this);
writer.writeAttribute("type", "submit", null); // instead of "button"
writer.writeAttribute("name", getClientId(context) + "_previous", "clientId");
writer.writeText("Previous", null);
// ...
writer.startElement("button", this);
writer.writeAttribute("type", "submit", null); // instead of "button"
writer.writeAttribute("name", getClientId(context) + "_next", "clientId");
writer.writeText("Next", null);
// ...
// The hidden input field in your existing code is fine as-is.
Then you can just check for them in the request parameter map. If you want to keep your component to be an UIInput
, then I suggest to perform the job in getConvertedValue()
method:
@Override
protected Object getConvertedValue(FacesContext context, Object submittedValue) throws ConverterException {
LocalDate localDate = (LocalDate) super.getConvertedValue(context, submittedValue);
if (localDate != null) {
Map<String, String> params = context.getExternalContext().getRequestParameterMap();
if (params.get(getClientId(context) + "_previous") != null) {
localDate = localDate.minusDays(1);
}
else if (params.get(getClientId(context) + "_next") != null) {
localDate = localDate.plusDays(1);
}
}
return localDate;
}
Then you can simply do the job in a value change listener:
<my:datePager value="#{bean.localDate}" valueChangeListener="#{bean.onpage}" />
public void onpage(ValueChangeEvent event) {
// ...
}
That's basically all. No need to manually fire the value change event. The rest of the logic is already taken care of by JSF.
The disadvantage of this approach, however, is that this might be invoked at the wrong moment in the JSF lifecycle. A value change listener is invoked during validations phase while you really want it to be invoked during invoke application phase. Imagine that this button is placed in a form whose state depends on the data associated with #{bean.localDate}
, then the update model values phase on them may go wrong because the localDate
had been changed too soon.
UICommand
If the abovementioned disadvantage is a real showstopper, although there is a work around, then you better convert the UIInput
to an UICommand
. It can't be both. You pick the one or the other. My personal advice would be to pick UICommand
as that's "more natural" wrt the behavior during the JSF lifecycle while having the sole purpose of the component in mind ("paginating to next/previous date"). Here are the steps:
Swap out extends UIInput
for extends UICommand
.
Remove the setConverter()
call in constructor.
Keep the encodeAll()
method. It's totally fine.
Remove the getConvertedValue()
method and implement decode()
as below:
@Override
public void decode(FacesContext context) {
Map<String, String> params = context.getExternalContext().getRequestParameterMap();
if (params.get(getClientId(context) + "_previous") != null) {
queueEvent(new ActionEvent(context, this));
}
else if (params.get(getClientId(context) + "_next") != null) {
queueEvent(new ActionEvent(context, this));
}
}
These queueEvent()
calls will cause the broadcast()
of the very same component to be called. So override it as below:
@Override
public void broadcast(FacesEvent event) throws AbortProcessingException {
FacesContext context = event.getFacesContext();
Map<String, String> params = context.getExternalContext().getRequestParameterMap();
LocalDate localDate = LocalDate.parse(params.get(getClientId())); // TODO: gracefully handle any conversion error.
if (params.get(getClientId(context) + "_previous") != null) {
localDate = localDate.minusDays(1);
}
else if (params.get(getClientId(context) + "_next") != null) {
localDate = localDate.plusDays(1);
}
getValueExpression("value").setValue(context.getELContext(), localDate);
super.broadcast(event); // Invokes bean method.
}
Note how the value
attribute is manually decoded, converted and updated in the model. Although this is passed around as a hidden input value, you might want to add some logic to gracefully handle any conversion error as indicated in the TODO
.
Now you can use it as below:
<my:datePager value="#{bean.localDate}" action="#{bean.onpage}" />
public void onpage() {
// ...
}
Upvotes: 5