Wicket の Autocomplete といえば、jQuery UI を利用したものが慣れ親しんでものであった。
最近よく使われる Bootstrap デザインを適用しても使えないわけではない、
CSSスタイルシートを合わせて書いていけば良いのだが、
Bootstrap と jQuery UI を併用が嫌で jQuery UI をやめたくなる。
datepicker も Bootstrap向けが存在するわけだし、
Draggable / Drop これさえ Bootstrap でできれば、
jQuery UI を完全にやめれるのではないか?
前置きはこのくらいで、本題!
typeahead.js/jquery_typeahead.md at master · twitter/typeahead.js · GitHub
twitter/typeahead.js が Bootstrap デザインと相性が良いようだ。
まず、twitter/Typeahead.js のリモートからマッチ候補を受信する書き方を把握しておく。。
JSソースとしては、
<input type="text" id="typeahead"> に対して
$('input#typeahead').typeahead({ highlight: true, minLength: 1 }, { //name : 'states', displayKey: 'display', limit: 7, source : new Bloodhound({ datumTokenizer: function(datum){ return Bloodhound.tokenizers.whitespace(datum.display); }, queryTokenizer: Bloodhound.tokenizers.whitespace, remote:{ wildcard: '%QUERY', url: 'http://xxxxxxxxxxxxx?query=%QUERY', transform: function(response){ return $.map(response, function(item){ return { display:item.value, id:item.id }; }); }, } }) }).on('typeahead:select', function(ev, item){ console.log('選択された id = ' + item.id); console.log('選択された 表示文字列 = ' + item.display); }).on('change', function(ev){ console.log($(this).val()); });
このようなJSソースをリソースとして用意するのではなく、
Wicket TextField の振るまい Behavior として
AJAX通信→ マッチ候補リスト表示させるようにするのです。
CSSは、以下に適当なものがあったので、
https://github.com/bassjobsen/typeahead.js-bootstrap-css
これを参考に以下のように用意します。
span.twitter-typeahead .tt-menu, span.twitter-typeahead .tt-dropdown-menu { cursor: pointer; position: absolute; top: 100%; left: 0; z-index: 1000; display: none; float: left; min-width: 100%; padding: 5px 0; margin: 2px 0 0; list-style: none; font-size: 14px; text-align: left; background-color: #ffffff; border: 1px solid #cccccc; border: 1px solid rgba(0, 0, 0, 0.15); border-radius: 4px; -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); background-clip: padding-box; } span.twitter-typeahead .tt-suggestion { display: block; padding: 3px 20px; clear: both; font-weight: normal; line-height: 1.42857143; color: #333333; white-space: nowrap; } span.twitter-typeahead .tt-suggestion.tt-cursor, span.twitter-typeahead .tt-suggestion:hover, span.twitter-typeahead .tt-suggestion:focus { color: #ffffff; text-decoration: none; outline: 0; background-color: #337ab7; } .input-group.input-group-lg span.twitter-typeahead .form-control { height: 46px; padding: 10px 16px; font-size: 18px; line-height: 1.3333333; border-radius: 6px; } .input-group.input-group-sm span.twitter-typeahead .form-control { height: 30px; padding: 5px 10px; font-size: 12px; line-height: 1.5; border-radius: 3px; } span.twitter-typeahead { width: 100%; } .input-group span.twitter-typeahead { display: block !important; height: 34px; } .input-group span.twitter-typeahead .tt-menu, .input-group span.twitter-typeahead .tt-dropdown-menu { top: 32px !important; } .input-group span.twitter-typeahead:not(:first-child):not(:last-child) .form-control { border-radius: 0; } .input-group span.twitter-typeahead:first-child .form-control { border-top-left-radius: 4px; border-bottom-left-radius: 4px; border-top-right-radius: 0; border-bottom-right-radius: 0; } .input-group span.twitter-typeahead:last-child .form-control { border-top-left-radius: 0; border-bottom-left-radius: 0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; } .input-group.input-group-sm span.twitter-typeahead { height: 30px; } .input-group.input-group-sm span.twitter-typeahead .tt-menu, .input-group.input-group-sm span.twitter-typeahead .tt-dropdown-menu { top: 30px !important; } .input-group.input-group-lg span.twitter-typeahead { height: 46px; } .input-group.input-group-lg span.twitter-typeahead .tt-menu, .input-group.input-group-lg span.twitter-typeahead .tt-dropdown-menu { top: 46px !important; }
Wicket の TextField を継承
キー入力イベント後のAJAX通信で返す JSON は、Goole gson で生成しています。
import java.util.List; import java.util.Optional; import org.apache.wicket.ajax.AbstractDefaultAjaxBehavior; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.ajax.attributes.AjaxRequestAttributes; import org.apache.wicket.markup.html.form.TextField; import org.apache.wicket.model.IModel; import org.apache.wicket.request.IRequestParameters; import org.apache.wicket.request.handler.TextRequestHandler; import com.google.gson.Gson; import com.google.gson.GsonBuilder; /** * Twitter Boostrap Typeahead TextField. */ public abstract class AjaxTypeahead<T extends Typeahead> extends TextField<String>{ private AbstractDefaultAjaxBehavior queryAjaxBehavior; private List<Typeahead> choicelist; public AjaxTypeahead(String id, IModel<String> model){ super(id, model); Gson gson = new GsonBuilder().serializeNulls().create(); queryAjaxBehavior = new AbstractDefaultAjaxBehavior(){ @Override protected void respond(AjaxRequestTarget target){ IRequestParameters p = getRequestCycle().getRequest().getQueryParameters(); Optional.ofNullable(p.getParameterValue("query").toString()).ifPresent(s->{ choicelist = getChoices(s.trim()); getComponent().getRequestCycle() .replaceAllRequestHandlers(new TextRequestHandler("application/json", "UTF-8", gson.toJson(choicelist))); }); Optional.ofNullable(p.getParameterValue("selected").toString()).ifPresent(s->{ String id = p.getParameterValue("id").toString(); for(Typeahead t:choicelist){ if (t.id.equals(id)){ onSelect(target, t); break; } } }); Optional.ofNullable(p.getParameterValue("change").toString()).ifPresent(s->{ String display = Optional.ofNullable(p.getParameterValue("display").toString()) .orElse("").trim(); onChange(target, display); }); getRequestCycle().getResponse().close(); } @Override protected void updateAjaxAttributes(AjaxRequestAttributes attributes) { super.updateAjaxAttributes(attributes); attributes.setDataType("json"); attributes.setWicketAjaxResponse(false); } }; add(queryAjaxBehavior); } @Override protected void onAfterRender(){ super.onAfterRender(); getResponse().write("<script type=\"text/javascript\">"); getResponse().write(" $(\"#" + this.getMarkupId(true) + "\").typeahead({highlight:true,minLength:1},{"); getResponse().write("displayKey: 'display',"); getResponse().write("limit: 7,"); getResponse().write("source : new Bloodhound({"); getResponse().write("datumTokenizer: function(datum){"); getResponse().write("return Bloodhound.tokenizers.whitespace(datum.display);"); getResponse().write("},"); getResponse().write("queryTokenizer: Bloodhound.tokenizers.whitespace,"); getResponse().write("remote:{"); getResponse().write("wildcard: '%QUERY',"); getResponse().write("url: '" + queryAjaxBehavior.getCallbackUrl() + "&query=%QUERY',"); getResponse().write("transform: function(r){"); getResponse().write("return $.map(r, function(t){"); getResponse().write("return { display:t.value, id:t.id };"); getResponse().write("});}}})"); getResponse().write("}).on('typeahead:select', function(ev, item){"); getResponse().write("var url = '" + queryAjaxBehavior.getCallbackUrl() + "' + '&selected=&id=' + item.id + '&display=' + item.display;"); getResponse().write("Wicket.Ajax.get({ u: url });"); getResponse().write("}).on('change', function(ev){"); getResponse().write("var url = '" + queryAjaxBehavior.getCallbackUrl() + "' + '&change=&display=' + $(this).val();"); getResponse().write("Wicket.Ajax.get({ u: url });"); getResponse().write("});"); getResponse().write("</script>"); } /** 入力文字→候補リスト */ protected abstract List<Typeahead> getChoices(String input); /** * 選択イベント捕捉. * @param target AjaxRequestTarget * @param id * @param dislay */ protected void onSelect(AjaxRequestTarget target, Typeahead typeahead){ } /** 変更イベント捕捉 */ protected void onChange(AjaxRequestTarget target, String dislay){ } }
表示用の素材、Typeahead は、id と プルダウン表示及び入力文字列をSerializableである。
この Typeahead を継承するかそのまま使用する。
import java.io.Serializable; /** * twitter/Typeahead.js element. */ public class Typeahead implements Serializable{ public String id; public String value; public Typeahead(){} public void setValue(String value){ this.value = value; } public void setId(String id){ this.id = id; } }
使用する例: Example
AjaxTypeahead<Typeahead> simpleField = new AjaxTypeahead<>("name", new Model<>()){ @Override protected List<Typeahead> getChoices(String input){ // 入力が空 Enter では、空リストを送る。 if (Strings.isEmpty(input)) return Collections.emptyList(); // TODO 入力 input にマッチするリストを返す。 return list; } @Override protected void onSelect(AjaxRequestTarget target, String id, String dislay){ // TODO 選択時の処理、選択した要素の id と 表示文字列 display を受信 } @Override protected void onChange(AjaxRequestTarget target, String dislay){ // TODO change イベントの処理、変更後の表示文字列 display を受信 } }; queue(simpleField);
あくまでも、TextField の継承であり、
フォーム送信で受け取る Wicket の Model オブジェクトは、String である。