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.Import; 019import org.apache.tapestry5.annotations.Parameter; 020import org.apache.tapestry5.annotations.SupportsInformalParameters; 021import org.apache.tapestry5.corelib.internal.ComponentActionSink; 022import org.apache.tapestry5.corelib.internal.FormSupportAdapter; 023import org.apache.tapestry5.corelib.internal.HiddenFieldPositioner; 024import org.apache.tapestry5.corelib.mixins.TriggerFragment; 025import org.apache.tapestry5.dom.Element; 026import org.apache.tapestry5.ioc.annotations.Inject; 027import org.apache.tapestry5.services.ClientDataEncoder; 028import org.apache.tapestry5.services.Environment; 029import org.apache.tapestry5.services.FormSupport; 030import org.apache.tapestry5.services.HiddenFieldLocationRules; 031import org.apache.tapestry5.services.javascript.JavaScriptSupport; 032import org.slf4j.Logger; 033 034/** 035 * A FormFragment is a portion of a Form that may be selectively displayed. Form elements inside a FormFragment will 036 * automatically bypass validation when the fragment is invisible. The trick is to also bypass server-side form 037 * processing for such fields when the form is submitted; client-side logic "removes" the 038 * {@link org.apache.tapestry5.corelib.components.Form#FORM_DATA form data} for the fragment if it is invisible when the 039 * form is submitted (e.g., the hidden form field is disabled); 040 * alternately, client-side logic can simply remove the form fragment element (including its visible and 041 * hidden fields) to prevent server-side processing. 042 * 043 * The client-side element will now listen to two new events defined by client-side constants: 044 * <dl> 045 * <dt>core/events.formfragment.changeVisibility or Tapestry.CHANGE_VISIBILITY_EVENT</dt> 046 * <dd>Change the visibility as per the event memo's visibility property. When the visibility changes, the correct 047 * animation is executed.</dd> 048 * <dt>core/events.formfragment.remove or Tapestry.HIDE_AND_REMOVE_EVENT</dt> 049 * <dd>Hides the element, then removes it from the DOM entirely. 050 * </dl> 051 * 052 * @tapestrydoc 053 * @see TriggerFragment 054 * @see Form 055 */ 056@SupportsInformalParameters 057@Import(module = "t5/core/form-fragment") 058public class FormFragment implements ClientElement 059{ 060 /** 061 * Determines if the fragment is initially visible or initially invisible (the default). This is only used when 062 * rendering; when the form is submitted, the hidden field value is used to determine whether the elements within 063 * the fragment should be processed (or ignored if still invisible). 064 */ 065 @Parameter 066 private boolean visible; 067 068 /** 069 * If true, then the fragment submits the values from fields it contains <em>even if</em> the fragment is not 070 * visible. 071 * The default is to omit values from fields when the enclosing fragment is non visible. 072 * 073 * @since 5.2.0 074 */ 075 @Parameter 076 private boolean alwaysSubmit; 077 078 /** 079 * Name of a function on the client-side Tapestry.ElementEffect object that is invoked to make the fragment visible. 080 * This is no longer used. 081 * 082 * @deprecated Deprecated in 5.4; clients that wish to animate should handle the <code>events.element.didShow</code> client-side event. 083 */ 084 @Parameter(defaultPrefix = BindingConstants.LITERAL) 085 private String show; 086 087 /** 088 * Name of a function on the client-side Tapestry.ElementEffect object that is invoked when the fragment is to be 089 * hidden. This is no longer used. 090 * 091 * @deprecated Deprecated in 5.4; clients that wish to animate should handle the <code>events.element.didHide</code> client-side event. 092 */ 093 @Parameter(defaultPrefix = BindingConstants.LITERAL) 094 private String hide; 095 096 /** 097 * The element to render for each iteration of the loop. The default comes from the template, or "div" if the 098 * template did not specific an element. 099 */ 100 @Parameter(defaultPrefix = BindingConstants.LITERAL) 101 private String element; 102 103 /** 104 * If bound, then the id attribute of the rendered element will be this exact value. If not bound, then a unique id 105 * is generated for the element. 106 */ 107 @Parameter(name = "id", defaultPrefix = BindingConstants.LITERAL) 108 private String idParameter; 109 110 /** 111 * The name of a javascript function that overrides the default visibility search bound. 112 * Tapestry normally ensures that not only the form fragment but all parent elements up to the containing body 113 * are visible when determining whether to submit the contents of a form fragment. This behavior can be modified by 114 * supplying a javascript function that receives the "current" element in the chain. Returning true will stop the 115 * search (and report ElementWrapper.deepVisible() as true). Returning false will continue the search up the chain. 116 * 117 * @since 5.3 118 * @deprecated Deprecated in 5.4 with no current replacement. 119 */ 120 @Parameter(defaultPrefix = BindingConstants.LITERAL, allowNull = false) 121 private String visibleBound; 122 123 @Inject 124 private Environment environment; 125 126 @Environmental 127 private JavaScriptSupport javascriptSupport; 128 129 @Inject 130 private ComponentResources resources; 131 132 private String clientId; 133 134 private ComponentActionSink componentActions; 135 136 @Inject 137 private Logger logger; 138 139 @Inject 140 private HiddenFieldLocationRules rules; 141 142 private HiddenFieldPositioner hiddenFieldPositioner; 143 144 @Inject 145 private ClientDataEncoder clientDataEncoder; 146 147 String defaultElement() 148 { 149 return resources.getElementName("div"); 150 } 151 152 /** 153 * Renders a <div> tag and provides an override of the {@link org.apache.tapestry5.services.FormSupport} 154 * environmental. 155 */ 156 void beginRender(MarkupWriter writer) 157 { 158 FormSupport formSupport = environment.peekRequired(FormSupport.class); 159 160 String clientId = getClientId(); 161 162 hiddenFieldPositioner = new HiddenFieldPositioner(writer, rules); 163 164 Element element = writer.element(this.element, 165 "id", clientId, 166 "data-component-type", "core/FormFragment"); 167 168 if (alwaysSubmit) { 169 element.attribute("data-always-submit", "true"); 170 } 171 172 resources.renderInformalParameters(writer); 173 174 if (!visible) 175 { 176 element.attribute("style", "display: none;"); 177 178 if (!alwaysSubmit) 179 { 180 javascriptSupport.require("t5/core/form-fragment").invoke("hide").with(clientId); 181 } 182 } 183 184 componentActions = new ComponentActionSink(logger, clientDataEncoder); 185 186 // Here's the magic of environmentals ... we can create a wrapper around 187 // the normal FormSupport environmental that intercepts some of the behavior. 188 // Here we're setting aside all the actions inside the FormFragment so that we 189 // can control whether those actions occur when the form is submitted. 190 191 FormSupport override = new FormSupportAdapter(formSupport) 192 { 193 @Override 194 public <T> void store(T component, ComponentAction<T> action) 195 { 196 componentActions.store(component, action); 197 } 198 199 @Override 200 public <T> void storeCancel(T component, ComponentAction<T> action) 201 { 202 componentActions.storeCancel(component, action); 203 } 204 205 @Override 206 public <T> void storeAndExecute(T component, ComponentAction<T> action) 207 { 208 componentActions.store(component, action); 209 210 action.execute(component); 211 } 212 }; 213 214 // Tada! Now all the enclosed components will use our override of FormSupport, 215 // until we pop it off. 216 217 environment.push(FormSupport.class, override); 218 219 } 220 221 /** 222 * Closes the <div> tag and pops off the {@link org.apache.tapestry5.services.FormSupport} environmental 223 * override. 224 * 225 * @param writer 226 */ 227 void afterRender(MarkupWriter writer) 228 { 229 Element hidden = hiddenFieldPositioner.getElement(); 230 231 hidden.attributes("type", "hidden", 232 233 "name", Form.FORM_DATA, 234 235 "value", componentActions.getClientData()); 236 237 writer.end(); // div 238 239 240 environment.pop(FormSupport.class); 241 242 resetClientId(); 243 } 244 245 @HeartbeatDeferred 246 void resetClientId() 247 { 248 clientId = null; 249 } 250 251 public String getClientId() 252 { 253 if (clientId == null) 254 { 255 clientId = resources.isBound("id") ? idParameter : javascriptSupport.allocateClientId(resources); 256 } 257 return clientId; 258 } 259}