Cannot Post in Angular 13 - javascript

I'm making my first steps into Angular, so I decided to start with a CRUD.
I've been doing this following a guide, but I got crashed into a concrete wall, as I've checked everything and couldn't find a solution.
I'm trying to create a Project (model), which has 3 columns:
Title
Description (Nullable)
AccessCode (Nullable)
I need to make a POST request to add the data into my database (Backed by Node.js and working fine from Postman)
This is the code I have:
add-project-component.ts
import {
Component,
OnInit
} from '#angular/core';
import {
Project
} from 'src/app/models/project.model';
import {
ProjectService
} from 'src/app/services/project.service';
#Component({
selector: 'app-add-project',
templateUrl: './add-project.component.html',
styleUrls: ['./add-project.component.css']
})
export class AddProjectComponent implements OnInit {
project: Project = {
title: '',
description: '',
accessCode: '',
};
submitted = false;
constructor(private projectService: ProjectService) {}
ngOnInit(): void {}
saveProject(): void {
const data = {
title: this.project.title,
description: this.project.description,
accessCode: this.project.accessCode
};
this.projectService.create(data)
.subscribe({
next: (res) => {
console.log(res);
this.submitted = true;
},
error: (e) => console.error(e)
});
}
newProject(): void {
this.submitted = false;
this.project = {
title: '',
description: '',
accessCode: ''
};
}
}
add-project-component.html
<div class="new-project">
<mat-toolbar>
<span>New Project</span>
</mat-toolbar>
<mat-card>
<mat-card-content *ngIf="!submitted">
<p>
<mat-form-field appearance="outline">
<mat-label>Title</mat-label>
<input id="title" required [(ngModel)]="project.title" matInput name="title" placeholder="Title">
</mat-form-field>
</p>
<p>
<mat-form-field appearance="outline">
<mat-label>Description</mat-label>
<textarea id="description" rows="6" [(ngModel)]="project.description" name="description" matInput placeholder="Description"></textarea>
</mat-form-field>
</p>
<p>
<mat-form-field appearance="outline">
<mat-label>Access Code</mat-label>
<input id="accessCode" [(ngModel)]="project.accessCode" name="accessCode" matInput placeholder="Access Code">
</mat-form-field>
</p>
<!-- FORM CONTENT -->
</mat-card-content>
<mat-card-actions *ngIf="!submitted">
<button mat-raised-button color="primary" (click)="newProject()">Create Project</button>
<!-- REGISTER BUTTON -->
</mat-card-actions>
</mat-card>
</div>
services/project.service.ts
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { Observable } from 'rxjs';
import { Project } from '../models/project.model';
const baseUrl = 'http://localhost:8080/api/projects';
#Injectable({
providedIn: 'root'
})
export class ProjectService {
constructor(private http: HttpClient) { }
getAll(): Observable<Project[]> {
return this.http.get<Project[]>(baseUrl);
}
get(id: any): Observable<Project> {
return this.http.get(`${baseUrl}/${id}`);
}
create(data: any): Observable<any> {
return this.http.post(baseUrl, data);
}
update(id: any, data: any): Observable<any> {
return this.http.put(`${baseUrl}/${id}`, data);
}
delete(id: any): Observable<any> {
return this.http.delete(`${baseUrl}/${id}`);
}
deleteAll(): Observable<any> {
return this.http.delete(baseUrl);
}
findByTitle(title: any): Observable<Project[]> {
return this.http.get<Project[]>(`${baseUrl}?title=${title}`);
}
}
Error 404
Can you guys help me out? What am I missing?
Thank you very much.

Your create function points to the wrong URL.
Your desired URL is /api/projects/create, but you are missing the /create part.
Change
create(data: any): Observable<any> {
return this.http.post(baseUrl, data);
}
to
create(data: any): Observable<any> {
return this.http.post(`${baseUrl}/create`, data);
}
given that const baseUrl = 'http://localhost:8080/api/projects'; is correct.
Note: For production you most likely want to change baseUrl to
const baseUrl = '/api/projects'; as the host will change (localhost vs your actual domain).

