Validation Error Message not getting displayed for custom validation in Angular 2 - validation

I have a register form where user need to provide username. When customer enters username, I want to show validation error message if that username already exists in db or not.
register.html
<-- code here-->
<div class="form-group">
<label for="username" class="col-sm-3 control-label">UserName</label>
<div class=" col-sm-6">
<input type="text" ngControl="userName" maxlength="45" class="form-control" [(ngModel)]="parent.userName" placeholder="UserName" #userName="ngForm" required data-is-unique/>
<validation-message control="userName"></validation-message>
</div>
</div>
<--code here-->
register.component.ts
import {Component} from 'angular2/core';
import {NgForm, FormBuilder, Validators, FORM_DIRECTIVES} from 'angular2/common';
import {ValidationService} from '../services/validation.service';
import {ValidationMessages} from './validation-messages.component';
#Component({
selector: 'register',
templateUrl: './views/register.html',
directives: [ROUTER_DIRECTIVES, ValidationMessages, FORM_DIRECTIVES],
providers: []
})
export class ParentSignUpComponent {
parentSignUpForm: any;
constructor(private _formBuilder: FormBuilder) {
this._stateService.isAuthenticatedEvent.subscribe(value => {
this.onAuthenticationEvent(value);
});
this.parent = new ParentSignUpModel();
this.parentSignUpForm = this._formBuilder.group({
'firstName': ['', Validators.compose([Validators.required, Validators.maxLength(45), ValidationService.nameValidator])],
'middleName': ['', Validators.compose([Validators.maxLength(45), ValidationService.nameValidator])],
'lastName': ['', Validators.compose([Validators.required, Validators.maxLength(45), ValidationService.nameValidator])],
'userName': ['', Validators.compose([Validators.required, ValidationService.checkUserName])]
});
}
}
validation-message.component
import {Component, Host} from 'angular2/core';
import {NgFormModel} from 'angular2/common';
import {ValidationService} from '../services/validation.service';
#Component({
selector: 'validation-message',
inputs: ['validationName: control'],
template: `<div *ngIf="errorMessage !== null" class="error-message"> {{errorMessage}}</div>`
})
export class ValidationMessages {
private validationName: string;
constructor (#Host() private _formDir: NgFormModel) {}
get errorMessage() {
let control = this._formDir.form.find(this.validationName);
for (let propertyName in control.errors) {
if (control.errors.hasOwnProperty(propertyName) && control.touched) {
return ValidationService.getValidatorErrorMessage(propertyName);
}
}
return null;
}
}
validation-service.ts
import {Injectable, Injector} from 'angular2/core';
import {Control} from 'angular2/common';
import {Observable} from 'rxjs/Observable';
import {Http, Response, HTTP_PROVIDERS} from 'angular2/http';
import 'rxjs/Rx';
interface ValidationResult {
[key:string]:boolean;
}
#Injectable()
export class ValidationService {
static getValidatorErrorMessage(code: string) {
let config = {
'required': 'This field is required!',
'maxLength': 'Field is too long!',
'invalidName': 'This field can contain only alphabets, space, dot, hyphen, and apostrophe.',
'userAlreadyInUse': 'UserName selected already in use! Please try another.'
};
return config[code];
}
static checkUserName(control: Control): Promise<ValidationResult> {
let injector = Injector.resolveAndCreate([HTTP_PROVIDERS]);
let http = injector.get(Http);
let alreadyExists: boolean;
if (control.value) {
return new Promise((resolve, reject) => {
setTimeout(() => {
http.get('/isUserNameUnique/' + control.value).map(response => response.json()).subscribe(result => {
if (result === false) {
resolve({'userAlreadyInUse': true});
} else {
resolve(null);
}
});
}, 1000);
});
}
}
}
Now, when i run, and give a username that already exists in db, the value of 'result' variable i am getting as false, which is expected and correct. But validation error message is not getting displayed. I am able to run and get validation error message for other custom validation functions. I am using Angular 2.0.0-beta.15. Can somebody help me to understand what could be the issue?

There are some known issues with async validation
https://github.com/angular/angular/issues/1068
https://github.com/angular/angular/issues/7538
https://github.com/angular/angular/issues/8118
https://github.com/angular/angular/issues/8923
https://github.com/angular/angular/issues/8022
This code can be simplified
return new Promise((resolve, reject) => {
setTimeout(() => {
http.get('/isUserNameUnique/' + control.value).map(response => response.json())
.subscribe(result => {
if (result === false) {
resolve({'userAlreadyInUse': true});
} else {
resolve(null);
}
});
}, 1000);
});
to
return http.get('/isUserNameUnique/' + control.value).map(response => response.json())
.timeout(200, new Error('Timeout has occurred.'));
.map(result => {
if (result === false) {
resolve({'userAlreadyInUse': true});
} else {
resolve(null);
}
}).toPromise();
Don't forget to import map, timeout, and toPromise.
If you use subscribe() instead of then() on the caller site, then you can event omit toPromise()

if you look into this -
'userName': ['', Validators.compose([Validators.required, ValidationService.checkUserName])] });
-- you can see I am using both synchronous and asynchronous validations together. When i changed method for checkUserName like 'Validators.composeAsync(ValidationService.checkUserName)' instead of Validators.compose method, error message got displayed.

