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
015package org.apache.tapestry5.corelib.components;
016
017import org.apache.tapestry5.*;
018import org.apache.tapestry5.annotations.Environmental;
019import org.apache.tapestry5.annotations.Parameter;
020import org.apache.tapestry5.annotations.Property;
021import org.apache.tapestry5.corelib.base.AbstractField;
022import org.apache.tapestry5.dom.Element;
023import org.apache.tapestry5.ioc.annotations.Inject;
024import org.apache.tapestry5.ioc.internal.util.CollectionFactory;
025import org.apache.tapestry5.services.ComponentDefaultProvider;
026import org.apache.tapestry5.services.Request;
027import org.apache.tapestry5.services.javascript.JavaScriptSupport;
028
029import java.util.Collections;
030import java.util.List;
031import 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 */
044public 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