UiCustomCells

如果您在应用程序中使用 CellTable 或其他单元格小部件,您可能希望创建针对您的数据量身定制的自定义单元格。单元格就像小部件轻量级对象,因此单个单元格可以渲染其自身的多个 DOM 实例,并且可以处理多个渲染实例的事件。这种模式非常适合 MVP 设计,正如我们将在此处展示的那样。

本开发者指南介绍了创建简单的一次性单元格以及创建可“换肤”的可重用单元格的模式。如果您不熟悉单元格小部件,您应该在继续之前阅读 单元格小部件开发者指南

  1. 单元格基础
    1. 实现 render() 方法
    2. 处理来自单元格的事件

单元格基础

实现 render() 方法

所有单元格都必须实现的核心方法是 Cell#render(Context, C value, SafeHtmlBuilder) 方法,该方法将参数化的 value 渲染到 SafeHtmlBuilder 中。在某些情况下,您只需要渲染简单的 HTML,例如包含一些内容的单个 div。在其他情况下,您可能需要渲染复杂的 HTML 结构,例如 CellList 示例中的结构,该结构包含一个图像和两行文本。无论哪种情况,基本原理都是相同的。

代码示例

ColorCell 是一个自定义单元格,它扩展了 AbstractCell<String> 并生成一个包含颜色名称的单个 div,并使用指定的颜色作为文本颜色进行样式设置。为每个渲染的值生成的 HTML 是 <div style="color: value">value</div>,其中 value 是单元格的颜色。在下面的示例中,ColorCell 传递给 CellList 以渲染各种颜色。第一行包含字符串“红色”,颜色为红色,第二行是“绿色”,颜色为绿色,依此类推,如下所示。

img

在实现渲染方法时,您应该遵循以下步骤

  1. 创建一个 AbstractCell 的子类,并使用要渲染的值类型进行参数化。

    **WARNING:** Implement the Cell interface directly at your own risk. The Cell interface may change in subtle but breaking ways as we
    continuously seek to improve performance. We provide AbstractCell so we can modify the Cell interface without breaking your code.
    
  2. 在渲染方法中,始终检查值是否为 null。即使您在数据中不包含空值,单元格小部件也可能会将 null 值传递到您的单元格以渲染“填充”单元格,例如,当您传递数据在可见范围中间时。您可以从渲染方法中提前退出,或者可以渲染一些占位符值。

  3. 如果值来自用户数据,请确保使用 SafeHtmlUtils 或其他转义库之一对其进行转义。渲染的单元格将被添加到文档中,不会进行额外的转义,因此,如果您的单元格渲染恶意 javascript 代码,它将被包含在文档中。 SafeHtmlBuilder 尝试强制执行此操作。
  4. 将表示单元格的 HTML 渲染到 SafeHtmlBuilder 中。

您可以在 CellExample.java 中下载此示例。

/**
 * Example of creating a custom {@link Cell}.
 */
public class CellExample implements EntryPoint {

  /**
   * A custom {@link Cell} used to render a string that contains the name of a
   * color.
   */
  static class ColorCell extends AbstractCell<String> {

    /**
     * The HTML templates used to render the cell.
     */
    interface Templates extends SafeHtmlTemplates {
      /**
       * The template for this Cell, which includes styles and a value.
       * 
       * @param styles the styles to include in the style attribute of the div
       * @param value the safe value. Since the value type is {@link SafeHtml},
       *          it will not be escaped before including it in the template.
       *          Alternatively, you could make the value type String, in which
       *          case the value would be escaped.
       * @return a {@link SafeHtml} instance
       */
      @SafeHtmlTemplates.Template("<div style=\"{0}\">{1}</div>")
      SafeHtml cell(SafeStyles styles, SafeHtml value);
    }

    /**
     * Create a singleton instance of the templates used to render the cell.
     */
    private static Templates templates = GWT.create(Templates.class);

