Angular 5 reactive form valueChanges does not work - javascript

I have a form inside a service:
this.settingsForm = this.formBuilder.group({
names: this.formBuilder.array([]),
globalIDs: this.formBuilder.array([]),
topics: this.formBuilder.array([]),
emails: this.formBuilder.array([]),
description: ''
});
and a getter for convenience-
get description(): FormControl{
return this.settingsForm.get('description') as FormControl;
}
in some directive I am injecting the service and have a input that attached to this control. html-
<textarea matInput
[formControl]="settingsDataService.description">
</textarea>
In the directive I have a listener:
ngOnInit() {
this.listenToSearchInput();
}
listenToSearchInput() {
this.settingsDataService.description.valueChanges.pipe(
distinctUntilChanged(),takeUntil(this.descriptionDestroy))
.subscribe(value => {
//DO SOMETHING
});
}
but when I am typing in the textarea, my subscriber does not get called.
Maybe it is relevenat- but after listenToSearchInput() is called, in the service after I get an answer from the server I am filling description by-
this.settingsForm.patchValue({description:description});
What can be the reason?

probably you need to call the function in onChanges life cycle hook of your directive.
onChanges(): void {
this.settingsDataService.description.valueChanges.pipe(
distinctUntilChanged(),takeUntil(this.descriptionDestroy))
.subscribe(value => {
//DO SOMETHING
});
}

I found the answer- in the function that gets data from the server, I create a new seetingsForm to clean the previous data..

Related

Ngfor doesn't actualize on reloading page (Angular + RXJS)

Hi i'm building a chat app with angular for a school project i'm using firebase for my backend and i have an issue with my ngfor.
For exemple if i reload the page i will see nothing unless i hover my routerlink on my navbar. However sometime it will work after some time on the page without any action
When i recieve message i need to be on the page to see them ...
When i reload my page in first time my array is empty this may be what makes the ngfor bug
array on reload.
I'm using ngOnInit() to subscribe :
messages: Message[];
messageSubscription: Subscription;
constructor(private messageService: MessageService, private router: Router) {
}
ngOnInit(): void {
this.messageSubscription = this.messageService.messageSubject.subscribe(
(messages: Message[]) => {
console.log(messages)
this.messages = messages;
}
);
this.messageService.getMessage();
this.messageService.emitMessage();
}
ngOnDestroy(): void {
this.messageSubscription.unsubscribe();
}
This is my html template :
<div *ngFor="let message of messages" class="message-box">
<img [src]="message.photoURL" class="profile-picture">
<div class="content-box">
<div class="information">
<p class="username">{{message.displayName}}</p>
<p class="date">{{message.createdAt | date: 'short'}}</p>
</div>
<p class="text">{{message.text}}</p>
</div>
</div>
Here you can find my service with my getMessage() function and emitMessage():
messages:Message[] = [];
messageSubject = new Subject<Message[]>();
constructor() { }
emitMessage(){
this.messageSubject.next(this.messages);
}
saveMessage(newMessage: Message){
firebase.database().ref('/message').push(newMessage);
}
getMessage(){
firebase.database().ref('/message')
.on('value', (data) => {
this.messages = data.val() ? Object.values(data.val()): [];
this.emitMessage();
});
}
And this is the repo of my project: https://github.com/Zuxaw/AngularChatApp
If anyone has a solution I'm interested
Problem is, your firebase library is not Angular specific.
This means you some times need to make sure its code, mostly its event callbacks, run within an Angular zone (google to read about it) to make sure a change detection 'tick' is invoked when data changes.
message.service.ts
import { Injectable, NgZone } from '#angular/core';
// ...
constructor(private zone: NgZone) { }
// ..
getMessage(){
firebase.database().ref('/message')
.on('value', (data) => {
this.zone.run(() => {
this.messages = data.val() ? Object.values(data.val()): [];
this.emitMessage();
});
});
}
I think you might need to use the child_added event instead of value in your getMessage method.
Check if you're receiving data on time in your getMessage method, if not it's most probably, because of the event.
But one thing that I don't understand is why you're calling emitMessage inside getMessage and also calling it inside your component after getMessage, try to evade that.

Getting Data Before Rendering Component/HTML File in Angular