Related

Problem with a variable undefined in a mat dialog text in Angular

I created a dialog using angular in order to collect a information and save it on my back-end.
The problem is when i will send it to my back-end, using my post method, the variable of the coment stay undefined. The variable in question is:
val: " "
this is my dialog ts file:
import { Component, OnInit, Inject } from '#angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '#angular/material/dialog'; import { IstManagementService } from './../../core/services/ist-management.service'; import { ReasonpopupService } from 'ClientApp/app/core/services/reasonpopup.service';
#Component({
selector: 'app-reasonpopup', templateUrl: './reasonpopup.component.html', styleUrls: ['./reasonpopup.component.css'] }) export class ReasonpopupComponent implements OnInit {
val : " "
/* reason2 : string = this.reason */
onSubmit() { this.MatDialogRef.close(this.val); }
getValue(val:string){
console.log(val)
}
constructor(
private istManagementService: IstManagementService,
public MatDialogRef: MatDialogRef<ReasonpopupComponent>,
#Inject(MAT_DIALOG_DATA) public data: any,
private shared:ReasonpopupService,
) { }
ngOnInit(): void {
this.shared.setMessage(this.val) }
reason :string closeDialog() {
this.MatDialogRef.close(false); }
}
my html file:
<div>
<div class="content-container">
<mat-icon id="close-icon" (click)="closeDialog()">close</mat-icon>
<span class="content-span full-width">{{ data.message }}</span>
</div>
<form #userForm="ngForm">
<div class="input-reason">
<mat-form-field class="example-full-width" appearance="fill" [style.width.px]=420 style="padding-bottom: 100px;" >
<mat-label>Leave a comment</mat-label>
<textarea
[(ngModel)]="val"
type="text"
ngModel class="form-control"
required
#text
minlength="3"
class="form-control"
name="tx"
matInput
placeholder="please describe the reason"
></textarea>
<span *ngIf="text.invalid && text.touched" class="error">input the reason</span>
</mat-form-field>
</div>
<button mat-raised-button id="no-button" [mat-dialog-close]="false">NO</button>
<button
mat-raised-button
[disabled]="userForm.invalid"
id="yes-button"
(click)="onSubmit()"
(click)="getValue(text.value)"
[mat-dialog-close]="data.text"
cdkFocusInitial
>
YES
</button>
</form>
</div>
The method where i pass my variable as argument on the other component that have the post method
saveRow() {
let addRowsRequest = {
IstValues: this.dataSource.data.filter(r => r.editing)
};
console.log(this.val)
this.istManagementService.postRecord(this.inputTable, addRowsRequest, this.val).subscribe(
(res: any) => {
console.log(this.dataSource.data.filter(r => r.editing));
this.dataSource.data.push(this.dataSource.data.filter(r => r.editing));
this.dataSource._updateChangeSubscription();
}
)
}
My setter and getter service that i created to share the variable between the components
mport { ReasonpopupComponent } from './../../tasks/reasonpopup/reasonpopup.component';
import { Injectable } from '#angular/core';
import { MatDialog } from '#angular/material/dialog';
#Injectable({
providedIn: 'root'
})
export class ReasonpopupService {
val:string
constructor(private messageDialog: MatDialog) { }
openReasonDialog(msg: string) {
return this.messageDialog.open(ReasonpopupComponent, {
width: '570px',
panelClass: 'confirm-dialog-container',
disableClose: true,
data: { message: msg }
})
}
setMessage(data: string){
this.val=data
console.log(this.val)
}
getMessage(){
return this.val
}
}
and finally, my service that contain all the CRUD methods
import { HttpClient, HttpHeaders, HttpParams } from '#angular/common/http';
import { Injectable } from '#angular/core';
import { Observable } from 'rxjs';
import { ISTGenericResponse } from '../../pages/ist-management/ist-generic-response';
import { environment } from 'ClientApp/environments/environment';
#Injectable({
providedIn: 'root'
})
export class IstManagementService {
constructor(private httpClient: HttpClient) { }
public getGenericStaticTablesFiltered(inputTable: string, inputKey: string, inputValue: string, inputComparison: string): Observable<ISTGenericResponse> {
var filter = "";
if (inputKey && inputValue) {
filter = "?key=" + inputKey + "&value=" + inputValue + "&comparison=" + inputComparison;
return this.httpClient.get<ISTGenericResponse>(environment.apiRoot + "/StaticTable/Filter/" + inputTable + filter);
}
else {
return this.httpClient.get<ISTGenericResponse>(environment.apiRoot + "/StaticTable/" + inputTable);
}
}
message:string
setMessage(data: string){
this.message=data
}
getMessage(){
return this.getMessage
}
postRecord(inputTable: string, addRecord: any, message:any ) {
return this.httpClient.post(environment.apiRoot + "/StaticTable/Add/" + inputTable, addRecord, message);
}
deleteRecord(inputTable: string, deleteRecord: any) {
const headers = new HttpHeaders({ 'Content-Type': 'application/json; charset=utf-8' });
return this.httpClient.request('delete', environment.apiRoot + "/StaticTable/Delete/" + inputTable, { body: deleteRecord, headers: headers });
}
editRecord(inputTable: string, editRecord: any): Observable<any> {
const headers = new HttpHeaders({ 'Content-Type': 'application/json; charset=utf-8' });
return this.httpClient.request('put', environment.apiRoot + "/StaticTable/Update/" + inputTable, { body: editRecord, headers: headers, });
}
}
Thank you in advanced
according to this code, you are calling this.shared.setMessage(this.val) inside the ngOnInit() method of ReasonpopupComponent
which will always be undefined because ngOnInit() is only called on the initialization of the component before the user inputs any data.
what you need to do is to move this.shared.setMessage(this.val) inside onSubmit() method, so in the end it looks like this
export class ReasonPopupComponent implements OnInit {
val = '';
constructor(
private istManagementService: IstManagementService,
public MatDialogRef: MatDialogRef<ReasonPopupComponent>,
#Inject(MAT_DIALOG_DATA) public data: any,
private shared:ReasonpopupService,
) { }
ngOnInit(): void {
}
onSubmit() {
this.shared.setMessage(this.val);
this.MatDialogRef.close(this.val);
}
closeDialog() {
this.MatDialogRef.close(false);
}
}
what would be even better, if your "other component" is the one opening the pop-up, you can make use of angular material dialog subscriber. as you see in onSubmit() method this.matDialogRef.close(this.val)is already called with the value. all you need to do is to subscribe to it on the "other component" like so.
dialogRef.afterClosed().subscribe(result => {
console.log(result)
});

