New output function - let's talk without a decorator in Angular
Tags: angular, signal, tutorial, webdev
Published: 2024-03-08 17:21:43

##Introduction

In this blog post, I would like to show a new feature in Angular 17.3.0-rc.0 that calls the output function. With the new output function, a child component can emit data to the parent without a decorator. Moreover, the return type of the new output function is `OutputEmitterRef` which can convert to an Observable through the rxjs-interop function, `outputToObservable`. Similarly, an Observable can convert to an `OutputEmitterRef` through `outputFromObservable` function to emit data to its parent.

In this post, I created 2 demos that are clones of the generic image placeholder site (https://dev.me/products/image-placeholder).  The demos are designed to demonstrate the usage of `output`, `outputToObservable`, and `outputFromObservable` respectively.  

Demo 1: The demo binds the signals to the template-driven form in the child component.  When a signal value updates, it emits the value to the RxJS operators to construct the full URL.  The URL is later converted to an OutputEmittRef using outputFromObservable.  The parent component queries the URL output, converts it to an Observable, and emits the value to the scan operator to count the number of changes.  

Demo 2:  The demo also binds the signals to the template-driven form in the child component.  When a signal value updates, the computed signal recalculates the value of the URL.   In the constructor of the child component, the effect uses the new output function to emit the URL to its parent. The parent component queries the URL output, converts it to an Observable, and emits the value to the scan operator to count the number of changes.  

###Demo 1: outputFromObservable and outputToObservable in action

```typescript
// image-placeholder.componen.ts

@Component({
  selector: 'app-image-placeholder',
  standalone: true,
  imports: [FormsModule],
  template: `
    <h3>Redo https://dev.me/products/image-placeholder</h3>
    <div class="container">
      <div class="field">
        <label for="text">
          <span>Text: </span>
          <input id="text" name="text" [(ngModel)]="text" />
        </label>
      </div>
      <div class="field">
        <label for="width">
          <span>Width: </span>
          <input id="width" name="width" [(ngModel)]="width" type="number" min="10" />
        </label>
      </div>
      <div class="field">
        <label for="height">
          <span>Height: </span>
          <input id="height" name="height" [(ngModel)]="height" type="number" min="10" />
        </label>
      </div>
      <div class="field">
        <label for="color">
          <span>Color: </span>
          <input id="color" name="color" [(ngModel)]="color" />
        </label>
      </div>
      <div class="field">
        <label for="backgroundColor">
          <span>Background color: </span>
          <input id="backgroundColor" name="backgroundColor" [(ngModel)]="backgroundColor" />
        </label>
      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ImagePlaceholderComponent {
  text = signal('Signal Output');
  width = signal(400);
  height = signal(120);
  color = signal('#fff');
  backgroundColor = signal('#000');

  placeholderUrl = outputFromObservable(toObservable(this.text)
    .pipe(
      combineLatestWith(toObservable(this.width),
        toObservable(this.height),
        toObservable(this.color),
        toObservable(this.backgroundColor)
      ),
      map(([text, width, height, textColor, bgColor]) => {
        const encodedText = text ? encodeURIComponent(text) : `${width} x ${height}`;
        const color = encodeURIComponent(textColor);
        const backgroundColor = encodeURIComponent(bgColor);

        return `https://via.assets.so/img.jpg?w=${width}&h=${height}&&tc=${color}&bg=${backgroundColor}&t=${encodedText}`;
      }),
      debounceTime(200)
    ));
}
```

`ImagePlaceholderComponent` has a template-driven form that allows users to input text, width, height, text color, and background color.  Each form field has a ngModel directive that reads and writes to the signal.

```typescript
toObservable(this.text)
    .pipe(
      combineLatestWith(toObservable(this.width),
        toObservable(this.height),
        toObservable(this.color),
        toObservable(this.backgroundColor)
      ),
      map(([text, width, height, textColor, bgColor]) => {
        const encodedText = text ? encodeURIComponent(text) : `${width} x ${height}`;
        const color = encodeURIComponent(textColor);
        const backgroundColor = encodeURIComponent(bgColor);

        return `https://via.assets.so/img.jpg?w=${width}&h=${height}&&tc=${color}&bg=${backgroundColor}&t=${encodedText}`;
      }),
      debounceTime(200)
)
```

`toObservable` converts the signals to the Observables and combines the latest values in a new Observable.  The new Observable is passed to the map operator to construct the new URL.  `debounce(200)` ensures the URL is only emitted to the parent when it does not change after 200 milliseconds.  Therefore, debounce prevents firing too many URL changes to the parent.   

```typescript
// Old

@Output()
placeholderUrl = toObservable(this.text).pipe(....);
```

```typescript
// New

placeholderUrl = outputFromObservable(toObservable(this.text).pipe(....));
```

The old way is to assign the Observable to the `placeholderUrl` directly and apply the Output decorator to it.  

The new way is to pass the Observable to the  `outputFromObservable` function to create an `OutputEmitterRef`.

###Create a parent component for the output event

```typescript
// main.ts

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ImagePlaceholderComponent, AsyncPipe],
  template: `
    <app-image-placeholder (placeholderUrl)="url = $event" />
    <p>URL: {{ url }}</p>
    <p>URL Change {{ urlChangeCount$ | async }} times.</p>
    <img [src]="url" alt="generic placeholder" />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App implements OnInit {
  url = '';
  child = viewChild.required(ImagePlaceholderComponent);
  urlChangeCount$!: Observable<number>;

  ngOnInit(): void {
    this.urlChangeCount$ = outputToObservable(this.child().placeholderUrl)
      .pipe(scan((acc) => acc + 1, 0));
  }
}
```

