How to access the properties of a formArray in HTML? - javascript

I'm trying to implement a reactive Angular form, but, I can't access the properties of the array on HTML, I never worked with reactive form, if anyone could guide me I would be grateful! I'm using Angular 10 and I have the following code:
TS
operationModel: IScriptOperationsModel;
formOperation: FormGroup;
constructor(
private fb: FormBuilder,
...
) {}
ngOnInit() {
this.operationModel = new IScriptOperationsModel();
this.operationModel.scriptOperationOrders = [];
this.buildForm(new IScriptOperationsModel());
this.scriptOperationsService.findScriptOperation(this.operationId).subscribe((operation) => {
this.operationModel = operation.data as IScriptOperationsModel; // api return
this.buildForm(this.operationModel); // I pass the return of the api to the form
});
}
buildForm(operation: IScriptOperationsModel) {
this.formOperation = this.fb.group({
descriptionCode: [operation.descriptionCode],
description: [operation.description],
workStations: this.fb.array([])
});
this.formOperation.setControl('workStations', this.fb.array(this.operationModel.scriptOperationOrders));
}
get workStations(): FormArray {
return this.formOperation.get('workStations') as FormArray;
}
HTML
<div
class="card"
[ngClass]="{'bg-principal': idx === 0, 'bg-alternative': idx !== 0}"
formArrayName="workStations"
*ngFor="let workstation of workStations.controls; index as idx"
>
<div class="card-body" [formGroupName]="idx">
<div class="form-row">
<div class="form-group col-md-1">
<label>Id Oper.</label>
<input
type="text"
name="idOperation"
class="form-control"
disabled
formControlName="rank" <!-- whatever with or without binding gives error -->
/>
</div>
<div class="form-group col-md-2">
<label>Time</label>
<input
type="time" class="form-control" name="defaultTime"
[formControlName]="defaultTime" <!-- whatever with or without binding gives error -->
/>
</div>
</div>
</div>
</div>
Models
export class IScriptOperationsModel extends Serializable {
public description: string;
public descriptionCode: string;
public scriptOperationOrders: IScriptOperationOrdersModel[]; // array which I need
}
export class IScriptOperationOrdersModel extends Serializable {
public rank: number;
public defaultTime: string;
public asset?: IAssetModel;
public provider?: IProviderModel;
}
error-handler.service.ts:87 Error: Cannot find control with path: 'workStations -> 0 -> rank' # undefined:undefined
NOTE: I already looked at some answers here on the site such as this, this and this , but none of them solved this problem!

your problem is here :
this.formOperation.setControl(
'workStations',
this.fb.array(this.operationModel.scriptOperationOrders) <== here the problem
);
you are passing an array of IScriptOperationOrdersModel instead of array of form group.
To make your code working, you have to loop on every element of this.operationModel.scriptOperationOrders array , and instanciate a new FormControl object then push it in the workStations form array.
To access its elements, you can use controls[indexOfGroup].rate
You can take a look at this simple example you will understand everything.

Related

How to bind to an Angular form from users selected option

