#Declarative approach for streams in directives?

1 messages Β· Page 1 of 1 (latest)

coarse zodiac
#

Hey guys, let's say I have the following code in a structural directive.

const controlStatus$ = this.control.statusChanges.pipe(startWith(this.control.status)) const formSubmit$ = this.formSubmitService.getFormSubmitObservable(); this.element.nativeElement.style.display = 'none combineLatest([formSubmit$, controlStatus$]) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(([submit]) => { if (this.control.status === 'INVALID' && (submit || this.control.dirty || control.touched)) { this.element.nativeElement.style.display = 'block } else { this.element.nativeElement.style.display = 'none } });

Is it possible to get rid of the subscribe and takeUntilDestroyed here by using a declarative approach? In a component it would be easy to assign the observable stream to the async pipe.
But I cannot find a similar approach for directives. In fact, I wonder if there is anything similar?

winged temple
#

You can use toSignal to transform the observable into a signal, and use host binding rather than direct dom manipulation to set the style.

#

Oops, no. No host binding on structural directives. But is your directive really structural?

coarse zodiac
#

here is full example

`@Directive({
selector: '[validationMessage]',
standalone: true,
})
export class ValidationMessageDirective implements OnDestroy, OnInit {
private readonly element: ElementRef = inject(ElementRef);
private readonly formSubmitService: FormSubmitService = inject(FormSubmitService);
private readonly destroyRef = inject(DestroyRef);

public accessorDirective = input(inject(HostControlDirective));

public ngOnInit(): void {
const control = this.accessorDirective().control;
const controlStatus$ = control.statusChanges.pipe(startWith(control.status));
const formSubmit$ = this.formSubmitService.getFormSubmitObservable();
this.element.nativeElement.style.display = 'none';
combineLatest([formSubmit$, controlStatus$])
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(([submit]) => {
if (control.status === 'INVALID' && (submit || control.dirty || control.touched)) {
this.element.nativeElement.style.display = 'block';
} else {
this.element.nativeElement.style.display = 'none';
}
});
}
}
`

#

so probably not structural only dom manipulation

brazen vessel
#

You can use a host style binding instead of direct DOM manipulation. Put the display style you want in a signal and then use

@Directive({
  host: {
    'style.display': 'display()'
  }
})
coarse zodiac
#

So i guess display is a signal of the directive and then I convert those two observables into signals as well and use it in combination with computed, where I set the display to none or block

brazen vessel
#

For example, yes.

#

You don't have to use signals, they just make it even more declarative

coarse zodiac
#

I know, just want to get my head around, but wouldn't it be an effect in this case?

brazen vessel
#

No, why?

#
@Directive({
  selector: '[validationMessage]',
  standalone: true,
  host: {
    'style.display': 'display()'
  }
})
export class ValidationMessageDirective {
  public accessorDirective = input(inject(HostControlDirective));
  private readonly formSubmitService: FormSubmitService = inject(FormSubmitService);

  readonly display: Signal<string>;

  constructor() {
    const controlStatus$ = toObservable(this.accessorDirective).pipe(
      switchMap(dir => {
        return dir.control.events.pipe(
          startWith(null),
          map(() => [dir.control.status, dir.control.dirty, dir.control.touched])
        );
      }),
    );
    const formSubmit$ = this.formSubmitService.getFormSubmitObservable();
    const display$ = combineLatest([formSubmit$, controlStatus$]).pipe(
      map(([submit, [status, dirty, touched]]) => {
        if (status === 'INVALID' && (submit || dirty || touched)) {
          return 'block';
        } else {
          return 'none';
        }
      })
    );
    this.display = toSignal(display$, {initialValue: 'none' })
  }
}
coarse zodiac
#

sorry got it with computed I always think of a number computation πŸ˜…

#

thx I will try it out

#

my first approach was to declare it directly as a member

` private display: Signal<string> = computed(() => {
const control = this.accessorDirective().control;
const controlStatus$ = control.statusChanges.pipe(startWith(control.status));
const formSubmit$ = this.formSubmitService.getFormSubmitObservable();

const controlStatusSignal = toSignal(controlStatus$);
const formSubmitSignal = toSignal(formSubmit$);

if (controlStatusSignal() === 'INVALID' && (formSubmitSignal() || control.dirty || control.touched)) {
  return 'block';
} else {
  return 'none';
}

});`

brazen vessel
#

My usual approach if I have observables and signals is to compose stuff in the observable world and in the end do toSignal, because the other way around is ... tricky (you can't just call toSignal from anywhere).

coarse zodiac
#

I guess i still need sometime and check out the docs a bit more, to get a full understanding. But this already helped me a lot πŸ™‚

coarse zodiac
#

I tried to make this work a couple of hours, but the issue I have with your approach is that, none of of those switchMap or map callbacks are ever reached πŸ€”
the only way I was able to make it work was with the combineLatest + subscribe

brazen vessel
#

Did you have the toSignal at the end? It subscribes to the observable (if you don't the observable obviously does nothing).

coarse zodiac
#

yes I have

#

constructor() { const controlStatus$ = toObservable(this.accessorDirective).pipe( switchMap((dir) => { console.log('πŸš€ ~ ValidationMessageDirective ~ switchMap ~ dir:', dir); return dir.control.valueChanges.pipe( startWith(null), map(() => [dir.control.status, dir.control.dirty, dir.control.touched]), tap(([status, dirty, touched]) => { console.log('πŸš€ ~ ValidationMessageDirective ~ map ~ status:', status); console.log('πŸš€ ~ ValidationMessageDirective ~ map ~ dirty:', dirty); console.log('πŸš€ ~ ValidationMessageDirective ~ map ~ touched:', touched); }), ); }), ); const formSubmit$ = this.formSubmitService.getFormSubmitObservable(); const display$ = combineLatest([formSubmit$, controlStatus$]).pipe( map(([submit, [status, dirty, touched]]) => { console.log('πŸš€ ~ ValidationMessageDirective ~ map ~ status:', status); if (status === 'INVALID' && (submit || dirty || touched)) { return 'block'; } else { return 'none'; } }), ); this.display = toSignal(display$, { initialValue: 'none' }); }

#

none of those logs are reached

#

which is weird

brazen vessel
#

return dir.control.valueChanges.pipe( This is different from mine and this will cause controlStatus$ tonot update properly when status, dirty and touched change

#

But it is very strange that none of the logs are reached at all.

#

This getFormSubmitObservable definitely emits?

coarse zodiac
#

ah yeah because it tells me that events does not exist on FormControl

brazen vessel
#

Ah, you need to update Angular πŸ˜„

coarse zodiac
#

yes it does

brazen vessel
#

I don't see the issue right now. Would be nice if you could post a Stackblitz πŸ™‚

coarse zodiac
#

update angular? πŸ˜…

#

which version?

#

probably v18?

brazen vessel
#

Yeah, I think events was added in 18

#

But don't quote me on that

#

If you can't update right now, there are some dedicated observables for status, dirty and touched I think

coarse zodiac
#

my guess is that the controlStatus$ is the problem

brazen vessel
#

At least this should be logged for sure:

console.log(':rocket: ~ ValidationMessageDirective ~ switchMap ~ dir:', dir);

coarse zodiac
#

yeah but it is not logged

#

there is something weird with the accessoDirective signal

#

this is a project I was involved lately

brazen vessel
#

It's just an input signal is it not?

coarse zodiac
#

yes it is an input signal

brazen vessel
#

Okay. Well, this is really hard to debug afar like this

coarse zodiac
#

so once the element gets rendered, the switchMap should fire

brazen vessel
#

Correct, considering you subscribed.

#

You said you made it work with manual subscription - can you show that?

coarse zodiac
#

check my initial post

#

that was the code I put into the ngOnInit and it works

#

`export class ValidationMessageDirective implements OnDestroy, OnInit {
private readonly element: ElementRef = inject(ElementRef);
private readonly formSubmitService: FormSubmitService = inject(FormSubmitService);
private readonly destroyRef = inject(DestroyRef);

public accessorDirective = input(inject(HostControlDirective));

public ngOnInit(): void {
const control = this.accessorDirective().control;
const controlStatus$ = control.statusChanges.pipe(startWith(control.status));
const formSubmit$ = this.formSubmitService.getFormSubmitObservable().pipe(startWith(false));
this.element.nativeElement.style.display = 'none';
combineLatest([formSubmit$, controlStatus$])
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(([submit]) => {
if (control.status === 'INVALID' && (submit || control.dirty || control.touched)) {
this.element.nativeElement.style.display = 'block';
} else {
this.element.nativeElement.style.display = 'none';
}
});`
}

brazen vessel
#

I thought you meant it was a variant of my code.

#

Something very weird is going on with that toObservable not firing.

coarse zodiac
#

yeah It does nothing πŸ˜€

brazen vessel
#

Well it does work fine, there is something else going on here. But I can't really tell you anything more but to please post a Stackblitz.

coarse zodiac
#

yeah of course I will try to do that

#

when where the host bindings like this introduced?

host: {
'style.display': 'display()',
},

angular v18?

brazen vessel
#

That syntax has existed since forever, but it used to be discouraged in favor of @HostBinding. But @HostBinding doesn't really support signals, so host is the recommended way now.

#

It still isn't super great, because it is not type-checked, but yeah.

#

That said... I just noticed I had a syntax error in my example code. It should be

@Directive({
  selector: '[validationMessage]',
  standalone: true,
  host: {
    '[style.display]': 'display()'
  }
})