システム開発で思うところ

Javaで主にシステム開発をしながら思うところをツラツラを綴る

JSFでエラーフィールドの背景色を変える

ConversationScopedで 戯れた日々を終え、メッセージ関係に戻って 新たな取り組みを。

入力フィールドの背景色を変えるという定番です。

定番ですが、ここまで引っ張ってしまったのは この定番をやろうと思ったときに

  • ViewIDをイイ感じに BeanValidationと連携しないと出来ない
  • 背景色を変える前に、そもそもメッセージを出力するところから整理できていない

ということが分かって、一からメッセージについて整理し直したためです。

まだ階層や繰り返し領域への仕組みは まだ出来ていませんが、とりあえず 最低限 求めている機能の実装が なんとか出来たので、 ようやく 本丸だったフィールドの背景色を変えるということについて考えられる土台ができたというところです。

やりたいこと

  • フィールドの色を変える

やりかた

主に JSFでエラー項目の背景色を変える - 中年プログラマーの息抜き を参考。

h:messageにメッセージ出力している 画面項目と関連する入力領域の背景色を変更する(CSSのクラスを付与する)という方式です。

実装

FacesContextに検証不正があったことを保存

これは、元々 やるべきだった実装が漏れていたということになると思います。
BeanValidationによりエラーを検知した場合には、当該Contextについては 検証不正情報があったことを保存すべきでした。

@Action
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
@Dependent
public class BeanValidationExceptionInterceptor {

    private final CurrentViewContext context;

    private final MessageConverter messageConverter;
    private final MessageWriter messageWriter;

    @Inject
    public BeanValidationExceptionInterceptor(CurrentViewContext context, MessageConverter messageConverter, MessageWriter messageWriter) {
        this.context = context;
        this.messageConverter = messageConverter;
        this.messageWriter = messageWriter;
    }

    @AroundInvoke
    public Object invoke(InvocationContext ic) throws Exception {

        String currentViewId = context.currentViewId();

        try {
            return ic.proceed();
        } catch (BeanValidationException ex) {
            ClientidMessages clientidMessages
                             = messageConverter.toClientidMessages(ex.getValidatedResults(),
                                                                   ic.getTarget().getClass().getSuperclass());

            messageWriter.appendErrorMessages(clientidMessages);
            
            //検証不正があるということを Contextにも保持させる
            FacesContext.getCurrentInstance().validationFailed();
            
            
            return currentViewId;
        }

    }
}

PhaseListner

クライアントにレスポンスするところ(メッセージ出力など 本対応以外のContextの更新が終わっている状態)で 背景色に関係する Context情報を更新します。

public class InputFieldColorHandlerPhaseListner implements PhaseListener {

    private static final long serialVersionUID = 1L;

    @Override
    public void afterPhase(PhaseEvent phaseEvent) {
    }

    @Override
    public void beforePhase(PhaseEvent phaseEvent) {
        FacesContext context = phaseEvent.getFacesContext();

        InputFieldColorHandler fieldColorHandler = CDI.current().select(InputFieldColorHandler.class).get();
        fieldColorHandler.updateErrorFieldColor(context);
    }

    @Override
    public PhaseId getPhaseId() {
        return PhaseId.RENDER_RESPONSE;
    }

}


実際のUIComponentの更新をしているクラス
h:messageにメッセージ出力されている画面項目に関係する input要素の cssのClassタグに 背景色を管理する error-fieldを追記します。

@ApplicationScoped
public class InputFieldColorHandler {

    private String errorClass;

    @PostConstruct
    public void init() {
        this.errorClass = "error-field";
    }

    public void updateErrorFieldColor(FacesContext context) {
        this.clearErrorColor(context);

        if (context.isValidationFailed() == false) {
            return;
        }

        context.getClientIdsWithMessages().forEachRemaining(clientId -> {
            UIComponent component = context.getViewRoot().findComponent(clientId);
            String styleClass = String.valueOf(component.getAttributes().get("styleClass"));
            if (styleClass != null) {
                component.getAttributes().put("styleClass", styleClass.trim() + " " + errorClass);
            }
        });

    }