In my component add new Item is not working in angular

All files are attached as below,
component.html - This is component file
component.ts - This is component ts file
admin-service - This is service file
admin.ts - This is model file
add-que.html - This is component html file.
<div>
<form [formGroup]="adminForm" (ngSubmit)="newQuestion()">
<div class="form-group">
<label for="exampleInputEmail1">Question</label>
<input formControlName="description" type="description" placeholder="Enter question"
class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp">
<small class="text-danger"
*ngIf="!adminForm.get('description').valid && adminForm.get('description').touched">
Please Enter a Question</small>
</div>
<div class="form-group m-auto">
<div class="col-6">
(a)<input formControlname="alternatives" type="text">
(b)<input formControlname="alternatives" type="text">
</div>
<div class="col-6">
(c)<input formControlname="alternatives" type="text">
(d)<input formControlname="alternatives" type="text">
</div>
</div>
<div class="m-auto">
<input type="submit" value="Add" class="btn btn-primary"/>
</div>
</form>
</div>
**add-que.ts** - This is component ts file.
import { Component, OnInit } from '#angular/core';
import { FormControl, FormGroup, Validators } from '#angular/forms';
import { Router } from '#angular/router';
import { AdminService } from '../service/admin.service';
#Component({
selector: 'app-add-question',
templateUrl: './add-question.component.html',
styleUrls: ['./add-question.component.css']
})
export class AddQuestionComponent implements OnInit {
adminForm = new FormGroup({
description: new FormControl("", [Validators.required]),
alternatives: new FormControl("", [Validators.required])
});
constructor(private adminService: AdminService, private router: Router) { }
ngOnInit(): void {
}
newQuestion(){
if(this.adminForm.valid){
this.adminService.addQue(this.adminForm.value).subscribe(res => {
this.adminForm.reset();
this.router.navigate(["/admin"]);
})
}
}
}
**admin-service.ts** - This is service ts file.
import { Injectable } from '#angular/core';
import { HttpClient, HttpHeaders } from '#angular/common/http';
import { Observable } from 'rxjs';
import { Admin } from "../model/admin";
#Injectable({
providedIn: 'root'
})
export class AdminService {
private ROOT_URL = "http://localhost:3300/questions";
private httpOptions = {
headers: new HttpHeaders().set("Content-Type", "application/json")
};
constructor(private http: HttpClient) { }
getQuestion(): Observable<Admin[]> {
return this.http.get<Admin[]>(this.ROOT_URL);
}
getQue(id: string){
return this.http.get<Admin>(`${this.ROOT_URL}/${id}`);
}
addQue(admin){
return this.http.post<any>(this.ROOT_URL, admin, this.httpOptions);
}
}
**admin.ts** - This is model ts file.
export interface Admin {
description: String,
alternatives: [
{
text: {
type: String,
required: true
},
isCorrect: {
type: Boolean,
required: true,
default: false
}
}
]
}
I did created add-question component with form as above. I did tried to add new question through form in angular but not get anything. i did attached my files as above.
you sould'nt use this.router.navigate(["/admin"]); to try "refresh a component". That's no work (there are no changes). In general you has a component with a ngOnInit. Simply get out the code under a ngOnInit in a function and call this function in subscribe, e.g.
ngOnInit()
{
this.init()
}
init()
{
..here you initialize your variables
}
submit(){
...
.subscribe(res=>{
....
this.init()
})
}