OK it's a bit more complicated than the headline..
This form I am working on is a form group. It has a few fields ( supplement name, description and tags) the supplement name one is what I need help with as I have not worked on a complicated form like this and want to get it right and not just offer a messy patch job.
Here is the expected logical order of what happens
user adds a new supplement by clicking on the field and begins typing "creatine" for example
there is a query sent out that fetches products based on the entry into the input and
returns a JSON that are offered as suggestions
user clicks the suggestion "creatine"
field is populated and binds
we add another entry through the "add option" and repeat for X amount of products we want to
add.
What actually happens
user adds new supplement by clicking the field and types "creatine" suggestion request is
sent off and populates the dropdown
user clicks on the suggestion "creatine" the field takes that value
value is actually blank
user adds another supplement but the previous selection is in the field
user clears it and types again
value is blank
What needs to happen is the user can add X amount of supplements and able to grab whatever option from the dropdown recommendation and it is added to the form group array and does not interfere with the other form group array values.
I know this is not the right way to bind the form and I don't think it's right the way i'm binding the mat input field to trigger the query and this is the reason why I'm asking the question again, to not offer a patch job.
Component code
import { Subscription } from 'rxjs/Subscription';
import { FormArray, FormBuilder, FormControl, FormGroup, Validators } from '#angular/forms';
import { UtilitiesService } from '../../utilities/utilities.service';
import { GetSupplementsService } from './get-supplements.service';
#Component({
selector: 'app-supplements',
templateUrl: './supplements.component.html',
styleUrls: ['./supplements.component.css'],
providers: [GetSupplementsService],
})
export class SupplementsComponent implements OnInit {
supplementForm: FormGroup;
queryField: FormControl = new FormControl();
private supplementInventoryResults: Array<ISupplementInventoryResponse>;
private eventForm: FormGroup;
private searchResults: any;
private searchSubscription: Subscription;
private addSupplementSubscription: Subscription;
subcription: Subscription;
constructor (
private bottomSheet: MatBottomSheet,
private _fb: FormBuilder,
private ref: ChangeDetectorRef,
private _utils: UtilitiesService,
private getSupplements: GetSupplementsService,
private router: Router
) { }
public ngOnInit(): void {
this.browsingStackHistory = false;
this.loading = true;
this.supplementForm = this._fb.group({ // the form in question
entryArray: this._fb.array([
this.getUnit()
])
});
this.searchSubscription =
this.queryField.valueChanges
.debounceTime(600)
.distinctUntilChanged()
.switchMap((query) => this.getSupplements.search_supplement_by_category(query))
.subscribe((result) => {
this.searchResults = result;
});
}
public ngOnDestroy(): void {
this.subcription.unsubscribe();
}
private getUnit(): FormGroup {
return this._fb.group({
supplementName: [''],
description: [''],
tags: ['']
});
}
private addUnit(): void {
const control = <FormArray>this.supplementForm.controls['entryArray'];
control.push(this.getUnit());
}
private removeUnit(i: number): void {
const control = <FormArray>this.supplementForm.controls['entryArray'];
control.removeAt(i);
}
private addSupplement(): void { // this will do the post to the service
const supplementObject = {
start: this._utils.get_upload_time(),
data: this.supplementForm.value,
rating: 0
};
}
}
Template
[![<mat-tab label="Add Stack (Test)">
<div style="padding:8px;">
<div fxLayout="row wrap">
<div fxFlex.gt-sm="50%" fxFlex="100">
<h1>Add New Supplements Stack</h1>
<form \[formGroup\]="supplementForm" class="log-workout">
<!-- Start form units array with first row must and dynamically add more -->
<div fxLayout="column" fxLayoutAlign="center center" class="row-height">
<div formArrayName="entryArray">
<mat-divider></mat-divider>
<!-- loop throught units -->
<div *ngFor="let reps of supplementForm.controls.entryArray.controls; let i=index">
<!-- row divider show for every nex row exclude if first row -->
<mat-divider *ngIf="supplementForm.controls.entryArray.controls.length > 1 && i > 0"></mat-divider>
<br>
<!-- group name in this case row index -->
<div \[formGroupName\]="i">
<!-- unit name input field -->
<div class="row">
<mat-form-field class="example-form">
<input matInput placeholder="Supplement Name" \[formControl\]="addSupplementField"
formControlName="supplementName" \[matAutocomplete\]="auto">
<mat-autocomplete #auto="matAutocomplete">
<mat-option *ngFor="let product of supplementResults" \[value\]="product?.product_name">
<img class="example-option-img" aria-hidden \[src\]="product?.product_image" height="25">
{{product?.product_name}}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<mat-form-field class="example-form">
<input matInput placeholder="Description" formControlName="description" required>
</mat-form-field>
<mat-form-field class="example-form">
<input matInput placeholder="Tags" formControlName="tags" required>
</mat-form-field>
</div>
<!-- row delete button, hidden if there is just one row -->
<button mat-mini-fab color="warn" *ngIf="supplementForm.controls.entryArray.controls.length > 1"
(click)="removeUnit(i)">
<mat-icon>delete forever</mat-icon>
</button>
</div>
</div>
<!-- New unit button -->
<mat-divider></mat-divider>
<mat-card-actions>
<button mat-raised-button (click)="addUnit()">
<mat-icon>add box</mat-icon>
Add Other Product
</button>
</mat-card-actions>
<button mat-raised-button (click)="addSupplement()">
<mat-icon>add box</mat-icon>
Add Supplement
</button>
</div>
</div>
<!-- End form units array -->
</form>
</div>
</div>
</div>][1]][1]
Having the below when the getUnit() function is called apparently binds it in the sense it will operate independently and without conflicts.
private getUnit(): FormGroup {
const formGroup = this._fb.group({
supplementName: [''],
review: [''],
rating: [''],
notes: [''],
tags: ['']
});
formGroup.get('supplementName').valueChanges
.debounceTime(300)
.distinctUntilChanged()
.switchMap((search) => this.getSupplements.search_supplement_by_category(search))
.subscribe((products) => {
this.supplementResults = products;
});
return formGroup;
}

