How to use array of objects for controls in Reactive Forms - forms

I need to dynamic create textarea for forms. I have the following model:
this.fields = {
isRequired: true,
type: {
options: [
{
label: 'Option 1',
value: '1'
},
{
label: 'Option 2',
value: '2'
}
]
}
};
And form:
this.userForm = this.fb.group({
isRequired: [this.fields.isRequired, Validators.required],
//... here a lot of other controls
type: this.fb.group({
options: this.fb.array(this.fields.type.options),
})
});
Part of template:
<div formGroupName="type">
<div formArrayName="options">
<div *ngFor="let option of userForm.controls.type.controls.options.controls; let i=index">
<textarea [formControlName]="i"></textarea>
</div>
</div>
</div>
So, as you can see I have an array of objects and I want to use label property to show it in a textarea. Now I see [object Object]. If I change options to a simple string array, like: ['Option 1', 'Option 2'], then all works fine. But I need to use objects. So, instead of:
<textarea [formControlName]="i"></textarea>
I have tried:
<textarea [formControlName]="option[i].label"></textarea>
But, it doesn't work.
How can I use an array of objects?
This is Plunkr

You need to add a FormGroup, which contains your label and value. This also means that the object created by the form, is of the same build as your fields object.
ngOnInit() {
// build form
this.userForm = this.fb.group({
type: this.fb.group({
options: this.fb.array([]) // create empty form array
})
});
// patch the values from your object
this.patch();
}
After that we patch the value with the method called in your OnInit:
patch() {
const control = <FormArray>this.userForm.get('type.options');
this.fields.type.options.forEach(x => {
control.push(this.patchValues(x.label, x.value))
});
}
// assign the values
patchValues(label, value) {
return this.fb.group({
label: [label],
value: [value]
})
}
Finally, here is a
Demo

The answer from AJT_82 was so useful to me, I thought I would share how I reused his code and built a similar example - one that might have a more common use case, which is inviting several people to sign-up at once. Like this:
I thought this might help others, so that's why I am adding it here.
You can see the form is a simple array of text inputs for emails, with a custom validator loaded on each one. You can see the JSON structure in the screenshot - see the pre line in the template (thanks to AJT), a very useful idea whilst developing to see if your model and controls are wired up!
So first, declare the objects we need. Note that 3 empty strings are the model data (which we will bind to the text inputs):
public form: FormGroup;
private control: FormArray;
private emailsModel = { emails: ['','','']} // the model, ready to hold the emails
private fb : FormBuilder;
The constructor is clean (for easier testing, just inject my userService to send the form data to after submit):
constructor(
private _userService: UserService,
) {}
The form is built in the init method, including storing a reference to the emailsArray control itself so we can check later whether its children (the actual inputs) are touched and if so, do they have errors:
ngOnInit() {
this.fb = new FormBuilder;
this.form = this.fb.group({
emailsArray: this.fb.array([])
});
this.control = <FormArray>this.form.controls['emailsArray'];
this.patch();
}
private patch(): void {
// iterate the object model and extra values, binding them to the controls
this.emailsModel.emails.forEach((item) => {
this.control.push(this.patchValues(item));
})
}
This is what builds each input control (of type AbstracControl) with the validator:
private patchValues(item): AbstractControl {
return this.fb.group({
email: [item, Validators.compose([emailValidator])]
})
}
The 2 helper methods to check if the input was touched and if the validator raised an error (see the template to see how they are used - notice I pass the index value of the array from the *ngFor in the template):
private hasError(i):boolean {
// const control = <FormArray>this.form.controls['emailsArray'];
return this.control.controls[i].get('email').hasError('invalidEmail');
}
private isTouched(i):boolean {
// const control = <FormArray>this.form.controls['emailsArray'];
return this.control.controls[i].get('email').touched;
}
Here's the validator:
export function emailValidator(control: FormControl): { [key: string]: any } {
var emailRegexp = /[a-z0-9._%+-]+#[a-z0-9.-]+\.[a-z]{2,3}$/;
if (control.value && !emailRegexp.test(control.value)) {
return { invalidEmail: true };
}
}
And the template:
<form [formGroup]="form" (ngSubmit)="onSubmit(form.value)" class="text-left">
<div formArrayName="emailsArray">
<div *ngFor="let child of form.controls.emailsArray.controls; let i=index">
<div class="form-group" formGroupName="{{i}}">
<input formControlName="email"
class="form-control checking-field"
placeholder="Email" type="text">
<span class="help-block" *ngIf="isTouched(i)">
<span class="text-danger"
*ngIf="hasError(i)">Invalid email address
</span>
</span>
</div>
</div>
</div>
<pre>{{form.value | json }}</pre>
<div class="form-group text-center">
<button class="btn btn-main btn-block" type="submit">INVITE</button>
</div>
</form>
For what it's worth, I had started with this awful mess - but if you look at the code below, you might more easily understand the code above!
public form: FormGroup;
public email1: AbstractControl;
public email2: AbstractControl;
public email3: AbstractControl;
public email4: AbstractControl;
public email5: AbstractControl;
constructor(
fb: FormBuilder
) {
this.form = fb.group({
'email1': ['', Validators.compose([emailValidator])],
'email2': ['', Validators.compose([emailValidator])],
'email3': ['', Validators.compose([emailValidator])],
'email4': ['', Validators.compose([emailValidator])],
'email5': ['', Validators.compose([emailValidator])],
});
this.email1 = this.form.controls['email1'];
this.email2 = this.form.controls['email2'];
this.email3 = this.form.controls['email3'];
this.email4 = this.form.controls['email4'];
this.email5 = this.form.controls['email5'];
}
and the above used 5 of these divs in the template - not very DRY!
<div class="form-group">
<input [formControl]="email1" class="form-control checking-field" placeholder="Email" type="text">
<span class="help-block" *ngIf="form.get('email1').touched">
<span class="text-danger" *ngIf="form.get('email1').hasError('invalidEmail')">Invalid email address</span>
</span>
</div>

