Angular component aware of event emitter's result - javascript

I have a generic button component:
#Component({
selector: "debounced-submit-button"
template: `
<button (click)="debounceClick.emit()" [disabled]="disabled">
<ng-content></ng-content>
</button>
`})
export class DebouncedSubmitButton {
#Input() disabled: boolean = false;
#Output() debounceClick = new EventEmitter();
}
And I use it like:
#Component({
selector: "example-component",
template: `
<debounced-submit-button (debounceClick)="makeBackendCall()" disabled="loading">
</debouncedSubmitButton>
`})
export class ExampleComponent {
loading = false;
makeBackendCall(): Promise<any> {
this.loading = true;
return apiService.makeCall()
.then(result => useResult(result))
.finally(() => this.loading = false);
}
}
So you can't click on the button again while the HTTP call is in progress. However, this requires me include a lot of boilerplate call to track "loading" wherever I have a button.
Is there any way to communicate back the results of the (debounce-click) event to the debounced-submit-button, so I can centrally locate my disabling code? Like, ideally, I just want
<debounced-submit-button (debounce-click)="makeBackendCall()">
and have the component be something like
#Component({
selector: "debounced-submit-button"
template: `
<button (click)="onClick($event)" [disabled]="disabled">
<ng-content></ng-content>
</button>
`})
export class DebouncedSubmitButton {
disabled: boolean = false;
#Output() debounceClick = new EventEmitter();
onClick() {
this.disabled = true;
// I don't think this works, but I want the return value of the callback function
httpCall = debounceClick.emit();
httpCall.finally(() => this.disabled = false);
}
}
Like, obviously this doesn't work, because the debounceClick event emitter could be subscribed to by multiple listeners, or no listeners. I'm just looking for a less-boilerplate-y way to communicate to the DebouncedSubmitButton that the API call is done, and the user should be able to interact with it again. Is there a way to do this?

Sounds to me like you just want to pass a function as an input.
template: `
<button (click)="_onClick($event)" [disabled]="disabled">
<ng-content></ng-content>
</button>
`})
export class DebouncedSubmitButton {
disabled: boolean = false;
#Input() onClick: () => Promise<any>;
_onClick() {
this.disabled = true;
this.onClick().finally(() => this.disabled = false;)
}
}
<debounced-submit-button [onClick]="makeBackendCall">
Just make sure you pass arrow functions instead of regular functions to maintain the lexical context of this.
export class ExampleComponent {
makeBackendCall = () => {
return this.apiService.makeCall()
.then(result => this.useResult(result))
}
}

create a flag in the api service, inside the service, fundamentally there will be four implementations mostly! ( GET,POST,PUT,DELETE ) so we just need to toggle the flag, we use the tap operator, which will execute after the API call happens, but will not do anything to the response. then use that flag to enable/disable buttons in any components. Please check the implementation below!
import { Injectable } from '#angular/core';
import { tap } from 'rxjs/operators';
#Injectable()
export class ApiService {
apiCallInProgress = false;
constructor(private http: HttpClient) { }
get(): Observable<any> {
this.apiCallInProgress = true;
return this.http.get('https://jsonplaceholder.typicode.com/users').pipe(
tap(() => {this.apiCallInProgress = false;}
);
}
}
Use the service variable in the component.
#Component({
selector: "debounced-submit-button"
template: `
<button (click)="debounceClick.emit()" [disabled]="getDisabled()">
<ng-content></ng-content>
</button>
`})
export class DebouncedSubmitButton {
#Output() debounceClick = new EventEmitter();
constructor(apiService: ApiService){}
getDisabled() {
return this.apiService.apiCallInProgress;
}
}

Related

How to use RXJS to make a click event emit a value in angular

