タツノオトシゴのブログ

主にJavaに関するものです。

Bootstrap with Spring MVC

今さらながら、GW中にBootstrapにやっと触れた。世の中の潮流に少し乗れたかな?

自分の知識では、「jQuery Mobile」で知識が止まっており、さあ、作業に取り掛かろうとしたけど、jQuery Mobileは使いつらすぎた。
せっかく本も買ったけど、意味なかった。

結局、CSS Frameworkとして「Bootstrap」「HTML Kick start」を候補に挙げて、今回は人気のあるBootstrapを採用してみました。

使用したBoostrapのバージョンは「3.1.1」です。

Bootstrapを使ってみた感想

機能が多く、jQuery UIの機能もほぼあるため、Bootstrap単体でことが足りて便利だと思います。

ただし、バージョンごとに記述方法が異なっており、ネットに転がっている情報をそのまま鵜呑みにすると、デザインくずれが発生します。

その点、「HTML Kick start」は非常にシンプルで、習得もしやすい。
ただし、動的な動作をさせたい場合は、他のライブラリを使ったりする必要があります。

「Bootstrap」「HTML Kick start」も、公式ページのドキュメントだけで十分で、習得も半日もあればできました。

使うだけならば、30分ちょっとでそれなりのものが作れます。

Spring MVCでBootsrapを利用する場合の問題点

Bootstrapでフォームのコントローラでエラー状態のデザインを表現するためには、inputタグやメッセージをdivで囲む必要があります。

そのため、囲むdivタグのclass属性において、入力項目ごとにエラーがあるか判定する必要があり、非常にJSPなどが汚くなってしまいます。

さらに、入力項目が大量にあると、その個数分、判定処理が必要になります。

<%================= 改善前のEL式、JSTLを使用した場合 ================%>

<%-- フォームに対してエラーがあるか判定し、結果を変数に格納する --%>
<spring:hasBindErrors name="searchCondtiionCommand">
<c:if test="${errors.getFieldErrorCount('keyword') > 0}">
    <c:set var="hasErrorKeyword" value="true"/>
</c:if>
</spring:hasBindErrors>

<form:form modelAttribute="searchCondtiionCommand" action="..." method="POST">
    
    <!-- divのclass属性にエラーを判定し、専用の値を設定する -->
    <div class="form-fontrol${hasErrorKeyword ?' has-error has-feedback':''}">
        <form:label path="keyword" class="control-label" >キーワード</form:label>
        <form:input id="keyword" path="keyword" class="form-control" placeholder="search" maxlength="100"/>
        
        <!-- アイコンを設定する際に、エラーがあるか判定する必要がある -->
        <c:if test="${hasErrorKeyword}">
            <span class="glyphicon glyphicon-remove form-control-feedback"></span>
        </c:if>
        <form:errors path="keyword" cssClass="help-block with-errors"/>
    </div>

    <button type="submit" name="search" class="btn btn-primary">
        <span class="glyphicon glyphicon-search"></span>
        <spring:message code="label.search"/>
    </button>

</form:form>

解決策

解決策として、カスタムタグを自作します。
Spring MVCのカスタムタグと同様に、属性「path」で入力項目を指定し、色々と判定をできるようにします。

  • <form2:element>というカスタムタグで、指定した入力項目にエラーがあるか判定し、エラーがある場合、専用のclass属性を出力できるようにする。
  • <form2:hasErrors>というカスタムタグで、指定した入力項目にエラーがあるか判定し、エラーがある場合、そのタグの中身を評価する。
<%================= 改善後の自作のカスタムタグを使用した場合 ================%>
<%@ taglib uri="http://spring-webmvc/modules/tags/form2" prefix="form2" %>

