001    // Copyright 2011 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    
015    package org.apache.tapestry5.corelib.components;
016    
017    import org.apache.tapestry5.*;
018    import org.apache.tapestry5.annotations.Environmental;
019    import org.apache.tapestry5.annotations.Parameter;
020    import org.apache.tapestry5.annotations.Property;
021    import org.apache.tapestry5.corelib.base.AbstractField;
022    import org.apache.tapestry5.dom.Element;
023    import org.apache.tapestry5.ioc.annotations.Inject;
024    import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
025    import org.apache.tapestry5.services.ComponentDefaultProvider;
026    import org.apache.tapestry5.services.Request;
027    import org.apache.tapestry5.services.javascript.JavaScriptSupport;
028    
029    import java.util.Collections;
030    import java.util.List;
031    import java.util.Set;
032    
033    /**
034     * A list of checkboxes, allowing selection of multiple items in a list.
035     * <p/>
036     * For an alternative component that can be used for similar purposes, see
037     * {@link Palette}.
038     *
039     * @tapestrydoc
040     * @see Form
041     * @see Palette
042     * @since 5.3
043     */
044    public class Checklist extends AbstractField
045    {
046    
047        /**
048         * Model used to define the values and labels used when rendering the
049         * checklist.
050         */
051        @Parameter(required = true)
052        private SelectModel model;
053    
054        /**
055         * The list of selected values from the
056         * {@link org.apache.tapestry5.SelectModel}. This will be updated when the
057         * form is submitted. If the value for the parameter is null, a new list
058         * will be created, otherwise the existing list will be cleared. If unbound,
059         * defaults to a property of the container matching this component's id.
060         */
061        @Parameter(required = true, autoconnect = true)
062        private List<Object> selected;
063    
064        /**
065         * A ValueEncoder used to convert server-side objects (provided from the
066         * "source" parameter) into unique client-side strings (typically IDs) and
067         * back. Note: this component does NOT support ValueEncoders configured to
068         * be provided automatically by Tapestry.
069         */
070        @Parameter(required = true, allowNull = false)
071        private ValueEncoder<Object> encoder;
072    
073        /**
074         * The object that will perform input validation. The validate binding prefix is generally used to provide
075         * this object in a declarative fashion.
076         */
077        @Parameter(defaultPrefix = BindingConstants.VALIDATE)
078        @SuppressWarnings("unchecked")
079        private FieldValidator<Object> validate;
080    
081        @Inject
082        private Request request;
083    
084        @Inject
085        private FieldValidationSupport fieldValidationSupport;
086    
087        @Environmental
088        private ValidationTracker tracker;
089    
090        @Inject
091        private ComponentResources componentResources;
092    
093        @Inject
094        private ComponentDefaultProvider defaultProvider;
095    
096        @Inject
097        private JavaScriptSupport javaScriptSupport;
098    
099        @Property
100        private List<Renderable> availableOptions;
101    
102        private MarkupWriter markupWriter;
103    
104        private final class RenderRadio implements Renderable
105        {
106            private final OptionModel model;
107    
108            private RenderRadio(final OptionModel model)
109            {
110                this.model = model;
111            }
112    
113            public void render(MarkupWriter writer)
114            {
115                final String clientId = javaScriptSupport.allocateClientId(componentResources);
116    
117                final String clientValue = encoder.toClient(model.getValue());
118    
119                final Element checkbox = writer.element("input", "type", "checkbox", "id", clientId, "name", getControlName(), "value", clientValue);
120    
121                if (getSelected().contains(model.getValue()))
122                {
123                    checkbox.attribute("checked", "checked");
124                }
125                writer.end();
126    
127                writer.element("label", "for", clientId);
128                writer.write(model.getLabel());
129                writer.end();
130            }
131        }
132    
133        void setupRender(final MarkupWriter writer)
134        {
135            markupWriter = writer;
136    
137            availableOptions = CollectionFactory.newList();
138    
139            final SelectModelVisitor visitor = new SelectModelVisitor()
140            {
141                public void beginOptionGroup(final OptionGroupModel groupModel)
142                {
143                }
144    
145                public void option(final OptionModel optionModel)
146                {
147                    availableOptions.add(new RenderRadio(optionModel));
148                }
149    
150                public void endOptionGroup(final OptionGroupModel groupModel)
151                {
152                }
153    
154            };
155    
156            model.visit(visitor);
157        }
158    
159        @Override
160        protected void processSubmission(final String controlName)
161        {
162    
163            final String[] parameters = request.getParameters(controlName);
164    
165            List<Object> selected = this.selected;
166    
167            if (selected == null)
168            {
169                selected = CollectionFactory.newList();
170            } else
171            {
172                selected.clear();
173            }
174    
175            if (parameters != null)
176            {
177                for (final String value : parameters)
178                {
179                    final Object objectValue = encoder.toValue(value);
180    
181                    selected.add(objectValue);
182                }
183    
184            }
185    
186            putPropertyNameIntoBeanValidationContext("selected");
187    
188            try
189            {
190                this.fieldValidationSupport.validate(selected, this.componentResources, this.validate);
191    
192                this.selected = selected;
193            } catch (final ValidationException e)
194            {
195                this.tracker.recordError(this, e.getMessage());
196            }
197    
198            removePropertyNameFromBeanValidationContext();
199        }
200    
201        Set<Object> getSelected()
202        {
203            if (selected == null)
204            {
205                return Collections.emptySet();
206            }
207    
208            return CollectionFactory.newSet(selected);
209        }
210    
211        /**
212         * Computes a default value for the "validate" parameter using
213         * {@link org.apache.tapestry5.services.FieldValidatorDefaultSource}.
214         */
215    
216        Binding defaultValidate()
217        {
218            return this.defaultProvider.defaultValidatorBinding("selected", this.componentResources);
219        }
220    
221        @Override
222        public boolean isRequired()
223        {
224            return validate.isRequired();
225        }
226    }
227