I hope not to get dom directly.Not use document.querySelector态ViewChild...
I need to create an Observable and mount the internal variables this.subscribe = subscribe to the component instance. I think this is not good, very jumping.
import { Component, OnDestroy, OnInit } from '#angular/core';
import { Observable, Subscriber, Subscription } from 'rxjs';
#Component({
selector: 'app-root',
template: '<button (click)="onClick()">button</button>',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {
subscribe!: Subscriber<void>
subscription!: Subscription
count: number = 0
ngOnInit(): void {
this.subscription = new Observable<void>((subscribe) => {
this.subscribe = subscribe
})
.subscribe(() => {
console.log('update', ++this.count)
})
}
ngOnDestroy(): void {
this.subscription?.unsubscribe()
}
onClick() {
this.subscribe.next()
}
}
It looks like you haven't discovered Subject yet because you have sort of reinvented it :-)
Basically Subject is an object that you can subscribe to as an Observable, but you can also push values through by calling its .next() method.
export class AppComponent implements OnInit, OnDestroy {
click$ = new Subject<void>()
subscription!: Subscription
count: number = 0
ngOnInit(): void {
this.subscription = this.click$.subscribe(() => {
console.log('update', ++this.count)
})
}
ngOnDestroy(): void {
this.subscription?.unsubscribe()
}
onClick() {
this.click$.next()
}
}
You could define your count as an observable by using the scan operator like this:
export class AppComponent implements OnInit, OnDestroy {
subscription!: Subscription
private click$ = new Subject<void>()
count$: Observable<number> = this.click$.pipe(
scan(previous => previous + 1, 0),
tap(count => console.log('update', count))
)
ngOnInit(): void {
this.subscription = this.count$.subscribe()
}
ngOnDestroy(): void {
this.subscription?.unsubscribe()
}
onClick() {
this.click$.next()
}
}
In many cases you don't need to subscribe in your component, you can use the async pipe in your template instead. This alleviates the need to keep track of Subscription and also doesn't require implementing OnInit and OnDestroy:
export class AppComponent {
private click$ = new Subject<void>()
count$ = this.click$.pipe(
scan(previous => previous + 1, 0),
tap(count => console.log('update', count))
);
onClick() {
this.click$.next()
}
}
Then in your template, do something like:
<p> {{ count$ | async }} </p>
Well you already have an observable, (click) is an event emitter which extends an RxJs subject. So (click)="onClick()" is telling your component to subscribe to the click event emitter of the button with your onClick function, what extra observable do you need?

Property change caused by route change won't update view - *ngIf

So I have a kind of custom select bar with products-header__select expanding the list on click. To do so I created the property expanded which is supposed to describe its current state. With *ngIf I either display it or not.
It works fine clicking the products-header__select. But a click on one of the expanded list's items changes the route, the path and some other element changes, but the products-header__select remains visible.
All good, but I want to collapse the list on route change - my approach was to listen to router events and then run expanded = false when the navigation has ended. - But somehow the view won't update and the list remains expanded, even though running console.log(this.expanded) inside of the router event returns false. Why won't it update then?
View:
<div class="products-header__select" (click)="expanded = !expanded">
<ul>
<li class="basic-text__small custom-select">{{mobileCategories ? (mobileCategories[0].name | transformAllProducts) : ''}}</li>
<div class="select-options" *ngIf="expanded">
<li class="basic-text__small" *ngFor="let category of mobileCategories.slice(1, mobileCategories.length); let i = index" routerLink="/products/{{category.name.toLowerCase()}}">
{{category?.name | transformAllProducts}}
</li>
</div>
</ul>
</div>
import {Component, Input, OnInit} from '#angular/core';
import {NavigationEnd, Router, RouterEvent} from '#angular/router';
#Component({
selector: 'app-products-header',
templateUrl: './products-header.component.html',
styleUrls: ['./products-header.component.scss']
})
export class ProductsHeaderComponent implements OnInit {
expanded = false;
url: string;
$categories;
#Input() set categories(value) {
if (value) {
this.$categories = value;
this.createArrayForMobile();
this.getActiveRoute();
}
}
mobileCategories: any[];
constructor(private router: Router) {
router.events.subscribe((event: RouterEvent) => {
if (event instanceof NavigationEnd) {
this.expanded = false;
this.url = event.url;
this.getActiveRoute();
}
});
}
ngOnInit(): void {
}
getActiveRoute() {
if (!this.mobileCategories) { return; }
const decodedUrl = decodeURI(this.url);
const index = this.mobileCategories.findIndex(item => decodedUrl.includes(item.name.toLowerCase()));
const obj = this.mobileCategories[index];
this.mobileCategories.splice(index, 1);
this.mobileCategories.unshift(obj);
}
createArrayForMobile() {
this.mobileCategories = [...this.$categories, {name: 'all'}];
}
}
That's how I use it:
<app-products-header [categories]="categories"></app-products-header>
Don't know the answer but things I would try would be:
Try subscribing to the router events in ngOnInit() {} rather than the constructor.
Try specifically calling change detection.
constructor(private router: Router, private cdr: ChangeDetectorRef) {
router.events.subscribe((event: RouterEvent) => {
if (event instanceof NavigationEnd) {
this.expanded = false;
this.url = event.url;
this.getActiveRoute();
this.cdr.detectChanges();
}
});}
Had to wrap the router event's code with a timeout:
constructor(private router: Router) {
router.events.subscribe((event: RouterEvent) => {
if (event instanceof NavigationEnd) {
setTimeout(() => {
this.expanded = false;
this.url = event.url;
this.getActiveRoute();
});
}
});
}

Method some() doesn't work as expected in Angular 8

I have an Angular app and I want to add follow/unfollow functionality for users. I'm trying to add isFollowed flag, so I will be able to know if user is followed or no, and depending on that I will show 2 different buttons: Follow and Unfollow. I'm using some() method for this purposes but it doesn't work. It shows me that isFollowed flag is undefined although it should show true or false. I don't understand where the problem is, here is my HTML relevant part:
<button *ngIf="!isFollowing; else unfollowBtn" class="btn" id="btn-follow" (click)="follow(id)">Follow </button>
<ng-template #unfollowBtn><button class="btn" id="btn-follow" (click)="unFollow(id)">Unfollow</button></ng-template>
TS component relevant part:
import { Component, OnInit } from '#angular/core';
import { Router, ActivatedRoute } from "#angular/router";
import { AuthenticationService } from '#services/auth.service';
import { FollowersService } from '#services/followers.service';
#Component({
selector: 'app-user',
templateUrl: './user.component.html',
styleUrls: ['./user.component.css']
})
export class UserComponent implements OnInit {
user;
id;
followers;
isFollowing: boolean;
constructor(
private authenticationService: AuthenticationService,
private followersService: FollowersService,
private router: Router,
private route: ActivatedRoute,
) { }
ngOnInit() {
this.id = this.route.snapshot.paramMap.get("id");
this.authenticationService.getSpecUser(this.id).subscribe(
(info => {
this.user = info;
})
);
this.followersService.getFollowing().subscribe(
data => {
this.followers = data;
this.isFollowing = this.followers.some(d => d.id == this.user.id);
}
);
}
follow(id) {
console.log('follow btn');
this.followersService.follow(id).subscribe(
(data => console.log(data))
)
this.isFollowing = true;
}
unFollow(id) {
console.log('unFollow btn');
this.followersService.unFollow(id).subscribe(
(data => console.log(data))
)
this.isFollowing = false;
}
}
Any help would be appreciated.
If you want it called everytime and to make sure this.user is populated. Then you could use a forkJoin
forkJoin(
this.authenticationService.getSpecUser(this.id),
this.followersService.getFollowing()
).pipe(
map(([info, data]) => {
// forkJoin returns an array of values, here we map those values to the objects
this.user = info;
this.followers = data;
this.isFollowing = this.followers.some(d => d.id == this.user.id);
})
);
Not tested this because I didn't have time. If you make a StackBlitz we could see it in action and try from there.
Hope this helps.

Angular : Output a callback of my Custom directive event and subscribe to it in my component

Under my Angular app , i ve done a Custom directive:
#Directive({
selector: '[appCustomEdit]'
})
export class CustomEditDirective implements OnChanges {
#Input() appCustomEdit: boolean;
private element: any;
constructor(private el: ElementRef, private renderer: Renderer2) {
this.element = el.nativeElement;
}
ngOnChanges(changes: SimpleChanges) {
if (changes.appCustomEdit.currentValue) {
const btnElement = (<HTMLElement>this.element)
.querySelector('.dx-link-save');
this.renderer.listen(btnElement, 'click', () => {
alert('Buton was clicked')
});
}
}
}
in myComponent.html i m using this directive :
<div>
<input [appCustomEdit]=true></input>
</div>
i need now to implement some event / observable outputed from the directive so that i can subscribe to it in myComponent.ts and make some actions.
I wonder how to do it ?
Suggestions ?
Well, direct answer to your question would be something like the following:
import {Directive, EventEmitter, HostListener, Output} from '#angular/core';
#Directive({
selector: '[appCustomInput]'
})
export class CustomInputDirective {
#Output()
myCustomEvent = new EventEmitter();
#HostListener('click')
onClick() {
this.myCustomEvent.emit();
}
}
And then use it like this:
<div>
<input appCustomInput (myCustomEvent)="onMyCustomEvent()"></input>
</div>
However, it is not clear what are you trying to achieve with this, so I cannot really say if this is the way to go or not.