```html
<app-image-placeholder (placeholderUrl)="url = $event" />
```

The `placeholderUrl` output event assigns the value to the `url` instance member

```typescript
child = viewChild.required(ImagePlaceholderComponent);
```

`viewChild.required` queries the `ImagePlaceholderComponent` instance in the demo.

```typescript
ngOnInit(): void {
    this.urlChangeCount$ = outputToObservable(this.child().placeholderUrl)
          .pipe(scan((acc) => acc + 1, 0));
}
```

`outputToObservable(this.child().placeholderUrl)` converts, `this.child().placeholderUrl`, that is an `OutputEmitterRef` to an Observable.  The Observable is passed to the `scan` operator to count the number of URL changes. 

```html
<p>URL Change {{ urlChangeCount$ | async }} times.</p>
```

`urlChangeCount$` Observable resolves and displays the count in the inline template.

###Demo 2: Demonstrate the new output function and outputToObservable

```typescript
// image-placeholder-component.ts

@Component({
  selector: 'app-image-placeholder',
  standalone: true,
  imports: [FormsModule],
  template: `
    <h3>Redo https://dev.me/products/image-placeholder</h3>
    <div class="container">
      <div class="field">
        <label for="text">
          <span>Text: </span>
          <input id="text" name="text" [(ngModel)]="text" />
        </label>
      </div>
      <div class="field">
        <label for="width">
          <span>Width: </span>
          <input id="width" name="width" [(ngModel)]="width" type="number" min="10" />
        </label>
      </div>
      <div class="field">
        <label for="height">
          <span>Height: </span>
          <input id="height" name="height" [(ngModel)]="height" type="number" min="10" />
        </label>
      </div>
      <div class="field">
        <label for="color">
          <span>Color: </span>
          <input id="color" name="color" [(ngModel)]="color" />
        </label>
      </div>
      <div class="field">
        <label for="backgroundColor">
          <span>Background color: </span>
          <input id="backgroundColor" name="backgroundColor" [(ngModel)]="backgroundColor" />
        </label>
      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ImagePlaceholderComponent {
  text = signal('Output function');
  width = signal(300);
  height = signal(100);
  color = signal('#fff');
  backgroundColor = signal('#000');

  url = computed(() => {
    const text = this.text() ? encodeURIComponent(this.text()) : `${this.width()} x ${this.height()}`;

    const color = encodeURIComponent(this.color());
    const backgroundColor = encodeURIComponent(this.backgroundColor());

    return `https://via.assets.so/img.jpg?w=${this.width()}&h=${this.height()}&&tc=${color}&bg=${backgroundColor}&t=${text}`;
  });

  placeholderUrl = output<string>({
    alias: 'url'
  });

  constructor() {
    effect(() => this.placeholderUrl.emit(this.url()))
  }
}
```

```typescript
url = computed(() => {
    const text = this.text() ? encodeURIComponent(this.text()) : `${this.width()} x ${this.height()}`;

    const color = encodeURIComponent(this.color());
    const backgroundColor = encodeURIComponent(this.backgroundColor());

    return `https://via.assets.so/img.jpg?w=${this.width()}&h=${this.height()}&&tc=${color}&bg=${backgroundColor}&t=${text}`;
});
```

`url` is a computed signal that recalculates when the user changes any form value.

```typescript
placeholderUrl = output<string>({ alias: 'url' });
```

`placeholderUrl` is an `OutputEmitterRef` of type string and with an alias, url.    Moreover, `alias` is the only property in OutputOptions.

```typescript 
constructor() {
   effect(() => this.placeholderUrl.emit(this.url()))
}
```

Inside the `effect()`, the function runs whenever the URL changes and this is the right place to emit the new URL to its parent.

```typescript
// main.ts

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ImagePlaceholderComponent, AsyncPipe],
  template: `
    <app-image-placeholder (url)="url = $event" />
    <p>URL: {{ url }}</p>
    <p>URL Change {{ urlChangeCount$ | async }} times.</p>
    <img [src]="url" alt="generic placeholder" />
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App implements OnInit {
  url = '';
  child = viewChild.required(ImagePlaceholderComponent);
  urlChangeCount$!: Observable<number>;

  ngOnInit(): void {
    this.urlChangeCount$ = outputToObservable(this.child().placeholderUrl)
      .pipe(scan((acc) => acc + 1, 0));
  }
}
```

