/* * Copyright 2008 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package com.google.gwt.user.client.ui; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.FormElement; import com.google.gwt.event.shared.EventHandler; import com.google.gwt.event.shared.GwtEvent; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.safehtml.client.SafeHtmlTemplates; import com.google.gwt.safehtml.shared.SafeHtml; import com.google.gwt.safehtml.shared.SafeUri; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.DeferredCommand; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.impl.FormPanelImpl; import com.google.gwt.user.client.ui.impl.FormPanelImplHost; /** * A panel that wraps its contents in an HTML <FORM> element. * * <p> * This panel can be used to achieve interoperability with servers that accept * traditional HTML form encoding. The following widgets (those that implement * {@link com.google.gwt.user.client.ui.HasName}) will be submitted to the * server if they are contained within this panel: * <ul> * <li>{@link com.google.gwt.user.client.ui.TextBox}</li> * <li>{@link com.google.gwt.user.client.ui.PasswordTextBox}</li> * <li>{@link com.google.gwt.user.client.ui.RadioButton}</li> * <li>{@link com.google.gwt.user.client.ui.SimpleRadioButton}</li> * <li>{@link com.google.gwt.user.client.ui.CheckBox}</li> * <li>{@link com.google.gwt.user.client.ui.SimpleCheckBox}</li> * <li>{@link com.google.gwt.user.client.ui.TextArea}</li> * <li>{@link com.google.gwt.user.client.ui.ListBox}</li> * <li>{@link com.google.gwt.user.client.ui.FileUpload}</li> * <li>{@link com.google.gwt.user.client.ui.Hidden}</li> * </ul> * In particular, {@link com.google.gwt.user.client.ui.FileUpload} is * <i>only</i> useful when used within a FormPanel, because the browser will * only upload files using form submission. * </p> * * <p> * <h3>Example</h3> * {@example com.google.gwt.examples.FormPanelExample} * </p> */ @SuppressWarnings("deprecation") public class FormPanel extends SimplePanel implements FiresFormEvents, FormPanelImplHost { /** * Fired when a form has been submitted successfully. */ public static class SubmitCompleteEvent extends GwtEvent<SubmitCompleteHandler> { /** * The event type. */ private static Type<SubmitCompleteHandler> TYPE; /** * Handler hook. * * @return the handler hook */ static Type<SubmitCompleteHandler> getType() { if (TYPE == null) { TYPE = new Type<SubmitCompleteHandler>(); } return TYPE; } private String resultHtml; /** * Create a submit complete event. * * @param resultsHtml the results from submitting the form */ protected SubmitCompleteEvent(String resultsHtml) { this.resultHtml = resultsHtml; } @Override public final Type<SubmitCompleteHandler> getAssociatedType() { return TYPE; } /** * Gets the result text of the form submission. * * @return the result html, or <code>null</code> if there was an error * reading it * @tip The result html can be <code>null</code> as a result of submitting a * form to a different domain. */ public String getResults() { return resultHtml; } @Override protected void dispatch(SubmitCompleteHandler handler) { handler.onSubmitComplete(this); } } /** * Handler for {@link FormPanel.SubmitCompleteEvent} events. */ public interface SubmitCompleteHandler extends EventHandler { /** * Fired when a form has been submitted successfully. * * @param event the event */ void onSubmitComplete(FormPanel.SubmitCompleteEvent event); } /** * Fired when the form is submitted. */ public static class SubmitEvent extends GwtEvent<SubmitHandler> { /** * The event type. */ private static Type<SubmitHandler> TYPE = new Type<SubmitHandler>(); /** * Handler hook. * * @return the handler hook */ static Type<SubmitHandler> getType() { if (TYPE == null) { TYPE = new Type<SubmitHandler>(); } return TYPE; } private boolean canceled = false; /** * Cancel the form submit. Firing this will prevent a subsequent * {@link FormPanel.SubmitCompleteEvent} from being fired. */ public void cancel() { this.canceled = true; } @Override public final Type<FormPanel.SubmitHandler> getAssociatedType() { return TYPE; } /** * Gets whether this form submit will be canceled. * * @return <code>true</code> if the form submit will be canceled */ public boolean isCanceled() { return canceled; } @Override protected void dispatch(FormPanel.SubmitHandler handler) { handler.onSubmit(this); } /** * This method is used for legacy support and should be removed when * {@link FormHandler} is removed. * * @deprecated Use {@link FormPanel.SubmitEvent#cancel()} instead */ @Deprecated void setCanceled(boolean canceled) { this.canceled = canceled; } } /** * Handler for {@link FormPanel.SubmitEvent} events. */ public interface SubmitHandler extends EventHandler { /** * Fired when the form is submitted. * * <p> * The FormPanel must <em>not</em> be detached (i.e. removed from its parent * or otherwise disconnected from a {@link RootPanel}) until the submission * is complete. Otherwise, notification of submission will fail. * </p> * * @param event the event */ void onSubmit(FormPanel.SubmitEvent event); } interface IFrameTemplate extends SafeHtmlTemplates { static final IFrameTemplate INSTANCE = GWT.create(IFrameTemplate.class); @Template("<iframe src=\"javascript:''\" name='{0}' " + "style='position:absolute;width:0;height:0;border:0'>") SafeHtml get(String name); } /** * Used with {@link #setEncoding(String)} to specify that the form will be * submitted using MIME encoding (necessary for {@link FileUpload} to work * properly). */ public static final String ENCODING_MULTIPART = "multipart/form-data"; /** * Used with {@link #setEncoding(String)} to specify that the form will be * submitted using traditional URL encoding. */ public static final String ENCODING_URLENCODED = "application/x-www-form-urlencoded"; /** * Used with {@link #setMethod(String)} to specify that the form will be * submitted using an HTTP GET request. */ public static final String METHOD_GET = "get"; /** * Used with {@link #setMethod(String)} to specify that the form will be * submitted using an HTTP POST request (necessary for {@link FileUpload} to * work properly). */ public static final String METHOD_POST = "post"; private static int formId = 0; private static FormPanelImpl impl = GWT.create(FormPanelImpl.class); /** * Creates a FormPanel that wraps an existing <form> element. * * This element must already be attached to the document. If the element is * removed from the document, you must call * {@link RootPanel#detachNow(Widget)}. * * <p> * The specified form element's target attribute will not be set, and the * {@link FormSubmitCompleteEvent} will not be fired. * </p> * * @param element the element to be wrapped */ public static FormPanel wrap(Element element) { // Assert that the element is attached. assert Document.get().getBody().isOrHasChild(element); FormPanel formPanel = new FormPanel(element); // Mark it attached and remember it for cleanup. formPanel.onAttach(); RootPanel.detachOnWindowClose(formPanel); return formPanel; } /** * Creates a FormPanel that wraps an existing <form> element. * * This element must already be attached to the document. If the element is * removed from the document, you must call * {@link RootPanel#detachNow(Widget)}. * * <p> * If the createIFrame parameter is set to <code>true</code>, then the wrapped * form's target attribute will be set to a hidden iframe. If not, the form's * target will be left alone, and the FormSubmitComplete event will not be * fired. * </p> * * @param element the element to be wrapped * @param createIFrame <code>true</code> to create an <iframe> element * that will be targeted by this form */ public static FormPanel wrap(Element element, boolean createIFrame) { // Assert that the element is attached. assert Document.get().getBody().isOrHasChild(element); FormPanel formPanel = new FormPanel(element, createIFrame); // Mark it attached and remember it for cleanup. formPanel.onAttach(); RootPanel.detachOnWindowClose(formPanel); return formPanel; } private String frameName; private Element synthesizedFrame; /** * Creates a new FormPanel. When created using this constructor, it will be * submitted to a hidden <iframe> element, and the results of the * submission made available via {@link SubmitCompleteHandler}. * * <p> * The back-end server is expected to respond with a content-type of * 'text/html', meaning that the text returned will be treated as HTML. If any * other content-type is specified by the server, then the result HTML sent in * the onFormSubmit event will be unpredictable across browsers, and the * {@link SubmitCompleteHandler#onSubmitComplete(com.google.gwt.user.client.ui.FormPanel.SubmitCompleteEvent) * onSubmitComplete} event may not fire at all. * </p> * * @tip The initial implementation of FormPanel specified that the server * respond with a content-type of 'text/plain'. This has been * intentionally changed to specify 'text/html' because 'text/plain' * cannot be made to work properly on all browsers. */ public FormPanel() { this(Document.get().createFormElement(), true); } /** * Creates a FormPanel that targets a {@link NamedFrame}. The target frame is * not physically attached to the form, and must therefore still be added to a * panel elsewhere. * * <p> * When the FormPanel targets an external frame in this way, it will not fire * the FormSubmitComplete event. * </p> * * @param frameTarget the {@link NamedFrame} to be targetted */ public FormPanel(NamedFrame frameTarget) { this(frameTarget.getName()); } /** * Creates a new FormPanel. When created using this constructor, it will be * submitted either by replacing the current page, or to the named * <iframe>. * * <p> * When the FormPanel targets an external frame in this way, it will not fire * the FormSubmitComplete event. * </p> * * @param target the name of the <iframe> to receive the results of the * submission, or <code>null</code> to specify that the current page * be replaced */ public FormPanel(String target) { super(Document.get().createFormElement()); setTarget(target); } /** * This constructor may be used by subclasses to explicitly use an existing * element. This element must be a <form> element. * * <p> * The specified form element's target attribute will not be set, and the * {@link FormSubmitCompleteEvent} will not be fired. * </p> * * @param element the element to be used */ protected FormPanel(Element element) { this(element, false); } /** * This constructor may be used by subclasses to explicitly use an existing * element. This element must be a <form> element. * * <p> * If the createIFrame parameter is set to <code>true</code>, then the wrapped * form's target attribute will be set to a hidden iframe. If not, the form's * target will be left alone, and the FormSubmitComplete event will not be * fired. * </p> * * @param element the element to be used * @param createIFrame <code>true</code> to create an <iframe> element * that will be targeted by this form */ protected FormPanel(Element element, boolean createIFrame) { super(element); FormElement.as(element); if (createIFrame) { assert ((getTarget() == null) || (getTarget().trim().length() == 0)) : "Cannot create target iframe if the form's target is already set."; // We use the module name as part of the unique ID to ensure that ids are // unique across modules. frameName = "FormPanel_" + GWT.getModuleName() + "_" + (++formId); setTarget(frameName); sinkEvents(Event.ONLOAD); } } /** * @deprecated Use {@link #addSubmitCompleteHandler} and * {@link #addSubmitHandler} instead */ @Deprecated public void addFormHandler(FormHandler handler) { ListenerWrapper.WrappedOldFormHandler.add(this, handler); } /** * Adds a {@link SubmitCompleteEvent} handler. * * @param handler the handler * @return the handler registration used to remove the handler */ public HandlerRegistration addSubmitCompleteHandler(SubmitCompleteHandler handler) { return addHandler(handler, SubmitCompleteEvent.getType()); } /** * Adds a {@link SubmitEvent} handler. * * @param handler the handler * @return the handler registration used to remove the handler */ public HandlerRegistration addSubmitHandler(SubmitHandler handler) { return addHandler(handler, SubmitEvent.getType()); } /** * Gets the 'action' associated with this form. This is the URL to which it * will be submitted. * * @return the form's action */ public String getAction() { return getFormElement().getAction(); } /** * Gets the encoding used for submitting this form. This should be either * {@link #ENCODING_MULTIPART} or {@link #ENCODING_URLENCODED}. * * @return the form's encoding */ public String getEncoding() { return impl.getEncoding(getElement()); } /** * Gets the HTTP method used for submitting this form. This should be either * {@link #METHOD_GET} or {@link #METHOD_POST}. * * @return the form's method */ public String getMethod() { return getFormElement().getMethod(); } /** * Gets the form's 'target'. This is the name of the {@link NamedFrame} that * will receive the results of submission, or <code>null</code> if none has * been specified. * * @return the form's target. */ public String getTarget() { return getFormElement().getTarget(); } /** * Fired when a form is submitted. * * @return true if the form is submitted, false if canceled */ public boolean onFormSubmit() { return onFormSubmitImpl(); } public void onFrameLoad() { onFrameLoadImpl(); } /** * @deprecated Use the {@link HandlerRegistration#removeHandler} method on the * object returned by and add*Handler method instead */ @Deprecated public void removeFormHandler(FormHandler handler) { ListenerWrapper.WrappedOldFormHandler.remove(this, handler); } /** * Resets the form, clearing all fields. */ public void reset() { impl.reset(getElement()); } /** * Sets the 'action' associated with this form. This is the URL to which it * will be submitted. * * @param url the form's action */ public void setAction(String url) { getFormElement().setAction(url); } /** * Sets the 'action' associated with this form. This is the URL to which it * will be submitted. * * @param url the form's action */ public void setAction(SafeUri url) { setAction(url.asString()); } /** * Sets the encoding used for submitting this form. This should be either * {@link #ENCODING_MULTIPART} or {@link #ENCODING_URLENCODED}. * * @param encodingType the form's encoding */ public void setEncoding(String encodingType) { impl.setEncoding(getElement(), encodingType); } /** * Sets the HTTP method used for submitting this form. This should be either * {@link #METHOD_GET} or {@link #METHOD_POST}. * * @param method the form's method */ public void setMethod(String method) { getFormElement().setMethod(method); } /** * Submits the form. * * <p> * The FormPanel must <em>not</em> be detached (i.e. removed from its parent * or otherwise disconnected from a {@link RootPanel}) until the submission is * complete. Otherwise, notification of submission will fail. * </p> */ public void submit() { // Fire the onSubmit event, because javascript's form.submit() does not // fire the built-in onsubmit event. if (!fireSubmitEvent()) { return; } impl.submit(getElement(), synthesizedFrame); } @Override protected void onAttach() { super.onAttach(); if (frameName != null) { // Create and attach a hidden iframe to the body element. createFrame(); Document.get().getBody().appendChild(synthesizedFrame); } // Hook up the underlying iframe's onLoad event when attached to the DOM. // Making this connection only when attached avoids memory-leak issues. // The FormPanel cannot use the built-in GWT event-handling mechanism // because there is no standard onLoad event on iframes that works across // browsers. impl.hookEvents(synthesizedFrame, getElement(), this); } @Override protected void onDetach() { super.onDetach(); // Unhook the iframe's onLoad when detached. impl.unhookEvents(synthesizedFrame, getElement()); if (synthesizedFrame != null) { // And remove it from the document. Document.get().getBody().removeChild(synthesizedFrame); synthesizedFrame = null; } } // For unit-tests. Element getSynthesizedIFrame() { return synthesizedFrame; } private void createFrame() { // Attach a hidden IFrame to the form. This is the target iframe to which // the form will be submitted. We have to create the iframe using innerHTML, // because setting an iframe's 'name' property dynamically doesn't work on // most browsers. Element dummy = Document.get().createDivElement(); dummy.setInnerSafeHtml(IFrameTemplate.INSTANCE.get(frameName)); synthesizedFrame = dummy.getFirstChildElement(); } /** * Fire a {@link FormPanel.SubmitEvent}. * * @return true to continue, false if canceled */ private boolean fireSubmitEvent() { FormPanel.SubmitEvent event = new FormPanel.SubmitEvent(); fireEvent(event); return !event.isCanceled(); } private FormElement getFormElement() { return getElement().cast(); } /** * Returns true if the form is submitted, false if canceled. */ private boolean onFormSubmitImpl() { return fireSubmitEvent(); } private void onFrameLoadImpl() { // Fire onComplete events in a deferred command. This is necessary // because clients that detach the form panel when submission is // complete can cause some browsers (i.e. Mozilla) to go into an // 'infinite loading' state. See issue 916. DeferredCommand.addCommand(new Command() { public void execute() { fireEvent(new SubmitCompleteEvent(impl.getContents(synthesizedFrame))); } }); } private void setTarget(String target) { getFormElement().setTarget(target); } }