I guess it's not possible with FormControlName.
You could use ngModel .. take a look at your modified plunker:
http://plnkr.co/edit/0DXSIUY22D6Qlvv0HF0D?p=preview
#Component({
selector: 'my-app',
template: `
<hr>
<form [formGroup]="userForm" (ngSubmit)="submit(userForm.value)">
<input type="checkbox" formControlName="isRequired"> Required Field
<div formGroupName="type">
<div formArrayName="options">
<div *ngFor="let option of userForm.controls.type.controls.options.controls; let i=index">
<label>{{ option.value.label }}</label><br />
<!-- change your textarea -->
<textarea [name]="i" [(ngModel)]="option.value.value" [ngModelOptions]="{standalone: true}" ></textarea>
</div>
</div>
</div>
<button type="submit">Submit</button>
</form>
<br>
<pre>{{userForm.value | json }}</pre>
`,
})
export class App {
name:string;
userForm: FormGroup;
fields:any;
ngOnInit() {
this.fields = {
isRequired: true,
type: {
options: [
{
label: 'Option 1',
value: '1'
},
{
label: 'Option 2',
value: '2'
}
]
}
};
this.userForm = this.fb.group({
isRequired: [this.fields.isRequired, Validators.required],
//... here a lot of other controls
type: this.fb.group({
// .. added map-function
options: this.fb.array(this.fields.type.options.map(o => new FormControl(o))),
})
});
}
submit(value) {
console.log(value);
}
constructor(private fb: FormBuilder) { }
addNumber() {
const control = <FormArray>this.userForm.controls['numbers'];
control.push(new FormControl())
}
}

You may try this
Typescript:
ngOnInit(): void {
this.user = this.fb.group({
Title: ['1'],
FirstName: ['', Validators.required],
LastName: ['', Validators.required],
ContactNumbers: this.fb.array([
this.fb.group({
PhoneNumber: ['', [Validators.required]],
IsPrimary: [true],
ContactTypeId: [1]
})
]),
Emails: this.fb.array([
this.fb.group({
Email: ['', [Validators.required, Validators.email]],
IsPrimary: [true]
})
]),
Address: this.fb.group({
Address1: ['', Validators.required],
Address2: [''],
Town: [''],
State: ['UP'],
Country: [{ value: 'India', disabled: true }],
Zip: ['', Validators.required]
})
});
}
get Emails() {
return this.landlord.get('Emails') as FormArray;
}
Add and remove
addMoreEmail(index: number) {
if (index == 0) {
this.Emails.push(this.fb.group({ Email: ['', [Validators.required, Validators.email]], IsPrimary: [false] }));
} else {
this.Emails.removeAt(this.Emails.value.indexOf(index));
}
}
HTML
<form [formGroup]="user"
<div formArrayName="Emails">
<div *ngFor="let email of Emails.controls; let i=index">
<div class="row" [formGroup]="email">
<div class="col-sm-10">
<div class="form-group">
<label for="i" class="label">Email</label>
<input type="email" nbInput fullWidth id="i" placeholder="Email"
formControlName="Email" required>
</div>
</div>
<div class="col-sm-2 position-relative">
<nb-icon icon="{{i==0?'plus':'minus'}}-round" pack="ion"
(click)="addMoreEmail(i)">
</nb-icon>
</div>
</div>
</div>
</div></div>

Related

Angular 2 nested forms with child components and validation

