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

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

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

vermeer.hatenablog.jp

の続きです。

私の仕組みでは、Application層の検証不正に対しても画面項目と関連付け出来るようにしています。
そこで その仕組みを利用して 先のパターンに独自の実装を追加します。

やりたいこと

メッセージ出力に関係なく、任意の入力項目の背景色を変更する

やりかた

  • BeanValidationExceptionの対象となったUIInputのClientIdを取得して出力先情報として保持
  • PhaseListnerで保持した情報を使う

前回のやり方は getClientIdsWithMessagesを使って、h:messageが出力対象になっている事が前提になっていますが、このやり方であれば h:messageの有無に関係なく背景色を変えられます。

実装

UIInputのClientIdを取得

前回の h:message に関する情報を取得するだけでしたが、今回は コンポーネント内すべてのUIInputのClientIdも取得します。

この情報とBeanValitionの結果をPhaseListnerで参照するようにします。

なお、メッセージ出力に関係する箇所は、前回同様 h:message に関する情報を元に編集をします。

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

    private final CurrentViewContext context;
    private final MessageConverter messageConverter;
    private final MessageWriter messageWriter;
    private final ClientComplementManager clientComplementManager;

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

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

        String currentViewId = context.currentViewId();

        try {
            return ic.proceed();
        } catch (BeanValidationException ex) {

            ClientIdsWithComponents clientIdsWithInputComponents = new InputComponentScanner().scan();

            ClientIdMessages clientidMessages
                             = messageConverter.toClientIdMessages(ex.getValidatedResults(),
                                                                   ic.getTarget().getClass().getSuperclass(),
                                                                   clientIdsWithInputComponents);

            ClientIdsWithComponents clientIdsWithHtmlMessages = new HtmlMessageScanner().scan();
            messageWriter.appendErrorMessageToComponent(clientidMessages.toClientIdMessagesForWriting(clientIdsWithHtmlMessages));

            FacesContext.getCurrentInstance().validationFailed();
            clientComplementManager.setClientidMessages(clientidMessages);

            return currentViewId;
        }

    }
}

PhaseListner

UIInputのClientIdと検証不正対象となった入力項目の関連付けを保持したインスタンスを参照先として追加します。

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();
        ClientComplementManager clientComplementManager = CDI.current().select(ClientComplementManager.class).get();
        fieldColorHandler.updateErrorFieldColor(context, clientComplementManager);
    }

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

}


実際のUIComponentの更新をしているクラス

既存の仕組み(updateColorHtmlMessage)も残しつつ、新たな制御(updateColorInputComponent)を追加します。

@ApplicationScoped
public class InputFieldColorHandler {

    private String errorClass;

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

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

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

        this.updateColorHtmlMessage(context);
        this.updateColorInputComponent(context, clientComplementManager);
    }

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

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

    private void updateColorInputComponent(FacesContext context, ClientComplementManager clientComplementManager) {
        clientComplementManager.clientIds().stream()
                .forEach(clientId -> {
                    UIComponent component = context.getViewRoot().findComponent(clientId);
                    String styleClass = String.valueOf(component.getAttributes().get("styleClass"));
                    if (styleClass != null && styleClass.contains(errorClass) == false) {
                        component.getAttributes().put("styleClass", styleClass.trim() + " " + errorClass);
                    }
                });
    }

}

Code

vermeer_etc / jsf-ddd / source / — Bitbucket

さいごに

背景色の変更は、概ね これで良いかなと思います。
ただ、参考にした

https://github.com/system-sekkei/isolating-the-domain

のように 背景色というよりも、指定領域のスタイルを変更する という要件には応えられていません。

つぎは、そのあたりかな?