Bug 1460815 - Provide a chrome-only callback on CustomElementRegistry so script can define CE lazily draft
authorTimothy Guan-tin Chien <timdream@gmail.com>
Fri, 11 May 2018 11:23:31 -0700
changeset 796072 211e78e8de641d42d1c8357d7a66e1de63b0859a
parent 795067 a382f8feaba41f1cb1cee718f8815cd672c10f3c
push id110155
push usertimdream@gmail.com
push dateWed, 16 May 2018 23:07:15 +0000
bugs1460815
milestone62.0a1
Bug 1460815 - Provide a chrome-only callback on CustomElementRegistry so script can define CE lazily This patch creates a chrome-only method customElements.setElementCreationCallback() so that custom elements migrated from XBL bindings doesn't have to be define()'d on document loading. With this method, we will set callbacks and the platform will get back to us when it encounters a matched custom element type -- and the callback will load the relevant script. It's important to note that the callback runs after construction of the first element; it will be upgraded when it's being appended. MozReview-Commit-ID: 80z72zwXRlf
dom/base/CustomElementRegistry.cpp
dom/base/CustomElementRegistry.h
dom/tests/mochitest/webcomponents/mochitest.ini
dom/tests/mochitest/webcomponents/test_custom_element_set_element_creation_callback.html
dom/webidl/CustomElementRegistry.webidl
--- a/dom/base/CustomElementRegistry.cpp
+++ b/dom/base/CustomElementRegistry.cpp
@@ -234,23 +234,25 @@ private:
 
 // Only needed for refcounted objects.
 NS_IMPL_CYCLE_COLLECTION_CLASS(CustomElementRegistry)
 
 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(CustomElementRegistry)
   tmp->mConstructors.clear();
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mCustomDefinitions)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mWhenDefinedPromiseMap)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mElementCreationCallbacks)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow)
   NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(CustomElementRegistry)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCustomDefinitions)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWhenDefinedPromiseMap)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mElementCreationCallbacks)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(CustomElementRegistry)
   for (ConstructorMap::Enum iter(tmp->mConstructors); !iter.empty(); iter.popFront()) {
     aCallbacks.Trace(&iter.front().mutableKey(),
                      "mConstructors key",
                      aClosure);
@@ -296,21 +298,50 @@ CustomElementRegistry::IsCustomElementEn
 {
   if (nsContentUtils::IsCustomElementsEnabled()) {
     return true;
   }
 
   return XRE_IsParentProcess() && nsContentUtils::AllowXULXBLForPrincipal(aDoc->NodePrincipal());
 }
 
+NS_IMETHODIMP
+CustomElementRegistry::RunCustomElementCreationCallback::Run()
+{
+  ErrorResult er;
+  nsDependentAtomString value(mAtom);
+  mCallback->Call(value, er);
+  MOZ_ASSERT(NS_SUCCEEDED(er.StealNSResult()),
+    "chrome JavaScript error in the callback.");
+
+  MOZ_ASSERT(mRegistry->mCustomDefinitions.GetWeak(mAtom),
+    "Callback should define the definition of type.");
+  MOZ_ASSERT(!mRegistry->mElementCreationCallbacks.GetWeak(mAtom),
+    "Callback should be removed.");
+
+  return NS_OK;
+}
+
 CustomElementDefinition*
 CustomElementRegistry::LookupCustomElementDefinition(nsAtom* aNameAtom,
-                                                     nsAtom* aTypeAtom) const
+                                                     nsAtom* aTypeAtom)
 {
   CustomElementDefinition* data = mCustomDefinitions.GetWeak(aTypeAtom);
+
+  if (!data) {
+    RefPtr<CustomElementCreationCallback> callback;
+    mElementCreationCallbacks.Get(aTypeAtom, getter_AddRefs(callback));
+    if (callback) {
+      RefPtr<Runnable> runnable =
+        new RunCustomElementCreationCallback(this, aTypeAtom, callback);
+      nsContentUtils::AddScriptRunner(runnable);
+      mElementCreationCallbacks.Remove(aTypeAtom);
+    }
+  }
+
   if (data && data->mLocalName == aNameAtom) {
     return data;
   }
 
   return nullptr;
 }
 
 CustomElementDefinition*