<form:form modelAttribute="searchCondtiionCommand" action="..." method="POST">
    
    <!-- divのclass属性にエラーを判定し、専用の値を設定する-->
    <form2:element path="keyword" element="div" cssClass="form-group" cssErrorClass="form-group has-error has-feedback">
        <form:label path="keyword" class="control-label" >キーワード</form:label>
        <form:input id="keyword" path="keyword" class="form-control" placeholder="search" maxlength="100"/>
        
        <!-- アイコンを設定する際に、エラーがあるか判定する必要がある -->
        <form2:hasErrors path="keyword">
            <span class="glyphicon glyphicon-remove form-control-feedback"></span>
        </form2:hasErrors>
        
        <%-- アイコンは↓の方法でも出力可能。エラー時のみ出力する。
        <form2:element path="keyword" element="span" cssClass="glyphicon glyphicon-remove form-control-feedback" outIfError="true" />
        --%>
        
        <form:errors path="keyword" cssClass="help-block with-errors"/>
    </form2:element><!-- /form-group -->

    <button type="submit" name="search" class="btn btn-primary">
        <span class="glyphicon glyphicon-search"></span>
        <spring:message code="label.search"/>
    </button>

</form:form>
カスタムタグ<form2:element>

SpringMVCのカスタムタグ用の抽象クラス「org.springframework.web.servlet.tags.form.AbstractHtmlElementTag」を使用します。

このクラスは、HTMLを出力するための抽象クラスです。

また、「AbstractDataBoundFormElementTag」を継承しており、入力項目ごとの属性「path」で関連付けられた値を処理するためのメソッドがそろっています。

SpringMVCのカスタムタグの共通属性「path」「cssClass」「cssErrorClass」などの処理は、抽象クラスないで予め実装されているため、定義は必要ありません。

import javax.servlet.jsp.JspException;

import org.springframework.util.StringUtils;
import org.springframework.web.servlet.tags.form.AbstractHtmlElementTag;
import org.springframework.web.servlet.tags.form.TagWriter;

/**
 * カスタムタグ<form2:element>の実体クラス。
 */
@SuppressWarnings("serial")
public class ElementTag extends AbstractHtmlElementTag {
    
    /**
     * カスタムタグの属性「element」の値。
     * ・出力するタグ名を保持する
     * ・ただし、属性「elementError」が指定されていればその値を利用する。
     */
    private String element;
    
    /**
     * カスタムタグの属性「elementError」の値。
     * ・エラー時に出力するタグ名を保持する
     */
    private String elementError;
    
    /**
     * カスタムタグの属性「outIfError」の値。
     * ・エラー時にしか出力しないかどうかのフラグ。
     */
    private boolean outIfError;
    
    private TagWriter tagWriter;
    
    @Override
    protected int writeTagContent(final TagWriter tagWriter) throws JspException {
        
        this.tagWriter = tagWriter;
        if(outIfError && !getBindStatus().isError()) {
            // エラー時のみにしか出力しない場合、処理を終了。
            return EVAL_BODY_INCLUDE;
        }
        
        if(getBindStatus().isError()) {
            if(StringUtils.hasLength(getElementError())) {
                tagWriter.startTag(getElementError());
            } else {
                tagWriter.startTag(getElement());
            }
        } else {
            tagWriter.startTag(getElement());
        }
        
        // 属性の設定
        writeDefaultAttributes(tagWriter);
        
        // 開始タグの出力
        tagWriter.forceBlock();
        
        return EVAL_BODY_INCLUDE;
    }
    
    @Override
    public int doEndTag() throws JspException {
        
        if(outIfError && !getBindStatus().isError()) {
            // エラー時のみにしか出力しない場合、処理を終了。
            return EVAL_PAGE;
        }
        
        
        // 終了タグの出力
        this.tagWriter.endTag();
        return EVAL_PAGE;
    }
    
     @Override
    protected String getName() throws JspException {
        // This also suppresses the 'id' attribute (which is okay for a <label/>)
        return null;
    }
    
    @Override
    protected String resolveId() throws JspException {
        Object id = evaluate("id", getId());
        if (id != null) {
            return super.resolveId();
        }
        return null;
    }

    
    public String getElement() {
        return element;
    }
    
