-
Notifications
You must be signed in to change notification settings - Fork 59
Hey!
First of all, sorry for using GitHub issues for asking questions but not sure what's the best place to ask for help. I want to ask what's the best way to access targets during the initialization process (according to docs,connectedCallback might be too early to query element from the DOM)? Let's say I'd like to setup / start dropzone on a specific target when component is initialized. When / how should I do it?
All reactions
-
👀 1
Replies: 3 comments 2 replies
Actually I've found html-parsed-element which addresses this problem (it's a nice abstraction over MutationObserver). Unfortunately, it does not allow to override connectedCallback via regular operator assignment (used by @controller decorator), so I've created a PR to fix it. Hopefully it gets merged, so we can access targets in the parsedCallback like this:
import HTMLParsedElement from 'html-parsed-element'; import { controller, attr, target } from '@github/catalyst' @controller export default class TextEditorElement extends HTMLParsedElement { @target area: HTMLInputElement parsedCallback() { this.area.value = 'Hello World!' } }
PS. TypeScript might complain about HTMLParsedElement compatilbity with HTMLElement (Argument of type 'typeof TextEditorElement' is not assignable to parameter of type 'CustomElement'. Type 'TextEditorElement' is missing the following properties from type 'HTMLElement': accessKey, accessKeyLabel, autocapitalize, dir, and 234 more.).
All reactions
PR got merged, and new version has been released so parsedCallback can be used with catalyst to initialize targets as shown above.
Should I add such info to the guides?
All reactions
Thanks for this discussion @lowski!
First of all, sorry for using GitHub issues for asking questions but not sure what's the best place to ask for help.
Totally fine! I have set up discussions for this repo and transferred your issue to Discussions, hopefully we can continue the chat here!
I want to ask what's the best way to access targets during the initialization process (according to docs,
connectedCallbackmight be too early to query element from the DOM)?
The short and blunt answer to this is, in my opinion, "don't". That's not very helpful though 😅, so I'll try to expand:
Generally speaking, I think the whole concept of children traversal during initialisation is misguided. Child elements can be swapped out from underneath a component at any point and so any one time initialisation such as addEventListener should be within a MutationObserver (which is exactly what Catalyst actions do, under the hood).
Consider the children of an element to be a piece of state. It is initialized and can be mutated outside of the control of the element, and as such the element should only query for its state at the time of need. If an element has private state to initialize it can do so in connectedCallback, but when it comes to initializing state of its descendants it should either delegate that to autonomous behaviours of the descendants (read: the descendants should be custom elements initializing their own state), or if that is not possible then it should do so robustly, understanding its descendants can be swapped out on a whim. To take your TextEditorElement example and apply this principle to it:
import { controller, attr, target } from '@github/catalyst' @controller export default class TextEditorElement extends HTMLParsedElement { @target area: HTMLInputElement #currentArea: HTMLInputElement connectedCallback() { new MutationObserver(() => { if (this.area !== this.#currentArea) { // this.area has changed so we should re-initialize it this.area.value = 'Hello World!' this.#area = this.area } }).observe(this, { subtree: true, childList: true }) } }
This code is a little tricky to properly read through but I think it's falling victim to contrived code. I think it's quite rare to want to instantiate values once upon initialization, perhaps some more common scenarios with solutions:
- Direct data binding of events. In this case use
data-action. - Initializing private state for targets. In this case consider making the targets controllers.
Actually I've found html-parsed-element which addresses this problem
It is not just parser race conditions that cause connectedCallback to fire potentially too early. As connectedCallback is fired as soon as the element is appended, depending on the calling code, it might be before elements are appended:
const editor = document.createElement('text-editor') document.body.appendChild(editor) // ^ connectedCallback has now been fired editor.appendChild(el)
The parsedCallback that html-parsed-element provides won't solve this; or if it does it only does so coincidentally (due to the asynchronous callback nature of MutationObserver). The following should hopefully demonstrate that:
const editor = document.createElement('text-editor') document.body.appendChild(editor) // ^ connectedCallback has now been fired // parsedCallback should be fired around here setTimeout(() => editor.appendChild(el), 100)
All reactions
Thank you @keithamus for taking the time to provide such detailed answer! I really appreciate that. I haven't considered that children my change once they are rendered (i.e. via child components). In some cases that assumption might be correct (i.e. in my TextEditorElement example, textarea should not changed as it suppose to be controlled by that wrapper / parent component only), in some it might not.
After going through your example with mutation observer, I started to understand the problem with using html-parsed-element . At the same time, I have a feeling (but please correct me if I'm wrong) that Catalyst is not the best tool for building wrappers around existing JS plugins (as you said "the whole concept of children traversal during initialisation is misguided"), as most of them relies on initialization with DOM element being part of the setup. Or maybe I'm looking from wrong side at this problem? Are there any examples of using Catalyst to wrap existing JS plugins?
All reactions
Yeah Catalyst (or Web Components in general) can be used for plugins or wrapping other apps but it very much depends on what the plugin/app does. For example we have Catalyst wrapping some React UI, but we avoid using @target altogether as React usually wants to keep its VirtualDOM the single source of truth.
Sadly I don't have a good straightforward answer, it's just another big "it depends" 😅