Automatisches Styling von nicht validen Eingabe-Komponenten

Cagatay Civici (Lead Developer von PrimeFaces) hat heute in seinem Blog einen Artikel veröffentlicht, der beschreibt, wie man Input Elementen auf einfachem Weg ein anderes Styling verpasst, wenn die Validierung fehlgeschlagen ist: Styling Invalid Input Fields with JSF.

Die vorgestellte Möglichkeit setzt dabei auf einen PostValidationListener, der am Ende der Validierungsphase aufgerufen wird und alle zu diesem Zeitpunkt invaliden Elementen eine zusätzliche CSS-Klasse gibt, die dann z.B. dem Text einen roten Hintergrund gibt.

Der Nachteil dieser Lösung ist, dass alle Änderungen am Zustand von Komponenten, die nach der Validierungsphase gemacht werden, nicht über diesen Listener laufen und somit auch kein Fehler-Styling bekommen.

So schön die Validierungsphase im JSF ist, ist es aber zumindest in meinen Projekten oft so, dass noch zusätzliche Validierungen in der Application-Phase notwendig sind, die natürlich auch in der GUI irgendwie dargestellt werden müssen. Das spätere Validieren ist z.B. oft notwendig, wenn es Abhängigkeiten zwischen verschiedenen Feldern gibt (z.B. ein Datum ist nur dann gültig, wenn es nach einem anderen Datum liegt) oder wenn ein eingegebener Wert komplexe Logik erfordert, die über EJB/JPA/Hibernate laufen muss und somit eine Transaktion und einen Datenbankzugriff erfordert.

Um dies nicht für jeden Einzelfall neu implementieren zu müssen oder pro Komponente einzeln im Template definieren zu müssen, setze ich einen PhaseListener ein, der in der Render-Phase aktiv wird (also nach der Validierungs- und nach der Aplication-Phase) und allen Komponenten, an denen direkt eine FacesMessage hängt, eine zusätzliche CSS-Klasse gibt. Der Code sieht ungefähr so aus:

public class CssStylePhaseListener implements PhaseListener {
	private static final String INVALID_INPUT_STYLE_CLASS = "error";
    
    public PhaseId getPhaseId() {  
		return PhaseId.RENDER_RESPONSE;  
	}  

	public void beforePhase(PhaseEvent arg0) {
		FacesContext context = FacesContext.getCurrentInstance();  
		UIViewRoot root =  context.getViewRoot();  
		Iterator<String> i = context.getClientIdsWithMessages();

		while (i.hasNext()) {  
			String id = i.next();  
			if (id==null||id.isEmpty()) {
				continue;
			}
			UIComponent component = root.findComponent(id);  		
			if (component instanceof UIInput) {  
				String style = (String) component.getAttributes().get("styleClass");  
				style = style == null ? "" : " " + style;  
				component.getAttributes().put("styleClass", INVALID_INPUT_STYLE_CLASS + style);  
			}  
		}  
		
	}
}

Ein Eintrag in der faces-config.xml ist auch hier noch notwendig:

<lifecycle>
	<phase-listener>com.entwicklertagebuch.CssStylePhaseListener</phase-listener>
</lifecycle>

Der PhaseListener geht dabei von der Annahme aus, dass direkt an einer Komponente ausschließlich Fehlermeldungen vorliegen und allgemeine Meldungen der Anwendung nicht direkt an einer Komponente hängen. Die automatisch erzeugten JSF-Fehlermeldungen aus der Validierungsphase erfüllen diese Voraussetzung immer. Möchte man eine Fehlermeldung aus der Anwendung heraus direkt an eine Komponente hängen, muss man dessen Client-ID kennen:

public void invalidateComponent(String id, String message) {
	UIComponent component = FacesContext.getCurrentInstance().getViewRoot().findComponent(id);
	UIInput input = (UIInput) component;
	input.setValid(false);
	FacesContext.getCurrentInstance().addMessage(input.getClientId(), new FacesMessage(message));
}

Als Entwickler muss man jetzt nur noch wissen und beachten, dass man Validierungs-Fehler bzw. “richtige” Fehlermeldungen an eine Komponente hängen muss, um dem Benutzer automatisch einen optische Rückmeldung zu geben, dass hier eine Fehleingabe vorliegt.

Ich finde diesen Ansatz kompletter, als den Ansatz von Cagatay, wobei auch mein Ansatz durchaus noch Abhängigkeiten hat, die man als Entwickler kennen muss, so z.B. die ID-Ermittlung der Komponente, die man entweder hard-codieren muss oder die – wenn man sie wie oben beschrieben ermitteln möchte – die verschiedenen NamingContainer auf der Seite berücksichtigen muss, siehe den JavaDoc-Kommentar von “findComponent”.

Leave a Reply

Your email address will not be published. Required fields are marked *

*