#viewChild signal, ResizeObserver and effects

17 messages · Page 1 of 1 (latest)

arctic osprey
#

Hello!

Given a change in the content of a container (string signal), I need to determine whether I should show something or not (boolean signal).

I can only determine whether to toggle the boolean signal based on an attribute of the container element (let's say it's a given scrollWidth )

So, I tried using:

// ...Template is <span #container>{{ content() }}</span>

readonly elementRef = viewChild<ElementRef<HTMLSpanElement>>('container');
readonly content = this.contentService.content; // Signal that can change

But if I use a computed for the boolean signal, I noticed that the elementRef used does not change when its width change (because content() changed for example).

So, I need to use a ResizeObserver to detect this, and it does. That's good.

I can have an effect that depends on the elementRef(), and disconnects on return. All good.

// Note! This effect does not run when the content of the span (content signal) changes. We would need to also add that dependency to this effect to trigger the check one the name changes...
effect(() => {
  const element = this.elementRef()?.nativeElement;
  if (!element) return;

  const observer = new ResizeObserver((entry) => {
    const el = entry[0].target;

    // Here I can check whether the element's size should update the boolean signal.
    this.booleanSignal.set(el.scrollWidth > VALUE);
  });

  observer.observe(element.nativeElement);
  return () => observer.disconnect();
});

But then it hit me: We are not supposed to update signals inside an effect (https://angular.dev/guide/signals#use-cases-for-effects).

Then how could this be achieved with a computed? Can it be achieved with it at all?

Is there a way to use the ResizeObserver as a signal itself?

Feels like I'm missing a piece of this puzzle. Let me know what do you think!

EDIT: The code here was written by myself here on DIscord as an example. Will update if there are glaring errors.

The web development framework for building modern apps.

bleak moon
#

Then how could this be achieved with a computed? Can it be achieved with it at all?

If you mean Signal<ResizeObserver>, then it appears less meaningful because it is not depedendant on signal (ignore the fact viewChild is a signal first). And, will still need disconnect.

Is there a way to use the ResizeObserver as a signal itself?

If you meanSignal<boolean>, then yes, looks like a right direction

#

The most canonical way is to write a small directive to wrap ResizeObserver, then <span (resizeChange)="doSomething($event)">

Otherwise you need viewChild.required for toSignal to work.

#

Alternatively, just continue what you have. Is ok to set signal value in effect if you know what you doing.

#

if you have viewChild.required, then no need effect since elementRef() never changes. observe and destroy in normal lifecycle

arctic osprey
#

I'll double check with required but the problem here is not the Signal<ElementRef> itself. That one changes from undefined to value and that's about it. Having it required will skip the undefined part.

We need a ResizeObserver from that ElementRef in order to listen to changes in size. That seems to be the only way to listen to them.

So, having the resizeObserver in an effect makes sense: We depend on the Signal<ElementRef> for it, as well as we need to disconnect on cleanup.

But the issue is, how can we communicate that desired size change back to other signals (a computed one that depends on this for example) without having us to manually set it inside the effect.

#

I'll try the resizeChange directive. That sounds like a good way to avoid this effect problem.

Edit: There is no such thing. Should've known. So the ResizeObserver remains.

bleak moon
#

You mean resizeChange ? You have to write the directive yourself

#
class ResizeDirective {
  readonly resizeChange = output<ReadonlyArray<Readonly<ResizeObserverEntry>>>();
  ...
}
arctic osprey
#

Creating a directive to solve this particular problem is out of question. I thought it was only a resize event related to the span I was missing.

bleak moon
# arctic osprey I'll double check with `required` but the problem here is not the `Signal<Elemen...

If no required then you only have boolean|null instead of boolean , if that is ok:

 const booleanRef: Signal<true|false|null> = toSignal(
  // no toObservableTiming issue for viewChild
  toObservable(this.elementRef).pipe(
    filter(Boolean), map(elementRef => elementRef.nativeElement),
    switchMap(htmlElement => new Observable(subscriber => {
        const resizeObserver = new ResizeObserver(x => subscriber.next(x));
        resizeObserver.observe(htmlElement);
        return resizeObserver.disconnect;
    }))
  )
);
ocean cove
#

I would recommend using a MutationObserver instead.
https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver

`readonly isOverflowing = signal(false);
readonly elementRef = viewChild<ElementRef<HTMLSpanElement>>('container');

// Setup observer only once when element is available
effect(() => {
const element = this.elementRef()?.nativeElement;
if (!element) return;

const observer = new MutationObserver(() => {
this.isOverflowing.set(element.scrollWidth > element.clientWidth);
});

observer.observe(element, {
childList: true,
characterData: true,
subtree: true,
});

// Initial check
this.isOverflowing.set(element.scrollWidth > element.clientWidth);

return () => observer.disconnect();
});
`

MDN Web Docs

The MutationObserver interface provides the ability to watch for changes being made to the DOM tree. It is designed as a replacement for the older Mutation Events feature, which was part of the DOM3 Events specification.

bleak moon
arctic osprey
#

Maybe the answer is: We cannot, and we have to deal with that effect updating a signal.

bleak moon
#

Given all the self imposed constraints, can't use Directive, can't required. Of course you are right 😅

bleak moon