tags:

views:

1335

answers:

2

I have to add a form to a small piece of AJAX functionality implemented in GWT. In HTML terms, I'd like

<label for="personName">Name:</label><input type="text" size="50" id="personName"/>

It appears the Label widget in GWT simply renders as a DIV.

Ideally I would like clicking on the label text to focus the associated input. This is built-in browser functionality I don't want to have to mess around with ClickHandlers on label divs!

Has anyone faced this issue? Does exist as a built-in widget but is called something else?

EDIT: Have come up with the following. Maybe there is a better way?

HTML label = new HTML();
label.setHTML("<label for='"+input.getElement().getId()+"'>"+labelText+"</label>");
+6  A: 

By popular demand, I present to you InputLabel, a <label> + <input type="text"> Widget :)

This is based on the CheckBox class (which wraps an <input type="checkbox"> element) - it hasn't been tested thoroughly - I leave that to the reader ;)

import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.InputElement;
import com.google.gwt.dom.client.LabelElement;
import com.google.gwt.event.dom.client.ChangeEvent;
import com.google.gwt.event.dom.client.ChangeHandler;
import com.google.gwt.event.dom.client.HasChangeHandlers;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.EventListener;
import com.google.gwt.user.client.ui.ButtonBase;
import com.google.gwt.user.client.ui.FormPanel;
import com.google.gwt.user.client.ui.HasName;
import com.google.gwt.user.client.ui.HasValue;
import com.google.gwt.user.client.ui.RadioButton;
import com.google.gwt.user.client.ui.UIObject;
import com.google.gwt.user.client.ui.Widget;

public class InputLabel extends ButtonBase implements HasName, HasValue<String>, HasChangeHandlers {
  InputElement inputElem;
  LabelElement labelElem;
  private boolean valueChangeHandlerInitialized;

  /**
   * Creates an input box with no label.
   */
  public InputLabel() {
    this(DOM.createInputText());
    //setStyleName("gwt-CheckBox"); //TODO: add a valid style name
  }

  /**
   * Creates an input box with the specified text label.
   * 
   * @param label the check box's label
   */
  public InputLabel(String label) {
    this();
    setText(label);
  }

  /**
   * Creates an input box with the specified text label.
   * 
   * @param label the input box's label
   * @param asHTML <code>true</code> to treat the specified label as html
   */
  public InputLabel(String label, boolean asHTML) {
    this();
    if (asHTML) {
      setHTML(label);
    } else {
      setText(label);
    }
  }

  protected InputLabel(Element elem) {
    super(DOM.createSpan());
    inputElem = InputElement.as(elem);
    labelElem = Document.get().createLabelElement();

    getElement().appendChild(labelElem);
    getElement().appendChild(inputElem);

    String uid = DOM.createUniqueId();
    inputElem.setPropertyString("id", uid);
    labelElem.setHtmlFor(uid);

    // Accessibility: setting tab index to be 0 by default, ensuring element
    // appears in tab sequence. FocusWidget's setElement method already
    // calls setTabIndex, which is overridden below. However, at the time
    // that this call is made, inputElem has not been created. So, we have
    // to call setTabIndex again, once inputElem has been created.
    setTabIndex(0);
  }

  public HandlerRegistration addValueChangeHandler(
      ValueChangeHandler<String> handler) {
    // Is this the first value change handler? If so, time to add handlers
    if (!valueChangeHandlerInitialized) {
      addChangeHandler(new ChangeHandler() {
        public void onChange(ChangeEvent event) {
          ValueChangeEvent.fire(InputLabel.this, getValue());
        }
      });
      valueChangeHandlerInitialized = true;
    }
    return addHandler(handler, ValueChangeEvent.getType());
  }

  /**
   * Returns the value property of the input element that backs this widget.
   * This is the value that will be associated with the InputLabel name and
   * submitted to the server if a {@link FormPanel} that holds it is submitted.
   * <p>
   * This will probably return the same thing as {@link #getValue}, left here for magic reasons.
   */
  public String getFormValue() {
    return inputElem.getValue();
  }

  @Override
  public String getHTML() {
    return labelElem.getInnerHTML();
  }

  public String getName() {
    return inputElem.getName();
  }

  @Override
  public int getTabIndex() {
    return inputElem.getTabIndex();
  }

  @Override
  public String getText() {
    return labelElem.getInnerText();
  }

  /**
   * Gets the text value of the input element. 
   * <p>
   * @return the value of the input box.
   * Will not return null
   */
  public String getValue() {
    if (isAttached()) {
      return inputElem.getValue();
    } else {
      return inputElem.getDefaultValue();
    }
  }

  @Override
  public boolean isEnabled() {
    return !inputElem.isDisabled();
  }

  @Override
  public void setAccessKey(char key) {
    inputElem.setAccessKey("" + key);
  }

  @Override
  public void setEnabled(boolean enabled) {
    inputElem.setDisabled(!enabled);
    if (enabled) {
      removeStyleDependentName("disabled");
    } else {
      addStyleDependentName("disabled");
    }
  }

  @Override
  public void setFocus(boolean focused) {
    if (focused) {
      inputElem.focus();
    } else {
      inputElem.blur();
    }
  }

