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" />
出力ページ
class
を jsf: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でエラー項目の背景色を変える - 中年プログラマーの息抜き
JSF の相関項目チェック by Bean Validation | 寺田 佳央 - Yoshio Terada
Code
vermeer_etc / jsf-ddd / source / — Bitbucket
さいごに
一般的な、エラー時の背景色制御は これで良いと思います。
ただ、この方式での課題は、h:message
の出力が必須となる方式ということです。
ぼんやりと オレオレFW的に進めている方式を活かせば、その課題も対応できるように思うので、次回は そのあたりをアレコレ試していければと思います。
*1:あくまで、自分の確認の範囲です。回避策があるかもしれませんし、JSF2.3になったら解消しているかもしれません。そこまで調べるのは 一旦 止めました