joist icon indicating copy to clipboard operation
joist copied to clipboard

DOMInjector providers do not apply for early attributeChangedCallback calls

Open Phoscur opened this issue 4 months ago • 6 comments

Describe the bug DOMInjector configured providers are not loaded for the outmost (App)Element.

To Reproduce Reproduction alongside #1273 in my "not absolutely minimal" joist-(hono-)vite test repo... Steps to reproduce the behavior:

  1. Checkout and install https://github.com/Phoscur/joist-vite
  2. Run npm start and open localhost:4200
  3. See (the suppressed) logs in the browser console: ConsoleDebug should log messages for AppElement

Only adding explicit use: ConsoleDebug provider on top of AppElement will work

Expected behavior Initialising DOMInjector before customElements should apply it for all Elements

Desktop (please complete the following information):

  • OS: Windows
  • Browser Chromium
  • Version 120

Additional context Added logger.type to debug Debug https://github.com/Phoscur/joist-vite/commit/6465058dc4847cdcd0ac16a9890be014d5923df8

Phoscur avatar Jul 31 '25 20:07 Phoscur

quick question. if you example. if you remove the logger from attribute changed. does it work? because attribute changed can be called before the element is actually connected.

deebloo avatar Jul 31 '25 20:07 deebloo

quick question. if you example. if you remove the logger from attribute changed. does it work? because attribute changed can be called before the element is actually connected.

oh yes, you mean using

// instead of
 const logger = this.#logger();
// shortcut
const logger = console;

https://github.com/Phoscur/joist-vite/blob/6465058dc4847cdcd0ac16a9890be014d5923df8/src/app/app.element.tsx#L71

and suddenly it's the right logger type!

Yes I've also seen attributeChangedCallback called before connectedCallback. Seems that explains it!

Phoscur avatar Jul 31 '25 21:07 Phoscur

@deebloo an idea for a solution:

attributeChangedCallback(name = 'lang', oldValue: string, newValue: string) {
    if (name !== 'lang' || !newValue) {
      return;
    }

    // default inject is not ready:
    // const logger = this.#logger();
    // instead awaited inject:
    this.#logger.ready().then((logger) => { // or similar, or const logger = await this.#logger().ready();  
        // get other dependencies
        const i18n = this.#i18n();
        .... actual code ....
    });

what if the dependency is already async? Do I then need to await it twice...?

Phoscur avatar Aug 10 '25 16:08 Phoscur

something like that could potentially help. What I currently do is track the current state using the @injected() lifecycle and don't run attributeChanged unless it has been connected. we COULD change the default behavior of attributeChanged for decorated injectables but that doesn't feel like a good solution.

What if we there was another utility function rather than just the lifecycle callbacks.

async attributeChangedCallback() {
  await injectorReady(this);

  // do the thing
}

deebloo avatar Aug 13 '25 13:08 deebloo

Keep it as simple as possible? ;)

Yes why not make it a utility function, don't know how big the usecase is. Maybe that reads better, or makes it more prominent. Especially if it's just one dependency it would be nice to have it built into context too though:

const logger = await this.#logger.domInjectorReady();
// or even 
const logger = await this.#logger(true);

Phoscur avatar Aug 13 '25 14:08 Phoscur

It might also be a documentation thing because we already have lifecycle callbacks for when the injector itself is ready. its just that attributeChangedCallback runs before any of that happens.

deebloo avatar Aug 13 '25 14:08 deebloo