    @Override
    public void render(Context context, String value, SafeHtmlBuilder sb) {
      /*
       * Always do a null check on the value. Cell widgets can pass null to
       * cells if the underlying data contains a null, or if the data arrives
       * out of order.
       */
      if (value == null) {
        return;
      }

      // If the value comes from the user, we escape it to avoid XSS attacks.
      SafeHtml safeValue = SafeHtmlUtils.fromString(value);

      // Use the template to create the Cell's html.
      SafeStyles styles = SafeStylesUtils.forTrustedColor(safeValue.asString());
      SafeHtml rendered = templates.cell(styles, safeValue);
      sb.append(rendered);
    }
  }

  /**
   * The list of data to display.
   */
  private static final List<String> COLORS = Arrays.asList("red", "green", "blue", "violet",
      "black", "gray");

  @Override
  public void onModuleLoad() {
    // Create a cell to render each value.
    ColorCell cell = new ColorCell();

    // Use the cell in a CellList.
    CellList<String> cellList = new CellList<String>(cell);

    // Push the data into the widget.
    cellList.setRowData(0, COLORS);

    // Add it to the root panel.
    RootPanel.get().add(cellList);
  }
}

处理来自单元格的事件

要实现的另一个核心方法(如果您想在单元格中处理事件)是 Cell#onBrowserEvent(Context, Element, C value, NativeEvent, ValueUpdater)。当在使用此单元格的 HTML 输出创建的任何 DOM 实例上发生事件时,将调用 onBrowserEvent。由于单元格是处理来自多个实例的事件的轻量级对象,因此有关实例的上下文信息将传递到该方法中。例如,Context 会告诉您单元格在其包含的 CellTable 中的位置(行和列索引)。

父元素指的是包含来自您的单元格的渲染 HTML 的 DOM 元素。它不是您单元格的最外层元素。单元格不必只有一个最外层元素。例如,TextCell 甚至不渲染 HTML 元素,它只渲染文本字符串。

指定要处理的事件

单元格必须声明它们要处理的事件类型,包含单元格的单元格小部件负责确保只有这些事件传递到单元格。为了指定您的单元格将处理哪些事件,您将事件类型传递到 AbstractCell(String…) 构造函数中。当调用 Cell#getConsumedEvents() 时,AbstractCell 会将事件返回到单元格小部件。

或者,您可以直接覆盖 Cell#getConsumedEvents()。例如,如果您正在扩展现有的单元格(它不公开事件类型构造函数),您可以覆盖 getConsumedEvents() 以返回您的子类使用的事件。

警告:getConsumedEvents() 的返回值在后续调用中不应更改。单元格小部件只需要在单元格第一次添加时调用该方法一次。

代码示例

下面的代码示例扩展了 ColorCell 以添加对处理 clickkeydown 事件的支持。如果用户单击单元格,我们将显示一个警报框,告诉我们我们选择了哪种颜色。

img

添加事件支持的过程如下

  1. 通过将它们传递到 AbstractCell(String…) 构造函数中来指定您要处理的事件。请参见下面关于 keydown 事件的说明。
  2. 覆盖 Cell#onBrowserEvent() 方法以处理事件。对于某些事件,您可能需要查看事件目标,然后再决定如何处理事件。在下面的示例中,我们只响应实际发生在渲染的 div 上的单击事件。
  3. 如以下示例注释中所述,onEnterKeyDown() 方法是 AbstractCell 中的一种特殊的便利方法,它有助于提供统一的用户体验,这样所有响应事件的单元格都可以通过使用键盘选择单元格并按 Enter 键来激活。否则,用户将无法在不使用鼠标的情况下与单元格进行交互。GWT 单元格遵循的惯例是在按下 Enter 键时切换编辑,但您可以自由地为您的应用定义行为。您的单元格的 getConsumedEvents() 方法必须包含“keydown”,以便 AbstractCell 调用 onEnterKeyDown()
  4. 可以选择调用 ValueUpdater#update(C) 以指示单元格的值已修改。例如,如果用户在文本框中键入新值,或者如果用户单击按钮,您可能会调用 update() 方法。