  /**
   * Set the value property on the input element that backs this widget. This is
   * the value that will be associated with the InputLabel's name and submitted to
   * the server if a {@link FormPanel} that holds it is submitted.
   * <p>
   * Don't confuse this with {@link #setValue}.
   * 
   * @param value
   */
  public void setFormValue(String value) {
    inputElem.setAttribute("value", value);
  }

  @Override
  public void setHTML(String html) {
    labelElem.setInnerHTML(html);
  }

  public void setName(String name) {
    inputElem.setName(name);
  }

  @Override
  public void setTabIndex(int index) {
    // Need to guard against call to setTabIndex before inputElem is
    // initialized. This happens because FocusWidget's (a superclass of
    // InputLabel) setElement method calls setTabIndex before inputElem is
    // initialized. See InputLabel's protected constructor for more information.
    if (inputElem != null) {
      inputElem.setTabIndex(index);
    }
  }

  @Override
  public void setText(String text) {
    labelElem.setInnerText(text);
  }

  /**
   * Sets the text in the input box.
   * <p>
   * Note that this <em>does not</em> set the value property of the
   * input element wrapped by this widget. For access to that property, see
   * {@link #setFormValue(String)}
   * 
   * @param value the text to set; must not be null
   * @throws IllegalArgumentException if value is null
   */
  public void setValue(String value) {
    setValue(value, false);
  }

  /**
   * Sets the text in the input box, firing {@link ValueChangeEvent} if
   * appropriate.
   * <p>
   * Note that this <em>does not</em> set the value property of the
   * input element wrapped by this widget. For access to that property, see
   * {@link #setFormValue(String)}
   *
   * @param value true the text to set; must not be null
   * @param fireEvents If true, and value has changed, fire a
   *          {@link ValueChangeEvent}
   * @throws IllegalArgumentException if value is null
   */
  public void setValue(String value, boolean fireEvents) {
    if (value == null) {
      throw new IllegalArgumentException("value must not be null");
    }

    String oldValue = getValue();
    inputElem.setValue(value);
    inputElem.setDefaultValue(value);
    if (value.equals(oldValue)) {
      return;
    }
    if (fireEvents) {
      ValueChangeEvent.fire(this, value);
    }
  }

  // Unlike other widgets the InputLabel sinks on its inputElement, not
  // its wrapper
  @Override
  public void sinkEvents(int eventBitsToAdd) {
    if (isOrWasAttached()) {
      Event.sinkEvents(inputElem, 
          eventBitsToAdd | Event.getEventsSunk(inputElem));
    } else {
      super.sinkEvents(eventBitsToAdd);
    }
  }


  /**
   * <b>Affected Elements:</b>
   * <ul>
   * <li>-label = label next to the input box.</li>
   * </ul>
   * 
   * @see UIObject#onEnsureDebugId(String)
   */
  @Override
  protected void onEnsureDebugId(String baseID) {
    super.onEnsureDebugId(baseID);
    ensureDebugId(labelElem, baseID, "label");
    ensureDebugId(inputElem, baseID, "input");
    labelElem.setHtmlFor(inputElem.getId());
  }

  /**
   * This method is called when a widget is attached to the browser's document.
   * onAttach needs special handling for the InputLabel case. Must still call
   * {@link Widget#onAttach()} to preserve the <code>onAttach</code> contract.
   */
  @Override
  protected void onLoad() {
    setEventListener(inputElem, this);
  }

  /**
   * This method is called when a widget is detached from the browser's
   * document. Overridden because of IE bug that throws away checked state and
   * in order to clear the event listener off of the <code>inputElem</code>.
   */
  @Override
  protected void onUnload() {
    // Clear out the inputElem's event listener (breaking the circular
    // reference between it and the widget).
    setEventListener(asOld(inputElem), null);
    setValue(getValue());
  }

  /**
   * Replace the current input element with a new one. Preserves
   * all state except for the name property, for nasty reasons
   * related to radio button grouping. (See implementation of 
   * {@link RadioButton#setName}.)
   * 
   * @param elem the new input element
   */
  protected void replaceInputElement(Element elem) {
    InputElement newInputElem = InputElement.as(elem);
    // Collect information we need to set
    int tabIndex = getTabIndex();
    String checked = getValue();
    boolean enabled = isEnabled();
    String formValue = getFormValue();
    String uid = inputElem.getId();
    String accessKey = inputElem.getAccessKey();
    int sunkEvents = Event.getEventsSunk(inputElem);   

    // Clear out the old input element
    setEventListener(asOld(inputElem), null);

    getElement().replaceChild(newInputElem, inputElem);

    // Sink events on the new element
    Event.sinkEvents(elem, Event.getEventsSunk(inputElem));
    Event.sinkEvents(inputElem, 0);
    inputElem = newInputElem;

    // Setup the new element
    Event.sinkEvents(inputElem, sunkEvents);
    inputElem.setId(uid);
    if (!accessKey.equals("")) {
      inputElem.setAccessKey(accessKey);
    }
    setTabIndex(tabIndex);
    setValue(checked);
    setEnabled(enabled);
    setFormValue(formValue);

    // Set the event listener
    if (isAttached()) {
      setEventListener(asOld(inputElem), this);
    }
  }

