-
Notifications
You must be signed in to change notification settings - Fork 383
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow mixins to access custom element lifecycle #512
Comments
Yeah, I suspected that at some point this was going to be a problem. At the moment, we just support the bare bone API from WC, where HTMLElement does not have those methods, so, invoking a super without the brand check throws. E.g.: class Base extends HTMLElement {
connectedCallback() {
// super.connectedCallback(); // throws
console.log('base connected');
}
disconnectedCallback() {
// super.disconnectedCallback(); // throws
console.log('base disconnected');
}
}
class Foo extends Base {
connectedCallback() {
super.connectedCallback();
console.log('foo connected');
}
disconnectedCallback() {
super.disconnectedCallback();
console.log('foo disconnected');
}
} So, there are few things:
Users of the Mixins should be aware that they are sugar layer for class inheritance, and if some of them requires them to call the super, and take care of some of the work. Also, mixins are probably on the 20 rather than on the 80, so, it might be fine. Let's debate more about this. |
Option 4 seems completely valid to me. Requiring the invocation of each constructor in the prototype chain resolves a class of problems which surface again here, specifically that any inherited class in the chain cannot function correctly without its constructor being called. The same holds true for base classes that use the lifecycle events. For example, one may need to clean up references when the element is disconnected. Without a way to guarantee disconnection cleanup, memory leaks will ensue for these cases and there will be no way to ensure they can't if the user writes bad code and doesn't call the super. The wire decorator has access to connected/disconnected lifecycle events, so there needs to be a way to do this for |
Let me re-read this and shall we have a meeting tomorrow to align since @pmdartus is back? |
I also think that option 4, it the best approach.
As @caridy pointed out, Mixin represent only the 20 of the 80/20. I would like to only restrict this warning to all the classes that doesn't inherit directly from the base |
After a failed attempted to implement 4, I can assure that this option is not really an option. Mostly because you don't have an opt out mechanism, and in many case, what you want is to NOT call the super. E.g.: if I have my own template, and I'm extending a component that is doing some work on its dom in renderedCallback, I don't want their code to run, because it will NOT work since the dom is very different. This is the same problem exhibit by option 1. I was tempted to try to implement the event, which at first seems simple enough and also could simplify the services (to avoid calling the service on every element when inserted), but that had a similar problem, people might decide to just Option 3 is also out because those hooks are extracted at the constructor level by spec, they are not picked from the instance. Btw, they are configurable, so in theory you could override them by using defineProperty on the instance, but that will not work due to the extraction mechanism. Also, the ergonomic of this for Mixin authors will be pretty bad. So, I will not consider this one an option. So, we are back to the whiteboard again :( |
I'd like to challenge the assumption that mixins should be concerned with lifecycle methods at all. The point of writing a mixin is to provide shared functionality to multiple components. This functionality should not be coupled to custom element lifecycle methods and I would go so far as to suggest that this is an anti-pattern. Coupling to lifecycle methods in mixins makes the mixin very brittle and far less flexible. It should be up to the component author to decide when to invoke that functionality. For instance, if I have a drag and drop mixin that uses lifecycle hooks, I may have something like this:
To consume this mixin, I might do something like this:
What happens if I want the drag and drop functionality not to happen on connect, but after some user interaction? Then I have to write something very awkward:
Instead of using lifecycle methods, mixing should instead provide methods that users can invoke whenever they want:
If we prevent mixins from using lifecycle methods, I don't see how this issue is a problem anymore |
The main reason for wanting this support is that it reduces how much is pushed back to individual component developers. We all know by years of experience that developers do the wrong thing on accident regularly, and forgetting or not knowing that they are supposed to add code is a common problem. This is particularly true if the code works functionally and there are no warnings or errors that prompt them that something is wrong. For instance, the developer of a mixin that registers a listener for a given component in a shared map will have the ability to ensure that memory leaks will not occur if the developer of the consuming component does not explicitly call another mixin API to disconnect them. The mixin developer is aware of the memory implications and for the need to clean it up. The component developer may not be aware of that because the way the memory is allocated and consumed is abstracted and opaque behind the mixin's API. The mixin developer is also unable to guarantee or enforce that its consumers call the disconnecting API. If a memory leak does occur, it will be associated to the mixin given the memory allocation even though the mixin is not responsible and has no power to fix it. The framework, on the other hand, doesn't require DOM event handlers to be removed as it is taken care of automatically. This is true for |
I will argue that the problems you are describing are not really about mixin but inheritance is general. I might have a subclass that doesn't honor the super hooks, because the super doesn't have them defined, but then they added it, and now my subclass is broken. |
Is there anything else here @jbenallen @davidturissini ? Can we close this? It feels to me that we explored most options, and this will remain as it is (status-quo). |
Circling back to this. In general, this is a reasonable limitation. However, I want to point out that the
|
@jbenallen I believe this should be possible via #899, we just need to validate that assumption. |
@ravijayaramappa this one could be solved by #1431 |
@caridy @jbenallen I believe we've since started moving away from mixins. Should we close this one? |
I don't know... I am up for closing it... but I don' tknow if @jbenallen or someone else might have strong opinions here. |
Is your feature request related to a problem? Please describe.
Mixins have no way to guarantee hooks into an element's lifecycle (
connectedCallback
,disconnectedCallback
,renderedCallback
). It is hindered in two ways.First, if a mixin defines a
connectedCallback
method in its class body, a custom element that overrides the same method will prevent the mixin's implementation from being called. The custom element would need to callsuper.connectedCallback()
in order to invoke the mixin's implementation. This has two problems. (1) The mixin can't guarantee or require that the custom element include this logic. (2) The custom element must know that the super includes that method because it is not included by default in theElement
class. Otherwise, the custom element must conditionally check for it and call it just in case the mixin defines it. For example:Second, if the mixin attempts to patch the instance in its constructor, attempting to set
this.connectedCallback
to another value, an error is thrown with this message:Cannot assign to read only property 'connectedCallback' of object '[object Object]'
Describe the solution you'd like
I'm not entirely sure what the best solution should look like in detail. The options I've considered are included below. Option 2 below is probably my favorite given its ergonomics and support for extending web components from the outside without breaking any semantics.
Describe alternatives you've considered
Option 1)
Changing the prototype chain sequence resolves this if the mixins are applied to the custom class itself instead of applied to the base class, creating a new base class to extend.
Example, given this mixin:
This works:
and this works:
This change in prototype chain works around the problem since it can be assumed that a mixin developer will be responsible enough to invoke the
super.connectedCallback
if it exists. However, it creates an awkward binding when the mixin provides methods on its prototype that the class calls from its own methods. It works in practice and runtime, however, the semantics don't match exactly.This is not my favorite solution, but it works currently.
Option 2)
Expose a way to add listeners for these lifecycle events. External code can listen to them as events using its
EventTarget
APIs.Example:
This is non-standard, but
renderedCallback
is currently supported and isn't in the standard either.Option 3)
Allow lifecycle methods (e.g.
connectedCallback
,disconnectedCallback
,renderedCallback
) to be writeable on the instance.Option 4)
Add linting rules that require lifecycle event methods to call
super.lifecycleMethod()
in their implementations. This would also require thatElement
has a default implementation to avoid errors. This is a breaking change.The text was updated successfully, but these errors were encountered: