I am trying to figure out form validation in Angular 2. Right now I have an add product form that is adding a product to a particular store's inventory. I want to validate that the product's price is more than the wholesale price of the product, so I added the wholesale price as a data-* attribute. What I can't figure out is how to reference that attribute with Angular 2's NgControl. Here is my code:
...
<div class="four wide field">
<label>Wholesale Price</label>
<div class="ui left icon input">
<input type="text" disabled [(ngModel)]="wholesalePrice" ngControl="wholesale" />
<i class="dollar icon"></i>
</div>
</div>
<div class="four wide field">
<label>Price</label>
<div class="ui left icon input">
<input type="text" [(ngModel)]="retailPrice" [attr.data-wholesale]="wholesalePrice" ngControl="price" required />
<i class="dollar icon"></i>
</div>
</div>
...
...
constructor(private fb: FormBuilder) {
this.form = fb.group({
price: ['price', Validators.compose([
Validators.required,
this.validator_moreThanWholesale
])],
quantity: ['quantity']
});
}
...
I would create a global validator for your form:
this.form = fb.group({
price: ['price', Validators.compose([
Validators.required
])],
quantity: ['quantity']
}, {
validator: this.validator_moreThanWholesale
}));
The validator_moreThanWholesale method will have all the controls as input so you will be able to check:
validator_moreThanWholesale(group: ControlGroup) {
var wholesale = group.controls.wholesale.value;
var price = group.controls.price.value;
return (price < wholesale) ? { moreThanWholesale: true } : null
}
See this question for more details:
Validators same as other value in form
Related
The form is having 1 textbox , 1 radio button and 1 multi select Checkbox The HTML template is like below
<form *ngIf="notificationSettings | async; else loading"
[formGroup]="notificationForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<div *ngFor="let option of notifyBackAlertOptions; let i=index">
<input type="checkbox" class="form-check-input" [value]="option.value" formControlName="notifyBackOptions" />
<label> {{option.name}} </label>
</div>
</div>
<div class="form-group">
<label for="notifyBackEmail">Where shall we send the alerts?</label>
<input type="email" class="form-control" formControlName="notifyBackEmail">
</div>
<div class="form-check" *ngFor="let option of discontinuedAlertOptions;">
<label>
<input formControlName="discontinuedOption" class="form-check-input"
type="radio"
name="discontinuedOption"
[value]="option.value" />
{{option.name}}
</label>
</div>
<div class="float-left">
<button class="btn btn-primary mr-1">Update</button>
</div>
</form>
<ng-template #loading>
Loading ---...
</ng-template>
The component is like below
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
export class NotifcationsComponent implements OnInit {
notificationSettings: Observable<NotificationSetting>;
notificationForm: FormGroup;
submitted = false;
notifyBackAlertOptions = [
{ name: 'Option 1', value: '1' },
{ name: 'Option 2', value: '2' },
{ name: 'Option 3', value: '3' },
{ name: 'Option 4', value: '4' }
];
discontinuedAlertOptions = [
{ name: 'Yes for any', value: '1' },
{name: 'Yes for all', value: '2' },
{ name: 'No', value: '3' }
];
constructor(private formBuilder: FormBuilder,private userService: UserService) { }
ngOnInit() {
this.getCurrentSettings();
this.notificationForm = this.formBuilder.group({
notifyBackEmail: [''],
discontinuedOption: [''],
notifyBackOptions: new FormArray([]),
});
}
getCurrentSettings(): void {
this.notificationSettings = this.userService
.getUserNotificationSettings()
.pipe(tap(data => {
console.log("GET")
this.notificationForm = this.formBuilder.group({
notifyBackEmail: new FormControl(data.notifyBackEmail),
discontinuedOption: new FormControl(data.discontinuedOption),
notifyBackOptions: new FormControl(data.notifyBackOptions)
});
console.log(this.notificationForm) //I can see the values are mapping correctly against notificationForm. Checkbox property ie notifyBackOptions value is coming as ["1", "2"] at this stage
}
));
//This code maps / sets the values of textbox and radio buttons correctly at page loading based on response from API. But not setting values for multiselect checkbox correctly.
//I can see all checkbox values are coming as selected. IN this case 4 checkbox values are selected
//How to set only the notifyBackOptions checkbox selected values marked as checked
}
// convenience getter for easy access to form fields in HTML page
get f() { return this.notificationForm.controls; }
onSubmit() {
this.submitted = true;
// stop here if form is invalid
if (this.notificationForm.invalid) {
return;
}
console.log(this.notificationForm.value);
}
}
The HTML is rendering correctly and capturing the values at form submission . At load time of the component i have to read the values from API endpoint and prefill form based on current settings
The JSON response from API endpoint is like below
{
notifyBackEmail: "email#email-domain.in"
notifyBackOptions: ["1","2"]
discontinuedOption: "1"
}
The existing implementation of getCurrentSettings() is setting values of radio and textbox correctly but not checkbox.
At present this is setting all values of checkbox as selected. How can i set the Checkbox values as selected based on response form API with help of model binding
Since you are handling with array of values, you need to create array of formControl instead of single formControl. Try something like this:
Try this:
notificationForm: FormGroup;
getCurrentSettings(): void {
this.notificationSettings = this.userService
.getUserNotificationSettings()
.pipe(
tap(data => {
const notifyBackOptions = data.notifyBackOptions;
const notificationControls = this.notifyBackAlertOptions.map(
item => new FormControl(notifyBackOptions.includes(item.value))
);
this.notificationForm = this.formBuilder.group({
notifyBackEmail: [''],
discontinuedOption: [''],
notifyBackOptions: this.formBuilder.array(notificationControls)
});
})
);
}
Then in HTML You need to add formArrayName directive to sync with FormGroup
<form [formGroup]="notificationForm" (ngSubmit)="onSubmit()">
<div class="form-group" formArrayName="notifyBackOptions">
<div *ngFor="let option of notifyBackOptionsArr.controls; let i=index">
<input [formControlName]="i" type="checkbox" class="form-check-input" />
<label> {{notifyBackAlertOptions[i].name}} </label>
</div>
</div>
<div class="form-group">
<label for="notifyBackEmail">Where shall we send the alerts?</label>
<input
type="email"
class="form-control"
formControlName="notifyBackEmail"
/>
</div>
<div class="form-check" *ngFor="let option of discontinuedAlertOptions;">
<label>
<input
formControlName="discontinuedOption"
class="form-check-input"
type="radio"
name="discontinuedOption"
[value]="option.value"
/>
{{option.name}}
</label>
</div>
<div class="float-left">
<button class="btn btn-primary mr-1">Update</button>
</div>
</form>
Working Example
I am currently developing a dynamic form builder it has an admin side and a user side, on the admin side you can set the name of the input and type eg: first name and textbox you can add or take away from fields and there are a set amount of types you can choose from, when you are done the admin will then save the form to the Database. On the user frontend the from is pulled from the Database and using patchValue the from is set the problem I am having is with the user's input to the questions on the form. I am trying to figure out how I can first set the type and placeholder of the input that the user uses to answer the questions.
As well as dynamically apply validation rules to each input, so, for instance, a user wants to fill out a form with their first name, last name, age, and CV for the first and last name I want to have required and a regex expression to only allow certain characters, for the age I want to have a min and max number and cv required. and when the user submits the form I only want to send the name and value of an input and not the whole form.
But keep in mind that these forms are completely dynamic so I need to validate the user's input based on the input type that is set
/*Typescript Code*/
import { Component, OnInit } from '#angular/core';
import { FormArray, FormBuilder, FormGroup, Validators } from '#angular/forms';
import { FormService} from '../form.service';
import { ActivatedRoute } from '#angular/router';
#Component({
selector: 'app-reactive',
templateUrl: './reactive.component.html',
styleUrls: ['./reactive.component.scss']
})
export class ReactiveComponent implements OnInit {
public form: FormGroup;
public fieldList: any;
types: Array<any>;
formData: any;
Param: string;
get contactFormGroup() {
return this.form.get('inputs') as FormArray;
}
constructor(
private route: ActivatedRoute,
private fb: FormBuilder,
private api: FormService) { }
ngOnInit() {
/*First I initlize the form*/
this.form = this.fb.group({
name: [null, Validators.compose([Validators.required])],
organization: [null, Validators.compose([Validators.required])],
inputs: this.fb.array([this.createForm()])
});
this.route.paramMap.subscribe(params => {
this.Param = params.get('id');
this.getForm(this.Param);
});
// set fieldslist to this field
this.fieldList = this.form.get('inputs') as FormArray;
}
// formgroup
/*The I initilize the form array of inputs*/
createForm(): FormGroup {
return this.fb.group({
type: [null, Validators.compose([Validators.required])],/*These come from the DB*/
name: [null, Validators.compose([Validators.required])],/*These come from the DB*/
value: [null, Validators.compose([Validators.required])] /*This gets filled in by the user*/
});
}
/*The I use my API service to call the DB using the form Id*/
getForm(id) {
this.api.getForm(id).subscribe(
(data: any) => this.setForm(data)
);
}
getFieldsFormGroup(index): FormGroup {
const formGroup = this.fieldList.controls[index] as FormGroup;
return formGroup;
}
/*Here I set my form data*/
setForm(data) {
const d = data.results;
this.form.patchValue({
name: [d[0].form_name],
organization: [d[0].org],
});
this.form.setControl('inputs', this.setExistingFields(d[0].fields));
}
/*Here I set my inputs array data*/
setExistingFields(fields: any): FormArray {
const formArray = new FormArray([]);
this.fieldList = formArray;
fields.forEach(f => {
formArray.push(this.fb.group({
name: f.name,
type: f.type,
value: f.value
}));
});
return formArray;
}
submit() {
/* Here when I send my form back to the server I only want to send the inputs name and value nothing else */
console.log(this.form.value.inputs);
}
}
/*HTML CODE*/
<div class="container p-1">
<div class="row">
<div class="col-12">
<form class="md-form" [formGroup]="form" #submittion (submit)="submit()">
<div class="card">
<div class="card-header">Form Name</div>
<div class="card-body">
<div class="row">
<div class="form-group col-6">
<i class="fab fa-wpforms prefix"></i>
<input class="form-control" placeholder="Form Name" formControlName="name" type="text" readonly>
</div>
<div class="form-group col-6">
<i class="far fa-building prefix"></i>
<input class="form-control" placeholder="Organization" formControlName="organization" type="text"
readonly>
</div>
</div>
</div>
<div class="card-header col-12">Please fill in all fields. </div>
<div class="card-body" formArrayName="inputs">
<div class="row">
<div class="col-6" *ngFor="let contact of contactFormGroup.controls; let i = index;">
<div [formGroupName]="i" class="row z-depth-1 m-1 p-1">
<div class="form-group col-6">
<input class="form-control" formControlName="name" type="text" readonly>
</div>
<div class="form-group col-6">
<1-- This is the input that the user will fill in and I am not sure how to dynamically set the type or placeholder this is what I am trying to achieve -->
<input class="form-control" type="" placeholder=""
formControlName="value" />
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="icon-bar round">
<button class="btn btn-success round m-1" type="submit" (click)="submit()"><i class="fas fa-save"> Save Form</i></button>
</div>
Any help is very much appricated on this I have been trying to figure it out for a while now, thank you
I have a simple dynamic radio button, in my example 3 are created from returning data.
When a radio value is selected it populates my reactive form with that value, "yes", "no" or "maybe". In theses objects there are other properties that I would like to be sent back as part of the form.
As below I return section, but also want to return sectionCode of the matched section.
Before radio selection is changed my form shows all properties correctly, but obviously I can only pass back one value, so as soon as I do the other object items are lost.
https://stackblitz.com/edit/angular-r4g6uv?file=src%2Fapp%2Fpersonal%2Fpersonal.component.ts
section:[
{
section: "yes",
sectionCode: "1"
},
{
section: "no",
sectionCode: "2"
},
{
section: "maybe",
sectionCode: "3"
}
]
component.html
<div formGroupName="radioButtons" class="form-group col-6 pl-0 pt-3">
<h2 class="mb-2">radioButtons</h2>
<label for="intel-form-submitter" appRequiredLabel>select</label><br />
<div class="form-check-inline" *ngFor="let item of personal.radioButtons.section">
<label for="{{item.section}}" class="col-12 customradio"
><span>{{item.section}}</span>
<input value="{{item.section}}" id="{{item.section}}" type="radio" formControlName="section"/>
<span class="checkmark"></span>
</label>
</div>
</div>
component.ts
createEntry(formBuilder: FormBuilder) {
return this.formBuilder.group({
title: this.personal.title,
creationDate: this.personal.creationDate,
radioButtons: this.formBuilder.group({
section: this.personal.radioButtons.section,
})
});
}
hope this helps you.
Bind the object instead of one value in the object. In Component.html
[value]="item"
Component.html
<form [formGroup]="form" novalidate>
<div formGroupName="radioButtons" novalidate>
<div class="form-check-inline" *ngFor="let item of section">
<label for="{{item.section}}" class="col-12 customradio"
><span>{{item.section}}</span>
<input **[value]="item"** id="{{item.section}}" type="radio" formControlName="section"/>
<span class="checkmark"></span>
</label>
</div>
</div>
<button type="button" (click)="save()" >Save</button>
</form>
{{form.value|json}}
Component.ts
xport class AppComponent implements OnInit {
section = [
{
section: "yes",
sectionCode: "1"
},
{
section: "no",
sectionCode: "2"
},
{
section: "maybe",
sectionCode: "3"
}
];
selectedSection: any;
form: FormGroup;
constructor(public formBuilder: FormBuilder) {}
ngOnInit() {
this.form = this.createEntry();
}
createEntry() {
return this.formBuilder.group({
radioButtons: this.formBuilder.group({
section: this.section[0]
})
});
}
save(){
console.log(this.form)
}
}
Find project in - https://stackblitz.com/edit/k-react-form-radio-test-5bixbv
You can bind the object itself instead of just section name.
<input [value]="item" id="{{item.section}}" type="radio"/>
Demo : https://stackblitz.com/edit/angular-h1mwhe
I am having problem accesssing products array located inside opticanOrders which is inside orderForm. In the console, I see that in order to access products array, I should reference it like that:
orderForm.controls.opticianOrders.controls.products.controls
But it doesn't work.
This is my component:
constructor(private customerService: CustomerService, private fb: FormBuilder) { }
orderForm: FormGroup;
ngOnInit() {
this.orderForm = this.fb.group({
name: [''],
surName: [''],
opticianOrders: this.fb.group({
orderDescription: [''],
products: this.fb.array([
this.initProduct()
])
}),
});
}
save(model: Customer) {
// call API to save customer
console.log(model);
}
onCancel(form: NgForm){
this.createState.emit(false);
}
initProduct(){
return this.fb.group({
name: [''],
manufacturerName: ['']
})
}
addProduct(){
const control = <FormArray>this.orderForm.controls['products'];
control.push(this.initProduct());
}
removeProduct(i: number){
const control = <FormArray>this.orderForm.controls['products']
}
Html
<form [formGroup]="orderForm" novalidate (ngSubmit)="save(orderForm)">
<!-- name -->
<div class="form-group">
<label>Name</label>
<input type="text" formControlName="name">
</div>
<!-- surName -->
<div class="form-group">
<label>Last Name</label>
<input type="text" formControlName="surName">
</div>
<div formGroupName="opticianOrders" class="form-group">
<label>Order Description</label>
<input type="text" formControlName="orderDescription">
</div>
<div formArrayName="products">
<div *ngFor="let product of orderForm.controls.opticianOrders.controls.products.controls; let i=index">
<div>
<span>Address {{i + 1}}</span>
<span *ngIf="orderForm.controls.opticianOrders.controls.products.controls.length > 1"
(click)="removeProduct(i)">
</span>
</div>
<div [formGroupName]="i">
<div>
<label>Product name</label>
<input type="text" formControlName="name">
</div>
</div>
</div>
</div>
<button type="submit" [disabled]="!orderForm.valid">Submit</button>
</form>
Please replace your HTML Code as per below
<form [formGroup]="orderForm" (ngSubmit)="save()">
<!-- name -->
<div class="form-group">
<label>Name</label>
<input type="text" formControlName="name">
</div>
<!-- surName -->
<div class="form-group">
<label>Last Name</label>
<input type="text" formControlName="surName">
</div>
<div class="form-group">
<label>Order Description</label>
<input type="text" formControlName="orderDescription">
</div>
<div formArrayName="products">
<div *ngFor="let product of orderForm.controls.products['controls']; let i=index">
<div>
<span><strong>Product {{i + 1}}</strong></span>
<span class="fa fa-times" *ngIf="orderForm.controls['products'].controls.length > 1" (click)="removeProduct(i)">
</span>
</div>
<div [formGroupName]="i">
<div>
<label>Product name</label>
<input type="text" formControlName="name">
</div>
<div>
<label>Product Manufacturer name</label>
<input type="text" formControlName="manufacturerName">
</div>
</div>
</div>
<div class="margin-20">
<a (click)="addProduct()" style="cursor: pointer; text-transform: uppercase; font-weight: 500">
Add another Entry +
</a>
</div>
</div>
<button class="btn btn-primary" type="submit" [disabled]="orderForm.invalid">Submit</button>
</form>
Please replace TS code as per below. I have used a trick in save form method, check whether it works for you or not?
constructor(private fb: FormBuilder) { }
orderForm: FormGroup;
ngOnInit() {
this.orderForm = this.fb.group({
name: ['', Validators.required],
surName: ['', Validators.required],
orderDescription: ['', Validators.required],
products: this.fb.array([
this.initProduct()
])
});
}
save() {
console.log(this.orderForm.value);
const obj = {
name: this.orderForm.value.name,
surName: this.orderForm.value.surName,
orderDescription: this.orderForm.value.orderDescription,
opticianOrders: {
products: this.orderForm.value.products
},
};
console.log(obj);
}
initProduct() {
return this.fb.group({
name: ['', Validators.required],
manufacturerName: ['', Validators.required]
})
}
addProduct() {
const control = <FormArray>this.orderForm.controls['products'];
control.push(this.initProduct());
}
removeProduct(i: number) {
const control = <FormArray>this.orderForm.controls['products'];
control.removeAt(i);
}
Your stackblitz code does not work. You did not import the ReactiveFormsModule and you implemented the forms code in the hello.component.ts also you put the template code in the app.component.html.
See my working sample on stackblitz. It let you add (click on Add) and remove (click on 'x') products from your FormArray.
Form value with two products:
{
name: 'John',
surName: 'Doe',
opticianOrders: {
orderDescription: '1234',
products: [
{ name: 'Cookies', manufacturerName: '' },
{ name: 'More Cookies', manufacturerName: '' }
]
}
}
For typescript this.orderForm.controls.opticianOrders is an AbstractControl which has no controls property. You have to cast it to a FormGroup first. Same with products, you have to cast it to a FormArray.
removeProduct(i: number){
const aFormGroup = this.orderForm.controls.opticianOrders as FormGroup;
const aFormArray = aFormGroup.controls.products as FormArray;
aFormArray.removeAt(i);
}
Try this
orderForm.controls.opticianOrders.controls
In angular 5 I am trying to add new row and remove row functionality. For add new row and delete row I have taken code from here. Everything is working fine but when I am trying to get the user entered value in the new row fields I am not getting those values.
I am getting the value for Event Name. But for other 3 fields like Package Name, Package Price, Max Purchase Limit, I am not getting them.
Here is the code what I have tried.
my event-create.component.html looks like this
<div class="container col-md-12">
<h1 class="page-header">Create Event</h1>
<div class="row show-hide-message">
<div [ngClass]= "messageClass">{{message}}</div>
</div>
<form [formGroup] = "form" (ngSubmit)="onEventSubmit()">
<fieldset>
<div class="form-group">
<label for="eventname">Event Name</label>
<div class='form-group' [ngClass]="{'has-error': form.controls.eventname.errors && form.controls.eventname.dirty,
'has-success': !form.controls.eventname.errors
}">
<input type="text" class="form-control" autocomplete="off" placeholder="Event Name" formControlName="eventname">
<ul class="help-block">
<li *ngIf="(form.controls.eventname.errors?.minlength ) && form.controls.eventname.dirty">Event name should be atleast 5 characters</li>
</ul>
</div>
</div>
<h4>Package Price</h4>
<hr>
<div class="row" formArrayName="sections">
<div class="col-md-12" *ngFor="let section of getSections(form); let i = index">
<div class="form-group col-md-5">
<label for="packagename">Package Name</label>
<input type="text" class="form-control" autocomplete="off" placeholder="Package Name" formControlName="packagename" >
</div>
<div class="form-group col-md-2">
<label for="packageprice">Package Price</label>
<input type="number" class="form-control" autocomplete="off" placeholder="Package Price" formControlName="packageprice" >
</div>
<div class="form-group col-md-2">
<label for="packagelimit">Max Purchase Limit</label>
<input type="number" class="form-control" formControlName="packagelimit" autocomplete="off" >
</div>
<div class="form-group col-md-1">
<br/>
<input type="button" (click)="addPackageRow()" class="btn btn-md btn-success" value="+" name="">
</div>
<div class="form-group col-md-1" *ngIf="getSections(form).length > 1">
<br/>
<input type="button" (click)="removeSection(i)" class="btn btn-md btn-error" value="-" name="">
</div>
</div>
</div>
<input [disabled]=!form.valid type="submit" class="btn btn-primary" value="Submit">
<pre>{{form.value | json}}</pre>
</fieldset>
</form>
</div>
and my event-create.component.ts looks like this
import { Component, OnInit } from '#angular/core';
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '#angular/forms';
import { FormControlName } from '#angular/forms/src/directives/reactive_directives/form_control_name';
#Component({
selector: 'app-event-create',
templateUrl: './event-create.component.html',
styleUrls: ['./event-create.component.css']
})
export class EventCreateComponent implements OnInit {
form : FormGroup;
packagesArray: FormArray;
constructor(
private formBuilder : FormBuilder,
) { this.createEventForm() }
createEventForm() {
this.form = this.formBuilder.group({
eventname: ['', Validators.compose([
Validators.required,
Validators.minLength(5)
])],
packages: this.packagesArray
})
}
ngOnInit() {
this.form = new FormGroup({
eventname: new FormControl(''),
sections: new FormArray([
this.initSection(),
]),
});
}
initSection() {
return new FormGroup({
packagename: new FormControl(''),
packageprice: new FormControl(''),
packagelimit: new FormControl('')
});
}
initItemRows() {
return this.formBuilder.group({
itemname: ['']
});
}
onEventSubmit() {}
public addPackageRow() {
const control = <FormArray>this.form.get('sections');
control.push(this.initSection());
}
initOptions() {
return new FormGroup({
optionTitle: new FormControl('')
});
}
addSection() {
const control = <FormArray>this.form.get('sections');
control.push(this.initSection());
}
getSections(form) {
return form.controls.sections.controls;
}
public removeSection(i){
const control = <FormArray>this.form.get('sections');
control.removeAt(i);
}
}
Its not showing user entered value in html page also not doing any validation for the eventname field.
Here is the demo what I have done so far
https://stackblitz.com/edit/angular-3s5wwf
Can someone tell me what is going wrong here. Any suggestion or advice will be really appreciable. Thanks.
You need to add this in line 23 and 24
<div class="row" formArrayName="sections">
<div class="col-md-12" *ngFor="let section of getSections(form); let i = index" [formGroupName]="i">
you missed adding the formGroupName for the formArrayName
workinglink