Traversing a nested FormArray in Angular 2

I am trying to traverse through a nested FormArray to keep track of a set of questions, each given their own 'FormControl'. I have created a FormArray called 'groups' as such. The hierarchy goes Groups[Questions[Answers]]
constructor(private surveyService: SurveyService, #Inject(FormBuilder) private fb: FormBuilder) {
this.survey = <Survey>SURVEYS[0];
this.reactiveForm = fb.group({
name: ['', Validators.required],
groups: fb.array(this.getGroups())
});
}
getGroups() : AbstractControl[] {
return Array.apply(null,
Array(this.survey.groups.length)).map((x, index) => new FormArray(this.getQuestions(index)))
}
getQuestions(index) : AbstractControl[] {
return Array.apply(null,
Array(this.survey.groups[index].questions.length)).map(x => new FormControl(x, Validators.required));
}
Here is my HTML code:
<form [formGroup]="reactiveForm">
<div formArrayName="groups">
<div *ngFor="let group of survey.groups; index as groupIndex" [formArrayName]=groupIndex>
<h1>{{group.groupName}}</h1>
<div *ngFor="let question of group.questions; index as questionIndex" [formArrayName]=questionIndex>
<h2>{{question.question}}</h2>
<label *ngFor="let answer of question.responses ; index as answerIndex">
<input type="radio" [value]=answerIndex [formControlName]=questionIndex>{{answer}}
</label>
</div>
</div>
</div>
</form>
I figure it to work as such: the first ngFor pulls the array of Questions, the second ngFor pulls the array of answers, and then the final ngFor uses [formControlName] in order to bind each question to the same Control, giving it a unique answer. However, the error I get is this:
Error: Cannot find control with path: 'groups -> 0 -> 0 -> 0'
If Groups[0] is an array and Groups[0][0] is also an array, why does the [0] of that suddenly not exist anymore? How should one go about traversing a FormArray like this?
Here is what you have created:
Now look at your template:
<form [formGroup]="reactiveForm">
<div formArrayName="groups">
<div ... [formArrayName]=groupIndex>
...
<div ... [formArrayName]=questionIndex>
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Do you see such FormArray in your FormGroup? I don't
<...
<label *ngFor="let answer of question.responses ; index as answerIndex">
<input ...[formControlName]=questionIndex>...
</label>
</div>
</div>
</div>
</form>
As you can see you Angular can't find FormControl by path groups -> 0 -> 0 -> 0 because it should be groups -> 0 -> 0
So if you will remove redundant [formArrayName]=questionIndex directive then it should work.
Ng-run Example
Tip: use <pre>{{reactiveForm.value | json}}</pre> to test FormGroup structure
For demo I used simple structure:
export class Group {
title: string;
questions: Question[] = [];
}
export class Question {
title: string;
answers: Answer[] = [];
}
export class Answer {
id: number;
title: string;
}
StackBlitz Demo

MEAN app with angular 2 error: Cannot read property _id of undefined

