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