I'm trying achieve a nested form with validation in Angular 2, I've seen posts and followed the documentation but I'm really struggling, hope you can point me in the right direction.
What I am trying to achieve is having a validated form with multiple children components. These children components are a bit complex, some of them have more children components, but for the sake of the question I think we can attack the problem having a parent and a children.
What am I trying to accomplish
Having a form that works like this:
<div [formGroup]="userForm" novalidate>
<div>
<label>User Id</label>
<input formControlName="userId">
</div>
<div>
<label>Dummy</label>
<input formControlName="dummyInput">
</div>
</div>
This requires having a class like this:
private userForm: FormGroup;
constructor(private fb: FormBuilder){
this.createForm();
}
private createForm(): void{
this.userForm = this.fb.group({
userId: ["", Validators.required],
dummyInput: "", Validators.required]
});
}
This works as expected, but now I want to decouple the code, and put the "dummyInput" functionality in a separate, different component. This is where I get lost. This is what I tried, I think I'm not far from getting the answer, but I'm really out of ideas, I'm fairly new to the scene:
parent.component.html
<div [formGroup]="userForm" novalidate>
<div>
<label>User Id</label>
<input formControlName="userId">
</div>
<div>
<dummy></dummy>
</div>
</div>
parent.component.ts
private createForm(): void{
this.userForm = this.fb.group({
userId: ["", Validators.required],
dummy: this.fb.group({
dummyInput: ["", Validators.required]
})
});
children.component.html
<div [formGroup]="dummyGroup">
<label>Dummy Input: </label>
<input formControlName="dummyInput">
</div>
children.component.ts
private dummyGroup: FormGroup;
I know something is not right with the code, but I'm really in a roadblock. Any help would be aprreciated.
Thanks.
you can add an Input in your children component to pass the FormGroup to it.and use FormGroupName to pass the name of your FormGroup :)
children.component.ts
#Input('group');
private dummyGroup: FormGroup;
parent.component.html
<div [formGroup]="userForm" novalidate>
<div>
<label>User Id</label>
<input formControlName="userId">
</div>
<div formGroupName="dummy">
<dummy [group]="userForm.controls['dummy']"></dummy>
</div>
</div>
Not going to lie, don't know how I didn't find this post earlier.
Angular 2: Form containing child component
The solution is to bind the children component to the same formGroup, by passing the formGroup from the parent to the children as an Input.
If anyone shares a piece of code to solve the problem in other way, I'll gladly accept it.
The main idea is that you have to treat the formGroup and formControls as variables, mainly javascript objects and arrays.
So I'll put some code in to make my point. The code below is somewhat like what you have. The form is constructed dynamically, just that it is split into sections, each section containing its share of fields and labels.
The HTML is backed up by typescript classes. Those are not here as they do not have much special. Just the FormSchemaUI, FormSectionUI and FormFieldUI are important.
Treat each piece of code as its own file.
Also please take note that formSchema: FormSchema is a JSON object that I receive from a service. Any properties of the UI classes that you do not see defined are inherited from their base Data clases. Those are not presented here.
The hierarchy is: FormSchema contains multiple sections. A section contains multiple fields.
<form (ngSubmit)="onSubmit()" #ciRegisterForm="ngForm" [formGroup]="formSchemaUI.MainFormGroup">
<button kendoButton (click)="onSubmit(ciRegisterForm)" [disabled]="!canSubmit()"> Register {{registerPageName}} </button>
<br /><br />
<app-ci-register-section *ngFor="let sectionUI of formSchemaUI.SectionsUI" [sectionUI]="sectionUI">
</app-ci-register-section>
<button kendoButton (click)="onSubmit(ciRegisterForm)" [disabled]="!canSubmit()"> Register {{registerPageName}} </button>
</form>
=============================================
<div class="row" [formGroup]="sectionUI.MainFormGroup">
<div class="col-md-12 col-lg-12" [formGroupName]="sectionUI.SectionDisplayId">
<fieldset class="section-border">
<legend class="section-border">{{sectionUI.Title}}</legend>
<ng-container *ngFor='let fieldUI of sectionUI.FieldsUI; let i=index; let even = even;'>
<div class="row" *ngIf="even">
<ng-container>
<div class="col-md-6 col-lg-6" app-ci-field-label-tuple [fieldUI]="fieldUI">
</div>
</ng-container>
<ng-container *ngIf="sectionUI.Fields[i+1]">
<div class="col-md-6 col-lg-6" app-ci-field-label-tuple [fieldUI]="sectionUI.FieldsUI[i+1]">
</div>
</ng-container>
</div>
</ng-container>
</fieldset>
</div>
</div>
=============================================
{{fieldUI.Label}}
=============================================
<ng-container>
<div class="row">
<div class="col-md-4 col-lg-4 text-right">
<label for="{{fieldUI.FieldDisplayId}}"> {{fieldUI.Label}} </label>
</div>
<div class="col-md-8 col-lg-8">
<div app-ci-field-edit [fieldUI]="fieldUI" ></div>
</div>
</div>
</ng-container>
=============================================
<ng-container [formGroup]="fieldUI.ParentSectionFormGroup">
<ng-container *ngIf="fieldUI.isEnabled">
<ng-container [ngSwitch]="fieldUI.ColumnType">
<input *ngSwitchCase="'HIDDEN'" type="hidden" id="{{fieldUI.FieldDisplayId}}" [value]="fieldUI.Value" />
<ci-field-textbox *ngSwitchDefault
[fieldUI]="fieldUI"
(valueChange)="onValueChange($event)"
class="fullWidth" style="width:100%">
</ci-field-textbox>
</ng-container>
</ng-container>
</ng-container>
=============================================
export class FormSchemaUI extends FormSchema {
SectionsUI: Array<FormSectionUI>;
MainFormGroup: FormGroup;
static fromFormSchemaData(formSchema: FormSchema): FormSchemaUI {
let formSchemaUI = new FormSchemaUI(formSchema);
formSchemaUI.SectionsUI = new Array<FormSectionUI>();
formSchemaUI.Sections.forEach(section => {
let formSectionUI = FormSectionUI.fromFormSectionData(section);
formSchemaUI.SectionsUI.push(formSectionUI);
});
formSchemaUI.MainFormGroup = FormSchemaUI.buildMainFormGroup(formSchemaUI);
return formSchemaUI;
}
static buildMainFormGroup(formSchemaUI: FormSchemaUI): FormGroup {
let obj = {};
formSchemaUI.SectionsUI.forEach(sectionUI => {
obj[sectionUI.SectionDisplayId] = sectionUI.SectionFormGroup;
});
let sectionFormGroup = new FormGroup(obj);
return sectionFormGroup;
}
}
=============================================
export class FormSectionUI extends FormSection {
constructor(formSection: FormSection) {
this.SectionDisplayId = 'section' + this.SectionId.toString();
}
SectionDisplayId: string;
FieldsUI: Array<FormFieldUI>;
HiddenFieldsUI: Array<FormFieldUI>;
SectionFormGroup: FormGroup;
MainFormGroup: FormGroup;
ParentFormSchemaUI: FormSchemaUI;
static fromFormSectionData(formSection: FormSection): FormSectionUI {
let formSectionUI = new FormSectionUI(formSection);
formSectionUI.FieldsUI = new Array<FormFieldUI>();
formSectionUI.HiddenFieldsUI = new Array<FormFieldUI>();
formSectionUI.Fields.forEach(field => {
let fieldUI = FormFieldUI.fromFormFieldData(field);
if (fieldUI.ColumnType != 'HIDDEN')
formSectionUI.FieldsUI.push(fieldUI);
else formSectionUI.HiddenFieldsUI.push(fieldUI);
});
formSectionUI.SectionFormGroup = FormSectionUI.buildFormSectionFormGroup(formSectionUI);
return formSectionUI;
}
static buildFormSectionFormGroup(formSectionUI: FormSectionUI): FormGroup {
let obj = {};
formSectionUI.FieldsUI.forEach(fieldUI => {
obj[fieldUI.FieldDisplayId] = fieldUI.FieldFormControl;
});
let sectionFormGroup = new FormGroup(obj);
return sectionFormGroup;
}
}
=============================================
export class FormFieldUI extends FormField {
constructor(formField: FormField) {
super();
this.FieldDisplayId = 'field' + this.FieldId.toString();
this.ListItems = new Array<SelectListItem>();
}
public FieldDisplayId: string;
public FieldFormControl: FormControl;
public ParentSectionFormGroup: FormGroup;
public MainFormGroup: FormGroup;
public ParentFormSectionUI: FormSectionUI;
public ValueChange: EventEmitter<any> = new EventEmitter<any>();
static buildFormControl(formFieldUI:FormFieldUI): FormControl {
let nullValidator = Validators.nullValidator;
let fieldKey: string = formFieldUI.FieldDisplayId;
let fieldValue: any;
switch (formFieldUI.ColumnType) {
default:
fieldValue = formFieldUI.Value;
break;
}
let isDisabled = !formFieldUI.IsEnabled;
let validatorsArray: ValidatorFn[] = new Array<ValidatorFn>();
let asyncValidatorsArray: AsyncValidatorFn[] = new Array<AsyncValidatorFn>();
let formControl = new FormControl({ value: fieldValue, disabled: isDisabled }, validatorsArray, asyncValidatorsArray);
return formControl;
}
}
To get a reference to the parent form simply use this (maybe not available in Angular 2. I've tested it with Angular 6):
TS
import {
FormGroup,
ControlContainer,
FormGroupDirective,
} from "#angular/forms";
#Component({
selector: "app-leveltwo",
templateUrl: "./leveltwo.component.html",
styleUrls: ["./leveltwo.component.sass"],
viewProviders: [
{
provide: ControlContainer,
useExisting: FormGroupDirective
}
]
})
export class NestedLevelComponent implements OnInit {
//form: FormGroup;
constructor(private parent: FormGroupDirective) {
//this.form = form;
}
}
HTML
<input type="text" formControlName="test" />
import { Directive } from '#angular/core';
import { ControlContainer, NgForm } from '../../../node_modules/#angular/forms';
#Directive({
selector: '[ParentProvider]',
providers: [
{
provide: ControlContainer,
useFactory: function (form: NgForm) {
return form;
},
deps: [NgForm]
}`enter code here`
]
})
export class ParentProviderDirective {
constructor() { }
}
<div ParentProvider >
for child
</div>
An alternative to the FormGroupDirective (as described in #blacksheep's answer) is the use of ControlContainer like so:
import { FormGroup, ControlContainer } from "#angular/forms";
export class ChildComponent implements OnInit {
formGroup: FormGroup;
constructor(private controlContainer: ControlContainer) {}
ngOnInit() {
this.formGroup = <FormGroup>this.controlContainer.control;
}
The formGroup can be set in the direct parent or further up (parent's parent, for example). This makes it possible to pass a from group over various nested components, without the need for a chain of #Input()s to pass the formGroup along. In any parent set the formGroup to make it available via ControlContainer in the child:
<... [formGroup]="myFormGroup">

Angular2: issues converting a form's value to a Model with default fields

I am busy with a Stock take form that needs to be posted to a REST API. I have a stockTake model which has three fields that get initialized with default values of 0. the three fields "StockTakeID, IDKey and PackSize" are not part of the angular form where the data needs to be entered. My issue is submitting the model with those three fields with default values to my RestService. When I submit the stockTakeForm.value I get an error as those three fields are not part of the data that's being submitted... Any idea how I can go about getting this to work?
my stock-take.model.ts:
export class StockTakeModel {
constructor(
StockTakeID: number = 0,
IDKey: number = 0,
BarCode: number,
ProductCode: string,
SheetNo: string,
BinNo: string,
Quantity: number,
PackSize: number = 0) { }
}
my stock-take.component.ts:
import { Component, OnInit } from '#angular/core';
import { FormBuilder, FormGroup, Validators } from '#angular/forms';
import { RestService } from '../../services/rest.service';
import { StockTakeModel } from '../../models/stock-take.model';
#Component({
selector: 'stock-take',
templateUrl: './stock-take.component.html',
styleUrls: ['./stock-take.component.css']
})
export class StockTakeComponent implements OnInit {
stockTakeForm: FormGroup;
constructor(private fb: FormBuilder, private restService: RestService) {
this.stockTakeForm = fb.group({
'sheetNo':['', Validators.required],
'binNo':['', Validators.required],
'barcode':['', Validators.required],
'Qty':['', Validators.required]
});
}
submitStockTake(stockTakeModel: StockTakeModel) {
//console.log(stockTakeModel);
this.restService.postStockTake(stockTakeModel)
.subscribe(
(res) => {
console.log(res);
},
(res) => {
console.log("failure: " + res);
}
);
this.stockTakeForm.reset();
}
ngOnInit() {
}
}
my stock-take.component.html:
<div class="row">
<div class="col-md-6 col-md-push-3">
<h1>Stock Take</h1>
<br /><br />
<form [formGroup]="stockTakeForm" role="form" (ngSubmit)="submitStockTake(stockTakeForm.value)">
<input type="text" placeholder="Enter Sheet Number" class="form-control" id="sheetNo" [formControl]="stockTakeForm.controls['sheetNo']" name="sheetNo">
<input type="text" placeholder="Enter Bin Number" class="form-control" id="binNo" [formControl]="stockTakeForm.controls['binNo']" name="binNo">
<input type="number" placeholder="Enter Barcode" class="form-control" id="bCode" [formControl]="stockTakeForm.controls['barcode']" name="barcode">
<input type="number" placeholder="Enter Quantity" clas="form-control" id="Qty" [formControl]="stockTakeForm.controls['Qty']" name="quantity">
<button class="btn btn-block" [disabled]="!stockTakeForm.valid">Submit</button>
</form>
</div>
</div>
updated stock-take.component:
import { Component, OnInit } from '#angular/core';
import { FormBuilder, FormGroup, Validators } from '#angular/forms';
import { RestService } from '../../services/rest.service';
import { StockTakeModel } from '../../models/stock-take.model';
#Component({
selector: 'stock-take',
templateUrl: './stock-take.component.html',
styleUrls: ['./stock-take.component.css']
})
export class StockTakeComponent implements OnInit {
stockTakeForm: FormGroup;
stockTakeModel: StockTakeModel;
constructor(private fb: FormBuilder, private restService: RestService) {
this.stockTakeForm = fb.group({
'sheetNo':['', Validators.required],
'binNo':['', Validators.required],
'barcode':['', Validators.required],
'Qty':['', Validators.required]
});
}
doStockTake(val: any) {
//console.log("val:" + JSON.stringify(val));
this.stockTakeModel = new StockTakeModel(0, 0, val[Object.keys(val)[2]], '', val[Object.keys(val)[0]], val[Object.keys(val)[1]], val[Object.keys(val)[3]], 0);
// console.log(this.stockTakeModel);
console.log(JSON.stringify(this.stockTakeModel));
}
submitStockTake(stockTakeModel: StockTakeModel) {
//console.log(stockTakeModel);
this.restService.postStockTake(stockTakeModel)
.subscribe(
(res) => {
console.log(res);
},
(res) => {
console.log("failure: " + res);
}
);
this.stockTakeForm.reset();
}
ngOnInit() {
}
}
One workaround would to create a new variable, e.g:
stockTakeModel: StockTakeModel;
if that doesn't work (as you said it complained about undefined), you can also instantiate it as an empty Object, like so:
stockTakeModel = {};
EDIT, so you changed your code to something like this: I also added some console.logs to show where it is undefined, as there was a little question about that.
stockTakeModel: StockTakeModel; // undefined at this point (or empty if you have defined it as empty above)!
// use Object keys to access the keys in the val object, a bit messy, but will work!
submitStockTake(val: any) {
this.stockTakeModel =
new StockTakeModel(0, 0, val[Object.keys(val)[2]], '', val[Object.keys(val)[0]],
val[Object.keys(val)[1]], val[Object.keys(val)[3]]);
// console.log(this.StockTakeModel); // now it should have all values!
this.restService.postStockTake(this.stockTakeModel)
.subscribe(d => { console.log(d)} ) // just shortened your code a bit
this.stockTakeForm.reset();
}
EDIT: So finally the last issue is solved, besides the solution above. It's because your StockTakeModel-class wasn't properly declared the value was undefined, or actually empty. So fix your class to:
export class StockTakeModel {
constructor(
public StockTakeID: number = 0, // you were missin 'public' on everyone
public IDKey: number = 0,
public BarCode: number,
public ProductCode: string,
public SheetNo: string,
public BinNo: string,
public Quantity: number,
public PackSize: number = 0) { }
}
Note also that in your 'updated' code you still have (ngSubmit)="submitStockTake(stockTakeForm.value) in your form, so your created doStockTake-method is not called.
I don't understand the construction [formControl] in your template.
There should be at least [ngModel].
Usualy I use, for two-way communication, the [(ngModel)]. Not the [formControl].
So your template code should look like this:
<div class="row">
<div class="col-md-6 col-md-push-3">
<h1>Stock Take</h1>
<br /><br />
<form [formGroup]="stockTakeForm" role="form" (ngSubmit)="submitStockTake(stockTakeForm.value)">
<input type="text" placeholder="Enter Sheet Number" class="form-control" id="sheetNo" [(ngModel)]="stockTakeForm.controls['sheetNo']" name="sheetNo">
<input type="text" placeholder="Enter Bin Number" class="form-control" id="binNo" [(ngModel)]="stockTakeForm.controls['binNo']" name="binNo">
<input type="number" placeholder="Enter Barcode" class="form-control" id="bCode" [(ngModel)]="stockTakeForm.controls['barcode']" name="barcode">
<input type="number" placeholder="Enter Quantity" clas="form-control" id="Qty" [(ngModel)]="stockTakeForm.controls['Qty']" name="quantity">
<button class="btn btn-block" [disabled]="!stockTakeForm.valid">Submit</button>
</form>
</div>
</div>

