Skip to content
This repository has been archived by the owner on Oct 26, 2021. It is now read-only.

Implementing customized built-in elements #42

Closed

Conversation

joeldenning
Copy link
Contributor

@joeldenning joeldenning commented Jan 9, 2017

This fixes #13. This implementation is hopefully very close to the spec for customized built-in elements.
Note that I based my branch off of #36 because the tests are failing on the master branch and I didn't want to duplicate the work that @bicknellr has already done in fixing the tests. This means that #36 should probably be merged in before this pull request.

bicknellr and others added 18 commits December 16, 2016 15:55
… match native, rather than being specifically true.

They should be true per spec but it seems like Safari sets writable to false.
* fix-tests:
  Remove unnecessary `HTMLElement#constructor` descriptor test.
  Test for 'HTMLElement#constructor' writable / configurable flags that match native, rather than being specifically true.
  Add promise polyfill for tests.
  Add bower dependency and installation.
  Update WCT to 6.0.0-prerelease.3 for ES2015 compilation.
  Remove use of `String#endsWith`.
  Update HTMLImports polyfill path.
  Update WCT and webcomponentsjs.
  Use one-liner for forcing the polyfill.
  Remove test assumption that `customElements` exists.
  `raw` -> `_native`
  `_orig` -> `_native`
  Use `HTMLNS`.
  `doc` -> `document`; `win` -> `window`
  Fix instanceof for built-in elements.
  📝 Fix gramatical error in comment
@joeldenning joeldenning changed the title Customized builtin Implementing customized built-in elements Jan 9, 2017
"webcomponentsjs": "webcomponents/webcomponentsjs#^0.7.22"
"web-component-tester": "6.0.0-prerelease.3",
"webcomponentsjs": "webcomponents/webcomponentsjs#v1",
"es6-promise": "stefanpenner/es6-promise#^4.0.0"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These changes are from #36

"google-closure-compiler": "^20160822.1.0",
"gulp": "^3.8.8",
"gulp-sourcemaps": "^1.6.0",
"web-component-tester": "^4.0.1"
"web-component-tester": "6.0.0-prerelease.3"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All changes to package.json are from #36

@@ -138,7 +138,7 @@ let Deferred;
/** @private {!Map<string, !CustomElementDefinition>} **/
this._definitions = new Map();

/** @private {!Map<Function, string>} **/
/** @private {!Map<Function, !CustomElementDefinition>} **/
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have changed this._constructors to so that it has the entire CustomElementDefinition, since we need more than the tagName now when looking up custom elements by their constructor

const nameError = checkValidCustomElementName(name);
if (nameError) throw nameError;

// 4, 5:
// 3:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The numbering has changed in the spec for customElements.define, so I updated it.

let _extends = options ? options.extends : null;

// 7:
if (_extends) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is stuff specific to customizedBuiltInElements

