Skip to content

Navigation Menu

Sign in
Sign up

Access targets during initialization #135

Unanswered
lcmen asked this question in Q&A
Mar 26, 2021 · 3 comments · 2 replies
Discussion options

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?

You must be logged in to vote

Replies: 3 comments 2 replies

Comment options

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.).

You must be logged in to vote
0 replies
Comment options

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?

You must be logged in to vote
0 replies
Comment options

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, connectedCallback might 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)
You must be logged in to vote
2 replies
Comment options

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?

Comment options

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" 😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
Q&A
Labels
None yet
2 participants
Converted from issue

This discussion was converted from issue #129 on March 30, 2021 14:11.

AltStyle によって変換されたページ (->オリジナル) /