Angular 2 - Posting / Putting multiple forms at once

I have a repeating Form for every CustomerProject. The repeating form shows all the Projects for a Customer. If i have for example 5 project forms, i want to edit 3 and add 2 and hit submit, posting / putting everything at once.
The question is how do i accomplish this?
Form HTML:
<form [formGroup]="myForm" novalidate (ngSubmit)="save(myForm)">
<!--projects-->
<div formArrayName="projects">
<div *ngFor="let project of myForm.controls.projects.controls; let i=index" class="panel panel-default">
<div class="panel-heading">
<span>Project {{i + 1}}</span>
<span class="glyphicon glyphicon-remove pull-right" *ngIf="myForm.controls.projects.controls.length > 1" (click)="removeProject(i)"></span>
</div>
<div class="panel-body" [formGroupName]="i">
<div class="form-group col-xs-6">
<label>Customer</label>
<input type="text" class="form-control" formControlName="customer_id">
<small [hidden]="myForm.controls.projects.controls[i].controls.customer_id.valid" class="text-danger">
Street is required
</small>
</div>
<div class="form-group col-xs-6">
<label>Project</label>
<input type="text" class="form-control" formControlName="project">
</div>
</div>
</div>
</div>
<div class="margin-20">
<a (click)="addProject()" style="cursor: default">
Add another project +
</a>
</div>
<div class="margin-20">
<button type="submit" class="btn btn-inverse pull-right" [disabled]="!myForm.valid">Submit</button>
</div>
Typescript
public myForm: FormGroup;
private projects: CustomerProject[];
private project: CustomerProject;
private showForm: boolean = false;
#Input() customer: Customer = new Customer(1, '', '', '', '', '', '', '');
#Input() listId: string;
#Input() editId: string;
constructor(
private _fb: FormBuilder,
private authService: AuthService,
private projectService: CustomerProjectService
) { }
loadProjects() {
this.projectService.getCustomerProjects(this.customer.id)
.subscribe(projects => {
this.projects = projects;
console.log(this.project);
this.initForm();
},
err => {
console.error(err);
this.authService.tokenValid();
})
}
initForm() {
this.myForm = this._fb.group({
projects: this._fb.array([])
});
// add existing customer projects
this.addProjects();
//this.addProject('', '');
this.showForm = true;
}
ngOnInit() {
this.loadProjects();
}
ngOnChanges(changes) {
EmitterService.get(this.editId).subscribe((customer: Customer) => {
this.customer = customer;
this.loadProjects();
});
}
initProject(customer_id: string, project: string) {
return this._fb.group({
customer_id: [customer_id],
project: [project]
})
}
addProject(customer_id: string, project: string) {
const control = <FormArray>this.myForm.controls['projects'];
control.push(this.initProject(customer_id, project));
}
addProjects() {
for (var i = 0; i < this.projects.length; i++){
this.project = this.projects[i];
var customer_id = this.project.customer_id;
var project = this.project.project;
this.addProject(customer_id, project);
}
}
removeProject(i: number) {
const control = <FormArray>this.myForm.controls['projects'];
control.removeAt(i);
}
save(model: any) {
}
Its not recommended to posting forms at once.
UI: it gets too way complex. showing multiple forms makes users baffled.
UX: users expect add, edit or remove does CRUD at the moment and would not wait to press submit button.
Software Architecture: it violates separation of concern principle.