  private Element asOld(com.google.gwt.dom.client.Element elem) {
    Element oldSchool = elem.cast();
    return oldSchool;
  }

  private void setEventListener(com.google.gwt.dom.client.Element e,
      EventListener listener) {
    DOM.setEventListener(asOld(e), listener);
  }

  @Override
  public HandlerRegistration addChangeHandler(ChangeHandler handler) {
      return addDomHandler(handler, ChangeEvent.getType());
  }
}


The answer below is left for those that prefer to use "standard" GWT Widgets and/or prefer to do it the other way :)

You can easily create a <label> element with DOM.createLabel():

LabelElement label = DOM.createLabel().cast();
label.setHtmlFor("inputId");

But I'd stick with the Widgets provided by GWT - they were built and chosen by the GWT so that they will look and behave exactly the same in all supported browsers. The approach they chose (for example, if you place an Image inline, it will be wrapped inside a table, iirc - because setting it inline via display:inline will not work in all browsers :cough:IE:cough:).

tl;dr: unless you have a very specific need (like creating your own, low-level elements), stick with the provided Widgets (or create own via Composite) - you'll benefit more.

PS: And if you are worried about web standards, accessibility, etc - don't, for example, most standard GWT widgets support ARIA - something you would have to do yourself, had you built your own components.

Edit: answer to AlexJReid's comment:

You can send data via form using FormPanel (it's worth noting that this way it will work on all browsers, because, unlike other browsers, IE6 fires a different event then the other browsers; additionally, the form's target will be set to an iframe - thanks to that, the page won't have to reload - that would beat the purpose of AJAX :)):

final FormPanel form = new FormPanel();
form.setAction("page.php");

TextBox box = new TextBox();
box.setName("name");
box.setText("fasdf");

Button button = new Button("Send", new ClickHandler() {
    @Override
    public void onClick(ClickEvent event) {
     form.submit();
    }
});

form.add(box);
form.add(button);

Note the box.setName("name"); line - this is were you set the name that will be used for the value of that TextBox when you submit this form. So, what Widgets does FormPanel support? Those that implement the com.google.gwt.user.client.ui.HasName interface:

  • TextBox
  • PasswordTextBox
  • RadioButton
  • SimpleRadioButton
  • CheckBox
  • SimpleCheckBox
  • TextArea
  • ListBox
  • FileUpload
  • Hidden

(you can add, of course, any Widget, but only the values of those above will be sent)

Last thing: unless you really have to use form (like when sending files, or something similar), the RequestBuilder might be worth trying - it's using the XmlHttpRequest behing the hood - the mother/father of AJAX ;)

Igor Klimer
Sticking only to widgets provided by GWT, how would you go about creating a form consisting of labels and inputs?
AlexJReid
I think you missed the point of the question. He seemed to have a working form, he just wanted the text label beside a TextBox to be a <label> so then the browser would automatically do the following: When clicking on the label, focus goes to the TextBox
Steve Armstrong
Wow, I never actually realized you could do that :) (then again, I don't see why one would click on the label, instead of the text field). The "cleanest" way to do this in GWT is probably to create a Composite that holds an Label and a TextBox, add a ClickHandler to the Label and voila ;)
Igor Klimer
As Steve says, I am familiar with building forms - I just wanted to use <label for=""> without having to manually register click handlers. This is still useful information for people trying to figure out FormPanel though.
AlexJReid
The InputLabel example has some problems: It's based on CheckBox, which means using setting it as 'text' input means a complete different implementation, one based on TextBoxBase. It's better to create a Widget, that extends composite, where you can set an input widget (e.g. setInputWidget or directly in the constructor) and internally place a label tag before that widget in the DOM. This means you can strip almost all code and use the standard GWT input widgets.
Hilbrand
I've made this answer community wiki, so you can edit it if you feel like it :) But this implementation should work for all text based `<input>` elements - if you make the `InputLabel(Element elem)` constructor publicly available and pass it an appropriate `InputElement` (like `DOM.createInputPassword()`). As for your "generic" approach - I don't think it would work for `<input type='file'>`. Either way, my example is just a hack that does it job - a more elegant solution is alway welcome :)
Igor Klimer
A: 

As far as I can tell there is no "label" widget, so you can create one yourself. Using the following example is a better way than setting via HTML. It will generate a real "label" tag.

import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.LabelElement;
import com.google.gwt.user.client.ui.SimplePanel;

public class LabelWidget extends SimplePanel {
  public LabelWidget() {
    super((Element) Document.get().createLabelElement().cast());
  }

  public void setHtmlFor(String htmlFor) {
     ((LabelElement)getElement().cast()).setHtmlFor(htmlFor);
  }
}

This class allows any widget to be set as content. If you also want to be able to set plain HTML as inner content (the labelText in your example) add implements HasHTML. Extending from HTML doesn't work since you can't call the super constructor with something other than div or span.

You could also combine a label tag with the input widget in one new Widget. If you use that combination more often in you project.

Disclaimer. I've not tested the code...

Hilbrand