pipe operator not behaving as expected RXJS - javascript

Please look at my component below the purpose to is to listen on changes to an input, which it does and then emit the value to the parent component. I created a pipe to only emit every so often and therby minimize the calls to the api, for some reason even though I can see through various console.log statements that it goes in the pipe, it emits the value on every change. What is it that I am missing:
import {ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output, KeyValueDiffers, DoCheck, KeyValueDiffer} from '#angular/core';
import {BehaviorSubject, Observable, of} from "rxjs";
import {debounceTime, distinctUntilChanged, map, skip, switchMap, takeUntil, tap} from "rxjs/operators";
#Component({
selector: 'core-ui-typeahead-filter',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './typeahead-filter.component.html',
})
export class TypeaheadFilterComponent implements DoCheck {
#Input() id: string;
#Input() name: string;
#Input() caption: string;
#Input() placeholder: string;
#Input() cssClass: string;
#Input() cssStyle: string;
#Input() function: any;
#Input() data: Observable<string[]>;
differ: any;
detectChange: string = '';
// term$ = new BehaviorSubject<string>('');
text$ = new Observable<string>();
#Output() onTypeahead: EventEmitter<any> = new EventEmitter<any>();
#Output() onSelect: EventEmitter<any> = new EventEmitter<any>();
constructor(private differs: KeyValueDiffers) {
this.differ = this.differs.find({}).create();
}
handleTypeahead = (text$: Observable<string>) =>
text$.pipe(
distinctUntilChanged(),
debounceTime(500),
).subscribe((value) => {
this.onTypeahead.emit(of(value))
})
handleSelectItem(item) {
this.onSelect.emit(item);
}
ngDoCheck() {
const change = this.differ.diff(this);
if (change) {
change.forEachChangedItem(item => {
if (item.key === 'detectChange'){
console.log('item changed', item)
this.text$ = of(item.currentValue);
this.handleTypeahead(this.text$);
}
});
}
}
}
More background: There is an ngModel on the input linked to detectChange when it changes then the ngDoCheck is called and executes. Everything is done in observables so in the parent I can subscribe to the incoming events.
EDIT -------------------------------------------------------------------
Tried the following solution based on my understanding of #ggradnig answer, sadly it skips over my pipe something seems wrong with it, really not sure what:
handleTypeahead = (text$: Observable<string>) => {
this.test.subscribe(this.text$);
this.test.pipe(
distinctUntilChanged(),
debounceTime(500),
// switchMap(value => text$)
).subscribe((value) => {
tap(console.log('im inside the subscription',value))
this.onTypeahead.emit(value)
})
}
handleSelectItem(item) {
this.onSelect.emit(item);
}
ngDoCheck() {
const change = this.differ.diff(this);
if (change) {
change.forEachChangedItem(item => {
if (item.key === 'detectChange'){
console.log('item changed', item)
this.text$ = of(item.currentValue);
this.handleTypeahead(this.test);
}
});
}
}
}

You can do the following -
export class TypeaheadFilterComponent implements DoCheck {
#Input() id: string;
#Input() name: string;
#Input() caption: string;
#Input() placeholder: string;
#Input() cssClass: string;
#Input() cssStyle: string;
#Input() function: any;
#Input() data: Observable<string[]>;
differ: any;
detectChange: string = '';
// term$ = new BehaviorSubject<string>('');
text$ = new BehaviorSubject<string>('');
serachTerm$: Observable<string>;
#Output() onTypeahead: EventEmitter<any> = new EventEmitter<any>();
#Output() onSelect: EventEmitter<any> = new EventEmitter<any>();
constructor(private differs: KeyValueDiffers) {
this.differ = this.differs.find({}).create();
}
// handleTypeahead = (text$: Observable<string>) =>
// text$.pipe(
// distinctUntilChanged(),
// debounceTime(500),
// ).subscribe((value) => {
// this.onTypeahead.emit(of(value))
// })
ngOnInit() {
this.serachTerm$ = this.text$
.pipe(
distinctUntilChanged(),
debounceTime(500),
//filter(), //use filter operator if your logic wants to ignore certain string like empty/null
tap(s => this.onTypeahead.emit(s))
);
}
handleSelectItem(item) {
this.onSelect.emit(item);
}
ngDoCheck() {
const change = this.differ.diff(this);
if (change) {
change.forEachChangedItem(item => {
if (item.key === 'detectChange'){
console.log('item changed', item)
this.text$.next(item.currentValue);
}
});
}
}
}
Now, at the bottom of your template put the following line -
<ng-container *ngIf="searchTerm$ | async"></ng-container>
Having this line will keep your component code free form managing the subscription [i.e. need not to subscribe/unsubscribe]; async pipe will take care of it.

Related

Angular 8.3.15 custom validator with parameter not working as expected

