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.components; 014 015import org.apache.tapestry5.*; 016import org.apache.tapestry5.annotations.Environmental; 017import org.apache.tapestry5.annotations.HeartbeatDeferred; 018import org.apache.tapestry5.annotations.Parameter; 019import org.apache.tapestry5.annotations.SupportsInformalParameters; 020import org.apache.tapestry5.dom.Element; 021import org.apache.tapestry5.http.TapestryHttpSymbolConstants; 022import org.apache.tapestry5.ioc.annotations.Inject; 023import org.apache.tapestry5.ioc.annotations.Symbol; 024import org.apache.tapestry5.ioc.internal.util.InternalUtils; 025import org.apache.tapestry5.services.compatibility.Compatibility; 026import org.apache.tapestry5.services.compatibility.Trait; 027import org.apache.tapestry5.services.javascript.JavaScriptSupport; 028 029/** 030 * Generates a <label> element for a particular field. It writes the CSS class "control-label". 031 * 032 * A Label will render its body, if it has one. However, in most cases it will not have a body, and will render its 033 * {@linkplain org.apache.tapestry5.Field#getLabel() field's label} as its body. Remember, however, that it is the 034 * field label that will be used in any error messages. The Label component allows for client- and server-side 035 * validation error decorations. 036 * 037 * @tapestrydoc 038 */ 039@SupportsInformalParameters 040public class Label 041{ 042 /** 043 * The for parameter is used to identify the {@link Field} linked to this label (it is named this way because it 044 * results in the for attribute of the label element). 045 */ 046 @Parameter(name = "for", required = true, allowNull = false, defaultPrefix = BindingConstants.COMPONENT) 047 private Field field; 048 049 /** 050 * Used to explicitly set the client-side id of the element for this component. Normally this is not 051 * bound (or null) and {@link org.apache.tapestry5.services.javascript.JavaScriptSupport#allocateClientId(org.apache.tapestry5.ComponentResources)} 052 * is used to generate a unique client-id based on the component's id. In some cases, when creating client-side 053 * behaviors, it is useful to explicitly set a unique id for an element using this parameter. 054 * 055 * Certain values, such as "submit", "method", "reset", etc., will cause client-side conflicts and are not allowed; using such will 056 * cause a runtime exception. 057 * @since 5.6.0 058 */ 059 @Parameter(defaultPrefix = BindingConstants.LITERAL) 060 private String clientId; 061 062 @Environmental 063 private ValidationDecorator decorator; 064 065 @Inject 066 private ComponentResources resources; 067 068 @Inject 069 private JavaScriptSupport javaScriptSupport; 070 071 @Inject 072 @Symbol(TapestryHttpSymbolConstants.PRODUCTION_MODE) 073 private boolean productionMode; 074 075 /** 076 * If true, then the body of the label element (in the template) is ignored. This is used when a designer places a 077 * value inside the <label> element for WYSIWYG purposes, but it should be replaced with a different 078 * (probably, localized) value at runtime. The default is false, so a body will be used if present and the field's 079 * label will only be used if the body is empty or blank. 080 */ 081 @Parameter 082 private boolean ignoreBody; 083 084 @Inject 085 private Compatibility compatibility; 086 087 private Element labelElement; 088 089 private String cssClass; 090 091 void pageLoaded() 092 { 093 cssClass = compatibility.enabled(Trait.BOOTSTRAP_4) ? "form-check-label" : "control-label"; 094 } 095 096 boolean beginRender(MarkupWriter writer) 097 { 098 decorator.beforeLabel(field); 099 100 labelElement = writer.element("label", "class", cssClass); 101 102 resources.renderInformalParameters(writer); 103 104 // Since we don't know if the field has rendered yet, we need to defer writing the for and id 105 // attributes until we know the field has rendered (and set its clientId property). That's 106 // exactly what Heartbeat is for. 107 108 updateAttributes(); 109 110 return !ignoreBody; 111 } 112 113 @HeartbeatDeferred 114 private void updateAttributes() 115 { 116 String fieldId = field.getClientId(); 117 118 if (!productionMode && fieldId == null) 119 { 120 // TAP5-2500 121 String warningText = "The Label component " + resources.getCompleteId() 122 + " is linked to a Field that failed to return a clientId. The 'for' attibute will not be rendered."; 123 javaScriptSupport.require("t5/core/console").invoke("warn").with(warningText); 124 } 125 126 String id = clientId != null ? clientId : javaScriptSupport.allocateClientId(fieldId + "-label"); 127 labelElement.attribute("id", id); 128 labelElement.forceAttributes("for", fieldId); 129 130 if (fieldId != null) 131 { 132 Element input = labelElement.getDocument().getElementById(field.getClientId()); 133 if (input != null) 134 { 135 input.attribute("aria-labelledby", id); 136 } 137 } 138 139 decorator.insideLabel(field, labelElement); 140 } 141 142 void afterRender(MarkupWriter writer) 143 { 144 // If the Label element has a body that renders some non-blank output, that takes precedence 145 // over the label string provided by the field. 146 147 boolean bodyIsBlank = InternalUtils.isBlank(labelElement.getChildMarkup()); 148 149 if (bodyIsBlank) 150 writer.write(field.getLabel()); 151 152 writer.end(); // label 153 154 decorator.afterLabel(field); 155 } 156}