How to trigger change() in a angular form by a custom control without an input

I do want to create a custom control which does not include any input. Whenever the control changes, I do want to save the complete form.
Our current approach uses the form-changed-event like this:
<form #demoForm="ngForm" (change)="onChange()">
<custom-input name="someValue" [(ngModel)]="dataModel">
</custom-input>
</form>
As you can see, we use the "change"-event to react to any change in the form.
This works fine as long as we have inputs, checkboxes, ... as controls.
But our custom control does only exist out of a simple div we can click on. Whenever I click on the div the value of the control is increased by 1. But the "change"-event of the form is not fired. Do I somehow have to link my custom control to the form? Or are there any events which need to be fired?
import { Component, forwardRef } from '#angular/core';
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '#angular/forms';
#Component({
selector: 'custom-input',
template: `<div (click)="update()">Click</div>`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}]
})
export class CustomInputComponent implements ControlValueAccessor {
private onTouchedCallback: () => void = () => {};
private onChangeCallback: (_: any) => void = () => {};
update(){
this.value++;
}
get value(): any {
return this.innerValue;
};
set value(v: any) {
console.log("Change to");
if (v !== this.innerValue) {
this.innerValue = v;
this.onChangeCallback(v);
}
}
writeValue(value: any) {
if (value !== this.innerValue) {
this.innerValue = value;
}
}
registerOnChange(fn: any) {
this.onChangeCallback = fn;
}
registerOnTouched(fn: any) {
this.onTouchedCallback = fn;
}
}
I've created a plunker to demonstrate the problem:
https://plnkr.co/edit/ushMfJfcmIlfP2U1EW6A
Whenever you click on "Click" the model-value is increased, but there is no output on the console, as the change-event is not fired... (There is a console.log linked to the change-event)
Thanks for your replies.
Finally I found the following solution to this problem:
As Claies mentioned in the comment, my custom component does not fire the change event. Therfore the form does never know about the change. This has nothing todo with angular, but as said is the expected behaviour of a input/form.
The easiest solution is to fire the change-event in the customcontrol when a change happens:
constructor(private element: ElementRef, private renderer: Renderer) {
}
public triggerChanged(){
let event = new CustomEvent('change', {bubbles: true});
this.renderer.invokeElementMethod(this.element.nativeElement, 'dispatchEvent', [event]);
}
That's it, whenever I called "onControlChange(..)" in my custom component, then I fire this event afterward.
Be aware, that you need the Custom-Event-Polyfill to support IE!
https://www.npmjs.com/package/custom-event-polyfill
You need to emit the click event of div to its parent. so that you can handle the event.
Plunker Link
Parent component:
import { Component, forwardRef, Output, EventEmitter } from '#angular/core'; // add output and eventEmitter
import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '#angular/forms';
#Component({
selector: 'custom-input',
template: `<div (click)="update($event)">Click</div>`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true
}]
})
export class CustomInputComponent implements ControlValueAccessor {
private onTouchedCallback: () => void = () => {};
private onChangeCallback: (_: any) => void = () => {};
#Output() clickEvent = new EventEmitter(); // add this
update(event){
this.value++;
this.clickEvent.emit(event); // emit the event on click event
}
get value(): any {
return this.innerValue;
};
}
child component:
//our root app component
import {Component} from '#angular/core'
#Component({
selector: 'demo-app',
template: `
<p><span class="boldspan">Model data:</span> {{dataModel}}</p>
<form #demoForm="ngForm">
<custom-input name="someValue"
[(ngModel)]="dataModel" (clickEvent) = onChange()> // handling emitted event here
Write in this wrapper control:
</custom-input>
</form>`
})
export class AppComponent {
dataModel: string = '';
public onChange(){
console.log("onChangeCalled");
}
}
Thanks Stefan for pointing me in the right direction.
Unfortuantely Renderer (which has invokeElementMethod()) has recently been deprecated in favor or Renderer2 (which does not have that method)
So the following worked for me
this.elementRef.nativeElement.dispatchEvent(new CustomEvent('change', { bubbles: true }));
It seems that change event is not fired on form when you call ControlValueAccessor onChange callback (callback passed in registerOnChange function), but valueChanges observable (on the whole form) is triggered.
Instead of:
...
<form (change)="onChange()">
...
you can try to use:
this.form.valueChanges
.subscribe((formValues) => {
...
});
Of course, you must get proper form reference in your component.

Categories

Resources