I'm having some trouble getting a custom validator working. I have other custom and non-custom validators working, but this one that I am passing a parameter to does not work as expected. Within the validator, it is recognizing that the validation code is working, but when looking at the form within field-validation-error, it is returning that the form is valid. Any help would be appreciated, thanks!
Within password.component.ts
this.passwordFormGroup = new FormGroup({
hintQuestionFormControl1: new FormControl(this.currentQuestions[0], Validators.required),
hintAnsFormControl1: new FormControl(this.currentAnswers[0], [Validators.required, EditAccountValidators.checkQuestionsDontContainAnswer('hintQuestionFormControl1')]),
});
Within edditAccountValidators.ts
export class EditAccountValidators {
public static checkQuestionsDontContainAnswer(correspondingQuestion: string): ValidatorFn {
return (control: FormControl) => {
if (control.parent) {
const question = control.parent.get(correspondingQuestion).value;
const answer = control.value;
if (question && answer) {
question.split(" ").forEach(function (wordInQuestion) {
answer.split(" ").forEach(function (wordInAnswer) {
if (wordInQuestion.toLowerCase().includes(wordInAnswer.toLowerCase())) {
console.log('same');
return {answerDoesntContainQuestion : true};
}
});
});
}
}
return null;
}
}
Within field-validation-error.component.ts
import {Component, Input, OnInit} from '#angular/core';
#Component({
selector: 'field-validation-error',
templateUrl: './field-validation-error.component.html',
styleUrls: ['./field-validation-error.component.css']
})
export class FieldValidationErrorComponent implements OnInit {
#Input() validatorName: string;
#Input() form: any;
errorMessage: string;
displayError: boolean;
ngOnInit(): void {
this.errorMessage = this.getValidatorErrorMessage();
this.form.valueChanges.subscribe(() => {
this.displayError = this.form.hasError(this.validatorName);
console.log(this.form);
});
}
private getValidatorErrorMessage() {
return this.validatorName;
}
}
calling Within password.component.html
<field-validation-error
[form]="passwordFormGroup.get('hintAnsFormControl1')"
[validatorName]="'answerDoesntContainQuestion'">
</field-validation-error>

Display an array of data from function in Angular

My goal is to display a cross or a check according to the vote.result data from the polls.
I had to use Angular only few times and I feel pretty lost honestly.
TS file (angular) :
#Component({
selector: 'app-deck-card',
templateUrl: './deck-card.component.html',
styleUrls: ['./deck-card.component.scss'],
})
export class DeckCardComponent implements OnInit {
#Input() isAnim: boolean;
#Input() inGame: boolean;
#Input() editMode: boolean;
#Input() readOnly: boolean;
#Input() deckIsBase: boolean;
#Input() card: CardDto;
#Input() polls: PollDto[];
#Input() isSearch: boolean;
#Input() isImport: boolean;
#Input() idDeck: number;
#Input() editRight: boolean;
#Output() changeVote = new EventEmitter<number>();
#Output() deleteEvent = new EventEmitter<number>();
#Output() duplicateEvent = new EventEmitter<CardDto>();
#Output() importEvent = new EventEmitter<CardDto>();
#Output() sharedToCommunityEvent = new EventEmitter<CardDto>();
safeIcon: SafeUrl | string;
votes: VoteDto[];
constructor(private readonly authState: AuthState,
private sanitizer: DomSanitizer) {
}
ngOnInit(): void {
this.safeIcon = this.sanitizer.bypassSecurityTrustUrl(this.card?.theme?.icon);
this.votes = this.polls?.find(p => p.card.id === this.card?.id)?.votes;
}
/**
* Emit the card ID to delete the card
* #return void
*/
deleteCard(): void {
this.deleteEvent.emit(this.card.id);
}
showTheResult(): string {
console.log(this.polls);
console.log(this.votes);
this.polls?.forEach(vote => {
if (vote.voted && vote.result == false) {
// display a mat-icon cross
console.log(vote)
return '<mat-icon>clear</mat-icon>'
} else if (vote.voted && vote.result == true) {
// display a mat-icon check
console.log(vote)
return '<mat-icon>done</mat-icon>'
}
});
return '';
}
}
My 2 console.log in showTheResult() are always undefined.
So, obviously, the console log in the if condition are never reached.
HTML file :
<div class="card-body" [class.card-body-readOnly]="readOnly">
<p class="main-text" [class.readOnly]="readOnly" [class.short]="inGame && isAnim"
[class.long]="!editMode && !isAnim">{{card?.text}}</p>
<p>{{showTheResult()}}</p>
<p>DISPLAY HERE THE MAT-ICON</p>
<span *ngIf="isAnim || editMode" class="sub-text">#{{card?.id}}</span>
</div>
can someone show me the way ?
The DTOs look like this:
export interface PollDto {
id: number;
result: boolean;
voted: boolean;
priority: number;
card: CardDto;
votes: VoteDto[];
}
export interface VoteDto {
participantId: number;
participantName?: string;
pollId: number;
result: boolean;
}
since your this.polls is an #Input(), you don't know if this variable is actually loaded when you reach ngOnInit lifecycle.
When working with #Input data, if you want to catch the moment data is loaded, you should watch the changes :
https://ultimatecourses.com/blog/detect-input-property-changes-ngonchanges-setters
ngOnChanges(changes: SimpleChanges) {
console.log(changes);
}
This way, you will see if ever your data are loaded, if not, that means the problem is in the parent container component.
Also, a quick note : I don't think you should return HTML in your
method, you probably want to handle this another way, with a directive
or something, this would not be a good practice.
Cheers ! :)