@@ -946,16 +977,38 @@ CustomElementRegistry::Define(const nsAS
    *        when-defined promise map.
    */
   RefPtr<Promise> promise;
   mWhenDefinedPromiseMap.Remove(nameAtom, getter_AddRefs(promise));
   if (promise) {
     promise->MaybeResolveWithUndefined();
   }
 
+  /**
+   * Clean-up mElementCreationCallbacks (if it exists)
+   */
+  mElementCreationCallbacks.Remove(nameAtom);
+
+}
+
+void
+CustomElementRegistry::SetElementCreationCallback(const nsAString& aName,
+                                                  CustomElementCreationCallback& aCallback,
+                                                  ErrorResult& aRv)
+{
+  RefPtr<nsAtom> nameAtom(NS_Atomize(aName));
+  if (mElementCreationCallbacks.GetWeak(nameAtom) ||
+      mCustomDefinitions.GetWeak(nameAtom)) {
+    aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+    return;
+  }
+
+  RefPtr<CustomElementCreationCallback> callback = &aCallback;
+  mElementCreationCallbacks.Put(nameAtom, callback.forget());
+  return;
 }
 
 void
 CustomElementRegistry::Get(JSContext* aCx, const nsAString& aName,
                            JS::MutableHandle<JS::Value> aRetVal)
 {
   RefPtr<nsAtom> nameAtom(NS_Atomize(aName));
   CustomElementDefinition* data = mCustomDefinitions.GetWeak(nameAtom);
--- a/dom/base/CustomElementRegistry.h
+++ b/dom/base/CustomElementRegistry.h
@@ -7,16 +7,17 @@
 #ifndef mozilla_dom_CustomElementRegistry_h
 #define mozilla_dom_CustomElementRegistry_h
 
 #include "js/GCHashTable.h"
 #include "js/TypeDecls.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/ErrorResult.h"
 #include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/CustomElementRegistryBinding.h"
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/FunctionBinding.h"
 #include "mozilla/dom/WebComponentsBinding.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsGenericHTMLElement.h"
 #include "nsWrapperCache.h"
 #include "nsContentUtils.h"
 
@@ -361,22 +362,47 @@ public:
   NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(CustomElementRegistry)
 
 public:
   static bool IsCustomElementEnabled(JSContext* aCx, JSObject* aObject);
   static bool IsCustomElementEnabled(nsIDocument* aDoc);
 
   explicit CustomElementRegistry(nsPIDOMWindowInner* aWindow);
 
+private:
+  class RunCustomElementCreationCallback : public mozilla::Runnable
+  {
+  public:
+    NS_DECL_NSIRUNNABLE
+    explicit RunCustomElementCreationCallback(CustomElementRegistry* aRegistry,
+                                              nsAtom* aAtom,
+                                              CustomElementCreationCallback* aCallback)
+      : mozilla::Runnable("CustomElementRegistry::RunCustomElementCreationCallback")
+#ifdef DEBUG
+      , mRegistry(aRegistry)
+#endif
+      , mAtom(aAtom)
+      , mCallback(aCallback)
+    {
+    }
+    private:
+#ifdef DEBUG
+      RefPtr<CustomElementRegistry> mRegistry;
+#endif
+      RefPtr<nsAtom> mAtom;
+      RefPtr<CustomElementCreationCallback> mCallback;
+  };
+
+public:
   /**
    * Looking up a custom element definition.
    * https://html.spec.whatwg.org/#look-up-a-custom-element-definition
    */
   CustomElementDefinition* LookupCustomElementDefinition(
-    nsAtom* aNameAtom, nsAtom* aTypeAtom) const;
+    nsAtom* aNameAtom, nsAtom* aTypeAtom);
 
   CustomElementDefinition* LookupCustomElementDefinition(
     JSContext* aCx, JSObject *aConstructor) const;
 
   static void EnqueueLifecycleCallback(nsIDocument::ElementCallbackType aType,
                                        Element* aCustomElement,
                                        LifecycleCallbackArgs* aArgs,
                                        LifecycleAdoptedCallbackArgs* aAdoptedCallbackArgs,
@@ -414,29 +440,36 @@ private:
     CustomElementDefinition* aDefinition);
 
   void UpgradeCandidates(nsAtom* aKey,
                          CustomElementDefinition* aDefinition,
                          ErrorResult& aRv);
 
   typedef nsRefPtrHashtable<nsRefPtrHashKey<nsAtom>, CustomElementDefinition>
     DefinitionMap;