Related

Using Form.Select from React Semantic UI with React-hooks

as the question suggests, I'm using RSUI with React-Hooks in a Next.js project and I'm trying to figure out how to send a payload from a Form.Select to a graphql endpoint. I've included a lot of extra code for context but really what I'm after is to successfully set "security_type" using setValues
import React, { useState } from 'react'
import Router from 'next/router'
import { Button, Checkbox, Form, Segment, Table } from 'semantic-ui-react'
import Layout from '../../components/layout'
import Loading from '../../components/loading'
import Error from '../../components/error'
import { useFetchUser } from '../../lib/user'
import { useQuery, useMutation } from '#apollo/react-hooks';
import query from '../../graphql/project/query';
import mutation from '../../graphql/project/mutation';
const security_types = [
{ key: 'Bank Guarantee', value: 'Bank Guarantee', text: 'Bank Guarantee', name: 'Bank Guarantee' },
{ key: 'Cash Retention', value: 'Cash Retention', text: 'Cash Retention', name: 'Cash Retention' },
{ key: 'Surety Bond', value: 'Surety Bond', text: 'Surety Bond', name: 'Surety Bond' },
];
function CreateProject() {
const { loading, error, data } = useQuery(query);
const [createProject] = useMutation(mutation,
{
onCompleted(data) {
Router.replace("/create_project", "/project/" + data.createProject.project_id, { shallow: true });
}
});
const { user, user_loading } = useFetchUser()
let [form, setValues] = useState({
project_number: '',
project_name: '',
security_type: '',
});
let updateField = e => {
console.log('e: ', e)
setValues({
...form,
[e.target.name]: e.target.value
});
};
let mutationData = ''
if (loading) return <Loading />;
if (error) return <Error />;
return (
<Layout user={user} user_loading={user_loading}>
<h1>Let's create a project</h1>
{user_loading ? <>Loading...</> :
<div>
<Segment>
<Form
onSubmit={e => {
e.preventDefault();
console.log('form: ', form)
createProject({ variables: { ...form } });
form = '';
}}>
<Form.Input
fluid
label='Project Number'
name="project_number"
value={form.project_number}
placeholder="Project Number"
onChange={updateField}
/>
<Form.Input
fluid
label='Project Name'
name="project_name"
value={form.project_name}
onChange={updateField}
/>
<Form.Select
fluid
selection
label='Security Type'
options={security_types}
placeholder='Security Type'
name="security_type"
value={form.security_type}
onChange={(e, { value }) => setValues(console.log('value: ', value), { "security_type": value })}
/>
<Button>Submit</Button>
</Form>
</Segment>
</div>
}
</Layout>
);
}
export default CreateProject;
I think all my troubles relate to the onChange section so any help would be great.

React form handleChange is not updating state