    void clearErrorColor(FacesContext context) {

        recursiveScan(context.getViewRoot().getChildren())
                .forEach(c -> {
                    String styleClass = String.valueOf(c.getAttributes().get("styleClass"));
                    if (styleClass != null && styleClass.contains(errorClass)) {
                        c.getAttributes().put("styleClass", styleClass.replace(errorClass, "").trim());
                    }
                });

    }

    private Set<UIComponent> recursiveScan(List<UIComponent> components) {
        Set<UIComponent> set = new HashSet<>();
        if (components == null) {
            return set;
        }

        components.forEach(component -> {
            set.add(component);
            set.addAll(recursiveScan(component.getChildren()));
        });
        return set;
    }

}


validation.css

とりあえず、強制的に背景色を変更

.error-field {
    background-color: bisque !important;
}


PhaseListenrの呼出している箇所

テンプレートxhtmlから、当該PhaseListnerを呼び出します。
faces-config.xml へのlifecycle 要素の追加ではありません

baseLayer.xhtml

<f:phaseListener type="ee.phaselistener.InputFieldColorHandlerPhaseListner" />

出力ページ

classjsf:styleClassに置き換え

一項目だけ抜粋

<div class="field">
    <label>利用者ID</label>
    <input jsf:styleClass="short-input" type="text" placeholder="someone@example.com" jsf:id="email" jsf:value="#{userRegistrationPage.email}"/>
    <h:message for="email" styleClass="error-message ui left pointing red basic label" />
</div>

ハマったところ

UIComponentが更新できない

FacesContext.getCurrentInstance()から取得したUIComponentは、参照は出来るのですが更新は出来ませんでした。

上述の通り、PhaseEvent の #getFacesContext()を更新対象として解決しました。

faces-config.xmlの指定では動かない

やろうとしていることとの相性なのか分かりませんが、faces-config.xmlに PhaseListnerを記載しても 想定通りの動きをしませんでした。
同じようなことで困っている人がいてくれて助かりました。
jsf 2 - How to apply a JSF2 phaselistener after viewroot gets built? - Stack Overflow

上述の通り、f:phaseListener で解決しました。

Injectが出来ない

JSF の相関項目チェック by Bean Validation | 寺田 佳央 - Yoshio Terada

を見ると出来そうなんですが、DIできませんでした。
あくまで想像ですが、faces-config.xmlの指定では動かないとも関連しているような気がします。

上述の通り、CDI.current().select()で解決しました。

jsf:styleClassと classの併用不可

classの方が強い、かつ 編集可能な要素は jsf:styleClassなので、内容変更しても反映されない。
JSF2.2から html friendly といいつつ、肝心のデザインを司る cssクラスの指定で 併用が出来ないのは、正直 かなりイケて無いと思う。
せめて 併用している場合は、UIComponentのベースは jsf:styleClassにして、classは無視してくれるだけでも良かったのに・・・*1

参考資料

JSFでエラーのある項目の背景色を変える - じゃばらの手記

JSFのバリデーション - DENの思うこと

JSF バリデーション

JSFでエラー項目の背景色を変える - 中年プログラマーの息抜き

JSF の相関項目チェック by Bean Validation | 寺田 佳央 - Yoshio Terada

Code

vermeer_etc / jsf-ddd / source / — Bitbucket

さいごに

一般的な、エラー時の背景色制御は これで良いと思います。
ただ、この方式での課題は、h:messageの出力が必須となる方式ということです。
ぼんやりと オレオレFW的に進めている方式を活かせば、その課題も対応できるように思うので、次回は そのあたりをアレコレ試していければと思います。


*1:あくまで、自分の確認の範囲です。回避策があるかもしれませんし、JSF2.3になったら解消しているかもしれません。そこまで調べるのは 一旦 止めました