```html
<app-image-placeholder (url)="url = $event" />
```

Since an alias is applied to the new output function, the output event is renamed to url in the parent. The url output event assigns the value to the url instance member

```typescript
child = viewChild.required(ImagePlaceholderComponent);
```

`viewChild.required` queries the `ImagePlaceholderComponent` instance in the demo.

```typescript
ngOnInit(): void {
    this.urlChangeCount$ = outputToObservable(this.child().placeholderUrl)
          .pipe(scan((acc) => acc + 1, 0));
 }
```

`outputToObservable(this.child().placeholderUrl)` converts, `this.child().placeholderUrl`, that is an `OutputEmitterRef` to an Observable.  The Observable is passed to the `scan` operator to count the number of URL changes. 

```html
<p>URL Change {{ urlChangeCount$ | async }} times.</p>
```

`urlChangeCount$` Observable resolves and displays the count in the inline template.

###Pros and Cons of both demos

- Readability:  Demo 2 is more readable than Demo 1 because url is a computed signal that builds from the form values. On the other hand,  Demo 1 uses toObservable, combineLatestWith, and map to build the same URL.

- Performance:  Demo 1 does not emit the URL as many times as Demo 2 with the help of debounceTime(200).  It can improve the performance of Demo 1 because the parent does not frequently request the remote server to get a new image and update the image element. 

- RxJS interop: OutputEmitterRef can pass to the outputToObservable function to convert into an Observable.  Then, the component can manipulate the Observable further to create new Observables to display in the template.

The following Stackblitz repos show the final results:

{%embed https://stackblitz.com/edit/angular-output-fn-mp42ug?file=src%2Fmain.ts %}

{%embed https://stackblitz.com/edit/angular-output-fn-wb6pwh?file=src%2Fmain.ts %}


This is the end of the blog post that analyzes data retrieval patterns in Angular. I hope you like the content and continue to follow my learning experience in Angular, NestJS and other technologies.

##Resources:

1. Stackblitz Demo with outputFromObservable: https://stackblitz.com/edit/angular-output-fn-mp42ug?file=src%2Fmain.ts
2. Stackblitz Demo with output(): https://stackblitz.com/edit/angular-output-fn-wb6pwh?file=src%2Fmain.ts
3. Github Repo: https://github.com/railsstudent/ng-output-demos