Form input onChange is not updating state. The action and reducer fire properly and backend rails API is updated, but the state does not change and nothing renders in the DOM.
I have used console logs to ensure that the action and reducer are working properly.
import React, { Component } from 'react';
import { Button, Form } from 'semantic-ui-react'
import { connect } from 'react-redux'
class TaskInput extends Component {
constructor() {
super()
this.state = {
name: ""
}
}
handleChange = (e) => {
this.setState({
name: e.target.value
})
}
handleSubmit = (e) => {
e.preventDefault()
this.props.addTask({name: this.state.name}, this.props.goal.id)
this.setState({
name: ''
})
}
render() {
return (
<Form className="new-task-form" onSubmit={(e) =>this.handleSubmit(e)}>
<Form.Field>
<label className="form-label">Add Task</label>
<input id="name" required value={this.state.name} onChange={(e) => this.handleChange(e)} />
</Form.Field>
<Button basic color='purple' type="submit">Add Task</Button>
<hr/>
</Form>
)
}
}
export default connect()(TaskInput)
import React, { Component } from 'react'
import { addTask, deleteTask } from '../actions/tasksActions'
import { connect } from 'react-redux'
import { fetchGoal } from '../actions/goalsActions'
import Tasks from '../components/Tasks/Tasks';
import TaskInput from '../components/Tasks/TaskInput';
class TasksContainer extends Component {
componentDidMount() {
this.props.fetchGoal(this.props.goal.id)
}
render(){
return(
<div>
<TaskInput
goal={this.props.goal}
addTask={this.props.addTask}
/>
<strong>Tasks:</strong>
<Tasks
key={this.props.goal.id}
tasks={this.props.goal.tasks}
deleteTask={this.props.deleteTask}
/>
</div>
)
}
}
const mapStateToProps = state => ({
tasks: state.tasksData
})
export default connect(mapStateToProps, { addTask, deleteTask, fetchGoal })(TasksContainer);
export default function taskReducer(state = {
loading: false,
tasksData: []
},action){
switch(action.type){
case 'FETCH_TASKS':
return {...state, tasksData: action.payload.tasks}
case 'LOADING_TASKS':
return {...state, loading: true}
case 'CREATE_TASK':
console.log('CREATE Task', action.payload )
return {...state, tasksData:[...state.tasksData, action.payload.task]}
case 'DELETE_TASK':
return {...state, loading: false, tasksData: state.tasksData.filter(task => task.id !== action.payload.id )}
default:
return state;
}
}
handleSubmit calls the action to addTask. handleChange updates state and renders the new task in DOM. handleSubmit is working. handleChange is not.

Fetch request in React: How do I Map through JSON array of objects, setState() & append?