I am building an application for the first time to store, update, view and delete Client profiles. I followed the Angular tour of heroes to build the basic app and then pieced together the mongodb and express portions from around the net.
I am getting this error in my browser console when i attempt to delete a client profile -
ERROR TypeError: Cannot read property '_id' of undefined
at ClientProfileComp.webpackJsonp.../../../../../src/app/components/clientProfile.component.ts.ClientProfileComp.delete
(clientProfile.component.ts:53)... (etc).
I have confirmed using postman that my express routing is working as intended. I am able to get all/create clients at /api/clients, as well as get, put and delete from /api/clients/:_id (where _id is the autogenerated id for each entry).
I believe the problem is in one of my component files, as the error only occurs when I attempt to delete or view specific client detail, which causes another type of error entirely (CastError). The problem likely began when I attempted to remove all mentions of clientProfile: ClientProfile[]; (or Hero in the case of the tutorial) as I am no longer importing the details from client.ts (hero.ts) since I am using a mongoose schema instead, and I do not believe I should be importing that schema into my front-end angular.
here is the delete section of clientProfile.service.ts:
delete(_id: number): Promise<void> {
const url = `${this.clientProfilesUrl}/${_id}`;
return this.http.delete(url, {headers: this.headers}).toPromise()
.then(() => null).catch(this.handleError);
}
and here is clientProfile.component.ts as requested (the most likely source of my problem being that i replaced all instances of clientProfile: ClientProfile; with clientProfile: any; without knowing what I was doing)
note the commented out import statement.
import { Component, OnInit } from '#angular/core';
import { Router } from '#angular/router';
//import { ClientProfile } from '../old/clientProfile';
import { ClientProfileService } from '../services/clientProfile.service';
#Component({
selector: 'app-clientprofile',
templateUrl: '../views/clientProfile.component.html',
styleUrls: [ '../styles/clientprofile.component.css' ]
})
export class ClientProfileComp implements OnInit {
selectedClientProfile: any;
clientProfiles: any = [];
clientProfile: any;
constructor(
private clientProfileService: ClientProfileService,
private router: Router
) { }
gotoDetail(): void {
this.router.navigate(['/detail', this.selectedClientProfile._id]);
}
getClientProfiles(): void {
this.clientProfileService.getClientProfiles().then(clientProfiles => {
this.clientProfiles = clientProfiles;
});
}
ngOnInit(): void {
this.getClientProfiles();
}
onSelect(clientProfile: any): void {
this.selectedClientProfile = clientProfile;
}
add(name: string, address: string): void {
name = name.trim();
address = address.trim();
if (!name) { return; }
this.clientProfileService.create(name, address).then(clientProfile => {
this.clientProfiles.push(clientProfile);
this.selectedClientProfile = null;
this.getClientProfiles();
});
}
delete(clientProfile: any): void {
this.clientProfileService.delete(clientProfile._id).then(() => {
this.clientProfiles = this.clientProfiles.filter(h => h !==
clientProfile);
if (this.selectedClientProfile === clientProfile) { this.selectedClientProfile = null; }
});
}
}
I have been poring over this all day, and have read a lot of similar posts here too - but most of the solutions don't seem to apply to this case. If anyone could point me in the right direction, i'd be really grateful. if any more code is needed to explain what i'm trying to do i will gladly post it.
Based on the error message it seems that the error is here:
this.router.navigate(['/detail', this.selectedClientProfile._id])
It appears that you only set it in onSelect and there are several places in your code that you are setting this.selectedClientProfile to null. That would be the best place to look.
If you'd like to create a plunker that demonstrates your issue, we could look at it further.
As a side note, you are using promises instead of the now more common Observables. If you want to change over to using Observables, I have a complete example of CRUD (create, read, update, and delete) operations here: https://github.com/DeborahK/Angular2-ReactiveForms in the APM folder.
Found out one problem - my delete button in the html file was in a div that only appeared when a client was selected, instead of next to each client. this was a result of a half-finished measure i took to ensure users don't just click delete willy-nilly on each client.
Code before:
<h2>Client Profiles</h2>
<div class="add-client">
<label>Add new client</label>
<input placeholder="Client Name (required)" #clientProfileName />
<input placeholder="Client Address" #clientProfileAddress />
<button (click)="add(clientProfileName.value, clientProfileAddress.value);
clientProfileName.value=''; clientProfileAddress.value=''">
Add</button>
</div>
<ul class="clientProfiles">
<li *ngFor="let clientProfile of clientProfiles"
[class.selected]="clientProfile === selectedClientProfile"
(click)="onSelect(clientProfile)">
<span class="badge">{{clientProfile.idnumber}}</span>
<span>{{clientProfile.name}}</span>
</li>
</ul>
<div *ngIf="selectedClientProfile">
<h2>
{{selectedClientProfile.name | uppercase}} selected
</h2>
<button (click)="gotoDetail()">View Details</button>
<button class="delete"
(click)="delete(clientProfile);
$event.stopPropagation()">Delete</button>
//delete button will only appear when a client is selected
</div>
Code now:
<h2>Client Profiles</h2>
<div class="add-client">
<label>Add new client</label>
<input placeholder="Client Name (required)" #clientProfileName />
<input placeholder="Client Address" #clientProfileAddress />
<button (click)="add(clientProfileName.value, clientProfileAddress.value);
clientProfileName.value=''; clientProfileAddress.value=''">
Add</button>
</div>
<ul class="clientProfiles">
<li *ngFor="let clientProfile of clientProfiles"
[class.selected]="clientProfile === selectedClientProfile"
(click)="onSelect(clientProfile)">
<span class="badge">{{clientProfile.idnumber}}</span>
<span>{{clientProfile.name}}</span>
<button class="delete"
(click)="delete(clientProfile);
$event.stopPropagation()">Delete</button>
//delete button appears next to each client
</li>
</ul>
<div *ngIf="selectedClientProfile">
<h2>
{{selectedClientProfile.name | uppercase}} selected
</h2>
<button (click)="gotoDetail()">View Details</button>
</div>