Angular HttpClient returns undefined instead of an array

So I have an API (which returns JSON) setup and it prints out a message that the users are getting fetched, but my console.log in angular of data returns undefined... Is it mandatory for my use-case to use an Interface or is a model like I use completely fine??
Bonus points if you also check out my error handling and point out mistakes with it.
My BackEnd service ts:
import { Injectable } from '#angular/core';
import { HttpClient, HttpHeaders, HttpResponse } from '#angular/common/http';
import { environment } from '../../../environments/environment';
import { Observable, of } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { User } from '../models/user.model';
const SERVICE_URL = 'http://localhost:3000/';
#Injectable()
export class AviorBackendService {
constructor(private client: HttpClient) { }
UserData: any;
httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
/**
* Handle Http operation that failed.
* Let the app continue.
* #param operation - name of the operation that failed
* #param result - optional value to return as the observable result
*/
private handleError<T>(operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
// TODO: send the error to remote logging infrastructure
console.error(error); // log to console instead
// TODO: better job of transforming error for user consumption
this.log(`${operation} failed: ${error.message}`);
// Let the app keep running by returning an empty result.
return of(result as T);
};
}
/**
* Log a failed AviorBackend error.
* #param message - message of the operation that failed
*/
private log(message: string) {
throw new Error(`AviorBackend: ${message}`);
}
getUserCollection() {
return this.client.get<User[]>(SERVICE_URL + 'users');
}
// Get user
getUser(firstname: User): Observable<any> {
const API_URL = `${SERVICE_URL}user/firstname/${firstname}`;
return this.client.get(API_URL, this.httpOptions).pipe(
map((res: Response) => {
return res || {};
}),
catchError(this.handleError())
);
}
/* addUser() {
return this.client.post(SERVICE_URL + 'user', this.UserData, this.httpOptions);
} */
// Add student
addUser(data: User): Observable<any> {
const API_URL = `${SERVICE_URL}user`;
return this.client.post(API_URL, data)
.pipe(
catchError(this.handleError())
);
}
// Update User
putUser(loginId, data: User): Observable<any> {
const API_URL = `${SERVICE_URL}update/${loginId}`;
return this.client.put(API_URL, data, this.httpOptions).pipe(
catchError(this.handleError())
);
}
// Delete student
deleteUser(id): Observable<any> {
const API_URL = `${SERVICE_URL}delete-student/${id}`;
return this.client.delete(API_URL).pipe(
catchError(this.handleError())
);
}
}
My User model ts:
import { Role } from './role';
export class User {
id?: number;
mandator?: number;
loginId?: string;
lastName?: string;
firstName?: string;
password?: string;
eMail?: string;
group: string;
role: Role;
active?: boolean;
token?: string;
}
My users component ts:
import { Component, OnInit } from '#angular/core';
import { AviorBackendService } from '../services/avior-backend.service';
import { UserCollection } from '../models/user-collection.model';
import { User } from '../models/user.model';
#Component({
selector: 'app-users',
templateUrl: './users.component.html',
styleUrls: ['./users.component.css']
})
export class UsersComponent implements OnInit {
selectedItem: string;
users: UserCollection;
user: User;
firstname: string;
selectedUser: User;
constructor(private aviorBackend: AviorBackendService) { }
ngOnInit() {
this.aviorBackend.getUserCollection().subscribe(data => {
// tslint:disable-next-line: no-string-literal
this.users = data['users'];
console.log(this.users);
});
}
clickItem(firstname) {
this.aviorBackend.getUser(firstname).subscribe(data => {
// tslint:disable-next-line: no-string-literal
this.selectedUser = data['user'];
console.log(this.selectedUser);
});
}
}
<div class="list-area">
<div class="col-lg-12">
<p class="list-header">Element overview</p>
<div class="form-group">
<label for="filter" class="lb-sm">Filter</label>
<input type="text" class="form-control input-sm" name="filter" id="filter">
</div>
<select size="20" multiple class="form-control" id="elementlist" [(ngModel)]="selectedItem" (click)="clickItem(firstname)">
<option *ngFor="let user of users">
{{user?.lastName}}, {{user?.firstName}}
</option>
</select>
</div>
</div>
<div class="content-area">
<div class="col-lg-12" *ngIf="selectedUser?.id">
<p>Element contents {{selectedUser?.id}}</p>
<div class="col-lg-12">
<div class="form-group">
<label for="firstName" class="lb-sm">First name</label>
<input type="text" class="form-control input-sm" name="firstName" id="firstName" [(ngModel)]="selectedUser.firstName">
</div>
</div>
</div>
</div>
UPDATE
My user-collection.model.ts:
import { User } from './user.model';
export class UserCollection {
user: User[];
}