Angular creating filter with pipe and map

I am fairly new to angular and I am trying to create a filter for a value.
In my component - I have => myData$: Observable<MyInterface>
and the interface is as follows
export class FoundValues {
customerName: string;
startDate: string;
endDate: string;
includes(values: string) : boolean {
value = value.toLowerCase();
return this.valueIncludes(this.customerName, value);
}
private valueIncludes(includedValue, value){
if (value) {
const value = value.toLowerCase().includes(includedValue);
return result;
} else {
return false;
}
}
}
export interface MyInterface {
found_values : Array<FoundValues>;
}
In my component ngOnInit(), I am trying to create a logic filter but not getting it as it return a type FoundValues[] and it's complaining that it's not the expected Observable return type.
export class MyComponent implements OnInit{
myData$ = Observable<MyInterface>;
myControl = new FormControl();
ngOnInit(): void{
this.filterData =
this.myControl.valueChanges.pipe(map(value=>this._filter(value)));
}
private _filter(value:string): .....{
--- need logic here ----
}
}
How can I create the filter so that if I type a customer name in my form it shows only the matching customer name?
You can use the combineLatest RxJS operator for filtering through as shown in the following code snippet,
export class MyComponent implements OnInit {
myData$ = Observable < MyInterface > ;
mySearchQuery$: Observable < any > ;
searchString = new FormControl('');
filterData: Observable < any >
constructor() {
mySearchQuery$ = this.searchString.valueChanges.startsWith('');
}
ngOnInit(): void {
this.filterData = this.searchQuery$.combineLatest(this.myData$).map(([queryString, listOfCustomers]) => {
return listOfCustomers.filter(customer => customer.name.toLowerCase().indexOf(queryString.toLowerCase()) !== -1)
})
}
}
The combineLatest RxJS operator takes in the observables myData$ and mySearchQuery and returns the observable filterData containing the emitted values that match the searchString.
usual design in angular would be different
https://stackblitz.com/edit/angular7-rxjs-pgvqo5?file=src/app/app.component.ts
interface Entity {
name: string;
//...other properties
}
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
name = new FormControl('');
data$: Observable<Array<Entity>> = of([
{ name: 'Jhon' },
{ name: 'Jane' },
{ name: 'Apple' },
{ name: 'Cherry' },
]);
filtered$: Observable<Array<Entity>>;
ngOnInit() {
// this can be moved to a util lib/file
const startWithPipe = pipe(
map(
([query, data]: [query: string, data: Array<Entity>]): Array<Entity> =>
data.filter((entity) =>
query ? entity.name.toLowerCase().startsWith(query) : true
)
)
);
this.filtered$ = this.name.valueChanges.pipe(
startWith(''),
debounceTime<string>(300),
withLatestFrom(this.data$),
startWithPipe
);
}

Angular Material table Datasource wont display array