Please, I am having issues working with some async data on angular which comes from my API. I’ve spent some time trying to figure out how to scale through, but I still get stuck.
Scenario
When on edit mode of a patient form, I need to call my centre service to get all available centres from db. When the data is returned, I need to process the data to check which centres a patient belong to, then use this on the html. But I see that the component renders before data is received. This is because, when I click save button to check the data, I see the data there. But in the method where I need to write some logic, when I try to inspect the data returned from the API, it remains undefined.
NB: I can’t use a resolver in this case because, I’m not using a router link to navigate to the page.
I’ve tried to use an async pipe to conditionally check and render the html only if I receive the data which was one solution that worked for someone else. But this seem not to work in my case as i still get undefined on the variable which is inside a method, and where I need to process the data returned before showing my component/html.
Goal
The goal is to first get all centres first before initializing the reactive form, so that i can handle the data on the getPatientCentres() method. I intend to use the data gotten from the API to pre-populate an array when creating the form.
Done other steps and research but the solution doesn’t seem to solve my case.
Any help or logic on how to proceed would be highly appreciated.
Here is my TS code
export class Patient2Component implements OnInit {
formTitle: string;
patientForm: FormGroup;
centreList: ICentre[] = [];
loadedData: boolean = false;
patient: IPatient;
constructor(
private activatedRoute: ActivatedRoute,
private router: Router,
private fb: FormBuilder,
private centreService: CentreService,
) { }
ngOnInit() {
this.getCentres();
this.initCentreForm();
this.checkParamsForEditAction();
}
initCentreForm() {
this.patientForm = this.fb.group({
id: [null],
firstName: ['', Validators.required],
lastName: ['', Validators.required],
centres: [this.centreList]
});
}
getCentres() {
this.centreService.getCentres().subscribe(res => {
this.centreList = res;
// this.loadedData = true;
});
}
checkParamsForEditAction() {
this.activatedRoute.data.subscribe(data => {
this.patient = data['patient'];
if (this.patient) {
this.formTitle = 'Edit Patient';
this.getPatientCentres(this.patient);
this.assignValuesToControl(this.patient);
}
});
}
assignValuesToControl(patient: IPatient) {
this.patientForm.patchValue({
id: patient.id,
firstName: patient.firstName || '',
lastName: patient.lastName || '',
});
}
getPatientCentres(patient: IPatient) {
const patientCentres = patient.patientCentres;
/**Here, the centreList is undefined since data has not returned yet
* And i need this data for processing.
*/
console.log(this.centreList);
}
save() {
/**Here, i can see the data */
console.log(this.centreList);
}
Try this
in ngOnInit
ngOnInit() {
this.initCentreForm();
this.getCentres(this.checkParamsForEditAction);
}
getCenters Method
getCentres(callBack?) {
this.centreService.getCentres().subscribe(res => {
this.centreList = res;
// this.loadedData = true;
if(callBack) {
callBack();
}
});
}
you can also use forkJoin, or async await
getCentres(callBack?) {
this.centreService.getCentres().subscribe(res => {
this.centreList = res;
// this.loadedData = true;
//Now call your function directly
this.checkParamsForEditAction();
});
}
Call your function after the get centers is loaded
Order of calling
this.initCentreForm();
this.getCentres();

#Input ngOnChanges generates a new nested array with my passed in array - how can I avoid this?

I'm pretty new to Angular and I'm not quite sure if I'm just doing something wrong with #Input() and ngOnChanges() in my code or if my whole setup isn't correct.
My setup is as follows:
I have some API generated data. My service gets the data and holds logic to do some filtering.
My parent component holds a filtercomponent which has a button "apply filter" (which uses the logic in my service) and a tablecomponent to display the data.
The filtering works fine and I do get the desired filtered data but how do I pass JUST this array to the tableviewcomponent? If I do it via #Input() and run ngOnChanges I get a nested array.
How do I solve this?
Parent TS:
tabellenDaten: any[];
constructor(private filterservice: BdService) {}
ngOnInit() {}
onDisplayTable(filter: BdFilter) {
this.filterservice.getBdTabelle(filter).subscribe(
(daten) => {
console.log('tabellendatenneu', daten);
this.tabellenDaten = daten;
},
(error) => console.log('error: ', error),
() => { }
);
}
Parent HTML which holds a filtercomponent and a tablecomponent:
<div class="v-flex-container-filter">
<app-allfilter-bd
(emitFilter)="onDisplayTable($event)"></app-allfilter-bd>
</div>
<div class="v-flex-container">
<app-tabelle
[tabellenDaten]="tabellenDaten"></app-tabelle>
</div>
When I log it I do get the desired table like: Array(148)[{...}, {...}, {...}, ...].
Now, when using [tabellenDaten]="tabellenDaten" and using #Input()...
Child TS (table logic):
#Input() tabellenDaten: any[];
ngOnChanges(...tabellenDaten: any) {
this.dataSource = new MatTableDataSource<any>(tabellenDaten);
console.log('TABELLENDATEN', tabellenDaten);
}
...I do get the results of the ngOnChanges method (currentValue, firstChange and previousValue) which ALSO holds my data array but how do I get JUST the single array?
Any help is very much appreciated.
The implementation of ngOnChanges is: ngOnChanges(changes: SimpleChanges): void
So your code should be:
ngOnChanges(changes: simpleChanges) {
// if 'tabellenDaten' has changed it will be available as a field on 'changes'.
if (changes.tabellenDaten) {
this.dataSource = new MatTableDataSource<any>(changes.tabellenDaten.currentValue);
}
}
Another approach is to use RxJS with async pipe provided by angular.
Instead of subscribe this.filterservice.getBdTabelle(filter), you can assign the observable :
tabellenDaten$: Observable<any[]>;
constructor(private filterservice: BdService) {}
ngOnInit() {}
onDisplayTable(filter: BdFilter) {
this.tabellenDaten$ = this.filterservice.getBdTabelle(filter);
}
Then in your template use async pipe :
<div class="v-flex-container-filter">
<app-allfilter-bd
(emitFilter)="onDisplayTable($event)"></app-allfilter-bd>
</div>
<div class="v-flex-container">
<app-tabelle
[tabellenDaten]="tabellenDaten$ | async"></app-tabelle>
</div>
And finally you don't need to assign your array to MatTableDataSource but you can pass you array directly to mat-table :
<mat-table [dataSource]="tabellenDaten">
You should always consider to not subscribe your Observable in your component unless you unsubscribe it manually. Otherwise use async and let the pipe subscribe/unsubscribe for you.

Subscribing to a change, but needing to see variable from where Service is used?

My code has been refactored and some extracted into a service that subscribes to functions. However, my original code had a call within the subscription that referenced a variable within the file, but now I'm not sure how to best reach it?
I am struggling with where to place the line:
this.select.reset('some string'); found within the subscribeToMessageService() function.
Original code
event.component.ts
select: FormControl;
#ViewChild('mySelect') mySelect: ElementRef;
subscribeToMessageService() {
this.messageService.serviceMsg
.subscribe(res => {
// unrelated code
this.select.reset('some string');
});
}
subscribeToEventService() {
this.eventService.eventSubject
.subscribe(res => {
this.select = new FormControl(res.status);
this.select.valueChanges.subscribe(value => {
// manual blurring required to stop error being thrown when popup appears
this.selector.nativeElement.blur();
// do something else
});
});
}
Refactored code
status.service.ts
subscribeToMessageService(): void {
this.messageService.serviceMsg
.subscribe(res => {
// unrelated code
// This is where 'this.select.reset('some string');' would have gone
});
}
status.component.ts
select: FormControl;
#ViewChild('exceptionalSelect') selector: ElementRef;
subscribeToEventService() {
this.eventService.eventSubject
.subscribe(res => {
this.select = new FormControl(res.status);
this.select.valueChanges.subscribe(value => {
// manual blurring required to stop error being thrown when popup appears
this.selector.nativeElement.blur();
this.onStatusChange(value);
});
});
}
Since you still want to subscribe to the original source messageService.serviceMsg your new StatusService needs to expose this observable to the injecting component (StatusComponent).
This can be done for example by creating a public observable in the StatusService (possibly by utilising rxjs Subject or angular EventEmitter) and triggering the emit in the subscription of messageService.serviceMsg.
Then your StatusComponent only needs to inject StatusService and do
this.statusService.serviceMsg // <-- might choose another name to make clear that this is passed on.
.subscribe(res => {
// unrelated code
this.select.reset('some string');
});

Angular async form validation using Firebase

I'm trying to implement a form validation that checks on Firebase if a username exists. If it doesn't, then the form becomes invalid.
The form validation works fine when I mock the data using an Observable. However, it doesn't work when I fetch the data from Firebase.
This works:
fromMockData(username: string): Observable<Usernames> {
return username === 'demo' ? Observable.of({ uid: 'test' }) : Observable.of(null);
}
This one doesn't:
fromFirebase(username: string): Observable<Usernames> {
return this.afs.doc(`usernames/${username}`).valueChanges();
}
I'm accessing both services from a validation service:
fromFirebase(input: FormControl): Observable<{[key: string]: any}> {
return this.service.fromFirebase(input.value).pipe(
map(user => user ? { invalidUsername: `#${input.value} already exists` } : null),
);
}
Any ideas why it doesn't work when fetching the data from Firebase?
PS. I can see the correct value when logging user into the console - even when using Firebase. However, it's not returning the proper value to the form's errors properties (it only works in the first case: creating an Observable with mock data).
This is my form, btw:
this.form = this.fb.group({
username: ['', [], [this.validator.fromFirebase.bind(this.validator)]],
});
Demo
Because Firebase returns a streaming of data, I need to use the first operator so that only the first item is emitted:
fromFirebase(input: FormControl): Observable<{[key: string]: any}> {
return this.service.fromFirebase(input.value).pipe(
first(),
map(user => user ? { invalidUsername: `#${input.value} already exists` } : null),
);
}

Categories