Friday, December 14, 2007

Set focus and highlight in JSF

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>
 * &lt;lifecycle&gt;
 *     &lt;phase-listener&gt;net.balusc.webapp.SetFocusListener&lt;/phase-listener&gt;
 * &lt;/lifecycle&gt;
 * </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:

Rafael Ponte said...

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>

BalusC said...

The intention was to set focus on the first input element where a validation or conversion error has occurred or any facesmessage is set.

Rafael Ponte said...

Ah ok! i'm sorry, you are right! Great post :)

ps.: the Blogger.com's comments system is terrible :(

fiestaspuntonet said...

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.

BalusC said...

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.

fiestaspuntonet said...

I'm using Tomcat 6.0.14. On its home page says that it uses JSP 2.1.

BalusC said...

To which servlet version is your web.xml set? It should be at least 2.4 and preferably 2.5.

fiestaspuntonet said...

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.

fiestaspuntonet said...

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}');">

gr8fanboy said...

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

gr8fanboy said...

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..

gr8fanboy said...

Found the answer here:
http://blogs.sun.com/rlubke/?page=1
(It's no longer required)

BalusC said...

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

Unknown said...

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..

FkJ said...

What about when using AJAX? I'm using RichFaces + A4J.

John Dondapati said...

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!

Rafaell Pinheiro said...

What about when using AJAX? I'm using RichFaces + A4J. [2]

Rafaell Pinheiro

Jasper said...

2 years on and still useful. Why work when you can Google? ;)

HimSss said...

Hi BalusC,

Thank you very much.
It worked perfect in my project.

Himaja

Tiago said...

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");
}
...

Unknown said...

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?

FkJ said...

http://www.primefaces.org:8080/prime-showcase/ui/focus.jsf

Unknown said...

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...

BalusC said...

@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.

The Budding Cosmopolitan said...

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

The Budding Cosmopolitan said...

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 "";
}
-------------------------------

bhartendu said...

setFocus('${focus}');
in my case ${focus} is giving null.

bhartendu said...

setFocus('${focus}');
in my case ${focus} is giving null.
but focus value is geeting se in map.

Yasser Yahia said...

Thanks a lot man that helped me a lot really

Thanks a million

Luca said...

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?

vrcca said...

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

Luca said...

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.

Tim said...

Do somebody know how to set the Servlet in Websphere web.xml to 2.4?

Tim said...
This comment has been removed by the author.
Tim said...
This comment has been removed by the author.
Anonymous said...

Note that when using an a4j:commandButton, instead of an h:commandButton, the request parameter will not be populated.