-
Notifications
You must be signed in to change notification settings - Fork 931
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
Disconnected handler for directives #283
Comments
Thanks for the thoughtful issue @simonbuchan! There's a few interesting things to unpack here. First, parts should possibly be resilient to having Second, I believe we have a similar problem with async iterable support. We should add tests and confirm. We have code to stop listening to new values if the iterable it no longer the current iterable for a Part, but not if the Part or directive is simply removed: https://github.com/Polymer/lit-html/blob/master/src/lib/async-replace.ts#L63 Third, I hope that we can have just one streaming type to support. Do you know why converting the observable to an async iterable wouldn't work? We also have Streams showing up soon, and I don't want to triple-up code, especially when Observable aren't a platform type. Fourth, while it'd be expensive and cumbersome to use MutationObservers to detect Part disconnection, we can use the Part system itself, assuming all structural mutations are done via a template. Directives are values, and we can do some action, like call a callback, when the directive it no longer the current value of a part. Parsed Parts themselves are never removed from a TemplateInstance, the whole TemplateInstance has to be removed, but we can know this most of the time and do some action there. Dynamic Parts, like for iterables, are explicitly removed too. |
It's safer to wrap an async iterator in an observable than vice versa, due to back-pressure: an observable could dispatch a million items synchronously, and the iterator wrapper would have to buffer them all, but in this use case that's not a huge deal. I'm not for sure certain, but I think Streams are a superset of both, so that might work out. In any case this issue is more about the fact that it's impossible for a late setValue() to be a program error right now, because it doesn't have any way to know that it should stop. Mutation observers was just pointing out this is very hard to workaround right now. I've actually been playing around with a cut down version of this to see how hard it is, and it's a bit of a mess. I think I nearly have a good long term design, (eg also handling weird combinations like iterables of observables in attributes) but it's basically a rewrite of the part system, so there's probably a better low hanging fruit. |
Also, when wrapping an observable in on async iterable, you must close the iterable with |
If you're interested, here's the fork I was talking about: https://github.com/simonbuchan/retemplate I don't actually expect you to use anything from it: it's actually a pretty aggressive refactor and stripping down of just The relevant bits for this issue though are:
There is example usage showing it automatically subscribing and unsubscribing to Regarding this issue, finding a way to do the moral equivalent of |
I've started working on this. It won't make 1.0, but probably will 1.1. Right now the design is to add Two things about this approach:
|
I'm somewhat concerned about point 2. I think most users don't use or need this feature, so the performance impact should be limited, or the feature should be opt-in somehow. |
In React land, mount/unmount handlers for custom components are not unpopular? Do you mean most instances won't use or not this feature, or do you have reason to think the usage would be different? |
Lit-html is designed to be a DOM rendering engine, not a full framework
like React.
Custom Elements are the platform counterpart of React's custom components,
and those have callbacks for being connected/disconnected. That API belongs
in the component model layer and not the DOM rendering layer.
The only reason to have a disconnect callback for Parts in lit-html is
because directives may need that information, which is an implementation
detail of directives. Perhaps there is a way to solve the problem there.
(Or perhaps users should be discouraged from putting too much logic in
directives)
…On Sun, Feb 3, 2019, 06:28 Simon Buchan ***@***.***> wrote:
I think most users don't use or need this feature
In React land, mount/unmount handlers for custom components are not
unpopular? Do you mean most instances won't use or not this feature, or do
you have reason to think the usage would be different?
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#283 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAEEyr1AedCYyMPn9I7fJsYpP3ry075uks5vJnNqgaJpZM4SWJ0F>
.
|
Please don't take this as being an attack, I'm really not sure what your intent is here, and I want to make sure there isn't a disconnect (... ha?) between the library design and the users. Unfortunately this means that it's hard not to use combative sounding language without weasel wording everything and increasing confusion!
This sounds like goalpost shifting or some sort of No True Scotsman. React's own description is "A JavaScript library for building user interfaces", and is commonly referred to as "The V in MVC". You can build an application with only react component state, but you really shouldn't. On the other hand, I could point at my own tiny, crappy library, which solves this particular issue by having a different interface that doesn't imply directives should have a disconnect by providing essentially a connect callback, and claim that your library is a "DOM rendering framework, not a simple library" - clearly there's differences in scope, but they are all "take some state, put DOM on screen". In the end, there's clearly an expectation for this feature, and I don't think you're trying to argue that it shouldn't exist, but if you're under the belief that most codebases should not be using this somewhere, or that there should not be codebases using it near exclusively, then that should be made very clear, and the alternatives strongly encouraged. For example, I've never bothered looking into using custom elements internally in a project, as they lose strong typing of the parameters, and for the cases I'd want a disconnect handler for they would be unbelievably heavyweight. On another tack, usage of observables tends to spread, much like promises do, throughout a codebase, and an app will tend to making just about every slot an observable over time. Perhaps that's not the best solution, but it's the least effort, and if you don't have a good answer, it's the one your users will use.
Most (correct) usages of react mount / unmount in react are to bind / unbind some external state to component state, which then re-renders the component with the new values. This is directly equivalent to directive connect / disconnect. My question was why you felt this not super-common, but one that will probably show up at least once, even if only in a library, in most React codebases would not show up in lit-html? The fact that they are "an implementation detail of directives" is irrelevant when when we're talking about implementing the details of directives 😉 |
Directives are not the same as components, and they are not designed to replace components. Using the React model, you have components with state, that react to being mounted and unmounted. In the "platform" model, Web Components are those stateful components that react to being mounted and unmounted. Lit-HTML is like the VirtualDOM engine inside React, which has nothing to do with state or anything like that, and only concerns itself with manipulating DOM nodes. Directives are sort of a hook that allows some flexibility in how to render things, and allow patterns such as deferred rendering or other complex behaviours such as stateful subtree sorting. These directives could have some form of state, usually for caching purposes, and in those cases it may be useful to have a disconnect hook to avoid memory leaks. However, none of the builtin directives need this, and it is not a common pattern, hence my statement that it is unusual and that I expect most developers won't be using this feature. My main point is that performance is also a feature, one which 100% of users want, and we should be careful with implementing more narrow features at the cost of performance. |
That all sounds reasonable. Are async iterables built-in? I'm pretty sure you should be calling If anything I think the issue is that directives are "too powerful" and look too much like a component api. (And nearly are.) |
You can currently asynchronously render any kind of value that you can normally render, which includes iterables. You may run into issues when the asynchronous value resolves but the Part it is rendering to is no longer 'connected', for example because the user changed to another page or something. This can be solved by chaining the value through some 'should this still be rendered' function before it is handed off to the Part to render. It moves the solution to userspace instead of the core library, but it saves a lot of complexity in the library that is not strictly necessary. Directives don't look like a component API to me. For example, they don't have connected/disconnected callbacks :) |
This is a "you break it, you buy it" situation. The design of lit-html, in particular the Consider the trivial example of a stream with the logged in user and another with the current time. The user name is pulled out and shown in a toolbar, and when moused over, new DOM for a popout with the rest of the user details and with relative times updating using the time stream. A few seconds after a roll-out, the DOM is removed. What does this code look like when it's correctly unsubscribing from everything correctly at the right time without support from lit-html? Your concrete advice so far has been to use custom elements, but wrapping each of those user details (of different shape and behavior) in a custom element just to correctly subscribe/unsubscribe would be a mess. On the other hand, lit-html obviously already internally has a connected/disconnected concept (otherwise it wouldn't blow up when values are pushed too late), so it would be extremely natural to just slap a mapped stream into a directive as a "show the last value out here". Are there any non-custom component / JS-land component systems you could instead recommend? That would make the case for a DOM engine / component system split much more compelling.
"connected" is the callback itself. Otherwise they give you pretty complete an API to hook your own logic to... except for the lack of disconnect - which by convention of observables and now React hooks would be just returning a cleanup function from the callback. All the linked issues above show that at least some of your users are viewing them this way. |
I think the example you mention is quite trivial to implement with lit-html. You don't need any kind of asynchronous rendering or other fancy utilities. You just have a declarative template that is rendered by lit-html, and a separate set of logic that determines what to render, and then just render the whole thing every time anything changes. This is exactly why lit-html is nice, you don't have to do any custom data monitoring and micro-updating specific locations in the DOM based on changes, you can just re-render everything every time and lit-html takes care of the complexity of determining what needs to be updated and what doesn't. It feels to me like you're trying to use directives in ways they aren't meant to be used. All the problems you mention seem to be general application logic problems that are solvable with any sort of application architecture (component based or not) without using lit-html specific things like directives. |
Hmm, I'll have to think on that. I think it makes sense to then say, in rxjs terms, "your app view state is one big stream of lit-html instances you feed to a single render function", but I'll have to play around with both that and trying to write such cleanup without a component system but still factorable, to talk about it in more concrete terms at worst. In particular, in rx it requires a mouse out handler inside a stream operator that will remove the outer stream later, and I don't know what that would look like, but maybe it's fine. It does feel like a big missed opportunity though, with directives being right there, and so, so close to being exactly what's needed, since lit-html is nearly tracking all the state needed already. Presuming @justinfagnani finds that directives could cleanly and efficiently support cleanup, people are going to use them that way, and that could be a very good thing if it leads to much simpler user code. (Just to be clear, this really isn't rx specific, despite me mentioning it so much, you could trivially interop with react in the same way with a react portal directive.) |
well this doesn't look good 😲 |
I implemented my own So far I'm pretty happy with my solution and I'm starting to think that it'd be best for us to make a separate directive for handling this. This way it'll keep the core lean. The code I'm using is: const partRefs = new Set()
directive(() => (part) => {
const isConnected = partRefs.has(part)
if (!isConnected) {
partRefs.set(part)
// trigger `onConnect` event
}
})
const render = (domNode) => {
litHtmlRender(/* templateResult */, domNode)
partRefs.forEach((part) => {
const isConnected = document.body.contains(part.startNode)
if (!isConnected) {
partRefs.delete(part)
// trigger `onDisconnect` event
}
})
} |
document.body.contains won't work with shadow dom |
weakrefs should help? But still lit-html needs to do extra work to make this work, as far as I can see from current implementation. And not sure yet if it is polyfillable:) |
Hi there guys! Currently simplest solution that i can work with is to use Observables with 'rxjs' Since i am working with Decorators and i can apply some things when component is mounted and unmounted. This functionality will render template based on observable. Define cls.subscriptions = new Map(); When component is mounted you could do: cls.prototype.connectedCallback = function() {
// Override subscribe method so we can set subscription to new Map() later when component is unmounted we can unsubscribe
Object.keys(this).forEach(observable => {
if (isObservable(this[observable])) {
const original = this[observable].subscribe.bind(this[observable]);
this[observable].subscribe = function(cb, err) {
const subscribe = original(cb, err);
cls.subscriptions.set(subscribe, subscribe);
return subscribe;
};
}
});
} Then when component is unmounted cls.prototype.disconnectedCallback = function() {
// Disconnect from all observables when component is about to unmount
cls.subscriptions.forEach(sub => sub.unsubscribe());
}; Decoratorimport { isObservable } from 'rxjs';
interface CustomElementConfig<T> {
extends?: string;
}
// From the TC39 Decorators proposal
interface ClassDescriptor {
kind: 'class';
elements: ClassElement[];
finisher?: <T>(clazz: Constructor<T>) => undefined | Constructor<T>;
}
// From the TC39 Decorators proposal
interface ClassElement {
kind: 'field' | 'method';
key: PropertyKey;
placement: 'static' | 'prototype' | 'own';
initializer?: Function;
extras?: ClassElement[];
finisher?: <T>(clazz: Constructor<T>) => undefined | Constructor<T>;
descriptor?: PropertyDescriptor;
}
type Constructor<T> = new (...args: unknown[]) => T;
const legacyCustomElement = (
tagName: string,
clazz: Constructor<HTMLElement>,
options: { extends: HTMLElementTagNameMap | string }
) => {
window.customElements.define(
tagName,
clazz,
options as ElementDefinitionOptions
);
return clazz;
};
const standardCustomElement = (
tagName: string,
descriptor: ClassDescriptor,
options: { extends: HTMLElementTagNameMap | string }
) => {
const { kind, elements } = descriptor;
return {
kind,
elements,
// This callback is called once the class is otherwise fully defined
finisher(clazz: Constructor<HTMLElement>) {
window.customElements.define(
tagName,
clazz,
options as ElementDefinitionOptions
);
}
};
};
export const customElement = <T>(
tag: string,
config?: CustomElementConfig<T>
) => (classOrDescriptor: Constructor<HTMLElement> | ClassDescriptor) => {
if (!tag || (tag && tag.indexOf('-') <= 0)) {
throw new Error(
`You need at least 1 dash in the custom element name! ${classOrDescriptor}`
);
}
const cls = classOrDescriptor as any;
cls.is = () => tag;
const connectedCallback = cls.prototype.connectedCallback || function() {};
const disconnectedCallback = cls.prototype.disconnectedCallback || function() {};
cls.subscriptions = new Map();
cls.prototype.disconnectedCallback = function() {
// Disconnect from all observables when component is about to unmount
cls.subscriptions.forEach(sub => sub.unsubscribe());
disconnectedCallback.call(this);
};
cls.prototype.connectedCallback = function() {
// Override subscribe method so we can set subscription to new Map() later when component is unmounted we can unsubscribe
Object.keys(this).forEach(observable => {
if (isObservable(this[observable])) {
const original = this[observable].subscribe.bind(this[observable]);
this[observable].subscribe = function(cb, err) {
const subscribe = original(cb, err);
cls.subscriptions.set(subscribe, subscribe);
return subscribe;
};
}
});
connectedCallback.call(this);
};
if (typeof cls === 'function') {
legacyCustomElement(tag, cls, { extends: config.extends });
} else {
standardCustomElement(tag, cls, { extends: config.extends });
}
};
Lit
|
@Stradivario while this might be a nice surface API for wrapping rendering, as far as I can tell, this isn't really using This issue is about being able to use alternatives systems, either component systems, or just value streams directly ( |
You are correct! Totally agree with you. One thing which is coming to my mind is that if for some reason 'lit-html' decide to re-render the whole template instead of single part of it will lead to subscribing many times to the same observable. Even the disconnected callback will not help that way... Watching this discussion with interest! Regards! |
@Stradivario - I had written this ages ago - https://github.com/prasannavl/icomponent - You might want to take a look at this - basically has everything you wrote there, and a bunch more, but in a more minimalist and generic way. @justinfagnani - Any progress on the |
Thank you very much i am checking what is going on there! |
I took some time to test the approach that I described in #283 (comment). Everything seems to work and the result is in https://gist.github.com/Legioth/2594a6043f54e391615cefea73a5a079. I realized that it's not enough to rely on I had to use the private I haven't run any benchmarks, but it seems like the performance overhead of this approach should be quite minimal. For parts that don't need the tracking, the overhead is just a single boolean check to see if the part is currently active. For parts that are tracked, I'm doing simple O(n) iterations over all child parts to detect changes and a tree traversal when some part is activated or deactivated. Based on this, I would hope that this approach or at least some enablers could be integrated into the core library. |
I'm also coming from the angle of "what's missing?" when trying to use just The ~70 lines linked below uses a class Perhaps this isn't even even necessary. Is it really required to automatically detect the disconnect of a component by its absence in a parent's render-function's template? Is manually calling connected/disconnected from the parent difficult, error-prone or at odds with the concept of declarative views? I haven't yet found a suggestion of how to actually use |
@jancellor we're working on a disconnect handler for lit-html 2.0. But to be clear, lit-html is not intended to have it's own component system. It's designed to be use with custom elements, both to instantiate other custom elements via plain tag names in the templates, and to be used as the template system for a custom element class like LitElement. Yes, directives can keep state, so you can abuse them to build a component system, but it's not something we're very interested in supporting. |
@justinfagnani, acknowledged. And thanks for explicitly stating it's designed to be used with custom elements. I'm still interested it how/if it can be used in a non-web-component component model (which as in my example above doesn't necessarily mean storing state in directives) but here may not be the best place to ask. |
@justinfagnani If you want to have a look at how Also, the library haunted implements a virtual component model on top of |
@justinfagnani Actually, we should have this feature not because we use lit-html for creating a component system, but because if a directive can be applied to the DOM element it definitely should have we a way to know that this element is not accessible anymore because lit-html removes it from the DOM. A good example is any async operations in directives. We can perform them on element render, but can't abort if the element removed. It's very common but at the same time very important thing. |
This feature has been implemented in the |
Thank you very much for the effort creating this PR! Much appreciated! @kevinpschaaf I have managed to create a whole I really don't see any problems working with WebComponents and LitHtml in production grade applications which for sure are not "toys" if you insist to show you an example please contact me at my email. Regards, |
FYI the |
I've been playing around with combining lit-html and observables, and it feels really nice!
The only issue I've run into is that, since observables are a push-based API, they must be explicitly unsubscribed when the source outlives the destination to avoid leaking events, memory etc....
The problem
Lets say I'm using a very simple directive:
And rendering it as such:
Everything is fine. However if you later replace the template:
The interval subscription is still running, as the
.do(console.log)
shows, and lit-html errors:To fix this, a directive needs to provide a way to know when new values should stop being pushed to the part. So far the only way I know of to do this with the current api is to somehow use
MutationObserver
to detectpart.instance.template.element
being removed, but that requires that you observechildList
mutations on the parent, which isn't present when the directive callback runs.Possible solutions
The ideal for me would be to simply accept observables 😉 (which are basically equivalent to directives) but a possibly smaller change would be to just steal a trick from observable subscriber functions and have directive callbacks optionally return a cleanup function:
The text was updated successfully, but these errors were encountered: