Can not read value of undefined, when components share data? - javascript

I am not able to populate my form with data that I have received from a method called getbyId() from a service, in my console I see that errors: cannot read truckId of undefined, Every solution i have found is saying my form is rendered faster than the object that i want to get with a getById() method and the solution should be *ngIf ="truck" which should ,make the form wait , but it doesn't solve it just refuses to display the form thats all. IN stackblitz below
https://stackblitz.com/edit/stackoverflow-49847806-kdjhdc?file=app%2Fservices%2Ftruck.service.ts
But In console i can see the URL and when I open it I see that it actually got the details from my back-end
service
getTruckById(id: number): Observable<Truck> {
const url = `${this.baseUrl}/${id}`;
return this.http.get(url, {headers: this.headers})
.pipe(map(this.extractData),
tap(data => console.log(JSON.stringify(data))),
catchError(this.handleError));
}
This is the edit component completely
export class EditTruckComponent implements OnInit {
five : number = 5;
seven: number = 7;
#Input() truck: Truck;
truckIdTOUpdate: number;
processValidation = false;
dates: string;
statusCode: number;
requestProcessing = false;
truckForm = new FormGroup({
truckCode: new FormControl(Validators.maxLength(this.seven), Validators.minLength(this.five)),
date: new FormControl('', Validators.required ),
descriptions: new FormControl(Validators.maxLength(this.seven), Validators.minLength(this.five))
});
constructor( private route: ActivatedRoute, private truckService: TruckService, private router: Router) {
}
ngOnInit() {
this.getTruckDetail();
}
back() {
this.router.navigate(['/trucks'])
}
getTruckDetail() {
const truckId = +this.route.snapshot.paramMap.get('truckId');
this.truckService.getTruckById(truckId)
.subscribe((truck) => this.truck = truck)
console.log("this is the truck" + this.truck);
}
processForm() {
this.processValidation = true;
if (this.truckForm.invalid) {
return; //Validation failed, exit from method.
}
// if we are here then all good
this.preProcessConfigurations()
let truckCode = this.truckForm.get('truckCode').value.trim();
let date = this.truckForm.get('date').value.trim();
let description = this.truckForm.get('descriptions').value.trim();
if (this.truck.truckId == undefined) {
let truck = new Truck(null, truckCode, date , description);
this.truckService.createTruck(truck).subscribe((truck) => {
console.log(truck)
this.router.navigate(['/trucks']);
}, errorCode => this.statusCode = errorCode);
} else {
this.truck = new Truck(this.truck.truckId, truckCode, date, description);
this.truckService.updateTrucks(this.truck).subscribe((truck)=> {
console.log(truck);
this.router.navigate(['/trucks']);
}, errorCode => this.statusCode = errorCode);
}
}
//Perform preliminary processing configurations
preProcessConfigurations() {
this.statusCode = null;
this.requestProcessing = true;
}
}
class="btn btn-light"
title="Save Truck"
data-toggle="tooltip"
data-placement="bottom">
<i class="fa fa-save"></i> <span class="d-none d-sm-inline" *ngIf="truck?.truckId ==undefined">Save</span>
<span class="d-none d-sm-inline" *ngIf="truck?.truckId">Update</span>
</a></li>
</ul>
</nav>
</header>
<section id="department">
<div class="container-fluid">
<div class="row">
<div class="col">
<div class="card">
<div class="card-body">
<form [formGroup]="truckForm" (ngSubmit)="processForm()" id="editFormTruck" >
<!--truck code-->
<div class="form-group">
<label class="form-control-label"
for="truckCode"></label>
<input formControlName="truckCode"
id="truckCode"
class="form-control"
type="text"
name="truckCode"
min="2018-04-11" required
[(ngModel)]="truck?.truckCode"/> Truck Code
<div class="alert alert-danger" role="alert" *ngIf="truckForm.get('truckCode').invalid && processValidation" required="required" [ngClass] ="'error'">
you must have a minimum of 5 chars and maximum of 7
</div>
<div *ngIf="statusCode === 409" [ngClass] = "'success'" class="alert alert-danger" role="alert">
Truck with such Code already exists try another TruckCode.
</div>
</div>
<!--purchasedDate-->
<div class="form-group" >
<label class="form-control-label" *ngIf="truckForm.get('date').invalid && processValidation" [ngClass] ="'error'"
for="purchasedDate">date is required.</label>
<input formControlName="date"
id="purchasedDate"
class="form-control"
type="date"
name="purchasedDate"
[(ngModel)]="truck?.purchasedDate "
/> Purchased Date
</div>
I have posted all because the undefined value starts from my button's save and update when I remove them it goes down to truckCode is undefined
For better understanding this is my truckComponent That navigates to EditTruckComponent
MY truckComponent shows a list of trucks and edit and delete functions
now on my edit function in html i have a i just have a router link
<tr *ngFor="let truck of trucks | async">
<td>{{truck.truckId}}</td>
<a routerLink="/truckProfile/">
<td>{{truck.truckCode}}</td>
</a>
<td>{{truck.purchasedDate | date: 'yyyy-MM-dd'}}</td>
<td>{{truck.descriptions}}</td>
<td class="text-right">
<a class="btn-sm btn-outline-secondary"
ngbTooltip="Edit Truck"
placement="top">
<i class="fa fa-bus"></i> <span
class="d-none d-md-inline" routerLink="/editsTrucks/{{truck.truckId}}" >Edit</span></a>
<span data-toggle="modal" data-target="#deleteDialog">
<a href="#" class="btn-sm btn-outline-secondary"
ngbTooltip="delete truck"
placement="top">
<i class="fa fa-remove"></i>
<span class="d-none d-md-inline" (click)="deleteTruck(truck)">Delete</span></a>
</span>
</td>
So i just do a navigate with router link and on my editTruckCompnent im doing a getById on init

The issue was in my java controller
#GetMapping(value = "/trucks/{truckId}")
#ResponseStatus(HttpStatus.OK)
public final TruckDto getTruckId(#PathVariable(value = "truckId")
final Integer truckId) {
LOGGER.debug("test: truckId({})", truckId);
Truck truck = truckService.getTruckById(truckId);
return mappingService.map(truck, TruckDto.class);
}
when i added this annotation it worked #ResponseBody
So response body tells the controller that the object returned should be serialized into JSON and passed back into the HttpResponse object.
This is why i was getting a HttpResponse Failure

It's a design issue. In my opinion, the right way to do it is :
The EditTruckComponent parent should be smart, do the http call and pass the truck to the EditTruckComponent (you could use async so you don't have to subscribe and unsubscribe manualy)
Here is a clue of how I imagine the parent component :
<app-edit-truck [truck]="truck$ | async"></app-edit-truck>
export class EditTruckParentComponent {
truck$: Observable < Truck > ;
constructor(private truckService: TruckService) {
this.truck$ = this.truckService.getTruckById(truckId);
}
}
Make the EditTruckComponent dumb, no service calls, and only inputs and outputs. Implement OnChanges to handle the truck input changes, and patch the form values.
The edit truck component would look like this :
export class EditTruckParent implements OnChanges {
#Input() truck: Truck;
constructor() {
}
ngOnChanges() {
this.truckForm.patchValue(truck);
}
}

Related

How to interact a front-end input via a webservice?

I have a page with an account number and a button to block the user's account. The webService interacts perfectly. When I click on the blocking button, the account is blocked!
Here is a small picture illustration.
I click on the button
The card displays that the account is blocked
Now, I want to add an input. The user can block the account only if he enters the password.
I added the input in HTML...
To test, the webService with the password, I run the cURL and it works.
{
"HEADER":{
"USER":"TOTO",
"CHANNEL":"B",
"LANGUAGE":"FR",
"SESSION":"",
"PASSWORD":"TOTO",
"NUMTRX":0,
"VERSION":"",
"LADACO": 0,
"LAHECO": 0,
"LADAPWD": 0,
"LAHEPWD": 0,
"IDENTIF": 0,
"AGDE": 0,
"TIERS": 0,
"EXPIRED": 0,
"EXPDATE": "0001-01-01",
"EXPDAYS": 0,
"CDEACC": ""
},
"CUSTOMER":{
"REFERENCE":"NA0052",
"ID":"",
"INTITULE1":"",
"INTITULE2":"",
"ACCESS":0,
"SERVICE":0,
"DEFAULTPTF":0
},
"MODE": "ETAT",
"PTF_IN": {
"ETAT": 4,
"MONTANT": 0
}
}
The return works
{"RETURNCODE":"OKK00","RETURNLIST":[]}
Thus, the webService works with the password. Now I need to integrate this into the front-end. But I don't understand how I have to integrate the password input?!
The page HTML is like this:
<form #formulaire="ngForm" (ngSubmit)="formulaire.form.valid && locking()">
<div class="row row-cols-3 pt-3">
<div class="col text-end">
<!-- Account number -->
<p for="account" class="form-label">Account number</p>
</div>
<div class="col-4">
<label class="fw-bold">{{ ((currentPortfolio$ | async)?.DEPO | accountDash) }} ({{
((currentPortfolio$ | async)?.NO_PTF) }})</label>
</div>
</div>
<div class="row row-cols-3 pt-3">
<div class="col text-end">
<!-- Password -->
<label for="passwordCurrent" class="form-label">Password</label>
</div>
<div class="col-4">
<input id="passwordCurrent" name="passwordCurrent" type="password" class="form-control" maxlength="30">
</div>
</div>
<div *ngIf="!show" class="row row-cols-3 pt-3">
<div class="col"></div>
<div class="col text-start">
<!-- Blocking -->
<button type="submit" class="btn-green" [disabled]="formulaire.form.invalid">Blocking</button>
</div>
</div>
</form>
Service.ts
#Injectable()
export class LockingService {
private readonly api: string = environment.api;
constructor(
private http: HttpClient,
private router: Router,
private store: Store
) {}
getLocking(): Observable<ApiResponse> {
return this.http.post<ApiResponse>(
this.api + `/WBBLOCKING`,
{
PTF_IN: {
ETAT: 4, // blocking
MONTANT: 0,
},
MODE: "ETAT",
},
{ headers: { AddCustomer: "" } }
);
}
getPortfolios(): Observable<SelectPortfolioResponse> {
return this.http.post<SelectPortfolioResponse>(
this.api + `/WBPORTFOLIO`,
{},
{ headers: { AddCustomer: "" } }
);
}
selectPortfolio(selectPortfolio: SelectPortfolio): Observable<void> {
return this.store.dispatch(
new SetCurrentPortfolioAction(
SelectPortfolio_To_Portfolio(selectPortfolio)
)
);
}
}
In the locking.component.ts I have this:
How do I interact the html input password in typescript?
Thank you very much for your help and explanation.
export class LockingComponent implements OnInit {
private unsubscribe$ = new Subject<void>();
show = false;
buttonName = "Show";
hide: any;
#Select(ProfileState.currentPortfolio)
currentPortfolio$!: Observable<Portfolio>;
#Select(AuthState.user) user$!: Observable<string>;
constructor(
private service: LockingService,
private store: Store,
private router: Router
) {}
ngOnInit(): void {}
locking(): void {
this.service
.getLocking()
.pipe(
takeUntil(this.unsubscribe$),
takeWhile((res) => res.RETURNCODE === ApiResponseCodeEnum.Ok),
tap(() => {
this.show = !this.show;
if (this.show) {
this.buttonName = "Hide";
console.log(this.show);
} else {
this.buttonName = "Show";
}
}),
concatMap(() => this.service.getPortfolios()),
takeWhile(
(res) =>
res.RETURNCODE === ApiResponseCodeEnum.Ok && res.PTF.length > 0
),
concatMap((res) => this.service.selectPortfolio(res.PTF[0]))
)
.subscribe();
}
}

User Array length and value in on place

I have an array that I need to repeat the fields based on its length and also use one of the array field to name the fields. How should I do this?
My sample code is here. Please take a look at it Demo
Detail
detail:any[]=[
{id:1,value:'RAM',subcatid:1},
{id:2,value:'LCD',subcatid:1},
{id:3,value:'CPU',subcatid:1},
{id:4,value:'GPU',subcatid:1},
{id:5,value:'Camera',subcatid:1},
{id:1,value:'TRAM',subcatid:2},
{id:2,value:'TLCD',subcatid:2},
{id:3,value:'TCPU',subcatid:2},
{id:4,value:'TGPU',subcatid:2},
{id:5,value:'TCamera',subcatid:2}
]
Fields are created correctly, but I do not know the names of the fields. How should I display. The value of the fields should also be presented and displayed. How should I do this?
this Real Code :
TS :
addProductFG:FormGroup;
cats:Category[];
subCats:Category[];
PD:Productdetail[];
selectedCat:number;
valueIngrident=new FormArray([]);
public loading=false;
public progress: number=0;
public messsage:string;
constructor(private http:HttpClient,private fb:FormBuilder,private productService:ProductinfoService,private catService:CategoryService) { }
ngOnInit() {
this.loading=true;
this.InitialFrom();
this.GetMainCat();
}
public CreateValueFiled(PD:Productdetail[]){
PD.map(element => {
this.valueIngrident.push(
new FormGroup({
infoId:new FormControl(element.id),
value:new FormControl('')
})
)
});
}
public GetMainCat(){
this.catService.GetMainCat().subscribe(
res=>{
this.cats=res;
this.loading=false;
}
)
}
get ValueFormControl(){
return this.addProductFG.get('values') as FormArray;
}
public InitialFrom():FormGroup{
this.addProductFG=this.fb.group({
productTitle:['',Validators.compose([Validators.required])],
productName:['',Validators.compose([Validators.required])],
color:['',Validators.compose([Validators.required])],
productImageName:['',Validators.compose([Validators.required])],
price:['',Validators.compose([Validators.required])],
gurantyMonth:['',Validators.compose([Validators.required])],
gurantyCompanyName:['',Validators.compose([Validators.required])],
values:this.valueIngrident
})
return this.addProductFG;
}
public ChangeSubCat(id:number){
this.loading=true;
this.catService.GetSubCatByCatId(id).subscribe(
res=>{
this.subCats=res;
this.loading=false;
}
)
}
public ChangeFormByType(id:number){
this.loading=true;
this.productService.GetPCIBySubId(id).subscribe(
res=>{
this.PD=res,
this.CreateValueFiled(this.PD),
this.loading=false;
}
)
}
and HTML Code :
<div class="form-inline lbin" formArrayName="values">
<div class="form-inline lbin" *ngFor="let valueCtrl of ValueFormControl.controls; let i=index" [formGroupName]="i">
<div class="form-inline lbin">
<label>{{PD.infoNames[i].infoName}}</label>
<input formControlName="value" >
</div>
</div>
</div>
and this is ProductDetail :
export interface Productdetail {
id:number;
catId:number;
infoNames:Detail[];
}
and this Detail :
export interface Detail {
infoName:string;
}
You has type error, must be
<!--is "AddP", not addP-->
<form *ngIf="AddP" [formGroup]="AddP">
<!--is "values" not valueIng, is formArrayName, not formArray-->
<div formArrayName="values">
<div *ngFor="let valueCtrl of ValueFormControl.controls; let i=index" [formGroupName]="i">
<div class="form-inline lbin">
<!--I supouse you wwant to do some like this-->
<label>{{detail[i].value}}</label>
<input formControlName="value" >
</div>
</div>
</div>
</form>
{{AddP?.value|json}}
Therefore in ngOnInit, you must call befor to CreateValueField
ngOnInit(){
this.CrateValueFiled();
this.InitForm();
}
Well, An idea to improve your code. Why your function CreateValueFiled not return a formArray, like
public CrateValueFiled(detail) {
let controls: FormGroup[] = [];
detail.forEach(el => {
controls.push(
new FormGroup({
id: new FormControl(el.id),
value: new FormControl('', Validators.compose([Validators.required]))
})
)
})
return new FormArray(controls)
}
Then your NgInit becomes
public InitForm(): FormGroup {
this.AddP = this.fb.group({
values: this.CrateValueFiled(this.detail)
});
// return this.AddP; <--you need'nt return this.AddP
}
see your forked stackblitz
NOTE: Try use camel-case notation
Instead of iterating over form array controls, iterate over data.
Forked and updated the solution here.
component html
<form [formGroup]="AddP" >
<div formArrayName="values">
<div *ngFor="let d of detail; let i=index" [formGroupName]="i">
<div class="form-inline lbin">
<label> {{d.value}}</label>
<input formControlName="value" >
</div>
</div>
</div>
</form>

Angular 6 Reactive Forms : How to set focus on first invalid input

Under my Angular 6 app , i'm using Reactive Forms .
My purpose is when submitting , i want to set focus on first invalid input when error.
My form looks like this :
<form [formGroup]="addItemfForm " (ngSubmit)="onSubmitForm()">
<div class="form-inline form-group">
<label class="col-md-2 justify-content-start">
Libellé du pef
<span class="startRequired mr-1"> *</span>
</label>
<input type="text" maxlength="100" formControlName="libellePef" class="col-md-6 form-control" placeholder="saisie obligatoire" [ngClass]="{ 'is-invalid': submitted && formFiels.libellePef.errors }" />
<div *ngIf="submitted && formFiels.libellePef.errors" class="col invalid-feedback">
<div class="col text-left" *ngIf="formFiels.libellePef.errors.required">Libellé du pef est requis.</div>
</div>
</div>
<div class="form-inline form-group">
<label class="col-md-2 justify-content-start">
Code Basicat
<span class="startRequired mr-1"> *</span>
</label>
<input type="text" maxlength="100" formControlName="codeBasicat" class="col-md-3 form-control" placeholder="saisie obligatoire" [ngClass]="{ 'is-invalid': submitted && formFiels.codeBasicat.errors }" />
<div *ngIf="submitted && formFiels.codeBasicat.errors" class="col invalid-feedback">
<div class="text-left" *ngIf="formFiels.codeBasicat.errors.required">Code Basicat est requis.</div>
</div>
</div>
<div class="form-inline form-group">
<label class="col-md-2 justify-content-start">
Nom de l'application
<span class="startRequired mr-1"> *</span>
</label>
<input type="text" maxlength="100" formControlName="nomApplication" class="col-md-6 form-control" placeholder="saisie obligatoire" [ngClass]="{ 'is-invalid': submitted && formFiels.nomApplication.errors }" />
<div *ngIf="submitted && formFiels.nomApplication.errors" class="col invalid-feedback">
<div class="text-left" *ngIf="formFiels.nomApplication.errors.required">Nom de l'application est requis.
</div>
</div>
</div>
</form>
Under my TS file , my form config looks like this :
this.addItemfForm = this.formBuilder.group({
libellePef: ['', Validators.required],
codeBasicat: ['', Validators.required ],
nomApplication: ['', Validators.required ],
urlCible: [''],
modeTransfert: [''],
});
I've tried the autofocus directive but that didn't work
Suggestions?
Use below code in your submit.
for (const key of Object.keys(this.addressForm.controls)) {
if (this.addressForm.controls[key].invalid) {
const invalidControl = this.el.nativeElement.querySelector('[formcontrolname="' + key + '"]');
invalidControl.focus();
break;
}
}
this.addressForm will be your FormGroup.
We don't even need directive here.
My Answer is inspired from yurzui's answer here. I'm using the logic from his answer to get the nativeElement of a particular FormControl by using it's FormControl.
This is the logic that does that:
const originFormControlNameNgOnChanges = FormControlName.prototype.ngOnChanges;
FormControlName.prototype.ngOnChanges = function () {
const result = originFormControlNameNgOnChanges.apply(this, arguments);
this.control.nativeElement = this.valueAccessor._elementRef.nativeElement;
return result;
};
Now, the form's errors field would be null even though it's fields are invalid. So to get the exact first field that's invalid, we'll have to loop through all the fields and check for validity for each of them. I can write this logic in the onSubmitForm() method. Something like this:
onSubmitForm() {
const fieldsToCheck = [
'codeBasicat',
'libellePef',
'nomApplication'
];
for (let i = 0; i < fieldsToCheck.length; i++) {
const fieldName = fieldsToCheck[i];
if (this.addItemfForm.get(fieldName).invalid) {
( < any > this.addItemfForm.get(fieldName)).nativeElement.focus();
break;
}
}
}
I've deliberately used for instead of Array.forEach as I wanted to break from the loop.
Hopefully this should do the trick for you.
Here's a Working Sample StackBlitz for your ref.
I did that using directives. So My form would look like this:
<form [formGroup]="userForm" (submit)="saveData()" appFocus >
...
</form>
and the code for the directive itself:
import { Directive, HostListener, Input, ElementRef } from '#angular/core';
import { NgForm } from '#angular/forms';
#Directive({
selector: '[appFocus]'
})
export class FocusDirective {
constructor(private el: ElementRef) { }
#Input() formGroup: NgForm;
#HostListener('submit', ['$event'])
public onSubmit(event): void {
if ('INVALID' === this.formGroup.status) {
event.preventDefault();
const formGroupInvalid = this.el.nativeElement.querySelectorAll('.ng-invalid');
(<HTMLInputElement>formGroupInvalid[0]).focus();
}
}
}
However this solution is incomplete as there is a lot of corner cases that have to be considered. For example what if the first element is radio button group. Dispatching focus event will automatically mark the filed. Second not every element to which angular ads ng-invalid will be an input.
We can set focus on first invalid input simply by just write this code in the submit() of the form.
if(this.form.invalid)
{
// Got focus to the error field
let invalidFields = [].slice.call(document.getElementsByClassName('ng-invalid'));
invalidFields[1].focus();
}
Try this:
import { Directive, HostListener, ElementRef} from '#angular/core';
#Directive({
selector: '[focusFirstInvalidField]'
})
export class FocusFirstInvalidFieldDirective {
constructor(private el: ElementRef) { }
#HostListener('submit')
onFormSubmit() {
const invalidElements = this.el.nativeElement.querySelectorAll('.ng-invalid');
if (invalidElements.length > 0) {
console.log(invalidElements[0]);
invalidElements[0].focus();
}
}
}
Remember to debug, see if element 0 is not your own form as it happened to me, so see right what field it is reporting as the first and put the position right.
This option does not work for me, but I managed to fix it by changing the code as follows:
#Directive({
selector: '[appErrorFocusin]'
})
export class ErrorFocusinDirective {
selectorToFocus : String = 'textArea,mat-select,select,input,button';
constructor(private el: ElementRef,
#Inject(DOCUMENT) private document: Document) { }
#Input() formGroup: NgForm;
#HostListener('submit', ['$event'])
public onSubmit(event): void {
if ('INVALID' === this.formGroup.status) {
event.preventDefault();
const formGroupInvalid = this.el.nativeElement.querySelectorAll('.ng- invalid,:not(.mat-badge-hidden).mat-badge');
let elementToOffset = this.getElementToOffset(formGroupInvalid[0]);
this.document.documentElement.scrollTop = elementToOffset.offsetTop;
this.setFocusOnError(elementToOffset);
}
}
getElementToOffset(element : any){
let defaultElement = element;
while (!(element.parentElement instanceof HTMLFormElement)){
if (element.parentElement){
element = element.parentElement;
}else{
return defaultElement;
}
}
return element;
}
setFocusOnError(elementToOffset : any){
let listaElementos = elementToOffset.querySelectorAll(this.selectorToFocus);
if (listaElementos.length>0){
listaElementos[0].focus();
}
}
}
A continuation to #Avinash's answer, instead of making it
querySelector('[formcontrolname="' + key + '"]');
We can add id's in the HTML to the input and simply make that as this:
querySelector('#'+ key +'.ng-invalid');

Angular Material MatChipList - how to use it on a Dynamic FormArray?

StackBlitz
Here is my FormArray (variants):
this.productGroup = this.fb.group({
name: '',
variants: this.fb.array([
this.fb.group({
type: '',
options: ''
})
])
})
I'm using MatChips to store a string array. This array needs to be passed to options:
<div formArrayName="variants" *ngFor="let item of productGroup.controls['variants'].controls; let i = index;">
<div [formGroupName]="i">
<div class="row">
<mat-form-field class="col-12">
<input formControlName="type">
</mat-form-field>
</div>
<div class="row">
<mat-form-field class="col-12">
<mat-chip-list #chipList>
<mat-chip *ngFor="let opt of typesOptions" [selectable]="true"
[removable]="true" (removed)="removeOpt(opt)">
{{opt}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input placeholder="Conjunto de opções deste Tipo"
formControlName="options"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addOpt($event)">
</mat-chip-list>
</mat-form-field>
</div>
</div>
<div class="row">
Add Variants
Remove Variants
</div>
</div>
Here is the methods:
// Dynamic Methods
addItem(): void {
this.variantsArray = this.productGroup.get('variants') as FormArray;
this.variantsArray.push(this.fb.group({
type: '',
options: ''
}));
}
removeItem(index: number) {
this.variantsArray.removeAt(index);
}
// MatChip Methods
addOpt(item: number, event: MatChipInputEvent): void {
const input = event.input;
const value = event.value;
// Add our fruit
if ((value || '').trim()) {
this.typesOptions.push(value.trim());
}
// Reset the input value
if (input) {
input.value = '';
}
}
removeOpt(opt: string): void {
const index = this.typesOptions.indexOf(opt);
if (index >= 0) {
this.typesOptions.splice(index, 1);
}
I'm successfully adding dynamic fields to my variants formArray. However MatChipList is the same for every dynamic field. I need to make MatChipList dynamic as well. Is there a way to achieve this? Like changing <mat-chip-list #chipList+i> or something like that.
EDIT: StackBlitz
I'm not sure the dom reference variable #chiplist is the issue. It looks like the matChipList is backed by the typesOptions array, but you only have one array. So every time you add a matChipList component it is still being backed by the same array as all the others. You need to have an array of typesOptions, an array of arrays. Then when you addItem, you also push a new sub array to typesOptions (and similarly remove one for removeItem).
I haven't coded this up, just a suggestion from looking at the code.
Edit - coded up a solution based on James's stackblitz.
https://stackblitz.com/edit/angular-3od6rd-jkidxf
Note I haven't looked in detail at how the delete variant holds together, ideally I'd probably like to use a key/value pair to track the variant options using the dom input element id as the key (which is in the MatChipInputEvent), instead of relying on the outer loop index.
Some code from the stackblitz:
export class ChipsOverviewExample {
productGroup: FormGroup;
variantsArray: FormArray;
typesOptionsArray: string[][] = [];
readonly separatorKeysCodes: number[] = [ENTER, COMMA];
constructor(private fb: FormBuilder) {}
ngOnInit() {
this.productGroup = this.fb.group({
name: '',
variants: this.fb.array([
this.fb.group({
type: '',
options: ''
})
]),
});
this.typesOptionsArray.push([]);
}
saveProduct(form: FormGroup) {
console.log(form);
}
// Add new item to FormArray
addItem(): void {
this.variantsArray = this.productGroup.get('variants') as FormArray;
this.variantsArray.push(this.fb.group({
type: '',
options: ''
}));
this.typesOptionsArray.push([]);
}
removeItem(index: number) {
this.variantsArray.removeAt(index);
}
addOpt(event: MatChipInputEvent, index: number): void {
const input = event.input;
const value = event.value;
// Add our fruit
if ((value || '').trim()) {
this.typesOptionsArray[index].push(value.trim());
}
// Reset the input value
if (input) {
input.value = '';
}
}
removeOpt(opt: string, index: number): void {
const optIndex = this.typesOptionsArray[index].indexOf(opt);
if (optIndex >= 0) {
this.typesOptionsArray[index].splice(optIndex, 1);
}
}
}
try to make the formGroup as a new component and input formGroup to it(not formGroupName).
<div formArrayName="variants" *ngFor="let item of productGroup.controls['variants'].controls; let i = index;">
<variant [varientGroup]="item"><varient>
<div class="row">
Add Variants
Remove Variants
</div>
</div>
varient component.html
<div [formGroup]="varientGroup">
<div class="row">
<mat-form-field class="col-12">
<input formControlName="type">
</mat-form-field>
</div>
<div class="row">
<mat-form-field class="col-12">
<mat-chip-list #chipList>
<mat-chip *ngFor="let opt of typesOptions" [selectable]="true"
[removable]="true" (removed)="removeOpt(opt)">
{{opt}}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input placeholder="Conjunto de opções deste Tipo"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
[matChipInputAddOnBlur]="true"
(matChipInputTokenEnd)="addOpt($event)">
</mat-chip-list>
</mat-form-field>
</div>
</div>
in varient.component.ts
#Input()varientGroup: FormGroup

Update parent when variable in child change - Angular

I have a parent which is a form.
This form is composed of two child components:
experiment create (parent)
creation-dataset (child)
creation-metadata (child)
I use a angular component -> mat-accordion to navigate through the two children components.
I use #Input to have the result of what is filled in the children component into the parent.
I want to submit the form only if a file is chosen for both of them. Therefore, I set a variable (in datasetList[i].fileValid) to say whether a file has been selected. Like this I disabled the button if a file is not updated. To disable the button I called the two function:
isDatasetFilesValid()
isMetadataFilesValid()
However, when the variable changed for the second child component it does not updated the disabled button.
This works, only if I press "previous" and "next". The button is not disabled anymore. Like if I needed to reload or refresh the parent. Maybe because of the life cycle ?
Parent Component:
export class ExperimentCreateComponent implements OnInit {
data: any = {};
datasetList: any = [{ fileValid: false }];
metadataList: any = [{ fileValid: false }];
// Functions to navigate through the expansion panels
setStep(index: number) {
this.step = index;
}
nextStep() {
this.step++;
}
prevStep() {
this.step--;
}
isDatasetFilesValid() {
return this.datasetList.findIndex(function(item, i) {
return item.fileValid == false;
});
}
isMetadataFilesValid() {
return this.metadataList.findIndex(function(item, i) {
return item.fileValid == false;
});
}
}
Parent HTML:
<div class="jumbotron">
<div class="container">
<div class="row">
<div class="col-sm-8 offset-sm-2">
<form name="form" (ngSubmit)="f.form.valid" #f="ngForm" novalidate>
<mat-accordion class="headers-align">
<mat-expansion-panel id="datasetUpload" [expanded]="step === 0" (opened)="setStep(1)" hideToggle="true">
<app-creation-dataset [datasetList]="datasetList"></app-creation-dataset>
<mat-action-row>
<button mat-button color="warn" (click)="prevStep()">Previous</button>
<button mat-button color="primary" (click)="nextStep()">Next</button>
</mat-action-row>
</mat-expansion-panel>
<mat-expansion-panel id="metadataUpload" [expanded]="step === 1" (opened)="setStep(2)" hideToggle="true">
<app-creation-metadata [metadataList]="metadataList"></app-creation-metadata>
<mat-action-row>
<button mat-button color="warn" (click)="prevStep()">Previous</button>
<button mat-button color="primary" type="submit" [disabled]="(isMetadataFilesValid() != -1) && (isDatasetFilesValid() != -1)" (click)="createExperiment()">End</button>
</mat-action-row>
</mat-expansion-panel>
</mat-accordion>
</form>
</div>
</div>
</div>
</div>
Child Component:
export class CreationDatasetComponent implements OnInit {
#Input() datasetList: any = [{ fileValid: false }];
fileSelected: File;
constructor(private papa: Papa, private cd: ChangeDetectorRef) {}
ngOnInit() {}
onChange(files: FileList, index: number, dom: any) {
// Option to parse the file with papaparse
let options = {
header: true,
error: (err, file) => {
this.datasetList[index].fileValid = false;
alert(
"Unable to parse CSV file, please verify the file can be accessed and try again. Error reason was: " +
err.code
);
return;
},
complete: (results, file) => {
console.log("Parsed:", results, file);
let filename = file.name;
// Add the dataset to the datasetList
this.datasetList[index].headers = results.meta.fields;
this.datasetList[index].values = results.data;
this.datasetList[index].filename = filename;
this.datasetList[index].is_metadata = false;
this.datasetList[index].fileValid = true;
this.cd.detectChanges();
}
};
this.fileSelected = files[0]; // Get the file
// Call the function to parse the file, option is the callback
this.papa.parse(this.fileSelected, options);
}
// Add a dataset form
addDataset() {
this.datasetList.push({ fileValid: false });
}
// Remove a dataset form
removeDataset(index: number) {
this.datasetList.splice(index, 1);
}
}
Child HTML:
<div *ngFor="let dataset of datasetList; let index = index">
<div id="datasetFiles">
<h6>Select the type of dataset and browse the files:</h6>
<div class="container">
<div class="row justify-content-between">
<div class="col-6 d-flex align-items-center">
<input id="file" #file (change)="onChange(file.files, index, $event.currentTarget)" type="file">
</div>
</div>
</div>
</div>
</div>
<div>
<button mat-icon-button color="primary" (click)="addDataset()">
<mat-icon>add_box</mat-icon>
</button>
</div>
So, to make this answer more clear, read comments on question.
I'm going to past the example for the #Output:
this is the CHILD.COMPONENT.TS
#Component({
selector: 'children',
templateUrl: './children.component.html',
styleUrls: ['./children.component.scss'],
providers: [{...
})
})
export class ChildrenComponent {
#Output() editedEmitter = new EventEmitter<number>();
private variableToPass = 10;
constructor() {}
functionToCall() {
this.editedEmitter.emit(20);
}
this is the PARENT.COMPONENT.HTML
<section>
<children (editedEmitter)="updateValue($event)"></children>
</section>
<!-- in the component you'll do
updateValue(val: number) {
this.variableToUpdate = val;
}
-->
[disabled] requires a condition (true or false), the code you put in: isMetadataFilesValid() != -1 isDatasetFilesValid() != -1 is not a proper condition, they are two. If you want both conditions to be true, use the && notation.
[disabled]="(isMetadataFilesValid() != -1) && (isDatasetFilesValid() != -1)"
Alternatively I suggest moving the condition to the functions themselves so they return a boolean.

Categories