This API returns a JSON array of objects, but I'm having trouble with setState and appending. Most documentation covers JSON objects with keys. The error I get from my renderItems() func is:
ItemsList.js:76 Uncaught TypeError: Cannot read property 'map' of undefined
in ItemsList.js
import React, { Component } from "react";
import NewSingleItem from './NewSingleItem';
import { findDOMNode } from 'react-dom';
const title_URL = "https://www.healthcare.gov/api/index.json";
class ItemsList extends Component {
constructor(props) {
super(props);
this.state = {
// error: null,
// isLoaded: false,
title: [],
url: [],
descrip: []
};
}
componentDidMount() {
fetch(title_URL)
.then(response => {
return response.json();
})
.then((data) => {
for (let i = 0; i < data.length; i++) {
this.setState({
title: data[i].title,
url: data[i].url,
descrip: data[i].bite,
});
console.log(data[i])
}
})
.catch(error => console.log(error));
}
renderItems() {
return this.state.title.map(item => {
<NewSingleItem key={item.title} item={item.title} />;
});
}
render() {
return <ul>{this.renderItems()}</ul>;
}
}
export default ItemsList;
Above, I'm trying to map through the items, but I'm not quite sure why I cant map through the objects I set in setState(). Note: even if in my setState() I use title: data.title, it doesnt work. Can someone explain where I'm erroring out? Thanks.
in App.js
import React, { Component } from "react";
import { hot } from "react-hot-loader";
import "./App.css";
import ItemsList from './ItemsList';
class App extends Component {
render() {
return (
<div className="App">
<h1> Hello Healthcare </h1>
<ItemsList />
<article className="main"></article>
</div>
);
}
}
export default App;
in NewSingleItem.js
import React, { Component } from "react";
const NewSingleItem = ({item}) => {
<li>
<p>{item.title}</p>
</li>
};
export default NewSingleItem;
The problem is this line:
this.setState({
title: data[i].title,
url: data[i].url,
descrip: data[i].bite,
});
When you state this.state.title to data[i].title, it's no longer an array. You need to ensure it stays an array. You probably don't want to split them up anyway, just keep them all in a self contained array:
this.state = {
// error: null,
// isLoaded: false,
items: [],
};
...
componentDidMount() {
fetch(title_URL)
.then(response => {
return response.json();
})
.then((data) => {
this.setState({
items: data.map(item => ({
title: item.title,
url: item.url,
descrip: item.bite,
})
});
console.log(data[i])
}
})
...
renderItems() {
return this.state.items.map(item => {
<NewSingleItem key={item.title} item={item.title} />;
});
}

The selected value in a mat-select is not sent to parent

I created a dropdown in an angular library to be used in our applications. I used angular-material2 for the dropdown (mat-select and mat-autocomplete).
I must be doing something wrong since I don't get the value when I use the dropdown in an app. I already tried pretty much everything I found on the net, with no result.
I commented most of it and I'm trying to solve the simplest version, but even in this case I'm not getting the value. Here is what I have now:
DropdownComponent.html library:
<mat-form-field appearance="outline">
<mat-select disableOptionCentering (selectionChange)="writeValue($event)" [multiple]="multi">
<mat-option *ngFor="let item of list" [value]="item">
{{ item }}
</mat-option>
</mat-select>
</mat-form-field>
DropdownComponent.ts library:
import {Component, OnInit, ViewEncapsulation, Input, forwardRef} from '#angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR, NG_VALIDATORS, FormControl} from '#angular/forms';
import {Observable} from 'rxjs';
#Component({
selector: 'pux-dropdown',
templateUrl: './dropdown.component.html',
styleUrls: ['./dropdown.component.scss'],
encapsulation: ViewEncapsulation.None,
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DropdownComponent), multi: true },
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => DropdownComponent), multi: true }
]
})
export class DropdownComponent implements OnInit, ControlValueAccessor {
#Input() list: any[] = [];
#Input() selected: any;
#Input() multi = false;
#Input() search = false;
items: any[] = [];
propagateChange = (_: any) => {};
validateFn: any = () => {};
constructor() { }
ngOnInit() {
this.items = this.list;
}
// Form
get value(): any { return this.selected; }
set value(newValue: any) {
if (newValue !== this.selected) {
this.writeValue(newValue);
this.registerOnChange(newValue);
this.selected = newValue;
}
}
registerOnChange(fn: any): void { this.propagateChange = fn; }
registerOnTouched(fn: any): void {}
setDisabledState(isDisabled: boolean): void {}
writeValue(obj: any): void {
if (obj !== null) {
this.selected = obj.value;
this.registerOnChange(this.selected);
console.log(this.selected);
}
}
validate(c: FormControl) { return this.validateFn(c); }
}
DropDownComponent.html application:
<div>
<form [formGroup]="selectForm" (ngSubmit)="saveSelect(selectForm)" #form1="ngForm">
<div>
<pux-dropdown formControlName="selectValue" [list]="list1"> </pux-dropdown>
</div> <br>
<button mat-flat-button="primary" type="submit" class="btn btn-primary">Save</button>
</form> <br>
<div>
Saved Value: {{selectValue | json}}
</div>
</div>
DropdownComponent.ts application:
import {Component, OnInit} from '#angular/core';
import {FormGroup, FormBuilder} from '#angular/forms';
const states = [
'Alabama', 'Alaska', 'Arizona', 'Arkansas', 'California', 'Colorado', 'Connecticut', 'Delaware',
'Florida', 'Georgia', 'Hawaii', 'Idaho', 'Illinois', 'Indiana', 'Iowa', 'Kansas', 'Kentucky',
'Louisiana', 'Maine', 'Maryland', 'Massachusetts', 'Michigan', 'Minnesota', 'Mississippi',
'Missouri', 'Montana', 'Nebraska', 'Nevada', 'New Hampshire', 'New Jersey', 'New Mexico',
'New York', 'North Carolina', 'North Dakota', 'Ohio', 'Oklahoma', 'Oregon', 'Pennsylvania',
'Rhode Island', 'South Carolina', 'South Dakota', 'Tennessee', 'Texas', 'Utah', 'Vermont',
'Virginia', 'Washington', 'West Virginia', 'Wisconsin', 'Wyoming'
];
#Component({
selector: 'app-dropdown',
templateUrl: './dropdown.component.html',
styleUrls: ['./dropdown.component.scss']
})
export class DropdownComponent implements OnInit {
list1;
multi: boolean;
selected: any;
search: boolean;
// Form
selectForm: FormGroup;
selectValue: string;
constructor(private fb: FormBuilder) { }
ngOnInit() {
this.list1 = states;
// Form
this.selectForm = this.fb.group({
selectValue: this.selected
});
}
saveSelect(formValues) {
console.log(formValues.value.selectValue);
this.selectValue = formValues.value.selectValue;
}
}
The console.log in writeValue in the library gives me the value I select in the dropdown, but the console.log in saveSelect shows me null. So the value isn't sent to the parent. Any idea what I'm doing wrong? Thank you in advance.
Your writeValue implementation needs to call the change function, but instead it is calling the registerOnChange function which is there for the form control to register its change function. Try something like this:
propagateChange: (value: any) => void = () => {};
registerOnChange(fn: (value: any) => void) { this.propagateChange = fn; }
writeValue(obj: any): void {
if (obj !== null && obj !== this.selected) {
this.selected = obj.value;
this.propagateChange(this.selected);
}
}

