Unit 14 - 套用 Boostrap 的 Server-Side validation CSS class 至表單欄位

2 minute read

JSF 教學

Unit 14

技術現況與問題

User Stroy

進入表單畫面在未提交表單前, 具有資料檢查的輸入欄位沒有任何的顏色或訊息顯示驗證結果。使用者提交表單後, 驗證通過的欄位以 Bootstrap 的 is-valid CSS 樣式顯示, 驗證失敗的以 is-invalid CSS 樣式顯示。

技術現況

Bootstrp 提供兩個 Server-Side validation 的樣式: is-validis-invalid。在 UIInput component 的 styleClass 套用這些樣式。

在 JSF 中可透過 EL 取得 UIInput Component 的驗證狀態。UIInput.isValid():Boolean 傳回元件的驗證結果。Fales 表示驗證失敗, True 表示成功或尚未進行驗證。

問題

UIInput.isValid():Boolean 的預設值為 True, 有可能表單尚未提交進行驗證。直接使用此預設值來設定 UIInput 的樣式會造成第一次進行表單時(尚未提交進行驗證前), 輸入欄位會套用 is-valid class, 向使用者表示這些欄位已經驗證通過。前述行為不符合使用者的操作經驗。

解決方式

  1. 在 JSF CDI Bean 中新增一個 flag 欄位, 記錄表單是否曾經被提交, 預設值為 False。
  2. 在 JSF 的 Post Validate Event 中更新 flag 欄位
  3. UIInputstyleClass 屬性, 同時使用 flag 欄位及元件的驗證狀態判斷是否要套用 is-valid 或者 is-invalid CSS 樣式
flag 欄位 UIInput.isValid() 運算結果為 套用樣式
False True False 不套用
True True True is-valid
False False 不會發生此種情況  
True False True is-invalid

前述套用況中, flag 欄位為 False 時, UIInput.isValid() 的值不可能為 False。因為其預設值為 True, 只有提交表單後 UIInput.isValid() 的值才有可能變成 False。

實作

JSF 頁面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<h:body>
    <f:event listener="#{bookEntryBean.processEvent}" type="postValidate" />
    <div class="container">
        <h:form>
           <div class="form-group">
                <h:outputLabel for="isbn">ISBN</h:outputLabel>
                <h:inputText id="isbn" 
                             ps:placeholder="長度 13 碼" 
                             validatorMessage="長度不足 13 碼"
                             value="#{bookEntryBean.book.isbn}"
                             binding="#{isbnComp}"
                             styleClass="form-control 
                             #{(isbnComp.valid and bookEntryBean.isFormSubmitted) ? 'is-valid' : ''}
                             #{!isbnComp.valid? 'is-invalid': ''}">
                    <f:validateLength minimum="13" maximum="13" />
                </h:inputText>
                <h:message for="isbn" errorClass="errorMsg"/>
            </div>
        </h:form>
    </div>
</h:body>

<h:inputText>binding 屬性<h:inputText> 對應到的 UIInput component 暴露到 EL Scope, 以便使用 EL 語法存取此元件。在 EL Scope 中, 元件的名稱為 isbnComp

<h:inputText>styleClass 屬性 使用 EL 語法依照 “是否已提交表單” 及 “元件是否驗證通過” 套用 in-valid 或者 is-invalid CSS 樣式。 bookEntryBean.isFormSubmitted 為 JSF CDI Bean 的特性, 記錄是否已提交表單。 isbnComp.validUIInput 元件的特性, 相當於呼叫 isbnComp.isValid()

JSF CDI Bean

JSF CDI Bean BookEntryBean 實作 SystemEventListen 介面的 processEvent()。若發生 PostValidateEvent 則標記表單已曾提交:

1
2
3
4
5
6
@Override
public void processEvent(SystemEvent se) throws AbortProcessingException {
    if (se instanceof PostValidateEvent){
        this.isFormSubmitted = true;
    }
}

完整程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package net.hychen39.jsfCDIBeans;
  
import javax.inject.Named;
import javax.enterprise.context.SessionScoped;
import java.io.Serializable;
import javax.annotation.PostConstruct;
import javax.faces.event.AbortProcessingException;
import javax.faces.event.PostValidateEvent;
import javax.faces.event.SystemEvent;
import javax.faces.event.SystemEventListener;
import net.hychen39.entities.Book;
  
/**
 *
 * @author hychen39@gm.cyut.edu.tw
 */
@Named(value = "bookEntryBean")
@SessionScoped
public class BookEntryBean implements Serializable, SystemEventListener {
    private Book book;
    private boolean isFormSubmitted;
  
    public Book getBook() {
        return book;
    }
  
    public void setBook(Book book) {
        this.book = book;
    }
  
    public boolean isIsFormSubmitted() {
        return isFormSubmitted;
    }
  
    public void setIsFormSubmitted(boolean isFormSubmitted) {
        this.isFormSubmitted = isFormSubmitted;
    }
  
    public BookEntryBean() {
    }
  
    @PostConstruct
    public void init(){
        book = new Book();
        this.isFormSubmitted = false;
    }
  
  
    @Override
    public void processEvent(SystemEvent se) throws AbortProcessingException {
        if (se instanceof PostValidateEvent){
            this.isFormSubmitted = true;
        }
    }
  
    @Override
    public boolean isListenerForSource(Object o) {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }
}

Updated: