Angular2 - Create a reusable, validated text input component - validation

I'm creating an Angular2 application with a Node backend. I will have forms that submit data to said backend. I want validation on both the client and server side, and I'd like to avoid duplicating these validation rules.
The above is somewhat irrelevant to the actual question, except to say that this is the reason why I'm not using the conventional Angular2 validation methods.
This leaves me with the following HTML structure:
<div class="form-group" [class.has-error]="hasError(name)">
<label class="control-label" for="name"> Property Name
<input id="name" class="form-control" type="text" name="name" [(ngModel)]="property.name" #name="ngModel" />
<div class="alert alert-danger" *ngIf="hasError(name)">{{errors.name}}</div>
</div>
<div class="form-group" [class.has-error]="hasError(address1)">
<label class="control-label" for="address1"> Address
<input id="address1" class="form-control" type="text" name="address1" [(ngModel)]="property.address.address1" #address1="ngModel" />
<div class="alert alert-danger" *ngIf="hasError(address1)">{{errors['address.address1']}}</div>
</div>
I will have some large forms and would like to reduce the verbosity of the above. I am hoping to achieve something similar to the following:
<my-text-input label="Property Name" [(ngModel)]="property.name" name="name"></my-text-input>
<my-text-input label="Address" [(ngModel)]="property.address.address1" name="address1" key="address.address1"></my-text-input>
I'm stumbling trying to achieve this. Particular parts that give me trouble are:
Setting up two-way binding on the ngModel (changes that I make in the component do not reflect back to the parent).
Generating the template reference variable (#name and #address1 attributes) based on an #Input variable to the component.
It just occurred to me that perhaps I don't need a separate template reference variable name for each instance of the component. Perhaps I can just use #input since it's only referenced from within that component. Thoughts?
I could pass errors or a constraints object to each instance of the component for validation, but I'd like to reduce repetition.
I realize that this is a somewhat broad question, but I believe that a good answer will be widely useful and very valuable to many users, since this is a common scenario. I also realize that I have not actually shown what I've tried (only explained that I have, indeed, put forth effort to solve this on my own), but I'm purposely leaving out code samples of what I've tried because I believe there must be a clean solution to accomplish this, and I don't want the answer to be a small tweak to my ugly, unorthodox code.

I think what you are looking for is custom form control. It can do everything you mentioned and reduce verbosity a lot. It is a large subject and I am not a specialist but here is good place to start: Angular 2: Connect your custom control to ngModel with Control Value Accessor.
Example solution:
propertyEdit.component.ts:
import {Component, DoCheck} from '#angular/core';
import {TextInputComponent} from 'textInput.component';
let validate = require('validate.js');
#Component({
selector: 'my-property-edit',
template: `
<my-text-input [(ngModel)]="property.name" label="Property Name" name="name" [errors]="errors['name']"></my-text-input>
<my-text-input [(ngModel)]="property.address.address1" label="Address" name="address1" [errors]="errors['address.address1']></my-text-input>
`,
directives: [TextInputComponent],
})
export class PropertyEditComponent implements DoCheck {
public property: any = {name: null, address: {address1: null}};
public errors: any;
public constraints: any = {
name: {
presence: true,
length: {minimum: 3},
},
'address.address1': {
presence: {message: "^Address can't be blank"},
length: {minimum: 3, message: '^Address is too short (minimum is 3 characters)'},
}
};
public ngDoCheck(): void {
this.validate();
}
public validate(): void {
this.errors = validate(this.property, this.constraints) || {};
}
}
textInput.component.ts:
import {Component, Input, forwardRef} from '#angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR, NgModel} from '#angular/forms';
const noop = (_?: any) => {};
#Component({
selector: 'my-text-input',
template: `
<div class="form-group" [class.has-error]="hasErrors(input)">
<label class="control-label" [attr.for]="name">{{label}}</label>
<input class="form-control" type="text" [name]="name" [(ngModel)]="value" #input="ngModel" [id]="name" />
<div class="alert alert-danger" *ngIf="hasErrors(input)">{{errors}}</div>
</div>
`,
providers: [
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => TextInputComponent), multi: true },
],
})
export class TextInputComponent implements ControlValueAccessor {
protected _value: any;
protected onChange: (_: any) => void = noop;
protected onTouched: () => void = noop;
#Input() public label: string;
#Input() public name: string;
#Input() public errors: any;
get value(): any {
return this._value;
}
set value(value: any) {
if (value !== this._value) {
this._value = value;
this.onChange(value);
}
}
public writeValue(value: any) {
if (value !== this._value) {
this._value = value;
}
}
public registerOnChange(fn: (_: any) => void) {
this.onChange = fn;
}
public registerOnTouched(fn: () => void) {
this.onTouched = fn;
}
public hasErrors(input: NgModel): boolean {
return input.touched && this.errors != null;
}
}

Related

React-Redux how to use reusable checkbox component for displaying data

I have been asked to tamper with React-Redux code (knowing very little at the moment) and update a colleague's front-end code. One of the application's functionality, is for the administrator to create alert notifications and distribute them across different departments. These departments are selected with checkboxes and finally with a 'Send' button, they alert everyone involved. The form with all the necessary fields, is saved in the database. The notification details page, has detailed information and the mockup that we are supposed to produce, has the involved departments with the same form of grouped checkeboxes (along with their checked/unchecked status).
My colleague had created a reusable component like so:
import React from "react";
import { connect } from "react-redux";
import {reset, change, registerField } from "redux-form";
import _ from "lodash";
import DepartmentTypeCheckBoxes from "./ThreatTypeCheckBoxes";
import { setNotifView, setNotifViewForm } from "Actions/notifView.action";
import { Label } from "reactstrap";
import { ICustomProps } from "Entities/baseForm";
interface INotificationState {
notifStatus?: boolean;
}
interface IProps extends ICustomProps {
registerField(): void;
resetForm(): void;
changeField(value: any): any;
setNotifView(view: any): void;
setNotifViewForm(form: any): void;
}
class DepartmentType extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
};
this.onFieldChange = this.onFieldChange.bind(this);
}
public componentWillMount() {
this.props.registerField();
}
public onFieldChange() {
if(this.state.status && this.state.status == true){
this.setState({ status: false })
this.props.changeField(false);
}
else{
this.setState({ status: true })
this.props.changeField(true);
}
}
public componentWillReceiveProps(nextProps: IProps , nextState: IState) {
}
public render() {
return (
<div className="form-group">
<div className="f" >
<Label for="type">Department Types</Label>
<div className="">
<div className="">
<DepartmentTypeCheckBoxes id="1" value="option1" label="Development" fieldName="development" formName="CreateAlertNotification"></DepartmentTypeCheckBoxes>
</div>
<div className="">
<DepartmentTypeCheckBoxes id="2" value="option2" label="Human resources" fieldName="humanResources" formName="CreateAlertNotification"></DepartmentTypeCheckBoxes>
</div>
<div className="">
<DepartmentTypeCheckBoxes id="3" value="option3" label="Consultance" fieldTag="consultance" formTag="CreateAlertNotification"></DepartmentTypeCheckBoxes>
</div>
</div>
<div className="">
<div className="">
<div className="">
<DepartmentTypeCheckBoxes id="4" value="option4" label="Logistics" fieldTag="logistics" formTag="CreateAlertNotification"></DepartmentTypeCheckBoxes>
</div>
</div>
<div className="">
{this.props.children && this.props.children}
</div>
</div>
</div>
</div>
);
}
}
const mapStateToProps = (state: any, ownProps: ICustomProps) => {
return {
};
};
const mapDispatchToProps = (dispatch: any, ownProps: ICustomProps) => {
const formTag = ownProps.formTag;
const fieldTag = ownProps.fieldTag;
return {
registerField: () => dispatch(registerField(formTag, fieldTag, "FieldArray")),
changeField: (value: any) => dispatch(change(formTag, fieldTag, value, false, false)),
setNotifView: (view: any) => dispatch(setNotifView(view)),
setNotifViewForm: (form: any) => dispatch(setNotifViewForm(form)),
resetFields: () => dispatch(reset("CreateAlertNotification")),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(DepartmentType);
and uses it in the submission form like so:
<Row>
<Col md="6">
{ initialValues.ShowDepartmentBoxes &&
<DepartmentType fieldTag="DepType" formTag="CreateAlertNotification">
<Field name="AnotherCustomField" className="form-control" component={renderField} type="text" label="General Information" />
</DepartmentType>
}
</Col>
<Col md="6">
<AnotherCustomField fieldTag="SomeFieldName" formTag="CreateAlertNotification" Mode="create"/>
</Col>
</Row>
I want to use the same DepartmentType field in my "Notification Details" page, with the values loaded in the notification object from the db. Assuming I have 4 bool values like
notification.IsHumanResourcesAlerted
notification.IsDevelopmentAlerted,
notification.IsLogisticsAlerted,
notification.IsConsultanceAlerted
how will I pass them in the details page that is NOT a form and the "value" in the DepartmentTypeCheckBoxes seems to be predefined?
I have not found anything relevant yet and because we are on a tight schedule, I want to try and come up with a solution as possible.
Any help is appreciated.
I might be misunderstanding the implementation of your form and your details page, but if you need the form to exist exactly as it is selected on your send page you could see how the values of this form are being dispatched. With that information you could build something into an existing reducer for your details page or create a new reducer that holds those values and then use them later on to display your details page.
This would most likely be considered improper usage of Redux store (see https://goshakkk.name/should-i-put-form-state-into-redux/ for why I feel that may be the case). But it would work for your implementation as I understand it.
Edit: I should also mention that to display this data you could just display it as the same form as before, but disable the checkboxes so that the preselected values you imported cannot be changed.

Form-level validation does not behave as expected

Using redux-form 7.0.3, I'm unable to get the Field validate API to do anything.
First, I created a basic, minimal example templated off of the docs.
import React from 'react'
import { Field, reduxForm } from 'redux-form'
// removed validation fns
const required = value => {
console.log('Test of validation fn')
return (value ? undefined : 'Required')
}
// unchanged
const renderField = ({
input,
label,
type,
meta: { touched, error, warning }
}) =>
<div>
<label>
{label}
</label>
<div>
<input {...input} placeholder={label} type={type} />
{touched &&
((error &&
<span>
{error}
</span>) ||
(warning &&
<span>
{warning}
</span>))}
</div>
</div>
// removed fields & made into React Component
class FieldLevelValidations extends Component {
render(){
const { handleSubmit } = this.props
return (
<form onSubmit={handleSubmit}>
<Field
name="test"
type="text"
component={renderField}
label="Test Component"
validate={required}
/>
<div>
<button type="submit">
Submit
</button>
</div>
</form>
)
}
}
export default reduxForm({
form: 'fieldLevelValidation'
})(FieldLevelValidations)
From this I would assume that the forms reducer processes an action that sets a prop that can be accessed in renderField, but it does not. Both error and warning are undefined, and required is never called. Additionally, there is no isValid or the like property set in the store. I don't know if this is a bug or if I'm missing some critical piece, but I would expect more to be happening here. Also, some greater detail in the docs would be nice.

FirebaseListObservable<any[]> as list for material 2 autocomplete not filtering

I am trying to use angularfire2 and I want to use the angular material2 autocomplete component. with my current setup, the autocomplete list is properly populated from firebase. However the filter function does not seem to be working and I can't figure out why. Is it because I am using switchmap instead of map like the material example is using(if I use map then the list is not populated and it throws errors)? does the filter function work differently for a FirebaseListObservable vs a normal Observable?
The component file
import { Component, OnInit } from '#angular/core';
import { MdDialogRef } from '#angular/material';
import { FormControl } from '#angular/forms';
import { AngularFire, FirebaseListObservable } from 'angularfire2';
#Component({
selector: 'budget-new-transaction',
templateUrl: './new-transaction.component.html',
styleUrls: ['./new-transaction.component.css']
})
export class NewTransactionComponent implements OnInit {
categories: FirebaseListObservable<any[]>;
categoryCtrl: FormControl;
filteredCategories: any;
constructor(public dialogRef: MdDialogRef<NewTransactionComponent>, public af: AngularFire, ) {
this.categories = af.database.list('/items');
this.categoryCtrl = new FormControl();
this.filteredCategories = this.categoryCtrl.valueChanges
.startWith(null)
.switchMap(name => this.filterCategories(name));
}
filterCategories(val: string) {
return val ? this.categories.filter(s => new RegExp(`^${val}`, 'gi').test(s))
: this.categories;
}
ngOnInit() {
}
}
The html file
<h3>Add User Dialog</h3>
<form #form="ngForm" (ngSubmit)="dialogRef.close(form.value)" ngNativeValidate>
<div fxLayout="column" fxLayoutGap="8px">
<md-input-container>
<input mdInput placeholder="Category" [mdAutocomplete]="auto" [formControl]="categoryCtrl">
</md-input-container>
<md-autocomplete #auto="mdAutocomplete">
<md-option *ngFor="let category of filteredCategories | async" [value]="category">
{{ category.$value }}
</md-option>
</md-autocomplete>
<md-input-container>
<textarea mdInput ngModel name="details" placeholder="Details" rows="15" cols="60" required></textarea>
</md-input-container>
<div fxLayout="row" fxLayoutGap="24px">
<md-checkbox ngModel name="isAdmin">Is Admin?</md-checkbox>
<md-checkbox ngModel name="isCool">Is Cool?</md-checkbox>
</div>
</div>
<md-dialog-actions align="end">
<button md-button type="button" (click)="dialogRef.close()">Cancel</button>
<button md-button color="accent">Save User</button>
</md-dialog-actions>
</form>
The problem is that you are using the RxJS filter operator when it appears that you should be using Array.prototype.filter.
this.categories is a FirebaseListObservable, so it will emit an array contianing the database reference's child items. That means you are passing an array to the regular expression's test method.
You should do something like this:
import 'rxjs/add/operator/map';
filterCategories(val: string) {
return val ?
this.categories.map(list => list.filter(
s => new RegExp(`^${val}`, 'gi').test(s.$value)
)) :
this.categories;
}
Also, you might want to move the creation of the RegExp to outside of the implicit filter loop.

How to validate a form on button submit

I have a form, which is getting validated once the control gets the class change from ng-untouched ng-pristine ng-invalid to ng-pristine ng-invalid ng-touched but if i have a form with more controls then i want the user to know which field he/she has missed out on the button submit. how can i do that using angularJS2. I have used ReactiveFormsModule to validate the controls
The following is my code: component
import { Component } from '#angular/core';
import { FormBuilder, Validators, FormGroup, FormControl } from '#angular/forms';
#Component({
selector: 'page',
templateUrl:'../app/template.html'
})
export class AppComponent {
registrationForm: FormGroup;
constructor(private fb: FormBuilder) {
this.registrationForm = fb.group({
username: ['', [Validators.required, Validators.minLength(4)]],
emailId: ['', [Validators.required, this.emailValidator]],
phonenumber: ['', [Validators.required, this.phoneValidation]]
})
}
emailValidator(control: FormControl): { [key: string]: any } {
var emailRegexp = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+#[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
if (control.value && !emailRegexp.test(control.value)) {
return { invalidEmail: true };
}
}
phoneValidation(control: FormControl) {
if (control['touched'] && control['_value'] != '') {
if (!/^[1-9][0-9]{10}$/.test(control['_value'])) {
return { 'invalidPhone': true }
}
}
}
}
The following is my code: module
import { NgModule } from '#angular/core';
import { BrowserModule } from '#angular/platform-browser';
import { ReactiveFormsModule } from '#angular/forms';
import { AppComponent } from '../app/component';
#NgModule({
imports: [BrowserModule, ReactiveFormsModule],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule { }
The following is my code: template
<form [formGroup]="registrationForm" (ngSubmit)="registrationForm.valid && submitRegistration(registrationForm.value)">
<input type="text" formControlName="username" placeholder="username" />
<div class='form-text error' *ngIf="registrationForm.controls.username.touched">
<div *ngIf="registrationForm.controls.username.hasError('required')">Username is required.</div>
</div>
<br />
<input type="text" formControlName="emailId" placeholder="emailId" />
<div class='form-text error' *ngIf="registrationForm.controls.emailId.touched">
<div *ngIf="registrationForm.controls.emailId.hasError('required')">email id is required.</div>
<div *ngIf="registrationForm.controls.emailId.hasError('invalidEmail')">
Email is not in correct format.
</div>
</div>
<br />
<input type="text" formControlName="phonenumber" placeholder="phonenumber" />
<div class='form-text error' *ngIf="registrationForm.controls.phonenumber.touched">
<div *ngIf="registrationForm.controls.phonenumber.hasError('required')">phone number is required.</div>
<div *ngIf="registrationForm.controls.phonenumber.hasError('invalidPhone')">Invalid phone number.</div>
</div>
<br />
<input type="submit" value="Submit" />
</form>
I thought of updating all the invalid controls class to ng-untouched ng-pristine ng-invalid but not sure if this is the right approach
Angular forms don't provide ways to specify when to do validation.
Just set a flag to true when the form is submitted and show validation warnings only when the flag is true using *ngIf="flag" or similar.
<form [formGroup]="registrationForm" (ngSubmit)="submitRegistration(registrationForm.value)">
<div *ngIf="showValidation> validation errors here </div>
showValidation:boolean = false;
submitRegistration() {
if(this.registrationForm.status == 'VALID') {
this.processForm();
} else {
this.showValidation = true;
}
}

Angular 2 - Required field validation if checkbox is selected

Hy guys I'm new to Angular2 and in JS frameworks in general. I'm flowing tutorials on official site and haven't been able to find the solution to this problem.
So I have checkbox which is optional but if the checkbox is "checked" a new input field is shown. this part is not a problem. The problem is that I'm using modal based validation and I can't figure out how to make this new input field required only if the checkbox is checked.
this is may implementation so far:
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<!--{{form}}-->
<div formGroupName="test">
<div class="field">
<div class="checkbox">
<input type="checkbox" name="entryRecurring" value="" id="entryRecurring" formControlName="entryRecurring" />
<label for="entryRecurring">
<div class="checkbox_icon"></div>
Recurring Entry
</label>
</div>
</div>
<div *ngIf="form.value.test.entryRecurring">
<div class="field">
<label for="entryRecurringAmount">Repeat Amount</label>
<input type="text" name="entryRecurringAmount" value="" id="entryRecurringAmount" formControlName="entryRecurringAmount" />
</div>
</div>
</div>
<div class="field last">
<button name="submit" id="submit" class="btn btn_sushi" [disabled]="!form.valid">Submit</button>
</div>
import {Component, Input, OnInit, OnChanges} from '#angular/core';
import { Validators } from '#angular/common';
import { REACTIVE_FORM_DIRECTIVES, FormGroup, FormControl, FormBuilder } from '#angular/forms';
import { FormMessages } from './../helpers/formMessages.component';
import {EntriesService} from './entries.service';
import {ValidationService} from '../helpers/validation.service';
import {Category, CategoryByType} from '../../mock/mock-categories';
#Component({
selector: 'entryForm',
templateUrl: 'app/components/entries/entriesEdit.template.html',
directives: [REACTIVE_FORM_DIRECTIVES, FormMessages],
providers: [EntriesService, ValidationService]
})
export class EntriesEditComponent implements OnInit, OnChanges {
#Input() control: FormControl;
public form:FormGroup;
public submitted:boolean = false;
// private selectedId: number;
categories: Category[];
categoriesSortedByType: CategoryByType[];
constructor(
private _fb:FormBuilder,
private _entriesService: EntriesService
// private _router: Router
) {
this.form = this._fb.group({
test: this._fb.group({
entryRecurring: [''],
entryRecurringAmount: [''],
})
});
}
onSubmit() {
this.submitted = true;
// console.log(this.form.value);
if (this.form.dirty && this.form.valid) {
this._entriesService.saveEntry(this.form.value);
}
}
You could do that by using a custom validation service.
import {NgFormModel} from "angular2/common";
import {Component, Host} from 'angular2/core';
#Component({
selector : 'validation-message',
template : `
<span *ngIf="errorMessage !== null">{{errorMessage}}</span>
`,
inputs: ['controlName : field'],
})
export class ControlMessages {
controlName : string;
constructor(#Host() private _formDir : NgFormModel){
}
get errorMessage() : string {
let input = this._formDir.form.find(this.controlName);
let checkBx = this._formDir.form.find('checkBoxName');
if(input.value.trim() === '' && checkBx.checked) {
return 'The input field is now required'
}
return null;
}
}
Then use the new component like bellow
<div *ngIf="form.value.test.entryRecurring">
<div class="field">
<label for="entryRecurringAmount">Repeat Amount</label>
<input type="text" name="entryRecurringAmount" value="" id="entryRecurringAmount" ngControl="entryRecurringAmount" />
<validation-message field="entryRecurringAmount"></validation-message>
</div>
</div>
Hope that helped!

Resources