Friday, December 14, 2007

Set focus in JSF

The power of a PhaseListener

The following example shows how to use a PhaseListener to set focus to the first next input element which has a faces message (which can be caused by a validation or conversion error or any other custom reason). It is relatively simple, it costs effectively only a few lines inside the beforePhase of the RENDER_RESPONSE and a small Javascript function.

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. 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="someForm">
    <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:message for="someForm" style="color: green;" />
    </h:panelGrid>
</h:form>

<script>
    setFocus('${focus}');
</script>

Here is how the Javascript function setFocus() look like:

function setFocus(id) {
    var element = document.getElementById(id);
    if (element && element.focus) {
        element.focus();
    }
}

And now the PhaseListener which sets the focus to the first next input element which has a faces message:

/*
 * 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 General Public License as published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 * 
 * This program 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
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with this program; if
 * not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 * 02110-1301, USA.
 */

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.
 * <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();

        // Get an iterator over all client ID's with messages.
        Iterator<String> clientIdsWithMessages = facesContext.getClientIdsWithMessages();

        // If there is a client ID with message ..
        if (clientIdsWithMessages.hasNext()) {
            // .. obtain the first client ID with message.
            String firstClientIdWithMessage = clientIdsWithMessages.next();

            // Set the focus to this client ID. The value will be available in ${focus} in JSP.
            facesContext.getExternalContext().getRequestMap().put("focus", firstClientIdWithMessage);
        }
    }

    /**
     * @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 General Public License

(C) December 2007, BalusC

14 comments:

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

vikram said...

Hi,
i am trying to use the focus like you have done using a phase listener.Here, i amd just setting focus for the first input field. But i am not able to set focus on the input field when i call the function at 'window.onload=set...'. But it can be called when i use onmouseover and is working fine.I want to set foucs as soon as the page loads. The page is actually a subview undera fragment in another .xhtml file which opens as a panel on click of a button.It goes like this:
'a4j:outputPanel...'
'ui:fragment rendered...'
' f:subview id...'
' a4j:include id="a" viewId="b" binding="c"'(single quotes added instead of "<".)
It will be great if you can give a solution.
Thanks Balus!

LookingForSpring 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

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

LookingForSpring 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