Skip to content

Conversation

@nolanlawson
Copy link
Contributor

@nolanlawson nolanlawson commented Mar 8, 2022

Details

Fixes #2191

This is an attempt to rebase #2564 and get it working.

The main goal is to allow third-party custom elements to use tag names that are the same as tag names already reserved by LWC custom elements:

// lwc-component.js
lwc.createElement('x-foo', { is: Foo })

// vanilla-component.js
customElements.define('x-foo', Foo)

It also allows LWC custom elements to re-use the same tag name to refer to different components:

// button-1.js
lwc.createElement('c-button', { is: Button1 })

// button-2.js
lwc.createElement('c-button', { is: Button2 })

Does this pull request introduce a breaking change?

  • ✅ No, it does not introduce a breaking change.

Does this pull request introduce an observable change?

  • ⚠️ Yes, it does include an observable change.

Probably there is something observable about changing how we do custom element registry scoping, but I haven't thought it through yet.

@nolanlawson
Copy link
Contributor Author

Most of the Karma tests are passing; almost all of the failures now are due to CustomElementConstructor, which is now throwing the error Illegal constructor.

I'm not sure how we plan to make CustomElementConstructor work with the new pivot design. My feeble brain is still struggling to understand all of our prototype/superclass/inheritance stuff.

@nolanlawson
Copy link
Contributor Author

OK, all Karma tests are passing now; it's just the server that's still busted.

@nolanlawson
Copy link
Contributor Author

Server is fixed, but compat karma tests need to be fixed.

@nolanlawson nolanlawson force-pushed the nolan/node-reactions-pivots branch from 7c4a7b9 to 476ca51 Compare March 11, 2022 18:48
@nolanlawson
Copy link
Contributor Author

@caridy Local tests are passing, so it looks like your technique works. 🙂 My PR is still a bit rough, but overall this looks promising!

Some downstream tests are failing, but it looks like it may just be Jest/JSDOM issues.

@nolanlawson
Copy link
Contributor Author

/nucleus test

attributeChangedCallback(name: string, oldValue: any, newValue: any) {
attributeChangedCallback.call(this, name, oldValue, newValue);
}
static observedAttributes = observedAttributes;
Copy link
Collaborator

Choose a reason for hiding this comment

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

this is one of the parts that worries me a little bit!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@caridy For the sake of my fuzzy memory, what part of it worries you?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I need to think more about it... but the fact that we now rely on observed attributes in the class that we produce, might introduce other issues with the inheritance path outside of LWC, and probably also conflict with authors list of attributes. I don't remember how observedAttributes from this record is constructed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If I'm not mistaken, we handle inheritance of observed attributes in base-bridge-element.ts:

// Specify attributes for which we want to reflect changes back to their corresponding
// properties via attributeChangedCallback.
defineProperty(HTMLBridgeElement, 'observedAttributes', {
get() {
return [...superObservedAttributes, ...keys(attributeToPropMap)];
},
});

Copy link
Collaborator

Choose a reason for hiding this comment

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

right, but beyond that, I don't know.

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 Karma tests are all passing currently. Is there a case you think isn't covered?

Copy link
Collaborator

Choose a reason for hiding this comment

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

yeah, I don't doubt that, but why was this change needed in the first place? If the custom element inherit the right thing... then you should be able to get the observedAttributes and others from there rather than rebuilding that yourself.

'If this is a Jest environment, check your Jest/JSDOM configuration.'
);
}
(nativeRegistry as any)[registryPatchedSymbol] = true;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If we don't add this check, then an error will be thrown eventually for "Illegal constructor." I saw this in two of our downstreams' Jest tests.

I considered wrapping this in a process.env.NODE_ENV condition, but what if a consumer loads two versions of the engine and one is minified? Better safe than sorry.

Copy link
Collaborator

Choose a reason for hiding this comment

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

this is one of the things that I want to discuss, this might actually be a red flag.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Chatted with Caridy, the concerns are:

  1. If the patched registry doesn't work when applied twice, then this may limit our ability to have multiple versions of the framework running on top of the same registry. Ideally it should be resilient to being applied twice.
  2. Abstracting the Custom Element Infrastructure out of Core Package #2799 might allow us to get rid of the UserCtor being passed in to the PivotCtor, we should explore that as well:

