WARNING - OUTDATED CONTENT!
This article is targeted on JSF 1.2. Whilst this can be used in JSF 2.0 as well, a better JSF 2.0 implementation is provided by OmniFaces with its
<o:highlight>
component. See also showcase example here.
The power of a PhaseListener
This article shows how to use a PhaseListener to set focus to the first input element which has a FacesMessage (which can be caused by a validation or conversion error or any other custom reason) and highlight all elements which has a FacesMessage. It is relatively simple, it costs effectively only a few lines inside the beforePhase of the RENDER_RESPONSE, two small Javascript functions for the focus and highlight and a single CSS class for the highlight.
Here is a sample form. Note the Javascript which should be placed at the very end of the HTML body, at least after the input element which should be focused and highlighted. The stuff is tested in a Java EE 5.0 environment with Tomcat 6.0 with Servlet 2.5, JSP 2.1 and JSF 1.2_07 (currently called Mojarra).
<h:form id="form"> <h:panelGrid columns="3"> <h:outputLabel for="input1" value="Enter input 1" /> <h:inputText id="input1" value="#{myBean.input1}" required="true" /> <h:message for="input1" style="color: red;" /> <h:outputLabel for="input2" value="Enter input 2" /> <h:inputText id="input2" value="#{myBean.input2}" required="true" /> <h:message for="input2" style="color: red;" /> <h:outputLabel for="input3" value="Enter input 3" /> <h:inputText id="input3" value="#{myBean.input3}" required="true" /> <h:message for="input3" style="color: red;" /> <h:panelGroup /> <h:commandButton value="Submit" action="#{myBean.doSomething}" /> <h:panelGroup /> </h:panelGrid> </h:form> <script> setHighlight('${highlight}'); setFocus('${focus}'); </script>
This is how the Javascript functions setFocus() and setHighlight() should look like:
/** * Set focus on the element of the given id. * @param id The id of the element to set focus on. */ function setFocus(id) { var element = document.getElementById(id); if (element && element.focus) { element.focus(); } } /** * Set highlight on the elements of the given ids. It basically sets the classname of the elements * to 'highlight'. This require at least a CSS style class '.highlight'. * @param ids The ids of the elements to be highlighted, comma separated. */ function setHighlight(ids) { var idsArray = ids.split(","); for (var i = 0; i < idsArray.length; i++) { var element = document.getElementById(idsArray[i]); if (element) { element.className = 'highlight'; } } }
And now the CSS style class for the highlight:
.highlight { background-color: #fcc; }
And finally the PhaseListener which sets the focus to the first input element which has a FacesMessage and highlights all input elements which has a FacesMessage:
/* * net/balusc/webapp/SetFocusListener.java * * Copyright (C) 2007 BalusC * * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Lesser General Public License as published by the Free Software Foundation, either version 3 * of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License along with this library. * If not, see <http://www.gnu.org/licenses/>. */ package net.balusc.webapp; import java.util.Iterator; import javax.faces.context.FacesContext; import javax.faces.event.PhaseEvent; import javax.faces.event.PhaseId; import javax.faces.event.PhaseListener; /** * This phase listener checks if there is a client ID with message and will set the client ID as * ${focus} in the request map. It will also gather all client IDs with message and will set it as * ${highlight} in the request map. * <p> * This phase listener should be configured in the faces-config.xml as follows: * <pre> * <lifecycle> * <phase-listener>net.balusc.webapp.SetFocusListener</phase-listener> * </lifecycle> * </pre> * * @author BalusC * @link http://balusc.blogspot.com/2007/12/set-focus-in-jsf.html */ public class SetFocusListener implements PhaseListener { // Actions ------------------------------------------------------------------------------------ /** * @see javax.faces.event.PhaseListener#getPhaseId() */ public PhaseId getPhaseId() { // Listen on render response phase. return PhaseId.RENDER_RESPONSE; } /** * @see javax.faces.event.PhaseListener#beforePhase(javax.faces.event.PhaseEvent) */ public void beforePhase(PhaseEvent event) { // Init. FacesContext facesContext = event.getFacesContext(); String focus = null; StringBuilder highlight = new StringBuilder(); // Iterate over all client ID's with messages. Iterator<String> clientIdsWithMessages = facesContext.getClientIdsWithMessages(); while (clientIdsWithMessages.hasNext()) { String clientIdWithMessages = clientIdsWithMessages.next(); if (focus == null) { focus = clientIdWithMessages; } highlight.append(clientIdWithMessages); if (clientIdsWithMessages.hasNext()) { highlight.append(","); } } // Set ${focus} and ${highlight} in JSP. facesContext.getExternalContext().getRequestMap().put("focus", focus); facesContext.getExternalContext().getRequestMap().put("highlight", highlight.toString()); } /** * @see javax.faces.event.PhaseListener#afterPhase(javax.faces.event.PhaseEvent) */ public void afterPhase(PhaseEvent event) { // Do nothing. } }
Define the SetFocusListener as follows in the faces-config.xml:
<lifecycle> <phase-listener>net.balusc.webapp.SetFocusListener</phase-listener> </lifecycle>
That's all, folks!
Back to top
Copyright - GNU Lesser General Public License
(C) December 2007, BalusC
36 comments:
Hy BalusC,
I believe that a best way to set focus on first input would be inserting a javascript code into a template page, the code could be something as code bellow:
Inputs.focusOnFirstInput = function () {
var forms = document.forms;
var len = forms.length;
for (var i = 0; i < len; i++) {
var form = forms[i];
for (var j = 0; j < form.length; j++) {
var input = form[j];
if (input.type != "hidden"
&& input.type != "button"
&& input.type != "submit") {
if (!input.disabled) {
input.focus();
return;
}
}
}
}
};
Then into your template page you may place at end of the page the called to the Inputs.focusOnFirstInput() function at as code bellow:
<script>
Inputs.focusOnFirstInput();
</script>
The intention was to set focus on the first input element where a validation or conversion error has occurred or any facesmessage is set.
Ah ok! i'm sorry, you are right! Great post :)
ps.: the Blogger.com's comments system is terrible :(
Hi BalusC,
I've tried this. And I have two problems.
1.- When I put the
<script>setFocus('${focus}')</script>
after </h:form>. Firefox 1.5 shows a blank page (Although I see in "View-source" that all the HTML has been generated).
2.- the ${focus} expression isn't changed into anything. In the generated page it remains the same.
Which JSP version are you using? This will work on a JSP 2.1 webcontainer (Tomcat 6.0, Glassfish, etc). In older versions you'll have to pass the ${focus} value as scriptlet or by JSTL.
I'm using Tomcat 6.0.14. On its home page says that it uses JSP 2.1.
To which servlet version is your web.xml set? It should be at least 2.4 and preferably 2.5.
Thank you.
It was set to "2.3" I've changed and now it works.
I'm going to do a little change in the listener just to by default set as the focus target the first field in the form. So, with this change, if there's no error, the first field in the form will get the focus.
Finally I've changed the JavaScript function setFocus instead of the Listener.
It looks like this:
function setFocus(field_focus, error_field) {
if(error_field != '')
{
field_focus = error_field;
}
var element = document.getElementById(field_focus);
if (element && element.focus) {
element.focus();
}
}
And called to the "setFocus" function in onLoad event of the body tag:
<body bgcolor="white"
onload="setFocus('myForm:myPreferredField','${focus}');">
Hi Balus.
I found you to be an invaluable source of wisdom when working on JSF before.
I've just started reading another book and come back back to using JSF again, a
nd they talk about needing four commons-jars (beanutils, collections, digetster amd logging) in
conjunction with jsf-1_1
As I recall you pointed me to mojarra-1.2_08-b06-FCS as the best source for JSF in the past.
1) Do I still need the commons jars as dependencies for mojarra when I deploy in Tomcat? Or,
2) Does the newer build do away with this?
Regards, Jeremy
Looking here:
http://www.mvnrepository.com/artifact/commons-digester/commons-digester/1.5
I am able to assume digester is the jar that depends on all the others, so I guess the real question is does mojarra, need digester..
Found the answer here:
http://blogs.sun.com/rlubke/?page=1
(It's no longer required)
It is indeed not needed anymore since 1.2_05. Besides, the latest final release of Mojarra is always the best choice. This blog site also contains a JSF 1.2 tutorial for Eclipse + Tomcat, it doesn't state that you need any 3rd party jars to get JSF 1.2 running: JSF tutorial with Eclipse and Tomcat
Hello BalusC,
Am using the following segment in my program..
h:inputText id="username" value="#{myBean.username}" required="true"
requiredMessage="#{ErrMsg.UserNameEmpty}"
/h:inputText
It producing the following error :
Attribute requiredMessage invalid for tag inputText according to TLD
Am using Eclipse3.3.1.1..
Whats d problem..
Pls help me..
What about when using AJAX? I'm using RichFaces + A4J.
Sweet Mother of Jesus, This is so easy!
Thank you, Thank you and THNAK YOU!
I just copy pasted your code and it works perfect. Thanks a zillion.
BalusC - YOU ROCK MAN!
What about when using AJAX? I'm using RichFaces + A4J. [2]
Rafaell Pinheiro
2 years on and still useful. Why work when you can Google? ;)
Hi BalusC,
Thank you very much.
It worked perfect in my project.
Himaja
We can improve the performance of phaselistener the code below.
...
if (facesContext.getViewRoot().findComponent(clientIdWithMessages) instanceof UIInput) {
UIInput input = (UIInput) facesContext.getViewRoot().findComponent(clientIdWithMessages);
input.getAttributes().put("styleClass", "highlight_error");
}
...
Hi BalusC,
it works! Thanks for this post. I have one question:
All my fields with validation errors are highlighted now as expected.
I implemented another JS-function to change the styleClass to the original value when my ajax-submit returns:
<h:inputText id="invoiceNumber" styleClass="inputRequired"
style="width:375px;"
label="#msgs.labelInvoiceNumber_9b}"
value="#movementController.header.invoiceNumber}" required="true">
<a4j:support event="onblur" ajaxSingle="true" oncomplete="changeStyleClass(this.id,'inputRequired');"/> </h:inputText>
But when rerendering the page, the "highlight"-styleClass is active for each affected component again.
Is there a way to switch the styleClass back on the UI-Element?
http://www.primefaces.org:8080/prime-showcase/ui/focus.jsf
I would be very very interested how this validation should be done with h:dataTable where you have the same ids for all the inputTexts...
@BenHur: the generated client ID is different for each. Rightclick page in browser and view source. Just try the phaselistener, it should work just fine.
Hey BalusC,
I tried your focus back script as explained. It worked for me. But I have some issues now.
I am validating a h:inputText on the onblur event using a4j:support. I check in the action method of the a4j:support whether the textvalue is empty. If so I set a boolean which is the render property of an a4j:outputPanel. This output panel contains your script.
When I used the phaseListener, its going into a kinda continuous loop.
I am not able to post the code because its not accepting certain tags
I am posting the code.
----------
-------------
setFocus('${focus}');
function setFocus(id) {
// alert(id);
var element = document.getElementById(id);
if (element && element.focus) {
element.focus();
}
}
--------------------------
resetStatusMessage function in Bean
public String resetStatusMessage(){
System.out.println("@@@@@@Resetting status message");
setStatusMessage("");
displayValidationPanel=false;
if("".equalsIgnoreCase(newContentPartner.getPartnerName())){
displayValidationPanel=true;
FacesContext.getCurrentInstance().addMessage ("include:baseForm:it_partnerName", new FacesMessage("Partner Name cannot be empty"));
}
return "";
}
-------------------------------
setFocus('${focus}');
in my case ${focus} is giving null.
setFocus('${focus}');
in my case ${focus} is giving null.
but focus value is geeting se in map.
Thanks a lot man that helped me a lot really
Thanks a million
I am in a portal context, using jsf 1.2 as portlet through a portlet bridge. The ${highlight} statement returns only the page name, not the component with messages. Is there a workaround for this?
If in your case you are using PrimeFaces to update components (ajax), it worked for me:
RequestContext context = RequestContext.getCurrentInstance();
if (context != null) {
context.execute("setFocus('" + focus + "');");
context.execute("setHighlight('" + highlight.toString() + "')");
}
I hope it helps
thanks vrcca for your comment, but I realized that the probem was I did not add properly the messages to facesContext (I have a custom error handling). Fixed that, the solution of this post works also in a portal context.
Do somebody know how to set the Servlet in Websphere web.xml to 2.4?
Note that when using an a4j:commandButton, instead of an h:commandButton, the request parameter will not be populated.
Post a Comment