if (options && options.is) {
// We're going to take care of setting the is attribute ourselves
isAttr = options.is;
delete options.is;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete options.is So that the native browser implementation doesn't freak out

const definition = customElements._definitions.get(tagName.toLowerCase());
if (isAttr) {
element.setAttribute('is', isAttr);
element.is = isAttr;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Set the is attribute and property on the newly created node

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don' think this property exists.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch -- I think I confused the "is value" with a property on the dom element. But it seems like the "is value" is just an internal thing not exposed to userland.

@@ -61,6 +61,9 @@
(() => {
'use strict';

// Do nothing if `customElements` does not exist.
if (!window.customElements) return;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is from #36

@@ -181,6 +246,72 @@ suite('Custom Element Reactions', function() {

});

suite('customized built-in elements', function() {
test('inherits native behavior such as a button\'s disabled property', function(done) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests that you can actually get the native behavior for specific elements (like disabled for buttons) to work just like it's native.

});
});

test('does not upgrade elements that don\'t match what extends was at definition', function() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is something I am actually not 100% sure on in the spec. It seems like you should not be able to <div is="x-foo" /> if you say customElements.define('x-foo', XFoo, {extends: 'span'}). But I was not able to find a definitive answer in the spec. Am happy to change if I'm wrong on this.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This appears to be correct. Spec breadcrumbs: https://html.spec.whatwg.org/#concept-try-upgrade will give a null definition in step 1 since https://html.spec.whatwg.org/#look-up-a-custom-element-definition will return null if name + local name do not match (in its step 5).

@blittle
Copy link

blittle commented Jan 9, 2017

Do the tests need to be retried?

@joeldenning
Copy link
Contributor Author

Yeah the tests pass locally for me but timed out in travis. Retrying would probably get them to run.

@joeldenning
Copy link
Contributor Author

Ping

@blittle
Copy link

blittle commented Mar 2, 2017

I'd love to hear an update about this!

@joeldenning
Copy link
Contributor Author

Ping.

Are there concerns around this pr that are preventing it from being reviewed? Maybe that webcomponents/custom-elements is hesitant about implementing customized built-in elements? Or is it just that since it's a big pull request it is hard to review?

@bicknellr
Copy link
Contributor

Sorry, @joeldenning, I owe you an explanation here. I recently ended up overhauling the polyfill so that we could remove most of our dependence on MutationObservers for the sake of performance and timing correctness. Unfortunately, this makes this PR effectively unmergable as there's no common code anymore. I should have paid more attention to what was already happening in the repo and let you know about these changes earlier.

I didn't end up implementing extension for built-ins during that process because, AFAIK, WebKit does not intend to implement it. I'd rather not have people end up stuck on the polyfill because they become dependent on this feature. Also, we won't have to worry about supporting a feature that's already been baked in if it does end up being dropped from the spec.

However, if it does make it through and WebKit decides to implement, then we'll definitely want it here. If you're still interested in implementing this on top of the new polyfill, I'd be glad to help explain how the new polyfill works and point you to the relevant spots. (I'm hoping that the recent updates made the polyfill a bit easier to navigate.) I just can't guarantee it's going to make it in at the moment - you'll have to keep an eye on the spec discussion for that.

@joeldenning
Copy link
Contributor Author

joeldenning commented Mar 7, 2017

@bicknellr thanks for the explanation. Do you know if/where there is more discussion about Webkit's disagreement with customized built in elements (beyond what is described in this issue in the CE spec)? I have been following it as best as I can, and as far as I can tell what you said is correct about webkit not planning to implement customized built ins.

About whether this polyfill should implement customized built-ins, though, would you be opposed to including support for customized built-ins if it came with a disclaimer in the documentation? Explaining the current status of the spec (v1 finalized) but that Webkit doesn't plan on implementing a specific part of the spec (customized built-in). Or perhaps as a separate script that is optionally included to support customized built ins?

I think that there is still value in fully implementing the v1 spec, as long as users know what they are getting themselves into. I also think that this polyfill is a great place to educate people on the spec and its browser support, since many developers will come here to learn about custom elements without ever having read the spec or the discussions surrounding it. Once people know that there is a risk that they will be stuck on the polyfill forever if they choose to use customized builtins, I feel that we have given them enough information to make that decision.

All that said, I understand that I'm merely an interested individual, who doesn't represent the vast swaths of Polymer users or the large corporations interested in the CE spec 😃 . Interested to hear others' thoughts on whether a polyfill should support a feature that is in the spec but may never be supported by all browsers.

@bedeoverend
Copy link

bedeoverend commented Mar 7, 2017

@bicknellr If I understand correctly, Chrome, Firefox (written into gecko source), and Edge (expressed intent in last TPAC meeting - potentially could now have changed) all intend to implement built-ins via is="". If it is the case that the majority of vendors intend to implement, will the polyfill implement also?

I understand wanting to hold off until at least one implementation exists, but if Safari is the only one planning on not implementing, and that was the reason the polyfill held off, the polyfill's functionality would differ from most browsers.

@madeleineostoja
Copy link

I strongly agree with @joeldenning here - extending built-ins is firmly in the v1 spec, and being actively shipped by multiple vendors. We'd be making a very big assumption that users don't want this polyfilled, because they'd be carrying the polyfill on Webkit, and making that decision for them.

This polyfill should follow the spec, regardless of vendors, and include a disclaimer in the docs that using a certain feature of the spec (extending built-ins) will have performance ramifications on Webkit. Then users are free to make up their own mind.

In practical terms - there are a lot of use-cases that just aren't possible without extending builtins, and having native perf in most vendors with a shim in webkit, vs. hacking something together with mutation observers for all vendors to meet these use-cases, is a compromise that I think a lot of devs would be happy making.

But, at the end of the day, it should be their choice. A polyfill's job should just be to follow the spec as best as it can and document any edge-cases / caveats. Not to mention that Webkit is the odd one out here, and there is vocal opposition to their position from a lot of people. A polyfill shouldn't take sides or dictate usage.

Copy link

@dominiccooney dominiccooney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drive by comments. I'm not super familiar with this polyfill so take these with appropriate skepticism.

@@ -451,6 +459,9 @@ let Deferred;
const walker = createTreeWalker(root);
do {
const node = /** @type {!HTMLElement} */ (walker.currentNode);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW I think this annotation is incorrect; you may have non-HTMLElements in a document, usually SVG.

@@ -451,6 +459,9 @@ let Deferred;
const walker = createTreeWalker(root);
do {
const node = /** @type {!HTMLElement} */ (walker.currentNode);
if (node.getAttribute('is')) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not super familiar with this polyfill, but if it hooks element creation it's more accurate to cache 'is' at creation time. The HTML spec calls this the element's "is value" IIRC.

I guess you probably need to look at attributes for parsed elements.

You may want to keep those values in a side WeakMap of Element -> string. That will avoid the closure type problem you mention below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that not looking at the is attribute would be ideal, but like you pointed out I don't think it's possible to avoid it for parsed elements. Since (per the spec) updating the is attribute after creation should not cause an element to be upgraded to a custom element, what I did here was only look at the is attribute when the MutationObserver says that a node was added, and never look at the is attribute after that.

Since this polyfill has been rewritten to rely on monkeypatched methods instead of mutation observers, I'll have to figure out how to get the same functionality without the MutationObserver telling me if it is a newly added node or not.

const definition = customElements._definitions.get(tagName.toLowerCase());
if (isAttr) {
element.setAttribute('is', isAttr);
element.is = isAttr;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don' think this property exists.

@@ -562,7 +573,7 @@ let Deferred;
const node = walker.currentNode;
if (node[_upgradedProp] && node[_attachedProp]) {
node[_attachedProp] = false;
const definition = this._definitions.get(node.localName);
const definition = getDefinition(this._definitions, node);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW the native implementation in Chrome does this differently: once a definition is associated with an element (at creation or upgrade) we store a pointer directly from the element to the definition.

Given that getDefinition is more complicated now, it might be worth revisiting the space/speed tradeoff.

I note that you're storing "is" on the element. If you wanted to save space you could switch the storage space for "is" to point to the full definition once you have it. If you ever happened to need is (I don't think you do; cloning goes back to the attribute value for example) you could retrieve it from the definition.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I like that idea. Putting it on the element instead of the looking it up would improve performance. And getting rid of the is property will offset the new storage space for the definition.

And regarding the is property, just reviewed the spec and looks like I got confused between the "is value" and an actual userland property on the dom element. So you're right, it should be removed from this polyfill

@joeldenning
Copy link
Contributor Author

@dominiccooney thanks for the feedback, will definitely incorporate it!

@bicknellr do you have thoughts on the discussion above about whether to implement customized built-ins for this polyfill? I am more than happy to redo my implementation to work with the monkeypatching approach, if you feel comfortable with that. As discussed above, I think that if we educate users of the polyfill about the potentially shaky status of customized builtins, we've given them the power to choose whether to use customized built-ins or not. I also agree with @bedeoverend that it would be unexpected if the polyfill does not have feature parity with the native implementation of the majority of browsers (Chrome, Firefox, Edge). What are your thoughts?

@bicknellr
Copy link
Contributor

bicknellr commented Mar 9, 2017

@joeldenning Yeah, having a polyfill that doesn't fully implement the spec feels wrong. More so if that spec is ends up implemented by multiple browsers. @dominiccooney mentioned to me that Chrome's built-in extension is behind a flag (maybe that's just because it's not totally there yet?). I think following suit by also putting the polyfill's implementation behind a flag would make sense. That way it's available but still requires opt-in and we can point out the continued debate about the feature where the flag is documented. I think that strikes a reasonable balance between being spec complete and being cautious about allowing users to write code that could potentially be non-portable in the future. WDYT?

@joeldenning
Copy link
Contributor Author

👍 I like that. Maybe something like customElements.enableCustomizedBuiltIns = true?

I'll start work on this again (based on the new monkeypatched way) in a new pr.

@avinoamr
Copy link

@joeldenning How is it looking? Are you still actively working on it? Any estimation on when you'll have something ready?

@joeldenning
Copy link
Contributor Author

Yeah, I have been working on it. I have it partially working, and am just adding tests and fixing bugs. I hope to have it finished and create a pr this upcoming week

@avinoamr
Copy link

@joeldenning thanks. can't wait :)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Custom Elements] Support extending elements
8 participants