class PivotCtor extends NativeHTMLElement {
constructor(UserCtor?: CustomElementConstructor) {

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 news, it works with two copies of the engine when running in Karma. Looks like this is a Jest-only issue.

global: {
branches: 80,
functions: 90,
functions: 85,
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 necessary because I added a Jest test to @lwc/engine-dom, and now Jest is complaining about the global-registry.ts file containing too much untested code. engine-dom is already well-tested elsewhere.

{
"path": "packages/lwc/dist/engine-dom/umd/es2017/engine-dom.min.js",
"maxSize": "17.5KB",
"maxSize": "18KB",
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 global-registry adds a decent chunk of code.

const TestCustomElement = Test.CustomElementConstructor;

expect(typeof TestCustomElement).toBe('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.

Could not figure out why, but this started throwing in IE. Didn't seem worth investigating beyond that.

Copy link
Contributor

Choose a reason for hiding this comment

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

It could be beneficial to throw an error when accessing CustomElementConstructor in an environment with no support for a native custom element eg. IE11.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Aha, so you're saying that the fact that the behavior changed here is not a big deal? What if someone is using their own custom elements polyfill with CustomElementConstructor in IE11 though?

@nolanlawson
Copy link
Contributor Author

Blocked by #2855

@nolanlawson nolanlawson force-pushed the nolan/node-reactions-pivots branch from 86ae839 to fc1d389 Compare May 27, 2022 17:45
Copy link
Contributor

@ravijayaramappa ravijayaramappa left a comment

Choose a reason for hiding this comment

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

LGTM. Please wait for @caridy to +1 since he is most adept at the Custom Registry spec.

@nolanlawson
Copy link
Contributor Author

@ravijayaramappa We should move createCustomElement out of the renderer factory though, right? Make it excluded just like createStylesheet?

@ravijayaramappa
Copy link
Contributor

ravijayaramappa commented Sep 30, 2022

We should move createCustomElement out of the renderer factory though, right? Make it excluded just like createStylesheet?

Yes. You can do it in this PR or a follow up.

@nolanlawson
Copy link
Contributor Author

@ravijayaramappa No problem. Done: f0a3746

@nolanlawson
Copy link
Contributor Author

/nucleus ignore --reason "lwc-platform needs updating"

@nolanlawson
Copy link
Contributor Author

nolanlawson commented Sep 30, 2022

Some feedback from discussion with @caridy:

Currently when we call lwc.createElement('x-foo'), then customElements.get('x-foo') will return the PivotCtor (which is useless without the UserCtor – it gives you a "dummy" custom element). I did it this way to match existing LWC behavior.

However, this may break a third-party component using this common pattern:

if (!customElements.get('my-tag')) {
  customElements.define('my-tag', ...)
}

The above will break if LWC reserves my-tag first.

However, it's worth acknowledging that even if we fix customElements.get and customElements.whenDefined to return undefined rather than the PivotCtor, we still can't fix the following case:

// LWC code
lwc.createElement('x-foo', ...)

// userland code
const elm = document.createElement('x-foo')
const PivotCtor = elm.constructor
new PivotCtor()

So in a sense, we can't fully hide the PivotCtor. But we can potentially hide it from customElements.get and customElements.whenDefined at least.

This means we also need to fix the case where someone does:

new PivotCtor(UserCtorThatWasNeverRegistered)

^ This should be detectable, and we should probably throw an error in this case.

@nolanlawson
Copy link
Contributor Author

Regarding the above: I added a TODO to return undefined from customElements.get/whenDefined (#3073), and I fixed the case where the user passes in an arbitrary UserCtor: f9e0c50

To avoid making this a "forever" PR, I should probably just keep adding TODOs and stop for now! 😛

@ravijayaramappa
Copy link
Contributor

ravijayaramappa commented Oct 8, 2022

@nolanlawson You probably have test cases for these, if not these should be add-ons to #3073

class Foo extends HTMLElement {}
customElements.define('x-foo', Foo);

const elm = document.createElement('x-foo');
console.log(elm instanceof Foo); // true
console.log(elm.constructor === Foo); // true

@nolanlawson
Copy link
Contributor Author

@ravijayaramappa In the pivots implementation, those last two statements would be false rather than true. The elm will be an instance of PivotCtor, which does not extend from Foo. However, we could add that test for the non-pivots case.

@nolanlawson
Copy link
Contributor Author

/nucleus test

@nolanlawson
Copy link
Contributor Author

It's time to merge this... it's behind a flag, and we can fix remaining issues in follow-up PRs. Thanks to everyone who reviewed!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Move LWC Custom Element registration out of the global custom element registry

5 participants