Angular 2 Interface

I'm trying to work with interfaces in Angular 2.
I've created a interface and a component.
Interface:
export interface Items {
id: number;
title: string;
message: string;
done: boolean;
}
Component:
export class AddComponent implements OnInit {
currentItem: string;
todos: any;
constructor(private router: Router) {
this.currentItem = (localStorage.getItem('currentItem')!==null) ? JSON.parse(localStorage.getItem('currentItem')) : [ ];
this.todos = this.currentItem;
}
addTodo(item: Items) {
this.todos.push({
id: item.id,
title: item.title,
message: item.message,
done: false
});
item.title = '';
item.message = '';
localStorage.setItem('currentItem', JSON.stringify(this.todos));
console.log('retorno: ' + this.todos.title + ' titulo: ' + item.title);
this.router.navigate(['./list']);
}
ngOnInit() {}
}
HTML:
<div class="container">
<form (submit)="addTodo()">
<div class="form-group">
<label>Id:</label>
<input [(ngModel)]="id" class="textfield form-control" name="id">
</div>
<div class="form-group">
<label>Titulo:</label>
<input [(ngModel)]="title" class="textfield form-control" name="title">
</div>
<div class="form-group">
<label>Mensagem:</label>
<input [(ngModel)]="message" class="textfield form-control" name="message">
</div>
<button type="submit" class="btn btn-success">Salvar</button>
</form>
</div>
I have an error: EXCEPTION: Cannot read property 'id' of undefined
Does anyone know how to solve it?
There may be a few things going wrong here.
The error is probably being caused by the fact that you're calling the addTodo function without an argument or an argument that is undefined. That's what causes the error you're describing.
Another possible problem may be that you have no class implementing the interface. Although this is not strictly necessary, it can help you make your code leverage the type safety of TypeScript better thereby helping you prevent errors.
Update: In your code update, you indeed call addTodo without a parameter, which causes it to be undefined in your function.
There are a few ways you can solve this, but I'll show you one. In your component, you can add the properties id, title, message (note that it might be better to place them in an object or rename them to keep things clear; this is just a minimal example). You can then use these properties to add your todo. So, instead of using item.id, item.title, and item.message you would use this.id, this.title, and this.message. These match the fields that you are referring to with your ngModel binding in the HTML template you provided.