/**
 * Example of creating a custom {@link Cell} that responds to events.
 */
public class CellWithEventsExample implements EntryPoint {

  /**
   * A custom {@link Cell} used to render a string that contains the name of a
   * color.
   */
  static class ColorCell extends AbstractCell<String> {

    /**
     * The HTML templates used to render the cell.
     */
    interface Templates extends SafeHtmlTemplates {
      /**
       * The template for this Cell, which includes styles and a value.
       * 
       * @param styles the styles to include in the style attribute of the div
       * @param value the safe value. Since the value type is {@link SafeHtml},
       *          it will not be escaped before including it in the template.
       *          Alternatively, you could make the value type String, in which
       *          case the value would be escaped.
       * @return a {@link SafeHtml} instance
       */
      @SafeHtmlTemplates.Template("<div style=\"{0}\">{1}</div>")
      SafeHtml cell(SafeStyles styles, SafeHtml value);
    }

    /**
     * Create a singleton instance of the templates used to render the cell.
     */
    private static Templates templates = GWT.create(Templates.class);

    public ColorCell() {
      /*
       * Sink the click and keydown events. We handle click events in this
       * class. AbstractCell will handle the keydown event and call
       * onEnterKeyDown() if the user presses the enter key while the cell is
       * selected.
       */
      super("click", "keydown");
    }

    /**
     * Called when an event occurs in a rendered instance of this Cell. The
     * parent element refers to the element that contains the rendered cell, NOT
     * to the outermost element that the Cell rendered.
     */
    @Override
    public void onBrowserEvent(Context context, Element parent, String value, NativeEvent event,
        ValueUpdater<String> valueUpdater) {
      // Let AbstractCell handle the keydown event.
      super.onBrowserEvent(context, parent, value, event, valueUpdater);

      // Handle the click event.
      if ("click".equals(event.getType())) {
        // Ignore clicks that occur outside of the outermost element.
        EventTarget eventTarget = event.getEventTarget();
        if (parent.getFirstChildElement().isOrHasChild(Element.as(eventTarget))) {
          doAction(value, valueUpdater);
        }
      }
    }

    @Override
    public void render(Context context, String value, SafeHtmlBuilder sb) {
      /*
       * Always do a null check on the value. Cell widgets can pass null to
       * cells if the underlying data contains a null, or if the data arrives
       * out of order.
       */
      if (value == null) {
        return;
      }

      // If the value comes from the user, we escape it to avoid XSS attacks.
      SafeHtml safeValue = SafeHtmlUtils.fromString(value);

      // Use the template to create the Cell's html.
      SafeStyles styles = SafeStylesUtils.forTrustedColor(safeValue.asString());
      SafeHtml rendered = templates.cell(styles, safeValue);
      sb.append(rendered);
    }

    /**
     * onEnterKeyDown is called when the user presses the ENTER key will the
     * Cell is selected. You are not required to override this method, but its a
     * common convention that allows your cell to respond to key events.
     */
    @Override
    protected void onEnterKeyDown(Context context, Element parent, String value, NativeEvent event,
        ValueUpdater<String> valueUpdater) {
      doAction(value, valueUpdater);
    }

    private void doAction(String value, ValueUpdater<String> valueUpdater) {
      // Alert the user that they selected a value.
      Window.alert("You selected the color " + value);

      // Trigger a value updater. In this case, the value doesn't actually
      // change, but we use a ValueUpdater to let the app know that a value
      // was clicked.
      valueUpdater.update(value);
    }
  }

  /**
   * The list of data to display.
   */
  private static final List<String> COLORS = Arrays.asList("red", "green", "blue", "violet",
      "black", "gray");

  @Override
  public void onModuleLoad() {
    // Create a cell to render each value.
    ColorCell cell = new ColorCell();

    // Use the cell in a CellList.
    CellList<String> cellList = new CellList<String>(cell);

    // Push the data into the widget.
    cellList.setRowData(0, COLORS);

    // Add it to the root panel.
    RootPanel.get().add(cellList);
  }
}