How and When to use subscribe?

Im very new to typescript.
I'm trying to understand the Observables but I'm kinda lost here.
The function below searches for videos on Youtube API v3.
Is it a good approach?
Is subscribing inside a function which will be called many times a good idea?
This function is called whenever user types something.
Should I have an unsubscribe somewhere?
searchVideos(searchbar: any): void{
const typedValue: string = searchbar.srcElement.value;
if(typedValue.length > 2){
this.videosList = this.youtubeProvider.searchVideos(typedValue);
this.videosList.subscribe(data => {
if( data.length == 0 ){
this.notFoundAnyVideo = true;
}else{
this.notFoundAnyVideo = false;
}
})
}
}
It's a good question!
They are some ways to answer your question:
1/ you can debounce the action which call your function
Imagine, your action is triggered by a keyup in input field:
HTML
<input type="text" (keyup)="onSearchKeyup(this.value, $event)">
Component
export class MyComponent implements OnInt {
onSearch$: Subject<string>
ngOnInt(): void {
this.onSearch$
.debounceTime(500) //-> put your time here
.subscribe(search => searchVideos(search)
}
onSearchKeyup(search: string, e: any) {
this.onSearch$.next(search)
e.preventDefault()
}
}
2/ you can cancel the observable with takeUntil
Component
export class MyComponent implements OnInt {
onStopSearch$: Subject<void> = new Subject<void>();
onSearchKeyup(search: string, e: any) {
this.onStopSearch$.next()
this.searchVideos(string)
e.preventDefault()
}
private searchVideos(search: string): void{
if(typedValue.length > 2){
this.videosList = this.youtubeProvider.searchVideos(typedValue);
this.videosList
.takeUntil(this.onSearchStop$)
.subscribe(data => {
if( data.length == 0 ){
this.notFoundAnyVideo = true;
}else{ this.notFoundAnyVideo = false; }
})
}
}
}
Of course you can combine 1 and 2
Why I use takeUntil to cancel my requests: https://medium.com/#benlesh/rxjs-dont-unsubscribe-6753ed4fda87
I suppose you could use RxJS all the way through, cause its reactive paradigm lends itself very well for search components. Take a look at the code below, I implemented variations of it in few applications.
import {Component, ViewChild, ElementRef} from "#angular/core";
#Component({
selector: 'search',
styleUrls: ['./search.component.scss'],
template: `
<form #searchBoxEl action="" class="search-form" [formGroup]="form">
<fieldset>
<input #searchBoxEl type="text" placeholder='Search for Youtube videos'
autocomplete="off" />
<nwum-list (itemSelected)="onItemSelect($event)"></nwum-list>
</fieldset>
</form>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SearchComponent implements OnInit {
#ViewChild('searchBoxEl') searchBoxEl: ElementRef;
componentDestroyed$: Subject<void> = new Subject<void>();
videosList: Video[];
constructor(public videoService: VideoService){}
ngOnInit(){
subscribeToSearchQueryChanges();
}
subscribeToSearchQueryChanges(){
const minNumOfChars = 2;
Observable.fromEvent(this.searchBoxEl.nativeElement, 'keyup')
.debounceTime(300)
.pluck('target', 'value')
.map(value => value.trim())
// .map(() => this.searchBoxEl.nativeElement.value.trim())
.filter(value => value.length >= minNumOfChars)
.takeUntil(this.componentDestroyed$)
.switchMap(value => this.videoService.fetchVideos(value))
.subscribe((videos: Video[]) => {
//show videos, etc
this.videosList = this.videoService.groupSuggestions(suggestions);
}, err => {
console.error('failed fetching videos', err);
this.removeAllSubscriptions();
this.subscribeToSearchQueryChanges();
});
this.addSubscription(sub);
}
ngOnDestroy() {
this.removeAllSubscriptions();
}
removeAllSubscriptions(){
this.componentDestroyed$.next();
this.componentDestroyed$.complete();
}
}

Resources