How to iterate formgroup with array in Angular2

I'm working on a model driven form and I can't get it to add items to a list being displayed with ngFor. I'm currently getting an error when trying to iterate my list.
Error:
Error: Cannot find control with path: 'locations -> i'
at new BaseException (exceptions.ts:21)
at _throwError (shared.ts:80)
at Object.setUpFormContainer (shared.ts:66)
at FormGroupDirective.addFormGroup (form_group_directive.ts:74)
at FormGroupName.AbstractFormGroupDirective.ngOnInit (abstract_form_group_directive.ts:37)
at DebugAppView._View_PersonFormComponent4.detectChangesInternal (PersonFormComponent.ngfactory.js:3197)
at DebugAppView.AppView.detectChanges (view.ts:260)
at DebugAppView.detectChanges (view.ts:378)
at DebugAppView.AppView.detectContentChildrenChanges (view.ts:279)
at DebugAppView._View_PersonFormComponent2.detectChangesInternal (PersonFormComponent.ngfactory.js:1995)
Raw person-form-builder.service.ts
formBuilderService:
import {Injectable} from "#angular/core";
import {Validators, FormBuilder} from '#angular/forms';
import {Person} from './../components/person/person';
#Injectable()
export class PersonFormBuilderService {
constructor(private fb: FormBuilder) {
}
getForm(person: Person) {
return this.fb.group({
_id: [person._id],
name: this.fb.group({
first: [person.name.first],
middle: [person.name.middle],
last: [person.name.last],
full: [person.name.full],
}),
locations: this.fb.array([
this.initLocation(),
]),
files: this.fb.array([
this.initFiles(),
]),
skills: this.fb.array([
this.initSkills(),
]),
});
}
initLocation() {
return this.fb.group({
isDefault: [false, Validators.required],
location: ['', Validators.required],
roles: ['', Validators.required],
isContact: [false, Validators.required],
contactPhone: ['', Validators.required],
contactPhoneExt: ['', Validators.required],
});
}
initFiles() {
return this.fb.group({
originalName: ['', Validators.required],
});
}
initSkills() {
return this.fb.group({
name: ['', Validators.required],
isrequired: [false, Validators.required],
expireDate: ['', Validators.required],
canOverride: [false, Validators.required],
});
}
addLocation(control) {
control.push(this.initLocation());
}
removeLocation(i: number, control) {
control.removeAt(i);
}
}
form:
<div formGroup="form">
<div formArrayName="locations">
<div *ngFor="let location of form.controls.locations.controls; let i=index">
<span>Location {{i + 1}}</span>
<div formGroupName="i">
<label class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect">
<input type="checkbox" class="mdl-checkbox__input"
formControlName="isDefault" [checked]="">
</label>
</div>
</div>
</div>
</div>
form-component.ts:
import {Component} from '#angular/core';
import {FormGroup, FormArray} from '#angular/forms';
#Component({
moduleId: module.id,
selector: 'person-form-component',
templateUrl: 'person-form.component.html',
providers: [PersonService, PersonFormBuilderService]
})
export class PersonFormComponent {
getPerson(personId) {
this.personService.getPerson(this.personId).subscribe(res => {
this.person = res.data;
this.form = this.personFormBuilderService.getForm(this.person);
});
}
addLocation() {
let control = <FormArray> this.form.controls['locations'];
this.personFormBuilderService.addLocation(control);
}
}
https://gist.github.com/jpstokes/11551ff5d8c76514005c6c9fd8a554dd
Here is the solution that worked for me.
https://plnkr.co/edit/cs244r
HTML Template:
<div>
<h2>RegionId: {{regionId}}</h2>
<h2>Region: {{region.name}}</h2>
</div>
<form [formGroup]="regionFormGroup">
Region Name: <input type="text" formControlName="regionName" [(ngModel)]="region.name" />
<div formArrayName="customersArray">
<table class="simple-table">
<tr>
<th>Customer</th>
<th>Current Period</th>
<th>Previous Period</th>
</tr>
<tbody>
<tr [formGroupName]="i" *ngFor="let customerGroup of regionFormGroup.controls.customersArray.controls; let i = index">
<td>{{region.customers[i].name}} - index {{i}}</td>
<td><input type="text" formControlName="currentPeriod" [(ngModel)]="region.customers[i].currentPeriod"/></td>
<td><input type="text" formControlName="previousPeriod" [(ngModel)]="region.customers[i].previousPeriod"></td>
</tr>
</tbody>
</table>
</div> <!-- end: div FormArrayName -->
</form>
Component.ts
export class AppComponent implements OnInit {
regionId: number = 5;
region: RegionModel;
regionFormGroup: FormGroup;
constructor( private customerService: CustomerService,private fb: FormBuilder) { }
ngOnInit(): void {
// initialize form
this.regionFormGroup = new FormGroup({
regionName: new FormControl(''),
customersArray: new FormArray([])
});
// Retrieve data from datasource
this.customerService.getCustomerByRegion(5)
.subscribe( (reg: RegionModel ) => {
this.region = reg;
this.buildForm();
});
}
buildForm = () : void => {
const customersControls = <FormArray>this.regionFormGroup.controls['customersArray'];
this.region.customers.forEach( (cust : CustomerModel) => {
customersControls.push(this.createCustomerFormGroup(cust));
console.log(customersControls);
});
}
createCustomerFormGroup(cust: CustomerModel) {
return this.fb.group({
currentPeriod: [''],
previousPeriod: ['']
});
}
}
Fixed it!!!
So apparently I need to read up on Angular2 Fundamentals because I thought my mistake was legal. Basically, I ignored the square brackets around formGroupName in the tutorial because I've done it numerous times before without issue, but not this time. So to fix I simply added the brackets:
<div formGroupName="i"> => <div [formGroupName]="i">

