I made a custom Toast component, which is called at each query error by a notification service (called by an http interceptor).
Here is what my custom toast component looks like :
toast.component.html
<div class="wrapper mx-auto shadow-max" *ngIf="toasts.length">
<div [#toast] *ngFor="let toast of toasts" class="mw-100" [ngClass]="getToastClass(toast)" role="alert">
<div class="toast-header">
{{ 'notify.header.' + getToastType(toast) | translate }}
<button
type="button"
class="close"
data-dismiss="alert"
aria-label="Close"
(click)="removeToast(toast)"
>
<i class="icon" aria-hidden="true">close</i>
</button>
</div>
<div class="toast-body">
<p>{{ 'notify.body.' + toast.message | translate }}</p>
<p>{{ toast.detailsMessage }}</p>
</div>
</div>
</div>
toast.component.ts
import { animate, style, transition, trigger } from '#angular/animations';
import { Component, Input, OnInit } from '#angular/core';
import { ToastType } from '../../enums/toast-type.enum';
import { Toast } from '../../model/toast.model';
import { Utils } from '../../utils/utils';
import { ToasterService } from './toaster.service';
#Component({
selector: 'bigdata-toaster',
moduleId: module.id,
templateUrl: './toast.component.html',
//styleUrls: ['./toast.component.scss'],
animations: [
trigger('toast', [
transition('void => *', [style({ transform: 'scale3d(.3, .3, .3)' }), animate(200)]),
transition('* => void', [animate(200, style({ transform: 'scale3d(.0, .0, .0)' }))])
])
]
})
export class ToastComponent implements OnInit {
#Input() id: string;
toasts: Toast[] = [];
constructor(private readonly toastService: ToasterService) {}
ngOnInit() {
this.toastService.getToast(this.id).subscribe((toast: Toast) => {
if (!toast.message) {
// clear alerts when an empty alert is received
this.toasts = [];
return;
}
// add alert to array
this.toasts.push(toast);
setTimeout(() => this.removeToast(toast), 5000);
});
}
removeToast(toast: Toast) {
this.toasts = this.toasts.filter(x => x !== toast);
}
getToastType(toast: Toast) {
return Utils.enumToString(ToastType, toast.type).toLocaleLowerCase();
}
getToastClass(toast: Toast) {
if (!toast) {
return;
}
switch (toast.type) {
case ToastType.Success:
return 'toast toast-success fade show';
case ToastType.Error:
return 'toast toast-danger fade show';
case ToastType.Info:
return 'toast toast-info fade show';
case ToastType.Warning:
return 'toast toast-warning fade show';
}
}
}
toaster.service.ts
import { Injectable } from '#angular/core';
import { NavigationStart, Router } from '#angular/router';
import { Observable, Subject } from 'rxjs';
import { filter } from 'rxjs/operators';
import { ToastType } from '../../enums/toast-type.enum';
import { Toast } from '../../model/toast.model';
#Injectable({
providedIn: 'root'
})
export class ToasterService {
private readonly subject = new Subject<Toast>();
private keepAfterRouteChange = false;
constructor(private readonly router: Router) {
// clear alert messages on route change unless 'keepAfterRouteChange' flag is true
this.router.events.subscribe(event => {
if (event instanceof NavigationStart) {
if (this.keepAfterRouteChange) {
// only keep for a single route change
this.keepAfterRouteChange = false;
} else {
this.clear();
}
}
});
}
getToast(toastId?: string): Observable<any> {
return this.subject.asObservable().pipe(filter((x: Toast) => x && x.toastId === toastId));
}
success(message: string, detailsMessage?: string) {
this.toast(new Toast({ message, type: ToastType.Success, detailsMessage }));
}
error(message: string, detailsMessage?: string) {
this.toast(new Toast({ message, type: ToastType.Error, detailsMessage }));
}
info(message: string, detailsMessage?: string) {
this.toast(new Toast({ message, type: ToastType.Info, detailsMessage }));
}
warn(message: string, detailsMessage?: string) {
this.toast(new Toast({ message, type: ToastType.Warning, detailsMessage }));
}
toast(toast: Toast) {
this.keepAfterRouteChange = toast.keepAfterRouteChange;
this.subject.next(toast);
}
clear(toastId?: string) {
this.subject.next(new Toast({ toastId }));
}
}
My notification service looks like this :
import {HttpErrorResponse} from '#angular/common/http';
import { Injectable } from '#angular/core';
// import { ActiveToast, IndividualConfig, ToastrService } from 'ngx-toastr';
import { ActiveToast, IndividualConfig } from 'ngx-toastr';
import {ToastComponent} from '../../components/toast/toast.component';
import {ToasterService} from '../../components/toaster/toaster.service';
/**
* Toaster Notification Component
*/
#Injectable()
export class NotificationService {
private readonly DEFAULT_TOASTR_SETTINGS: Partial<IndividualConfig> = {
closeButton: true,
positionClass: 'toast-top-right',
toastComponent: ToastComponent, // custom toast
toastClass: 'toast',
progressBar: true,
tapToDismiss: false,
disableTimeOut: true,
enableHtml: true
};
constructor(private toastrService: ToasterService) {
}
showErrorNotification(err: HttpErrorResponse) {
this.toastrService.success('approve_success');
}
}
I declared my notification service in my app.module.ts :
providers: [
NotificationService,
{
provide: HTTP_INTERCEPTORS, useClass: AppHttpInterceptor, multi: true
}
]
And my HTTP interceptor :
import {HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '#angular/common/http';
import {Injectable} from '#angular/core';
import {ToastrService} from 'ngx-toastr';
import {Observable, of} from 'rxjs';
import {catchError} from 'rxjs/operators';
import {NotificationService} from './shared/services/notifications/notifications.service';
#Injectable()
export class AppHttpInterceptor implements HttpInterceptor {
// constructor(public toastr: ToastrService) {
// }
constructor(public notif: NotificationService) {
}
intercept(
req: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
catchError((err: any) => {
if (err instanceof HttpErrorResponse) {
// this.notif.showNotification(MESSAGE_TYPE.Error, err.message, err.status.toString(), {disableTimeOut: true});
this.notif.showErrorNotification(err);
}
return of(err);
}));
}
}
My HTTP interceptor is working, I can log something.
But I don't have anything on my UI. Anything in the console too. I've been widely inspired from another code, so I think I get something wrong.
Please what am I doing wrong here ? Thanks.
I had a similar problem using primeng toast sent in ngOnInit won't appear, since the component is not rendered yet.
Tryp to wrap the toast request within a setTimeout call, this helped in my case.
Example:
success(message: string, detailsMessage?: string) {
setTimeout( () => this.toast(new Toast({ message, type: ToastType.Success, detailsMessage }));
}
See https://stackblitz.com/edit/angular-primeng-toast-oninit for details.
The reason for this workaround is explained in Call setTimeout without delay
Related
import { NgForm } from '#angular/forms';
import { Exercise } from './../exercise.model';
import { TrainingService } from './../training.service';
import { Component, OnInit, ViewChild } from '#angular/core';
import { AngularFirestore } from 'angularfire2/firestore';
import { Observable, Subscriber } from 'rxjs';
import 'rxjs/add/operator/map';
#Component({
selector: 'app-new-training',
templateUrl: './new-training.component.html',
styleUrls: ['./new-training.component.css'],
})
export class NewTrainingComponent implements OnInit {
exercises: Observable<Exercise[]>;
constructor(
private trainingService: TrainingService,
private db: AngularFirestore
) {}
ngOnInit(): void {
this.exercises = this.db
.collection('availableExercises')
.snapshotChanges()
.map((docArray) => {
return docArray.map((doc) => {
return {
id: doc.payload.doc.data().id,
name: doc.payload.doc.data().name,
duration: doc.payload.doc.data().duration,
calories: doc.payload.doc.data().calories,
};
});
});
}
onStartTraining(form: NgForm) {
this.trainingService.startExercise(form.value.exercise);
}
}
Have issue with id, name, duration, and calories.. they are all underlined and error says: Property does not exist on type 'unknown' for all four. So not sure what the issue is. I have tried as well
id: doc.payload.doc['id'],
name: doc.payload.doc['name'],
duration: doc.payload.doc['duration'],
calories: doc.payload.doc['calories'],
doesn't work as well. Was repeating after Maximilian Schwarzmuller's tutorial. Would appriciate any help.
The answer was to change the next line on the function ngOnInit from:
return docArray.map((doc) => {
Into:
return docArray.map((doc: any) => {
This will allow you to allow you to avoid type checking during compilation. As mentioned here
I call service which make http call, I assign response to component variable now when I try access that component variable to view it display blank.
Means component variable assign in subscribe successfully but cant acceess in html view.
I think view is loaded before values assign to component data.
component
import {Component, OnInit, ChangeDetectionStrategy} from '#angular/core';
import { UserService } from '../../../../../core/services/users/user.service';
import { HttpClient } from '#angular/common/http';
#Component({
selector: 'm-user-list',
templateUrl: './user-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserListComponent implements OnInit {
list;
roles = {};
current_page: any
totalRecords: any
public showContent: boolean = false;
constructor(private userService: UserService, private http: HttpClient) {
}
ngOnInit() {
this.getRecords();
}
getRecords(){
this.getResultedPage(1);
}
getResultedPage(page){
return this.userService.getrecords()
.subscribe(response => {
this.list = response.data;
});
}
}
Service
import { Injectable } from '#angular/core';
import { Observable, of, throwError } from 'rxjs';
import { HttpClient, HttpParams , HttpErrorResponse, HttpHeaders } from '#angular/common/http';
import { map, catchError, tap, switchMap } from 'rxjs/operators';
const httpOptions = {
headers: new HttpHeaders({'Content-Type': 'application/json'})
};
import { UtilsService } from '../../services/utils.service';
import { AppConfig } from '../../../config/app'
#Injectable({
providedIn: 'root'
})
export class UserService{
public appConfig: AppConfig;
public API_URL;
constructor(private http: HttpClient, private util: UtilsService) {
this.appConfig = new AppConfig();
this.API_URL = this.appConfig.config.api_url;
}
private extractData(res: Response) {
let body = res;
return body || { };
}
private handleError(error: HttpErrorResponse) {
if (error.error instanceof ErrorEvent) {
// A client-side or network error occurred. Handle it accordingly.
console.error('An error occurred:', error.error.message);
} else {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong,
console.error(
`Backend returned code ${error.status}, ` +
`body was: ${error.error}`);
}
// return an observable with a user-facing error message
return throwError('Something bad happened; please try again later.');
};
getrecords(): Observable<any> {
return this.http.get('/api/users', httpOptions).pipe(
map(this.extractData),
catchError(this.handleError));
}
}
Why my movie component is not updating after fetching the data. Also not saving the data if I have added a new movie or made changes in existing movies.
It is just saving and fetching the data which is written in movie.service.ts file. Also the fetched data is not rendering on the movie component.
Data-storage.service
import { Injectable } from '#angular/core';
import { MovieService } from '../movies/movies.service';
import { HttpClient, HttpHeaders, HttpParams, HttpRequest } from '#angular/common/http';
import { Movie } from '../movies/movie.model';
import { Observable} from 'rxjs';
import { map } from 'rxjs/operators';
// import 'rxjs/Rx';
// import 'rxjs/Rx';
#Injectable({
providedIn: 'root'
})
export class DataStorageService {
constructor(private httpClient: HttpClient,
private movieService: MovieService,) { }
storeMovies(): Observable<any> {
const req = new HttpRequest('PUT', 'https://moviepedia-4211a.firebaseio.com/movies.json', this.movieService.getMovies(), {reportProgress: true});
return this.httpClient.request(req);
}
getMovies() {
this.httpClient.get<Movie[]>('https://moviepedia-4211a.firebaseio.com/movies.json', {
observe: 'body',
responseType: 'json'
})
.pipe(map(
(movies) => {
console.log(movies);
return movies;
}
))
.subscribe(
(movies: Movie[]) => {
this.movieService.setMovies(movies);
}
);
}
}
movie.service.ts :
import { Injectable } from '#angular/core';
import {Subject} from 'rxjs';
import { Movie } from './movie.model';
#Injectable()
export class MovieService {
moviesChanged = new Subject<Movie[]>();
private movies: Movie[] = [
new Movie(
'Movie test', 'Movie details', 'https://s18672.pcdn.co/wp-content/uploads/2018/01/Movie-300x200.jpg'
),
new Movie(
'Movie test 2', 'Movie details 2', 'https://s18672.pcdn.co/wp-content/uploads/2018/01/Movie-300x200.jpg'
),
new Movie(
'Movie test 2', 'Movie details 3', 'https://s18672.pcdn.co/wp-content/uploads/2018/01/Movie-300x200.jpg'
)
];
constructor(){}
getMovie(index: number) {
return this.movies[index];
}
getMovies() {
return this.movies.slice();
}
addMovie(movie: Movie) {
this.movies.push(movie);
this.moviesChanged.next(this.movies.slice());
}
updateMovie(index: number, newMovie: Movie) {
this.movies[index] = newMovie;
this.moviesChanged.next(this.movies.slice());
}
deleteMovie(index: number) {
this.movies.splice(index, 1);
this.moviesChanged.next(this.movies.slice());
}
setMovies(movies: Movie[]) {
this.movies = movies;
this.moviesChanged.next(this.movies.slice());
}
}
movie.model.ts
export class Movie {
public name: string;
public description: string;
public imagePath: string;
constructor(name: string, description: string, imagePath: string) {
this.name = name;
this.description = description;
this.imagePath = imagePath;
}
}
movie.component :
import { Component, OnInit, EventEmitter, Output, OnDestroy } from '#angular/core';
import { Movie } from '../movie.model'
import { MovieService } from '../movies.service';
import { Router, ActivatedRoute } from '#angular/router';
import { Subscription } from 'rxjs';
#Component({
selector: 'app-movie-list',
templateUrl: './movie-list.component.html',
styleUrls: ['./movie-list.component.css']
})
export class MovieListComponent implements OnInit, OnDestroy {
subscription: Subscription;
movies: Movie[] = [];
constructor(private movieService: MovieService,
private router: Router,
private route: ActivatedRoute) { }
ngOnInit() {
this.subscription = this.movieService.moviesChanged
.subscribe(
(movies: Movie[]) => {
this.movies = movies;
}
);
this.movies = this.movieService.getMovies();
}
onNewMovie() {
this.router.navigate(['new'], {relativeTo: this.route});
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
What can I do save and fetch data which will render on page.
I think the problem with your PUT request is that the url you used expects json but you are sending Movie object. you should send and receive json to this url.
wish it helps ...
I am building an Angular 4 app that requires the BriteVerify email validation on form fields in several components. I am trying to implement this validation as a custom async validator that I can use with reactive forms. Currently, I can get the API response, but the control status is stuck in pending state. I get no errors so I am a bit confused. Please tell me what I am doing wrong. Here is my code.
Component
import { Component,
OnInit } from '#angular/core';
import { FormBuilder,
FormGroup,
FormControl,
Validators } from '#angular/forms';
import { Router } from '#angular/router';
import { EmailValidationService } from '../services/email-validation.service';
import { CustomValidators } from '../utilities/custom-validators/custom-validators';
#Component({
templateUrl: './email-form.component.html',
styleUrls: ['./email-form.component.sass']
})
export class EmailFormComponent implements OnInit {
public emailForm: FormGroup;
public formSubmitted: Boolean;
public emailSent: Boolean;
constructor(
private router: Router,
private builder: FormBuilder,
private service: EmailValidationService
) { }
ngOnInit() {
this.formSubmitted = false;
this.emailForm = this.builder.group({
email: [ '', [ Validators.required ], [ CustomValidators.briteVerifyValidator(this.service) ] ]
});
}
get email() {
return this.emailForm.get('email');
}
// rest of logic
}
Validator class
import { AbstractControl } from '#angular/forms';
import { EmailValidationService } from '../../services/email-validation.service';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
export class CustomValidators {
static briteVerifyValidator(service: EmailValidationService) {
return (control: AbstractControl) => {
if (!control.valueChanges) {
return Observable.of(null);
} else {
return control.valueChanges
.debounceTime(1000)
.distinctUntilChanged()
.switchMap(value => service.validateEmail(value))
.map(data => {
return data.status === 'invalid' ? { invalid: true } : null;
});
}
}
}
}
Service
import { Injectable } from '#angular/core';
import { HttpClient,
HttpParams } from '#angular/common/http';
interface EmailValidationResponse {
address: string,
account: string,
domain: string,
status: string,
connected: string,
disposable: boolean,
role_address: boolean,
error_code?: string,
error?: string,
duration: number
}
#Injectable()
export class EmailValidationService {
public emailValidationUrl = 'https://briteverifyendpoint.com';
constructor(
private http: HttpClient
) { }
validateEmail(value) {
let params = new HttpParams();
params = params.append('address', value);
return this.http.get<EmailValidationResponse>(this.emailValidationUrl, {
params: params
});
}
}
Template (just form)
<form class="email-form" [formGroup]="emailForm" (ngSubmit)="sendEmail()">
<div class="row">
<div class="col-md-12 col-sm-12 col-xs-12">
<fieldset class="form-group required" [ngClass]="{ 'has-error': email.invalid && formSubmitted }">
<div>{{ email.status }}</div>
<label class="control-label" for="email">Email</label>
<input class="form-control input-lg" name="email" id="email" formControlName="email">
<ng-container *ngIf="email.invalid && formSubmitted">
<i class="fa fa-exclamation-triangle" aria-hidden="true"></i> Please enter valid email address.
</ng-container>
</fieldset>
<button type="submit" class="btn btn-primary btn-lg btn-block">Send</button>
</div>
</div>
</form>
There's a gotcha!
That is, your observable never completes...
This is happening because the observable never completes, so Angular does not know when to change the form status. So remember your observable must to complete.
You can accomplish this in many ways, for example, you can call the first() method, or if you are creating your own observable, you can call the complete method on the observer.
So you can use first()
UPDATE TO RXJS 6:
briteVerifyValidator(service: Service) {
return (control: AbstractControl) => {
if (!control.valueChanges) {
return of(null);
} else {
return control.valueChanges.pipe(
debounceTime(1000),
distinctUntilChanged(),
switchMap(value => service.getData(value)),
map(data => {
return data.status === 'invalid' ? { invalid: true } : null;
})
).pipe(first())
}
}
}
A slightly modified validator, i.e always returns error: STACKBLITZ
OLD:
.map(data => {
return data.status === 'invalid' ? { invalid: true } : null;
})
.first();
A slightly modified validator, i.e always returns error: STACKBLITZ
So what I did was to throw a 404 when the username was not taken and use the subscribe error path to resolve for null, and when I did get a response I resolved with an error. Another way would be to return a data property either filled width the username or empty
through the response object and use that insead of the 404
Ex.
In this example I bind (this) to be able to use my service inside the validator function
An extract of my component class ngOnInit()
//signup.component.ts
constructor(
private authService: AuthServic //this will be included with bind(this)
) {
ngOnInit() {
this.user = new FormGroup(
{
email: new FormControl("", Validators.required),
username: new FormControl(
"",
Validators.required,
CustomUserValidators.usernameUniqueValidator.bind(this) //the whole class
),
password: new FormControl("", Validators.required),
},
{ updateOn: "blur" });
}
An extract from my validator class
//user.validator.ts
...
static async usernameUniqueValidator(
control: FormControl
): Promise<ValidationErrors | null> {
let controlBind = this as any;
let authService = controlBind.authService as AuthService;
//I just added types to be able to get my functions as I type
return new Promise(resolve => {
if (control.value == "") {
resolve(null);
} else {
authService.checkUsername(control.value).subscribe(
() => {
resolve({
usernameExists: {
valid: false
}
});
},
() => {
resolve(null);
}
);
}
});
...
I've been doing it slightly differently and faced the same issue.
Here is my code and the fix in case if someone would need it:
forbiddenNames(control: FormControl): Promise<any> | Observable<any> {
const promise = new Promise<any>((resolve, reject) => {
setTimeout(() => {
if (control.value.toUpperCase() === 'TEST') {
resolve({'nameIsForbidden': true});
} else {
return null;//HERE YOU SHOULD RETURN resolve(null) instead of just null
}
}, 1);
});
return promise;
}
I tries using the .first(). technique described by #AT82 but I didn't find it solved the problem.
What I eventually discovered was that the form status was changing but it because I'm using onPush, the status change wasn't triggering change detection so nothing was updating in the page.
The solution I ended up going with was:
export class EmailFormComponent implements OnInit {
...
constructor(
...
private changeDetector: ChangeDetectorRef,
) {
...
// Subscribe to status changes on the form
// and use the statusChange to trigger changeDetection
this.myForm.statusChanges.pipe(
distinctUntilChanged()
).subscribe(() => this.changeDetector.markForCheck())
}
}
import { Component,
OnInit } from '#angular/core';
import { FormBuilder,
FormGroup,
FormControl,
Validators } from '#angular/forms';
import { Router } from '#angular/router';
import { EmailValidationService } from '../services/email-validation.service';
import { CustomValidators } from '../utilities/custom-validators/custom-validators';
#Component({
templateUrl: './email-form.component.html',
styleUrls: ['./email-form.component.sass']
})
export class EmailFormComponent implements OnInit {
public emailForm: FormGroup;
public formSubmitted: Boolean;
public emailSent: Boolean;
constructor(
private router: Router,
private builder: FormBuilder,
private service: EmailValidationService
) { }
ngOnInit() {
this.formSubmitted = false;
this.emailForm = this.builder.group({
email: [ '', [ Validators.required ], [ CustomValidators.briteVerifyValidator(this.service) ] ]
});
}
get email() {
return this.emailForm.get('email');
}
// rest of logic
}
Im trying a simple profile app, and all the sudden Im getting error TS2554
ERROR in /app/components/profile/profile.component.ts(25,3): error TS2554: Expected 1 arguments, but got 0.
import { Component, OnInit } from '#angular/core';
import { AuthService } from '../../services/auth.service';
import { FlashMessagesService } from 'angular2-flash-messages';
import { Router } from '#angular/router';
#Component({
selector: 'app-profile',
templateUrl: './profile.component.html',
styleUrls: ['./profile.component.css']
})
export class ProfileComponent implements OnInit {
user: Object;
constructor(
private auth: AuthService,
private flashMsg: FlashMessagesService,
private router: Router
) {
}
ngOnInit() {
this.auth.getProfile().subscribe( profile => {
this.user = profile.user;
},
err => {
return false;
});
}
}
auth.service.ts
import { Injectable } from '#angular/core';
import { Http, Headers } from '#angular/http';
import 'rxjs/add/operator/map';
import { tokenNotExpired } from 'angular2-jwt';
#Injectable()
export class AuthService {
authToken: any;
user: any;
constructor(
private http: Http
) {
}
getProfile(user) {
let headers = new Headers();
this.loadToken();
headers.append('Authorization', this.authToken);
headers.append('Content-Type','application/json');
return this.http.get('http://localhost:3000/users/profile', {headers:headers})
.map(res => res.json());
}
loadToken() {
const token = localStorage.getItem('id_token');
this.authToken = token;
}
}
Your getProfile is expecting an argument named user but you are not passing it from the component
You need to pass an argument as follows,
this.auth.getProfile(user).subscribe( profile => {
this.user = profile.user;
},
err => {
return false;
});
or if you don't need an argument , remove it from your service method.