Pass required to custom component Angular

I have a custom component to use for phone numbers
I need to use the required flag for it
Here is HTML of component
<form #phoneForm="ngForm" novalidate name="PhoneForm">
<div class="form-row">
<div class="form-group col-md-3">
<p-dropdown
#phoneCodeInput = ngModel
[disabled]="!countrycodes.length"
[options]="countrycodes"
autoWidth="false"
[(ngModel)]="phoneCode"
(ngModelChange)="onNumberChange()"
[style]="{ width: '100%', height: '100%'}"
name="countryCodes"
[autoWidth]="true"
></p-dropdown>
</div>
<div class="form-group col-md-9">
<input
[readonly] = "isReadOnly"
#phoneNumberInput = ngModel
number-directive
class="form-control"
placeholder="Enter phone number"
[required] = "isFieldRequired"
[(ngModel)]="phoneNumber"
(ngModelChange)="onNumberChange()"
class="form-control"
type="text"
name="name"
maxlength="11"
/>
</div>
</div>
<validation-messages [formCtrl]="phoneNumberInput"></validation-messages>
</form>
Here is a typescript code of the component, where I use the Input parameter to make validation
import { AppComponentBase } from '#shared/common/app-component-base';
import {
Component,
OnInit,
Injector,
AfterContentChecked,
ViewChild,
forwardRef,
Input,
} from '#angular/core';
import * as lookup from 'country-telephone-data';
import { SelectItem } from 'primeng/api';
import { ControlValueAccessor, ValidationErrors, NG_VALUE_ACCESSOR, NG_VALIDATORS } from '#angular/forms';
#Component({
selector: 'phone-number',
templateUrl: './phone-number.component.html',
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: PhoneNumberComponent, multi: true },
{
provide: NG_VALIDATORS,
useExisting: forwardRef(() => PhoneNumberComponent),
multi: true
}
]
})
export class PhoneNumberComponent extends AppComponentBase
implements OnInit, ControlValueAccessor, AfterContentChecked {
#Input() isRequired: boolean;
#ViewChild('phoneForm') phoneForm;
constructor(injector: Injector) {
super(injector);
}
countrycodes: SelectItem[] = [];
phoneCode: string;
phoneNumber: string;
required: string | boolean;
isFieldRequired: boolean = false;
isReadOnly: boolean = false;
private changed = [];
private touched = [];
disabled: boolean;
ngAfterContentChecked(): void {
this.checkValidity();
}
checkValidity(): void {}
propagateChange = (_: any) => {};
get phoneNumberResult(): string {
const result = `${this.phoneCode ? this.phoneCode : ''} ${
this.phoneNumber ? this.phoneNumber : ''
}`;
return result;
}
set phoneNumberResult(value: string) {
if (this.phoneNumberResult !== value) {
const [phoneCode, phoneNumber] = value.split(' ');
this.phoneCode = phoneCode;
this.phoneNumber = phoneNumber;
this.changed.forEach(f => f(value));
}
}
writeValue(obj: string): void {
this.phoneNumberResult = obj ? obj : '+44';
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {
this.touched.push(fn);
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
ngOnInit(): void {
if (this.isRequired === true) {
this.isFieldRequired = true;
}
lookup.allCountries.forEach(element => {
this.countrycodes.push({
label: `+${element.dialCode}`,
value: `+${element.dialCode}`,
});
});
}
onNumberChange(): void {
this.propagateChange(this.phoneNumberResult);
}
validate(): ValidationErrors {
if (!this.phoneForm.valid) {
return { message: 'custom error' };
}
return null;
}
registerOnValidatorChange(fn: () => void): void {
this.checkValidity = fn;
}
}
Now I use input parameters to implement the required functionality
here is how I use my component now
<phone-number [isRequired] =" isMobileNumberRequired" id="" #mobileEdit name="mobile" [(ngModel)]="tenant.mobileNumber" (ngModelChange)="onMobileChanged()"></phone-number>
I need to use just required flag at component call instead of passing parameters. How I can do it?
you can use <mat-form-field> component. then you can control required and also error message
<mat-form-field>
<input matInput placeholder="Enter Phone Number" [formControl]="phoneNumber" required>
<mat-error *ngIf="phoneNumber.invalid">{{getErrorMessage()}}</mat-error>
</mat-form-field>
for better understand you can follow this link and for example.
Maybe ngRequired and <input ng-model="required" id="required" />?

Angular 4: reactive form control is stuck in pending state with a custom async validator

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
}

Categories

Resources