001 // Copyright May 4, 2006 The Apache Software Foundation 002 // 003 // Licensed under the Apache License, Version 2.0 (the "License"); 004 // you may not use this file except in compliance with the License. 005 // You may obtain a copy of the License at 006 // 007 // http://www.apache.org/licenses/LICENSE-2.0 008 // 009 // Unless required by applicable law or agreed to in writing, software 010 // distributed under the License is distributed on an "AS IS" BASIS, 011 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012 // See the License for the specific language governing permissions and 013 // limitations under the License. 014 package org.apache.tapestry.dojo.form; 015 016 import org.apache.tapestry.*; 017 import org.apache.tapestry.engine.DirectServiceParameter; 018 import org.apache.tapestry.engine.IEngineService; 019 import org.apache.tapestry.engine.ILink; 020 import org.apache.tapestry.form.ValidatableField; 021 import org.apache.tapestry.form.ValidatableFieldSupport; 022 import org.apache.tapestry.json.IJSONWriter; 023 import org.apache.tapestry.json.JSONObject; 024 import org.apache.tapestry.services.DataSqueezer; 025 import org.apache.tapestry.valid.ValidatorException; 026 027 import java.util.ArrayList; 028 import java.util.HashMap; 029 import java.util.List; 030 import java.util.Map; 031 032 /** 033 * An html field similar to a <code>select</code> input field that 034 * is wrapped by a dojo ComboBox widget. 035 * 036 * This component uses the {@link IAutocompleteModel} to retrieve and match against 037 * selected values. 038 * 039 * @author jkuhnert 040 */ 041 public abstract class Autocompleter extends AbstractFormWidget implements ValidatableField, IJSONRender, IDirect 042 { 043 // mode, can be remote or local (local being from html rendered option elements) 044 private static final String MODE_REMOTE = "remote"; 045 private static final String MODE_LOCAL = "local"; 046 047 /** 048 * 049 * {@inheritDoc} 050 */ 051 protected void renderFormWidget(IMarkupWriter writer, IRequestCycle cycle) 052 { 053 IAutocompleteModel model = getModel(); 054 if (model == null) 055 throw Tapestry.createRequiredParameterException(this, "model"); 056 057 Object value = getValue(); 058 Object key = value != null ? model.getPrimaryKey(value) : null; 059 060 renderDelegatePrefix(writer, cycle); 061 062 writer.begin("select"); 063 writer.attribute("name", getName()); 064 writer.attribute("autocomplete", "off"); // turn off native html autocomplete 065 066 if (isDisabled()) 067 writer.attribute("disabled", "disabled"); 068 069 renderIdAttribute(writer, cycle); 070 071 renderDelegateAttributes(writer, cycle); 072 073 getValidatableFieldSupport().renderContributions(this, writer, cycle); 074 075 // Apply informal attributes. 076 renderInformalParameters(writer, cycle); 077 078 writer.print(" "); 079 080 if (isLocal()) 081 { 082 List list = model.getValues(""); 083 for (int i=0; i<list.size(); i++) 084 { 085 Object optionKey = model.getPrimaryKey(list.get(i)); 086 writer.begin("option"); 087 writer.attribute("value", getDataSqueezer().squeeze(optionKey)); 088 if (optionKey!=null && optionKey.equals(key)) 089 writer.attribute("selected", "selected"); 090 writer.print(model.getLabelFor(list.get(i))); 091 writer.end(); 092 } 093 } 094 095 writer.end(); 096 renderDelegateSuffix(writer, cycle); 097 098 Map parms = new HashMap(); 099 parms.put("id", getClientId()); 100 101 JSONObject json = new JSONObject(); 102 if (!isLocal()) 103 { 104 ILink link = getDirectService().getLink(true, new DirectServiceParameter(this)); 105 json.put("dataUrl", link.getURL() + "&filter=%{searchString}"); 106 } 107 json.put("mode", isLocal() ? MODE_LOCAL : MODE_REMOTE); 108 json.put("widgetId", getName()); 109 json.put("name", getName()); 110 json.put("searchDelay", getSearchDelay()); 111 json.put("fadeTime", getFadeTime()); 112 json.put("maxListLength", getMaxListLength()); 113 json.put("forceValidOption", isForceValidOption()); 114 json.put("disabled", isDisabled()); 115 116 json.put("value", getDataSqueezer().squeeze(key)); 117 json.put("label", value != null ? model.getLabelFor(value) : ""); 118 119 parms.put("props", json.toString()); 120 parms.put("form", getForm().getName()); 121 parms.put("widget", this); 122 123 PageRenderSupport prs = TapestryUtils.getPageRenderSupport(cycle, this); 124 getScript().execute(this, cycle, prs, parms); 125 } 126 127 /** 128 * {@inheritDoc} 129 */ 130 public void renderComponent(IJSONWriter writer, IRequestCycle cycle) 131 { 132 IAutocompleteModel model = getModel(); 133 134 if (model == null) 135 throw Tapestry.createRequiredParameterException(this, "model"); 136 137 List filteredValues = model.getValues(getFilter()); 138 139 if (filteredValues == null) 140 return; 141 142 Object key = null; 143 String label = null; 144 145 JSONObject json = writer.object(); 146 147 for (int i=0; i < filteredValues.size(); i++) { 148 Object value = filteredValues.get(i); 149 150 key = model.getPrimaryKey(value); 151 label = model.getLabelFor(value); 152 153 json.put(getDataSqueezer().squeeze(key), label ); 154 } 155 156 } 157 158 /** 159 * @see org.apache.tapestry.form.AbstractFormComponent#rewindFormComponent(org.apache.tapestry.IMarkupWriter, org.apache.tapestry.IRequestCycle) 160 */ 161 protected void rewindFormWidget(IMarkupWriter writer, IRequestCycle cycle) 162 { 163 String value = cycle.getParameter(getName()); 164 165 Object object = null; 166 167 try 168 { 169 if (value != null && value.length() > 0) 170 object = getModel().getValue(getDataSqueezer().unsqueeze(value)); 171 172 getValidatableFieldSupport().validate(this, writer, cycle, object); 173 174 setValue(object); 175 } 176 catch (ValidatorException e) 177 { 178 getForm().getDelegate().record(e); 179 } 180 } 181 182 /** 183 * {@inheritDoc} 184 */ 185 public boolean isStateful() 186 { 187 return true; 188 } 189 190 /** 191 * Triggerd by using filterOnChange logic. 192 * 193 * {@inheritDoc} 194 */ 195 public void trigger(IRequestCycle cycle) 196 { 197 setFilter(cycle.getParameter("filter")); 198 } 199 200 public abstract IAutocompleteModel getModel(); 201 202 /** How long to wait(in ms) before searching after input is received. */ 203 public abstract int getSearchDelay(); 204 205 /** The duration(in ms) of the fade effect of list going away. */ 206 public abstract int getFadeTime(); 207 208 /** The maximum number of items displayed in select list before the scrollbar is activated. */ 209 public abstract int getMaxListLength(); 210 211 /** Forces select to only allow valid option strings. */ 212 public abstract boolean isForceValidOption(); 213 214 /** Forces select to work in local mode (no xhr). */ 215 public abstract boolean isLocal(); 216 217 /** @since 2.2 * */ 218 public abstract Object getValue(); 219 220 /** @since 2.2 * */ 221 public abstract void setValue(Object value); 222 223 /** @since 4.1 */ 224 public abstract void setFilter(String value); 225 226 /** @since 4.1 */ 227 public abstract String getFilter(); 228 229 /** Injected. */ 230 public abstract DataSqueezer getDataSqueezer(); 231 232 /** 233 * Injected. 234 */ 235 public abstract ValidatableFieldSupport getValidatableFieldSupport(); 236 237 /** 238 * Injected. 239 */ 240 public abstract IEngineService getDirectService(); 241 242 /** 243 * Injected. 244 */ 245 public abstract IScript getScript(); 246 247 /** 248 * @see org.apache.tapestry.form.AbstractFormComponent#isRequired() 249 */ 250 public boolean isRequired() 251 { 252 return getValidatableFieldSupport().isRequired(this); 253 } 254 255 /** 256 * {@inheritDoc} 257 */ 258 public List getUpdateComponents() 259 { 260 List comps = new ArrayList(); 261 comps.add(getClientId()); 262 263 return comps; 264 } 265 266 /** 267 * {@inheritDoc} 268 */ 269 public boolean isAsync() 270 { 271 return true; 272 } 273 274 /** 275 * {@inheritDoc} 276 */ 277 public boolean isJson() 278 { 279 return true; 280 } 281 }