+  typedef nsRefPtrHashtable<nsRefPtrHashKey<nsAtom>, CustomElementCreationCallback>
+    ElementCreationCallbackMap;
   typedef nsClassHashtable<nsRefPtrHashKey<nsAtom>,
                            nsTHashtable<nsRefPtrHashKey<nsIWeakReference>>>
     CandidateMap;
   typedef JS::GCHashMap<JS::Heap<JSObject*>,
                         RefPtr<nsAtom>,
                         js::MovableCellHasher<JS::Heap<JSObject*>>,
                         js::SystemAllocPolicy> ConstructorMap;
 
   // Hashtable for custom element definitions in web components.
   // Custom prototypes are stored in the compartment where definition was
   // defined.
   DefinitionMap mCustomDefinitions;
 
+  // Hashtable for chrome-only callbacks that is called *before* we return
+  // a CustomElementDefinition, when the typeAtom matches.
+  // The callbacks are registered with the setElementCreationCallback method.
+  ElementCreationCallbackMap mElementCreationCallbacks;
+
   // Hashtable for looking up definitions by using constructor as key.
   // Custom elements' name are stored here and we need to lookup
   // mCustomDefinitions again to get definitions.
   ConstructorMap mConstructors;
 
   typedef nsRefPtrHashtable<nsRefPtrHashKey<nsAtom>, Promise>
     WhenDefinedPromiseMap;
   WhenDefinedPromiseMap mWhenDefinedPromiseMap;
@@ -479,16 +512,20 @@ public:
 
   void Define(const nsAString& aName, Function& aFunctionConstructor,
               const ElementDefinitionOptions& aOptions, ErrorResult& aRv);
 
   void Get(JSContext* cx, const nsAString& name,
            JS::MutableHandle<JS::Value> aRetVal);
 
   already_AddRefed<Promise> WhenDefined(const nsAString& aName, ErrorResult& aRv);