Angular 2 Dynamic Form - Implement Clear Functionality

I'm trying to implement Clear functionality for Angualr 2 dynamic forms, which should clear all the values to null. But i'm getting an error when i try to use this.questions[i].value="null"; or this.questions[i].value="undefinded"; or this.questions[i].value=''; to replace each value in the form.
Working Code: http://plnkr.co/edit/SL949g1hQQrnRUr1XXqt?p=preview
Typescript class code:
import { Component, Input, OnInit } from '#angular/core';
import { FormGroup, REACTIVE_FORM_DIRECTIVES } from '#angular/forms';
import { QuestionBase } from './question-base';
import { QuestionControlService } from './question-control.service';
#Component({
selector: 'dynamic-form',
templateUrl: 'app/dynamic-form.component.html',
directives: [REACTIVE_FORM_DIRECTIVES],
providers: [QuestionControlService]
})
export class DynamicFormComponent implements OnInit {
#Input() questions: QuestionBase<any>[] = [];
form: FormGroup;
payLoad:object;
questiont: QuestionBase<any>;
constructor(private qcs: QuestionControlService) { }
ngOnInit() {
this.form = this.qcs.toFormGroup(this.questions);
console.log("Form Init",this.questions);
this.questiont = JSON.parse(JSON.stringify(this.questions));
}
onSubmit() {
this.payLoad = JSON.stringify(this.form.value);
this.payLoad2=this.payLoad;
this.questiont = JSON.parse(JSON.stringify(this.questions));
}
cancel(){
console.log("Canceled");
this.questions = JSON.parse(JSON.stringify(this.questiont));
}
clear(){
for(var i=0;i<this.questions.length;i++){
this.questions[i].value='';
}
console.log("Cleared");
}
}
HTML code:
<div>
<form [formGroup]="form">
<div *ngFor="let question of questions" class="form-row">
<label [attr.for]="question.key">{{question.label}}</label>
<div [ngSwitch]="question.controlType">
<input *ngSwitchCase="'textbox'" [formControlName]="question.key"
[id]="question.key" [type]="question.type" [(ngModel)]="question.value">
<select [id]="question.key" [(ngModel)]="question.value" *ngSwitchCase="'dropdown'" [formControlName]="question.key" >
<option *ngFor="let opt of question.options" [ngValue]="opt.key" >{{opt.value}}</option>
</select>
</div>
<div class="errorMessage" *ngIf="!form.controls[question.key].valid">{{question.label}} is required</div>
</div>
<div class="form-row">
<button type="submit" [disabled]="!form.valid" (click)="onSubmit()">Save</button>
<button type="button" class="btn btn-default" (click)="cancel()">Cancel</button>
<button type="button" class="btn btn-default" (click)="clear()">Clear</button>
</div>
</form>
<div *ngIf="payLoad" class="form-row">
<strong>Saved the following values</strong><br>{{payLoad}}
</div>
</div>
Is there any way that can be done without getting error.
The problem is with your logic to check and display a message if a field is required.
Your plunker code was throwing the exception Expression has changed after it was checked. Previous value: 'false'. Current value: 'true'
The problem is that it runs over your template code with values for questions[i].value but then it changes once you call clear. Angular compares questions[i].value and notices it's different then when it started rendering and thinks it made a mistake. It didn't pick up that you changed the value yet. You have to let it know the value was changed.
If you were to run the code with prod enabled, it wouldn't run this safety check and you'd get no error, but don't enable prod mode just to fix this.
Solution
The fix is import ChangeDetectorRef from #angular/core to dynamic.form.component.ts
Add private cdr: ChangeDetectorRef to the constructor
and add this.cdr.detectChanges(); to the end of your clear() method.
This will let know angular pick up that changes have happened and it won't think it made a mistake
P.S. the lines that are culprits are
<div class="errorMessage" *ngIf=" form && !form.controls[question.key].valid">{{question.label}} is required</div>
and
group[question.key] = question.required ? new FormControl(question.value || '', Validators.required) : new FormControl(question.value || ''); });

Categories

Resources