UiBinder Optimizations
+ Decrease number of attachments to get fields by id
General algorithm is:
- Attach the html to the DOM
- Pluck out all DOM fields
- Pluck out all fields for widget replacement
- Detach the HTML
- Finalize replacement of elements with widgets
patch by: tstanis at google.com
review by: rjrjr at google.com
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@6307 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/tools/api-checker/config/gwt16_20userApi.conf b/tools/api-checker/config/gwt16_20userApi.conf
index 0794a4b..04471a9 100644
--- a/tools/api-checker/config/gwt16_20userApi.conf
+++ b/tools/api-checker/config/gwt16_20userApi.conf
@@ -90,3 +90,5 @@
com.google.gwt.i18n.client.LocaleInfo::getCurrentLocale() FINAL_ADDED
com.google.gwt.i18n.client.LocaleInfo::getLocaleName() FINAL_ADDED
com.google.gwt.i18n.client.LocaleInfo::isRTL() FINAL_ADDED
+# added addAndReplaceElement(Widget, Element) in 2.0
+com.google.gwt.user.client.ui.HTMLPanel::addAndReplaceElement(Lcom/google/gwt/user/client/ui/Widget;Ljava/lang/String;) OVERLOADED_METHOD_CALL
diff --git a/user/src/com/google/gwt/uibinder/client/UiBinderUtil.java b/user/src/com/google/gwt/uibinder/client/UiBinderUtil.java
index 55763d5..02459b9 100644
--- a/user/src/com/google/gwt/uibinder/client/UiBinderUtil.java
+++ b/user/src/com/google/gwt/uibinder/client/UiBinderUtil.java
@@ -26,12 +26,49 @@
* so please don't use them for non-UiBinder code.
*/
public class UiBinderUtil {
+ /**
+ * Temporary attachment record that keeps track of where an element was
+ * before attachment. Use the detach method to put things back.
+ *
+ */
+ public static class TempAttachment {
+ private final Element element;
+ private final Element origParent;
+ private final Element origSibling;
+
+ private TempAttachment(Element origParent, Element origSibling,
+ Element element) {
+ this.origParent = origParent;
+ this.origSibling = origSibling;
+ this.element = element;
+ }
+
+ /**
+ * Restore to previous DOM state before attachment.
+ */
+ public void detach() {
+ // Put the panel's element back where it was.
+ if (origParent != null) {
+ origParent.insertBefore(element, origSibling);
+ } else {
+ orphan(element);
+ }
+ }
+ }
+
private static Element hiddenDiv;
-
- public static Element attachToDomAndGetChild(Element element, String id) {
+
+ /**
+ * Attaches the element to the dom temporarily. Keeps track of where it is
+ * attached so that things can be put back latter.
+ *
+ * @return attachment record which can be used for reverting back to previous
+ * DOM state
+ */
+ public static TempAttachment attachToDom(Element element) {
// TODO(rjrjr) This is copied from HTMLPanel. Reconcile
ensureHiddenDiv();
-
+
// Hang on to the panel's original parent and sibling elements so that it
// can be replaced.
Element origParent = element.getParentElement();
@@ -39,19 +76,10 @@
// Attach the panel's element to the hidden div.
hiddenDiv.appendChild(element);
-
- // Now that we're attached to the DOM, we can use getElementById.
- Element child = Document.get().getElementById(id);
-
- // Put the panel's element back where it was.
- if (origParent != null) {
- origParent.insertBefore(element, origSibling);
- } else {
- orphan(element);
- }
-
- return child;
+
+ return new TempAttachment(origParent, origSibling, element);
}
+
public static Element fromHtml(String html) {
ensureHiddenDiv();
diff --git a/user/src/com/google/gwt/uibinder/parsers/DomElementParser.java b/user/src/com/google/gwt/uibinder/parsers/DomElementParser.java
index aab67e0..c6858f2 100644
--- a/user/src/com/google/gwt/uibinder/parsers/DomElementParser.java
+++ b/user/src/com/google/gwt/uibinder/parsers/DomElementParser.java
@@ -23,22 +23,24 @@
/**
* Parses a dom element and all of its children. Note that this parser does not
* make recursive calls to parse child elements, unlike what goes on with widget
- * parsers. Instead, we consume the inner html of the given element into
- * a single string literal, used to instantiate the dom tree at run time.
+ * parsers. Instead, we consume the inner html of the given element into a
+ * single string literal, used to instantiate the dom tree at run time.
*/
public class DomElementParser implements ElementParser {
public void parse(XMLElement elem, String fieldName, JClassType type,
UiBinderWriter writer) throws UnableToCompleteException {
- HtmlInterpreter interpreter =
- new HtmlInterpreter(writer, fieldName, new HtmlMessageInterpreter(writer,
- fieldName));
+ HtmlInterpreter interpreter = new HtmlInterpreter(writer, fieldName,
+ new HtmlMessageInterpreter(writer, fieldName));
interpreter.interpretElement(elem);
+ writer.beginAttachedSection(fieldName);
String html = elem.consumeOpeningTag() + elem.consumeInnerHtml(interpreter)
- + elem.getClosingTag();
+ + elem.getClosingTag();
+ writer.endAttachedSection();
writer.setFieldInitializer(fieldName, String.format(
- "(%1$s) UiBinderUtil.fromHtml(\"%2$s\")", type.getQualifiedSourceName(), html));
+ "(%1$s) UiBinderUtil.fromHtml(\"%2$s\")",
+ type.getQualifiedSourceName(), html));
}
}
diff --git a/user/src/com/google/gwt/uibinder/parsers/HTMLPanelParser.java b/user/src/com/google/gwt/uibinder/parsers/HTMLPanelParser.java
index 759e009..6aba4e7 100644
--- a/user/src/com/google/gwt/uibinder/parsers/HTMLPanelParser.java
+++ b/user/src/com/google/gwt/uibinder/parsers/HTMLPanelParser.java
@@ -46,8 +46,10 @@
*/
HtmlInterpreter htmlInterpreter = makeHtmlInterpreter(fieldName, writer);
+ writer.beginAttachedSection(fieldName + ".getElement()");
String html = elem.consumeInnerHtml(InterpreterPipe.newPipe(widgetInterpreter,
htmlInterpreter));
+ writer.endAttachedSection();
/*
* HTMLPanel has no no-arg ctor, so we have to generate our own, using the
diff --git a/user/src/com/google/gwt/uibinder/parsers/HasHTMLParser.java b/user/src/com/google/gwt/uibinder/parsers/HasHTMLParser.java
index 5d06369..67b44e6 100644
--- a/user/src/com/google/gwt/uibinder/parsers/HasHTMLParser.java
+++ b/user/src/com/google/gwt/uibinder/parsers/HasHTMLParser.java
@@ -30,8 +30,9 @@
HtmlInterpreter interpreter =
HtmlInterpreter.newInterpreterForUiObject(writer, fieldName);
+ writer.beginAttachedSection(fieldName + ".getElement()");
String html = elem.consumeInnerHtml(interpreter);
-
+ writer.endAttachedSection();
// TODO(jgw): throw an error if there's a conflicting 'html' attribute.
if (html.trim().length() > 0) {
writer.genStringPropertySet(fieldName, "HTML", html);
diff --git a/user/src/com/google/gwt/uibinder/parsers/WidgetInterpreter.java b/user/src/com/google/gwt/uibinder/parsers/WidgetInterpreter.java
index efe2bab..08b0784 100644
--- a/user/src/com/google/gwt/uibinder/parsers/WidgetInterpreter.java
+++ b/user/src/com/google/gwt/uibinder/parsers/WidgetInterpreter.java
@@ -18,7 +18,6 @@
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.uibinder.rebind.UiBinderWriter;
import com.google.gwt.uibinder.rebind.XMLElement;
-import com.google.gwt.uibinder.rebind.XMLElement.PostProcessingInterpreter;
import java.util.Collections;
import java.util.HashMap;
@@ -29,7 +28,7 @@
* instances. Declares the appropriate widget, and replaces them in the markup
* with a <span>.
*/
-class WidgetInterpreter implements PostProcessingInterpreter<String> {
+class WidgetInterpreter implements XMLElement.Interpreter<String> {
private static final Map<String, String> LEGAL_CHILD_ELEMENTS;
private static final String DEFAULT_CHILD_ELEMENT = "span";
@@ -58,11 +57,6 @@
return tag;
}
- /**
- * A map of widget idHolder field names to the element it references.
- */
- private final Map<String, XMLElement> idToWidgetElement =
- new HashMap<String, XMLElement>();
private final String fieldName;
private final UiBinderWriter uiWriter;
@@ -72,14 +66,28 @@
this.uiWriter = writer;
}
- public String interpretElement(XMLElement elem) throws UnableToCompleteException {
+ public String interpretElement(XMLElement elem)
+ throws UnableToCompleteException {
if (uiWriter.isWidgetElement(elem)) {
// Allocate a local variable to hold the dom id for this widget. Note
// that idHolder is a local variable reference, not a string id. We
// have to generate the ids at runtime, not compile time, or else
// we'll reuse ids for any template rendered more than once.
String idHolder = uiWriter.declareDomIdHolder();
- idToWidgetElement.put(idHolder, elem);
+ String childField = uiWriter.parseElementToField(elem);
+ uiWriter.ensureFieldAttached(fieldName);
+
+ String elementPointer = idHolder + "Element";
+ uiWriter.addInitStatement(
+ "com.google.gwt.user.client.Element %s = " +
+ "com.google.gwt.dom.client.Document.get().getElementById(%s).cast();",
+ elementPointer, idHolder);
+ // Delay replacing the placeholders with the widgets until after
+ // detachment so as not to end up attaching the widget to the DOM
+ // unnecessarily
+ uiWriter.addDetachStatement(
+ "%1$s.addAndReplaceElement(%2$s, %3$s);",
+ fieldName, childField, elementPointer);
// Create an element to hold the widget.
String tag = getLegalPlaceholderTag(elem);
@@ -87,25 +95,4 @@
}
return null;
}
-
- /**
- * Called by {@link XMLElement#consumeInnerHtml} after all elements have
- * been handed to {@link #interpretElement}. Parses each widget element
- * that was seen, and generates a
- * {@link com.google.gwt.user.client.ui.HTMLPanel#addAndReplaceElement} for
- * each.
- */
- public String postProcess(String consumedHtml) throws UnableToCompleteException {
- /*
- * Generate an addAndReplaceElement(widget, idHolder) for each widget found
- * while parsing the element's inner HTML.
- */
- for (String idHolder : idToWidgetElement.keySet()) {
- XMLElement childElem = idToWidgetElement.get(idHolder);
- String childField = uiWriter.parseElementToField(childElem);
- uiWriter.addInitStatement("%1$s.addAndReplaceElement(%2$s, %3$s);", fieldName,
- childField, idHolder);
- }
- return consumedHtml;
- }
}
diff --git a/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java b/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java
index 739f365..4b3c54a 100644
--- a/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java
+++ b/user/src/com/google/gwt/uibinder/rebind/UiBinderWriter.java
@@ -241,6 +241,22 @@
private String rendered;
+ /**
+ * Stack of element variable names that have been attached.
+ */
+ private final LinkedList<String> attachSectionElements = new LinkedList<String>();
+ /**
+ * Maps from field element name to the temporary attach record variable name.
+ */
+ private final Map<String, String> attachedVars = new HashMap<String, String>();
+ private int nextAttachVar = 0;
+
+ /**
+ * Stack of statements to be executed after we detach the current attach
+ * section.
+ */
+ private final LinkedList<List<String>> detachStatementsStack = new LinkedList<List<String>>();
+
UiBinderWriter(JClassType baseClass, String implClassName,
String templatePath, TypeOracle oracle, MortalLogger logger)
throws UnableToCompleteException {
@@ -266,6 +282,18 @@
}
/**
+ * Statements to be excuted right after the current attached element is
+ * detached. This is useful for doing things that might be expensive while the
+ * element is attached to the DOM.
+ *
+ * @param format
+ * @param args
+ */
+ public void addDetachStatement(String format, Object... args) {
+ detachStatementsStack.getFirst().add(String.format(format, args));
+ }
+
+ /**
* Add a statement to be run after everything has been instantiated, in the
* style of {@link String#format}
*/
@@ -282,6 +310,17 @@
}
/**
+ * Begin a section where a new attachable element is being parsed. Note that
+ * attachment is only done when actually needed.
+ *
+ * @param element to be attached for this section
+ */
+ public void beginAttachedSection(String element) {
+ attachSectionElements.addFirst(element);
+ detachStatementsStack.addFirst(new ArrayList<String>());
+ }
+
+ /**
* Declare a field that will hold an Element instance. Returns a token that
* the caller must set as the id attribute of that element in whatever
* innerHTML expression will reproduce it at runtime.
@@ -298,11 +337,12 @@
*/
public String declareDomField(String fieldName, String parentElementExpression)
throws UnableToCompleteException {
+ ensureAttached(parentElementExpression);
String name = declareDomIdHolder();
setFieldInitializer(fieldName, "null");
addInitStatement(
- "%s = UiBinderUtil.attachToDomAndGetChild(%s, %s).cast();", fieldName,
- parentElementExpression, name);
+ "%s = com.google.gwt.dom.client.Document.get().getElementById(%s).cast();",
+ fieldName, name);
addInitStatement("%s.removeAttribute(\"id\");", fieldName);
return tokenForExpression(name);
}
@@ -391,6 +431,48 @@
}
/**
+ * End the current attachable section. This will detach the element if it was
+ * ever attached and execute any detach statements.
+ */
+ public void endAttachedSection() {
+ String elementVar = attachSectionElements.removeFirst();
+ List<String> detachStatements = detachStatementsStack.removeFirst();
+ if (attachedVars.containsKey(elementVar)) {
+ String attachedVar = attachedVars.remove(elementVar);
+ addInitStatement("%s.detach();", attachedVar);
+ for (String statement : detachStatements) {
+ addInitStatement(statement);
+ }
+ }
+ }
+
+ /**
+ * Ensure that the specified element is attached to the DOM.
+ *
+ * @param element variable name of element to be attached
+ */
+ public void ensureAttached(String element) {
+ String attachSectionElement = attachSectionElements.getFirst();
+ if (!attachedVars.containsKey(attachSectionElement)) {
+ String attachedVar = "attachRecord" + nextAttachVar;
+ addInitStatement(
+ "UiBinderUtil.TempAttachment %s = UiBinderUtil.attachToDom(%s);",
+ attachedVar, attachSectionElement);
+ attachedVars.put(attachSectionElement, attachedVar);
+ nextAttachVar++;
+ }
+ }
+
+ /**
+ * Ensure that the specified field is attached to the DOM.
+ *
+ * @param field variable name of the field to be attached
+ */
+ public void ensureFieldAttached(String field) {
+ ensureAttached(field + ".getElement()");
+ }
+
+ /**
* Finds the JClassType that corresponds to this XMLElement, which must be a
* Widget or an Element.
*
@@ -738,6 +820,23 @@
}
/**
+ * Ensures that all of the internal data structures are cleaned up correctly
+ * at the end of parsing the document.
+ *
+ * @throws UnableToCompleteException
+ */
+ private void ensureAttachmentCleanedUp() throws UnableToCompleteException {
+ if (!attachSectionElements.isEmpty()) {
+ throw new IllegalStateException("Attachments not cleaned up: "
+ + attachSectionElements);
+ }
+ if (!detachStatementsStack.isEmpty()) {
+ throw new IllegalStateException("Detach not cleaned up: "
+ + detachStatementsStack);
+ }
+ }
+
+ /**
* Given a DOM tag name, return the corresponding
* {@link com.google.gwt.dom.client.Element} subclass.
*/
@@ -898,9 +997,9 @@
StringWriter stringWriter = new StringWriter();
IndentedWriter niceWriter = new IndentedWriter(
new PrintWriter(stringWriter));
-
writeBinder(niceWriter, rootField);
+ ensureAttachmentCleanedUp();
return stringWriter.toString();
}
@@ -986,7 +1085,6 @@
addWidgetParser("RadioButton");
addWidgetParser("CellPanel");
addWidgetParser("CustomButton");
-
addWidgetParser("DockLayoutPanel");
addWidgetParser("StackLayoutPanel");
diff --git a/user/src/com/google/gwt/user/client/ui/HTMLPanel.java b/user/src/com/google/gwt/user/client/ui/HTMLPanel.java
index a00b1c5..a5805fc 100644
--- a/user/src/com/google/gwt/user/client/ui/HTMLPanel.java
+++ b/user/src/com/google/gwt/user/client/ui/HTMLPanel.java
@@ -108,6 +108,20 @@
}
/**
+ * Adds a child widget to the panel, replacing the HTML element.
+ *
+ * @param widget the widget to be added
+ * @param toReplace the element to be replaced by the widget
+ */
+ public void addAndReplaceElement(Widget widget, Element toReplace) {
+ // Logic pulled from super.add(), replacing the element rather than adding.
+ widget.removeFromParent();
+ getChildren().add(widget);
+ toReplace.getParentNode().replaceChild(widget.getElement(), toReplace);
+ adopt(widget);
+ }
+
+ /**
* Adds a child widget to the panel, replacing the HTML element specified by a
* given id.
*
@@ -120,14 +134,10 @@
if (toReplace == null) {
throw new NoSuchElementException(id);
}
-
- // Logic pulled from super.add(), replacing the element rather than adding.
- widget.removeFromParent();
- getChildren().add(widget);
- toReplace.getParentNode().replaceChild(widget.getElement(), toReplace);
- adopt(widget);
+
+ addAndReplaceElement(widget, toReplace);
}
-
+
/**
* Finds an {@link Element element} within this panel by its id.
*
diff --git a/user/test/com/google/gwt/user/client/ui/HTMLPanelTest.java b/user/test/com/google/gwt/user/client/ui/HTMLPanelTest.java
index 13993e8..d70f1bf 100644
--- a/user/test/com/google/gwt/user/client/ui/HTMLPanelTest.java
+++ b/user/test/com/google/gwt/user/client/ui/HTMLPanelTest.java
@@ -120,6 +120,27 @@
}
/**
+ * Ensures that addAndReplaceChild() puts the widget in exactly the right place in the DOM.
+ */
+ public void testAddAndReplaceElementForElement() {
+ HTMLPanel hp = new HTMLPanel("<div id='parent'>foo<span id='placeholder'></span>bar</div>");
+
+ RootPanel.get().add(hp);
+ com.google.gwt.user.client.Element placeholder = hp.getElementById("placeholder");
+
+ Button button = new Button("my button");
+ hp.addAndReplaceElement(button, placeholder);
+
+ assertEquals("parent", button.getElement().getParentElement().getId());
+
+ Node prev = button.getElement().getPreviousSibling();
+ assertEquals("foo", prev.getNodeValue());
+
+ Node next = button.getElement().getNextSibling();
+ assertEquals("bar", next.getNodeValue());
+ }
+
+ /**
* Tests table root tag.
*/
public void testCustomRootTagAsTable() {