+
+  // Chrome-only method that give JS an opportunity to only load the custom
+  // element definition script when needed.
+  void SetElementCreationCallback(const nsAString& aName, CustomElementCreationCallback& aCallback, ErrorResult& aRv);
 };
 
 class MOZ_RAII AutoCEReaction final {
   public:
     // JSContext is allowed to be a nullptr if we are guaranteeing that we're
     // not doing something that might throw but not finish reporting a JS
     // exception during the lifetime of the AutoCEReaction.
     AutoCEReaction(CustomElementReactionsStack* aReactionsStack, JSContext* aCx)
--- a/dom/tests/mochitest/webcomponents/mochitest.ini
+++ b/dom/tests/mochitest/webcomponents/mochitest.ini
@@ -22,16 +22,17 @@ skip-if = !debug # TestFunctions only ap
 [test_custom_element_upgrade.html]
 support-files =
   test_upgrade_page.html
   upgrade_tests.js
 [test_custom_element_lifecycle.html]
 [test_custom_element_stack.html]
 [test_custom_element_define.html]
 [test_custom_element_define_parser.html]
+[test_custom_element_set_element_creation_callback.html]
 [test_custom_element_template.html]
 [test_detached_style.html]
 [test_document_adoptnode.html]
 [test_document_importnode.html]
 [test_event_retarget.html]
 [test_event_stopping.html]
 [test_template.html]
 [test_template_xhtml.html]
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/webcomponents/test_custom_element_set_element_creation_callback.html
@@ -0,0 +1,170 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1460815
+-->
+<head>
+  <title>Test for customElements.setElementCreationCallback</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1460815">Bug 1460815</a>
+<div>
+</div>
+
+<script>
+
+let registry = SpecialPowers.wrap(customElements);
+
+function simpleTest() {
+  let callbackCalled = false;
+  class XObjElement extends HTMLElement {};
+  registry.setElementCreationCallback("x-html-obj-elem", (type) => {
+    if (callbackCalled) {
+      ok(false, "Callback should not be invoked more than once.");
+    }
+    callbackCalled = true;
+    is(type, "x-html-obj-elem", "Type is passed to the callback.");
+    customElements.define("x-html-obj-elem", XObjElement);
+  });
+  ok(!callbackCalled, "Callback should not be called.");
+  let el = document.createElement("x-html-obj-elem");
+  ok(callbackCalled, "Callback should be called.");
+  isnot(Object.getPrototypeOf(el), XObjElement.prototype, "Created element is not upgraded.");
+  document.body.appendChild(el);
+  is(Object.getPrototypeOf(el), XObjElement.prototype, "Created element should have the prototype of the custom type.");
+}
+
+function multipleDefinitionTest() {
+  let callbackCalled = false;
+  class XObjElement1 extends HTMLElement {};
+  class XObjElement2 extends HTMLElement {};
+  let callback = (type) => {
+    if (callbackCalled) {
+      ok(false, "Callback should not be invoked more than once.");
+    }
+    callbackCalled = true;
+    is(type, "x-html-obj-elem1", "Type is passed to the callback.");
+    customElements.define("x-html-obj-elem1", XObjElement1);
+    customElements.define("x-html-obj-elem2", XObjElement2);
+  };
+  registry.setElementCreationCallback("x-html-obj-elem1", callback);
+  registry.setElementCreationCallback("x-html-obj-elem2", callback);
+  ok(!callbackCalled, "Callback should not be called.");
+  let el1 = document.createElement("x-html-obj-elem1");
+  ok(callbackCalled, "Callback should be called.");
+  isnot(Object.getPrototypeOf(el1), XObjElement1.prototype, "Created element is not upgraded.");
+  document.body.appendChild(el1);
+  is(Object.getPrototypeOf(el1), XObjElement1.prototype, "Created element should have the prototype of the custom type.");
+  let el2 = document.createElement("x-html-obj-elem2");
+  is(Object.getPrototypeOf(el2), XObjElement2.prototype, "Created element should have the prototype of the custom type.");
+}
+
+function throwIfDefined() {
+  let callbackCalled = false;
+  class XObjElement3 extends HTMLElement {};
+  customElements.define("x-html-obj-elem3", XObjElement3);
+  try {
+    registry.setElementCreationCallback(
+      "x-html-obj-elem3", () => callbackCalled = true);
+  } catch (e) {
+    ok(true, "Set a callback on defined type should throw.");
+  }
+  ok(!callbackCalled, "Callback should not be called.");
+}
+
+function throwIfSetTwice() {
+  let callbackCalled = false;
+  registry.setElementCreationCallback(
+    "x-html-obj-elem4", () => callbackCalled = true);
+  try {
+    registry.setElementCreationCallback(
+      "x-html-obj-elem4", () => callbackCalled = true);
+  } catch (e) {
+    ok(true, "Callack shouldn't be set twice on the same type.");
+  }
+  ok(!callbackCalled, "Callback should not be called.");
+}
+
+function simpleExtendedTest() {
+  let callbackCalled = false;
+  class ExtendButton extends HTMLButtonElement {};
+  registry.setElementCreationCallback("x-extended-button", (type) => {
+    if (callbackCalled) {
+      ok(false, "Callback should not be invoked more than once.");
+    }
+    callbackCalled = true;
+    customElements.define("x-extended-button", ExtendButton, { extends: "button" });
+    is(type, "x-extended-button", "Type is passed to the callback.");
+  });
+  ok(!callbackCalled, "Callback should not be called.");
+  let el = document.createElement("button", { is: "x-extended-button"});
+  ok(callbackCalled, "Callback should be called.");
+  isnot(Object.getPrototypeOf(el), ExtendButton.prototype, "Created element is not upgraded.");
+  document.body.appendChild(el);
+  is(Object.getPrototypeOf(el), ExtendButton.prototype, "Created element should have the prototype of the extended type.");
+  is(el.getAttribute("is"), "x-extended-button", "The |is| attribute of the created element should be the extended type.");
+}
+
+function simpleInnerHTMLTest() {
+  let callbackCalled = false;
+  class XObjElement4 extends HTMLElement {};
+  registry.setElementCreationCallback("x-html-obj-elem5", (type) => {
+    if (callbackCalled) {
+      ok(false, "Callback should not be invoked more than once.");
+    }
+    callbackCalled = true;
+    is(type, "x-html-obj-elem5", "Type is passed to the callback.");
+    customElements.define("x-html-obj-elem5", XObjElement4);
+  });
+  ok(!callbackCalled, "Callback should not be called.");
+  let p = document.createElement("p");
+  p.innerHTML = "<x-html-obj-elem5></x-html-obj-elem5>";
+  let el = p.firstChild;
+  ok(callbackCalled, "Callback should be called.");
+  isnot(Object.getPrototypeOf(el), XObjElement4.prototype, "Created element is not upgraded.");
+  document.body.appendChild(p);
+  is(Object.getPrototypeOf(el), XObjElement4.prototype, "Created element should have the prototype of the custom type.");
+}
+
+function twoElementInnerHTMLTest() {
+  let callbackCalled = false;
+  class XObjElement5 extends HTMLElement {};
+  registry.setElementCreationCallback("x-html-obj-elem6", (type) => {
+    if (callbackCalled) {
+      ok(false, "Callback should not be invoked more than once.");
+    }
+    callbackCalled = true;
+    is(type, "x-html-obj-elem6", "Type is passed to the callback.");
+    customElements.define("x-html-obj-elem6", XObjElement5);
+  });
+  ok(!callbackCalled, "Callback should not be called.");
+  let p = document.createElement("p");
+  p.innerHTML =
+    "<x-html-obj-elem6></x-html-obj-elem6><x-html-obj-elem6></x-html-obj-elem6>";
+  let el1 = p.firstChild;
+  let el2 = p.lastChild;
+  ok(callbackCalled, "Callback should be called.");
+  isnot(Object.getPrototypeOf(el1), XObjElement5.prototype, "Created element is not upgraded.");
+  isnot(Object.getPrototypeOf(el2), XObjElement5.prototype, "Created element is not upgraded.");
+  document.body.appendChild(p);
+  is(Object.getPrototypeOf(el1), XObjElement5.prototype, "Created element should have the prototype of the custom type.");
+  is(Object.getPrototypeOf(el2), XObjElement5.prototype, "Created element should have the prototype of the custom type.");
+}
+
+function startTest() {
+  simpleTest();
+  multipleDefinitionTest();
+  throwIfDefined();
+  throwIfSetTwice();
+  simpleExtendedTest();
+  simpleInnerHTMLTest();
+  twoElementInnerHTMLTest();
+}
+
+startTest();
+
+</script>
+</body>
+</html>
--- a/dom/webidl/CustomElementRegistry.webidl
+++ b/dom/webidl/CustomElementRegistry.webidl
@@ -3,17 +3,20 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 // https://html.spec.whatwg.org/#dom-window-customelements
 [Func="CustomElementRegistry::IsCustomElementEnabled"]
 interface CustomElementRegistry {
   [CEReactions, Throws]
   void define(DOMString name, Function functionConstructor,
               optional ElementDefinitionOptions options);
+  [ChromeOnly, Throws]
+  void setElementCreationCallback(DOMString name, CustomElementCreationCallback callback);
   any get(DOMString name);
   [Throws]
   Promise<void> whenDefined(DOMString name);
 };
 
 dictionary ElementDefinitionOptions {
   DOMString extends;
 };
 
+callback CustomElementCreationCallback = void (DOMString name);