    public void setElement(String element) {
        this.element = element;
    }
    
    public String getElementError() {
        return elementError;
    }
    
    public void setElementError(String elementError) {
        this.elementError = elementError;
    }
    
     public boolean isOutIfError() {
        return outIfError;
    }
    
    public void setOutIfError(boolean outIfError) {
        this.outIfError = outIfError;
    }
}
<!-- tldファイル -->
<?xml version="1.0" encoding="UTF-8"?>
<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
        version="2.0">

    <description>Spring Framework JSP Form Tag Library for Another</description>
    <tlib-version>1.0</tlib-version>
    <short-name>form2</short-name>
    <uri>http://spring-webmvc/modules/tags/form2</uri>
    <tag>
        <description>Renders field errors in an HTML custom tag.</description>
        <name>element</name>
        <tag-class>sample.web.tags.ElementTag</tag-class>
        <body-content>JSP</body-content>
        <attribute>
            <description>HTML tag</description>
            <name>element</name>
            <required>true</required>
            <rtexprvalue>true</rtexprvalue>
        </attribute>
        <attribute>
            <description>HTML error tag</description>
            <name>elementError</name>
            <required>false</required>
            <rtexprvalue>true</rtexprvalue>
        </attribute>
        <attribute>
            <description>output tag, when has error. default false.</description>
            <name>outIfError</name>
            <required>false</required>
            <rtexprvalue>true</rtexprvalue>
        </attribute>
        <attribute>
            <description>Path to errors object for data binding</description>
            <name>path</name>
            <required>true</required>
            <rtexprvalue>true</rtexprvalue>
        </attribute>
        <attribute>
            <description>HTML Standard Attribute</description>
            <name>id</name>
            <required>false</required>
            <rtexprvalue>true</rtexprvalue>
        </attribute>
        ・・・以降は、HTMLの属性の定義なので省略。
            他のSpring用のカスタムタグの定義を参照。
    <tag>

</taglib>
カスタムタグ<form2:hasError>

SpringMVCのカスタムタグ用の抽象クラス「org.springframework.web.servlet.tags.form.AbstractDataBoundFormElementTag」を使用します。
入力項目ごとの属性「path」で関連付けられた値を処理するためのメソッドがそろっています。

エラーがあるかどうかの判定だけのため、非常にシンプルです。

import javax.servlet.jsp.JspException;

import org.springframework.web.servlet.tags.form.AbstractDataBoundFormElementTag;
import org.springframework.web.servlet.tags.form.TagWriter;


/**
 * カスタムタグ<form2:hasErrors>の実体クラス。
 */
@SuppressWarnings("serial")
public class HasErrorsTag extends AbstractDataBoundFormElementTag {
    
    @Override
    protected int writeTagContent(final TagWriter tagWriter) throws JspException {
        
        if(getBindStatus().isError()) {
            return EVAL_BODY_INCLUDE;
        } else {
            return SKIP_BODY;
        }
        
    }
    
    @Override
    public int doEndTag() {
        return EVAL_PAGE;
    }
}
<!-- tldファイル -->
<?xml version="1.0" encoding="UTF-8"?>
<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
        version="2.0">
    <description>Spring Framework JSP Form Tag Library for Another</description>
    <tlib-version>1.0</tlib-version>
    <short-name>form2</short-name>
    <uri>http://spring-webmvc/modules/tags/form2</uri>
    <tag>
        <description>Provides Errors instance in case of field errors.</description>
        <name>hasErrors</name>
        <tag-class>sample.web.tags.HasErrorsTag</tag-class>
        <body-content>JSP</body-content>
        <attribute>
            <description>Path to errors object for data binding</description>
            <name>path</name>
            <required>true</required>
            <rtexprvalue>true</rtexprvalue>
        </attribute>
    </tag>

</taglib>