001// Licensed under the Apache License, Version 2.0 (the "License");
002// you may not use this file except in compliance with the License.
003// You may obtain a copy of the License at
004//
005//     http://www.apache.org/licenses/LICENSE-2.0
006//
007// Unless required by applicable law or agreed to in writing, software
008// distributed under the License is distributed on an "AS IS" BASIS,
009// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
010// See the License for the specific language governing permissions and
011// limitations under the License.
012
013package org.apache.tapestry5.corelib.mixins;
014
015import org.apache.tapestry5.Field;
016import org.apache.tapestry5.MarkupWriter;
017import org.apache.tapestry5.SymbolConstants;
018import org.apache.tapestry5.ValidationDecorator;
019import org.apache.tapestry5.annotations.Environmental;
020import org.apache.tapestry5.annotations.HeartbeatDeferred;
021import org.apache.tapestry5.annotations.InjectContainer;
022import org.apache.tapestry5.dom.Element;
023import org.apache.tapestry5.ioc.annotations.Inject;
024import org.apache.tapestry5.ioc.annotations.Symbol;
025import org.apache.tapestry5.services.javascript.JavaScriptSupport;
026
027/**
028 * Applied to a {@link org.apache.tapestry5.Field}, this provides the outer layers of markup to correctly
029 * render text fields, selects, and textareas using Bootstrap:
030 * an outer {@code <div class="field-group">} containing a {@code <label class="control-label">} and the field itself.
031 * Actually, the class attribute of the div is defined by the  
032 * {@link SymbolConstants#FORM_GROUP_WRAPPER_CSS_CLASS} and
033 * the class attribute of label is defined by the {@link SymbolConstants#FORM_GROUP_LABEL_CSS_CLASS}.
034 * <code>field-group</code> and <code>control-label</code> are the default values. 
035 * As with the {@link org.apache.tapestry5.corelib.components.Label} component, the {@code for} attribute is set (after the field itself
036 * renders).
037 *
038 *
039 * You can also use the {@link SymbolConstants#FORM_GROUP_FORM_FIELD_WRAPPER_ELEMENT_NAME} symbol
040 * to optionally wrap the input field in an element and {@link SymbolConstants#FORM_GROUP_FORM_FIELD_WRAPPER_ELEMENT_CSS_CLASS}
041 * to give it a CSS class. This is useful for Bootstrap form-horizontal forms.
042 * Setting {@link SymbolConstants#FORM_GROUP_FORM_FIELD_WRAPPER_ELEMENT_NAME} to <code>div</code>,
043 * {@link SymbolConstants#FORM_GROUP_FORM_FIELD_WRAPPER_ELEMENT_CSS_CLASS} to <code>col-sm-10</code>
044 * and {@link SymbolConstants#FORM_GROUP_LABEL_CSS_CLASS} to <code>col-sm-2</code>
045 * will generate labels 2 columns wide and form fields 10 columns wide.
046 *
047 *
048 * This component is not appropriate for radio buttons or checkboxes as they use a different class on the outermost element
049 * ("radio" or "checkbox") and next the element inside the {@code <label>}.
050 *
051 *
052 * @tapestrydoc
053 * @since 5.4
054 * @see SymbolConstants#FORM_GROUP_WRAPPER_CSS_CLASS
055 * @see SymbolConstants#FORM_GROUP_FORM_FIELD_WRAPPER_ELEMENT_NAME
056 * @see SymbolConstants#FORM_GROUP_FORM_FIELD_WRAPPER_ELEMENT_CSS_CLASS
057 * @see SymbolConstants#FORM_GROUP_LABEL_CSS_CLASS
058 * @see SymbolConstants#FORM_FIELD_CSS_CLASS
059 */
060public class FormGroup
061{
062    @InjectContainer
063    private Field field;
064    
065    @Inject
066    @Symbol(SymbolConstants.FORM_GROUP_LABEL_CSS_CLASS)
067    private String labelCssClass;
068    
069    @Inject
070    @Symbol(SymbolConstants.FORM_GROUP_WRAPPER_CSS_CLASS)
071    private String divCssClass;
072    
073    @Inject
074    @Symbol(SymbolConstants.FORM_GROUP_FORM_FIELD_WRAPPER_ELEMENT_NAME)
075    private String fieldWrapperElementName;
076
077    @Inject
078    @Symbol(SymbolConstants.FORM_GROUP_FORM_FIELD_WRAPPER_ELEMENT_CSS_CLASS)
079    private String fieldWrapperElementCssClass;
080
081    private Element label;
082    
083    private Element fieldWrapper;
084
085    @Environmental
086    private ValidationDecorator decorator;
087    
088    @Inject
089    private JavaScriptSupport javaScriptSupport;
090
091    void beginRender(MarkupWriter writer)
092    {
093        writer.element("div", "class", 
094                !("form-group".equals(divCssClass)) ? ("form-group" + " " + divCssClass) : divCssClass);
095
096        decorator.beforeLabel(field);
097
098        label = writer.element("label", "class", labelCssClass);
099        writer.end();
100
101        fillInLabelAttributes();
102
103        decorator.afterLabel(field);
104        
105        if (fieldWrapperElementName.length() > 0) {
106            fieldWrapper = writer.element(fieldWrapperElementName);
107            if (fieldWrapperElementCssClass.length() > 0) {
108                fieldWrapper.attribute("class", fieldWrapperElementCssClass);
109            }
110        }
111        
112    }
113
114    @HeartbeatDeferred
115    void fillInLabelAttributes()
116    {
117        label.attribute("for", field.getClientId());
118        label.text(field.getLabel());
119    }
120
121    void afterRender(MarkupWriter writer)
122    {
123        if (fieldWrapper != null) 
124        {
125            writer.end(); // field wrapper
126        }
127        
128        // TAP5-2662
129        final Element inputElement = writer.getDocument().getElementById(field.getClientId());
130        if (inputElement != null) 
131        {
132            final String clientId = field.getClientId();
133            final String labelId = javaScriptSupport.allocateClientId(clientId + "-label");
134            label.attribute("id", labelId);
135            inputElement.attribute("aria-labelledby", labelId);
136        }
137        
138        writer.end(); // div.form-group
139    }
140}