001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.markup.html.form;
018
019import java.util.Locale;
020
021import org.apache.wicket.markup.ComponentTag;
022import org.apache.wicket.model.IModel;
023import org.apache.wicket.model.Model;
024import org.apache.wicket.util.convert.ConversionException;
025import org.apache.wicket.util.convert.IConverter;
026import org.apache.wicket.util.lang.Objects;
027import org.apache.wicket.util.value.IValueMap;
028import org.apache.wicket.validation.validator.RangeValidator;
029
030/**
031 * A {@link TextField} for HTML5 &lt;input&gt; with type <em>number</em>.
032 * <p>
033 * The {@code <input>}'s value will be rendered in floating-point representation, as required by
034 * the <a href="https://www.w3.org/TR/html-markup/input.number.html">HTML specification</a>. Use a simple
035 * {@code TextField} to use a locale specific conversion of numbers.
036 * <p>
037 * Automatically validates the input against the configured {@link #setMinimum(Number) min} and
038 * {@link #setMaximum(Number) max} attributes. If any of them is <code>null</code> then respective
039 * MIN_VALUE or MAX_VALUE for the number type is used. If the number type has no minimum and/or
040 * maximum value then {@link Double#MIN_VALUE} and {@link Double#MAX_VALUE} are used respectfully.
041 * 
042 * @param <N>
043 *            the type of the number
044 */
045public class NumberTextField<N extends Number & Comparable<N>> extends TextField<N>
046{
047        private static final long serialVersionUID = 1L;
048
049        /**
050         * Use this as a marker of step attribute value "any"
051         * Because the w3c spec requires step to be a non-negative digit
052         * greater than zero we use zero as delegate for "any" keyword.
053         */
054        public static final Double ANY = Double.valueOf(0d);
055
056        private RangeValidator<N> validator;
057
058        private IModel<N> minimum;
059
060        private IModel<N> maximum;
061
062        private IModel<N> step;
063
064        /**
065         * Construct.
066         * 
067         * @param id
068         *            The component id
069         */
070        public NumberTextField(String id)
071        {
072                this(id, null, null);
073        }
074
075
076        /**
077         * Construct.
078         *
079         * @param id
080         *            The component id
081         * @param type
082         *            The type to use when updating the model for this text field
083         */
084        public NumberTextField(String id, Class<N> type)
085        {
086                this(id, null, type);
087        }
088
089
090        /**
091         * Construct.
092         * 
093         * @param id
094         *            The component id
095         * @param model
096         *            The input value
097         */
098        public NumberTextField(String id, IModel<N> model)
099        {
100                this(id, model, null);
101        }
102
103        /**
104         * Construct.
105         * 
106         * @param id
107         *            The component id
108         * @param model
109         *            The input value
110         * @param type
111         *            The type to use when updating the model for this text field
112         */
113        public NumberTextField(String id, IModel<N> model, Class<N> type)
114        {
115                super(id, model, type);
116
117                validator = null;
118                minimum = Model.of((N)null);
119                maximum = Model.of((N)null);
120                step = Model.of((N)null);
121        }
122        
123        /**
124         * Sets the minimum allowed value
125         *
126         * @param minimum
127         *            the minimum allowed value
128         * @return this instance
129         */
130        public NumberTextField<N> setMinimum(final N minimum)
131        {
132                this.minimum = Model.of(minimum);
133                return this;
134        }
135
136        /**
137         * Sets the maximum allowed value
138         *
139         * @param maximum
140         *            the maximum allowed value
141         * @return this instance
142         */
143        public NumberTextField<N> setMaximum(final N maximum)
144        {
145                this.maximum = Model.of(maximum);
146                return this;
147        }
148
149        /**
150         * Sets the step attribute
151         *
152         * @param step
153         *            the step attribute
154         * @return this instance
155         */
156        public NumberTextField<N> setStep(final N step)
157        {
158                this.step = Model.of(step);
159                return this;
160        }
161
162        /**
163         * Sets the minimum allowed value
164         * 
165         * @param minimum
166         *            the minimum allowed value
167         * @return this instance
168         */
169        public NumberTextField<N> setMinimum(final IModel<N> minimum)
170        {
171                this.minimum = minimum;
172                return this;
173        }
174
175        /**
176         * Sets the maximum allowed value
177         *
178         * @param maximum
179         *            the maximum allowed value
180         * @return this instance
181         */
182        public NumberTextField<N> setMaximum(final IModel<N> maximum)
183        {
184                this.maximum = maximum;
185                return this;
186        }
187
188        /**
189         * Sets the step attribute
190         *
191         * @param step
192         *            the step attribute
193         * @return this instance
194         */
195        public NumberTextField<N> setStep(final IModel<N> step)
196        {
197                this.step = step;
198                return this;
199        }
200
201        @Override
202        protected void onConfigure()
203        {
204                super.onConfigure();
205
206                if (validator != null)
207                {
208                        remove(validator);
209                        validator = null;
210                }
211
212                final N min = minimum.getObject();
213                final N max = maximum.getObject();
214                if (min != null || max != null)
215                {
216                        validator = RangeValidator.range(min, max);
217                        add(validator);
218                }
219        }
220
221        @SuppressWarnings("unchecked")
222        private Class<N> getNumberType()
223        {
224                Class<N> numberType = getType();
225                if (numberType == null && getModelObject() != null)
226                {
227                        numberType = (Class<N>)getModelObject().getClass();
228                }
229                return numberType;
230        }
231
232        @Override
233        protected void onComponentTag(final ComponentTag tag)
234        {
235                super.onComponentTag(tag);
236
237                IValueMap attributes = tag.getAttributes();
238
239                final N min = minimum.getObject();
240                if (min != null)
241                {
242                        attributes.put("min", Objects.stringValue(min));
243                }
244                else
245                {
246                        attributes.remove("min");
247                }
248
249                final N max = maximum.getObject();
250                if (max != null)
251                {
252                        attributes.put("max", Objects.stringValue(max));
253                }
254                else
255                {
256                        attributes.remove("max");
257                }
258
259                final N _step = step.getObject();
260                if (_step != null)
261                {
262                        if (_step.doubleValue() == ANY)
263                        {
264                                attributes.put("step", "any");
265                        }
266                        else
267                        {
268                                attributes.put("step", Objects.stringValue(_step));
269                        }
270                }
271                else
272                {
273                        attributes.remove("step");
274                }
275        }
276
277        @Override
278        protected String[] getInputTypes()
279        {
280                return new String[] {"number"};
281        }
282
283        /**
284         * The formatting for {@link Locale#ENGLISH} might not be compatible with HTML (e.g. group
285         * digits), thus use {@link Objects#stringValue(Object)} instead.
286         * 
287         * @return value
288         */
289        @Override
290        protected String getModelValue()
291        {
292                N value = getModelObject();
293                if (value == null)
294                {
295                        return "";
296                }
297                else
298                {
299                        return Objects.stringValue(value);
300                }
301        }
302
303        /**
304         * Always use {@link Locale#ENGLISH} to parse the input.
305         */
306        @Override
307        public void convertInput()
308        {
309                IConverter<N> converter = getConverter(getNumberType());
310
311                try
312                {
313                        setConvertedInput(converter.convertToObject(getInput(), Locale.ENGLISH));
314                }
315                catch (ConversionException e)
316                {
317                        error(newValidationError(e));
318                }
319        }
320}