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
// 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.
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.
// Old
@Output()
placeholderUrl = toObservable(this.text).pipe(....);
// 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
// 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));
}
}
<app-image-placeholder (placeholderUrl)="url = $event" />
The placeholderUrl
output event assigns the value to the url
instance member
child = viewChild.required(ImagePlaceholderComponent);
viewChild.required
queries the ImagePlaceholderComponent
instance in the demo.
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.
<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
// 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()))
}
}
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.
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.
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.
// 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));
}
}
<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
child = viewChild.required(ImagePlaceholderComponent);
viewChild.required
queries the ImagePlaceholderComponent
instance in the demo.
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.
<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:
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:
- Stackblitz Demo with outputFromObservable: https://stackblitz.com/edit/angular-output-fn-mp42ug?file=src%2Fmain.ts
- Stackblitz Demo with output(): https://stackblitz.com/edit/angular-output-fn-wb6pwh?file=src%2Fmain.ts
- Github Repo: https://github.com/railsstudent/ng-output-demos
Top comments (2)
Hi Connie Leung,
I love your articles !
Your tips are very useful
Thanks for sharing
Thank you so much. angular team is constantly improving Angular to make it easy for developers to build applications