Im having an issue displaying data in a table using datasource i will add my code below please let me know if u see what im doing wrong here I cant see to figure it out
export interface userData {
email: string;
plans: [];
}
export interface PlanData {
amount: number;
channel: string;
expiration: Date;
active: boolean;
};
#Component({
selector: 'app-plan-manager',
templateUrl: './plan-manager.component.html',
styleUrls: ['./plan-manager.component.css']
})
export class PlanManagerComponent implements OnInit {
displayedColumns: string[] = ['active', 'amount', 'channel', 'power'];
array = []
dataSource: PlanData[] = [];
constructor(public router: Router, private firestore: AngularFirestore, private auth: AngularFireAuth, private snackBar: MatSnackBar) { }
ngOnInit(): void {
this.auth.user.subscribe(user => {
this.firestore.collection('users').doc<userData>(user.email).valueChanges().subscribe(userDoc => {
if(userDoc.plans.length === 0){
this.snackBar.open('You have no active plans please purchase a plan here and it will be listed on the dashboard','',{duration: 5000})
this.router.navigate(['plans'])
return;
}
userDoc.plans.forEach((plan, i) => {
this.firestore.collection('plans').doc<PlanData>(plan).valueChanges().subscribe(data => {
//data looks like this {amount: 45, active: true, channel: "test"}
this.dataSource.push(data)
})
})
})
})
}
please let me know if any more code snippets is required ive reread the docs many times on this and just cant figure it out any help would be appreciated
Im able to get it to display 1 row of data using this code here but for some reason it will only display the first object in the datasource nothing else i have a feeling that has something todo with the forEach loop
export interface userData {
email: string;
plans: [];
}
export interface PlanData {
amount: number;
channel: string;
active: boolean;
};
#Component({
selector: 'app-plan-manager',
templateUrl: './plan-manager.component.html',
styleUrls: ['./plan-manager.component.css']
})
export class PlanManagerComponent implements OnInit {
displayedColumns: string[] = ['active', 'amount', 'channel'];
array = []
dataSource: PlanData[] = [];
constructor(private changeDetectorRefs: ChangeDetectorRef, public router: Router, private firestore: AngularFirestore, private auth: AngularFireAuth, private snackBar: MatSnackBar) { }
ngOnInit(): void {
this.auth.user.subscribe(user => {
this.firestore.collection('users').doc<userData>(user.email).valueChanges().subscribe(userDoc => {
if(userDoc.plans.length === 0){
this.snackBar.open('You have no active plans please purchase a plan here and it will be listed on the dashboard','',{duration: 5000})
this.router.navigate(['plans'])
return;
}
userDoc.plans.forEach((plan, i) => {
this.firestore.collection('plans').doc<PlanData>(plan).valueChanges().subscribe(data => {
this.array.push(data)
this.dataSource = this.array
})
})
})
})
}```
Well after rereading docs i have found i was being totally ineffecient and rewrote the code this does everything i was trying to achieve below.
export class PlanManagerComponent implements OnInit {
displayedColumns: string[] = ['active', 'amount', 'channel'];
dataSource: MatTableDataSource<any>;
constructor(private changeDetectorRefs: ChangeDetectorRef, public router: Router, private firestore: AngularFirestore, private auth: AngularFireAuth, private snackBar: MatSnackBar) { }
ngOnInit(): void {
this.dataSource = new MatTableDataSource();
this.auth.user.subscribe(user => {
console.log(user.email)
this.firestore.collection('users').doc(user.email).collection('/plans').valueChanges().subscribe(userDoc => {
this.dataSource.data = userDoc;
});
});
};
};

Unit testing dialog componentInstance event emitter in entry component

I have an entry Dialog component(EntryDialog) and an actual Dialog component(MyDialog). I have a subscriber in entry component which is subscribing to an event emitter in my main dialog component. Struggling with testing this below part, would appreciate your help.
Part to test
this.dialogRef.componentInstance.myEventEmitter.subscribe((type: string) => {
if (type) {
this.assignSample(type);
}
});
Entry Component
#Component({
selector: 'app-entry-dialog',
})
#Component({template: ''})
export class EntryDialog {
dialogRef: MatDialogRef<MyDialog>;
constructor(
private readonly dialog: MatDialog,
) {
this.dialogRef = this.dialog.open(MyDialog, {
data: someData,
disableClose: true,
});
this.dialogRef.componentInstance.myEventEmitter.subscribe((type: string) => {
if (type) {
this.assignSample(type);
}
});
}
private assignSample(type: string) {
// some code here
}
Main Dialog Component
#Component({
selector: 'app-my-dialog',
templateUrl: './my_dialog.ng.html',
})
export class MyDialog {
#Output() myEventEmitter = new EventEmitter<string>(true);
constructor(
#Inject(MAT_DIALOG_DATA) readonly sample: string,
public dialogRef: MatDialogRef<MyDialog>,
) {
merge(
this.dialogRef.backdropClick(),
this.dialogRef.keydownEvents().pipe(filter(
(keyboardEvent: KeyboardEvent) => keyboardEvent.key === 'Escape')))
.pipe(take(1))
.subscribe(() => {
this.dialogRef.close();
});
}
emitEvent() {
this.myEventEmitter.emit("data");
this.dialogRef.close();
}
you can use a mock for it, and you don't need even TestBed here.
it('', () => {
// creating mocked dependencies
const mockDialogRef: any = {
componentInstance: {
myEventEmitter: new Subject(),
},
};
const mockMatDialog: any = {
open: jasmine.createSpy(),
};
mockMatDialog.open.and.returnValue(mockDialogRef);
// action
const instance = new EntryDialog(mockMatDialog);
// checking open call
expect(mockMatDialog.open).toHaveBeenCalledWith(MyDialog, {
data: someData,
disableClose: true,
});
mockDialogRef.componentInstance.myEventEmitter.next('typeToTest');
// assertions
});

Categories

Resources