AmpersandJs: Render sub-view

getting into AmpersandJs, and having a big showstopper by not figuring out why my sub-view-form isn't rendering it's markup.
My MainView.render, works as should:
render: function() {
BaseView.prototype.render.apply(this, arguments);
this.collectionView = this.renderCollection(this.collection, HSEventView, '.item-container');
this.renderSubview(new HSEventEditView({
action: 'create'
}), '.create-event');
return this;
},
Next, my SubView.render (HSEventEditView):
render: function () {
this.renderWithTemplate();
this.form = new EditForm({
el: this.query('.edit-form'),
submitCallback: function (data) {
debug('submit', data);
}
});
this.registerSubview(this.form);
debug('render.form.el', this.form.el)
}
And finally my FormView:
module.exports = FormView.extend({
initialize: function() {
debug('initialize', this.el)
},
autoRender: true,
fields: function () {
debug('fields!')
var fields = [
new InputView({
label: 'Name',
name: 'name',
value: utils.getDotted(this, 'model.name'),
placeholder: 'Name',
parent: this
})
];
debug('fields', fields)
}
});
The MainView and SubView renders fine, but the 'div.edit-form' DOM-node, where the form markup should be, is empty.
I have tried all variations of including a subview I could dig up, but obviously I am blind to something.
Thanks!
PS! Here is the rendered markup:
<section class="page">
<h2>Events collection</h2>
<hr>
<div class="tools">(Tools comming...)</div>
<hr>
<div class="item-container events">
<div class="item event">
<h3>Event: <span data-hook="name">Event 1</span></h3>
</div>
</div>
<hr>
<div class="create-event">
<div class="item event">
<h3>Create event: <span data-hook="name"></span></h3>
<div class="edit-form"></div>
</div>
</div>
</section>
Since fields is a function, you need to return the fields. In your Formview:
fields: function () {
debug('fields!')
var fields = [
new InputView({
label: 'Name',
name: 'name',
value: this.model.name,
placeholder: 'Name',
parent: this
})
];
debug('fields', fields)
// need to return the fields you've declared
return fields;
}