Support upgrading existing elements already on the page.

R=justinfagnani@google.com

Review URL: https://codereview.chromium.org//333073002

git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart/pkg/web_components@37338 260f80e4-7a28-3924-810f-c04153c831b5
diff --git a/lib/dart_support.js b/lib/dart_support.js
index 81e7d9b..efa1301 100644
--- a/lib/dart_support.js
+++ b/lib/dart_support.js
@@ -70,6 +70,9 @@
 (function (doc) {
   var upgraders = {};       // upgrader associated with a custom-tag.
   var unpatchableTags = {}; // set of custom-tags that can't be patched.
+  var pendingElements = {}; // will upgrade when/if an upgrader is installed.
+  var upgradeOldElements = true;
+
   var originalRegisterElement = doc.registerElement;
   if (!originalRegisterElement) {
     throw new Error('document.registerElement is not present.');
@@ -93,9 +96,19 @@
     var original = proto.createdCallback;
     var newCallback = function() {
       original.call(this);
-      var name = this.getAttribute('is') || this.localName;
-      var upgrader = upgraders[name.toLowerCase()];
-      if (upgrader) upgrader(this);
+      var name = (this.getAttribute('is') || this.localName).toLowerCase();
+      var upgrader = upgraders[name];
+      if (upgrader) {
+        upgrader(this);
+      } else if (upgradeOldElements) {
+        // Save this element in case we can upgrade it later when an upgrader is
+        // registered.
+        var list = pendingElements[name];
+        if (!list) {
+          list = pendingElements[name] = [];
+        }
+        list.push(this);
+      }
     };
 
     var descriptor = Object.getOwnPropertyDescriptor(proto, 'createdCallback');
@@ -121,8 +134,25 @@
     }
     upgraders[name] = upgrader;
     if (unpatchableTags[name]) reportError(name);
+    if (upgradeOldElements) {
+      // Upgrade elements that were created before the upgrader was registered.
+      var list = pendingElements[name];
+      if (list) {
+        for (var i = 0; i < list.length; i++) {
+          upgrader(list[i]);
+        }
+      }
+      delete pendingElements[name];
+    } else {
+      console.warn("Didn't expect more Dart types to be registered. '" + name
+          + "' elements that already exist in the page might not be wrapped.");
+    }
   }
 
+  function onlyUpgradeNewElements() {
+    upgradeOldElements = false;
+    pendingElements = null;
+  }
 
   // Native custom elements outside the app in Chrome have constructor
   // names like "x-tag", which need to be translated to the DOM
@@ -151,5 +181,6 @@
   }
 
   doc._registerDartTypeUpgrader = registerDartTypeUpgrader;
+  doc._onlyUpgradeNewElements = onlyUpgradeNewElements;
   doc.registerElement = registerElement;
 })(document);
diff --git a/lib/interop.dart b/lib/interop.dart
index 058ce76..415c7fe 100644
--- a/lib/interop.dart
+++ b/lib/interop.dart
@@ -31,3 +31,13 @@
       dartType, extendsTag: extendsTag);
   _doc.callMethod('_registerDartTypeUpgrader', [tagName, upgrader.upgrade]);
 }
+
+/// This function is mainly used to save resources. By default, we save a log of
+/// elements that are created but have no Dart type associated with them. This
+/// is so we can upgrade them as soon as [registerDartType] is invoked. This
+/// function can be called to indicate that we no longer are interested in
+/// logging element creations and that it is sufficient to only upgrade new
+/// elements as they are being created. Typically this is called after the last
+/// call to [registerDartType] or as soon as you know that no element will be
+/// created until the call to [registerDartType] is made.
+void onlyUpgradeNewElements() => _doc.callMethod('_onlyUpgradeNewElements');
diff --git a/pubspec.yaml b/pubspec.yaml
index a94addf..b8f0bc5 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: web_components
-version: 0.3.5-dev.1
+version: 0.3.5-dev.2
 author: Polymer.dart Authors <web-ui-dev@dartlang.org>
 homepage: https://www.dartlang.org/polymer-dart/
 description: >
diff --git a/test/interop_test.dart b/test/interop_test.dart
index 64dfb64..cbad6e1 100644
--- a/test/interop_test.dart
+++ b/test/interop_test.dart
@@ -12,15 +12,9 @@
 import 'package:web_components/interop.dart';
 import 'package:web_components/polyfill.dart';
 
-final globalSetup = customElementsReady.then((_) {
-    registerDartType('x-a', XAWrapper);
-    registerDartType('x-b', XBWrapper, extendsTag: 'div');
-    registerDartType('x-c', XCWrapper);
-  });
-
 main() {
   useHtmlConfiguration();
-  setUp(() => globalSetup);
+  setUp(() => customElementsReady);
 
   test('interop is supported', () {
     expect(isSupported, isTrue);
@@ -37,21 +31,53 @@
     expect(b is XBWrapper, isFalse, reason: 'x-b should not be upgraded yet');
     expect(_readX(b), 1);
 
+    var d = document.querySelector('x-d');
+    expect(d is HtmlElement, isTrue, reason: 'x-d is HtmlElement');
+    expect(d is XDWrapper, isFalse, reason: 'x-d should not be upgraded yet');
+    expect(_readX(d), 2);
+
+    /// Note: this registration has a global side-effect and is assumed in the
+    /// following tests.
+    registerDartType('x-a', XAWrapper);
+    registerDartType('x-b', XBWrapper, extendsTag: 'div');
+    registerDartType('x-c', XCWrapper);
+    onlyUpgradeNewElements();
+    registerDartType('x-d', XDWrapper); // late on purpose.
+
+    a = document.querySelector('x-a');
+    expect(a is HtmlElement, isTrue, reason: 'x-a is HtmlElement');
+    expect(a is XAWrapper, isTrue, reason: 'x-a is upgraded to XAWrapper');
+    expect(a.x, 0);
+    expect(a.wrapperCount, 0);
+
+    b = document.querySelector('[is=x-b]');
+    expect(b is DivElement, isTrue, reason: 'x-b is DivElement');
+    expect(b is XBWrapper, isTrue, reason: 'x-b is upgraded to XBWrapper');
+    expect(b.x, 1);
+    expect(b.wrapperCount, 1);
+
+    // x-d was not upgraded because its registration came after we stopped
+    // upgrading old elements:
+    d = document.querySelector('x-d');
+    expect(d is HtmlElement, isTrue, reason: 'x-d is HtmlElement');
+    expect(d is XDWrapper, isFalse, reason: 'x-d should not be upgraded yet');
+    expect(_readX(d), 2);
+
     var c = document.querySelector('x-c');
     expect(c is HtmlElement, isTrue, reason: 'x-c is HtmlElement');
     expect(c is XCWrapper, isFalse, reason: 'x-c should not be upgraded yet');
     expect(_readX(c), null, reason: 'x-c has not been registered in JS yet');
   });
 
-  test('events seen for anything created after registering Dart type', () {
+  test('anything created after registering Dart type is upgraded', () {
     context.callMethod('addA');
     var list = document.querySelectorAll('x-a');
     expect(list.length, 2);
     var a = list[1];
     expect(a is HtmlElement, isTrue, reason: 'x-a is HtmlElement');
     expect(a is XAWrapper, isTrue, reason: 'x-a is upgraded to XAWrapper');
-    expect(a.x, 2);
-    expect(a.wrapperCount, 0);
+    expect(a.x, 3);
+    expect(a.wrapperCount, 2);
 
     context.callMethod('addB');
     list = document.querySelectorAll('[is=x-b]');
@@ -59,8 +85,18 @@
     var b = list[1];
     expect(b is DivElement, isTrue, reason: 'x-b is DivElement');
     expect(b is XBWrapper, isTrue, reason: 'x-b is upgraded to XBWrapper');
-    expect(b.x, 3);
-    expect(b.wrapperCount, 1);
+    expect(b.x, 4);
+    expect(b.wrapperCount, 3);
+
+    // New instances of x-d should be upgraded regardless.
+    context.callMethod('addD');
+    list = document.querySelectorAll('x-d');
+    expect(list.length, 2);
+    var d = list[1];
+    expect(d is HtmlElement, isTrue, reason: 'x-d is HtmlElement');
+    expect(d is XDWrapper, isTrue, reason: 'x-d is upgraded to XDWrapper');
+    expect(d.x, 5);
+    expect(d.wrapperCount, 4);
   });
 
   test('events seen if Dart type is registered before registerElement', () {
@@ -71,8 +107,8 @@
     context.callMethod('registerC');
     c = document.querySelector('x-c');
     expect(c is XCWrapper, isTrue);
-    expect(c.x, 4);
-    expect(c.wrapperCount, 2);
+    expect(c.x, 6);
+    expect(c.wrapperCount, 5);
 
     context.callMethod('addC');
     var list = document.querySelectorAll('x-c');
@@ -81,8 +117,8 @@
     c = list[1];
     expect(c is HtmlElement, isTrue, reason: 'x-c is HtmlElement');
     expect(c is XCWrapper, isTrue, reason: 'x-c is upgraded to XCWrapper');
-    expect(c.x, 5);
-    expect(c.wrapperCount, 3);
+    expect(c.x, 7);
+    expect(c.wrapperCount, 6);
   });
 }
 int _count = 0;
@@ -105,3 +141,7 @@
 class XCWrapper extends HtmlElement with Wrapper {
   XCWrapper.created() : super.created();
 }
+
+class XDWrapper extends HtmlElement with Wrapper {
+  XDWrapper.created() : super.created();
+}
diff --git a/test/interop_test.html b/test/interop_test.html
index 3c4f10e..036c7ef 100644
--- a/test/interop_test.html
+++ b/test/interop_test.html
@@ -27,8 +27,14 @@
     B.prototype.createdCallback = function() { this.x = counter++; };
     B.extends = 'div';
 
+    var D = function() {};
+    D.prototype = Object.create(HTMLElement.prototype);
+    D.prototype.inc = function() { this.x = counter++; };
+    D.prototype.createdCallback = function() { this.inc(); };
+
     document.registerElement('x-a', A);
     document.registerElement('x-b', B);
+    document.registerElement('x-d', D);
 
     function registerC() {
       var proto = Object.create(HTMLElement.prototype, {
@@ -48,10 +54,14 @@
     function addC() {
       document.body.appendChild(document.createElement('x-c'));
     }
+    function addD() {
+      document.body.appendChild(document.createElement('x-d'));
+    }
   </script>
   <x-a id="i1"></x-a>
   <div is="x-b" id="i2"></div>
-  <x-c id="i3"></x-b>
+  <x-c id="i3"></x-c>
+  <x-d id="i4"></x-d>
   <script type="text/javascript"
       src="/root_dart/tools/testing/dart/test_controller.js"></script>
   %TEST_SCRIPTS%