Angular .removeAt(i) at FormArray does not update in DOM - Angular - javascript

I thought it was an issue with my implementation but seems that my code for creating dynamic FormArray should be functional based from this question I raised. When I integrate it to my project, the remove function does delete the element from the FormArray, but it does not reflect in the interface/ does not remove object from the DOM. What could be causing this?
import {
Component,
VERSION
} from '#angular/core';
import {
FormGroup,
FormControl,
FormArray,
Validators,
FormBuilder
} from '#angular/forms';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
objectProps: any[];
public dataObject = [{
"label": "Name",
"name": "name",
"type": "text",
"data": ""
},
{
"label": "Contacts",
"name": "contacts",
"type": "inputarray",
"array": []
}
]
form: FormGroup;
constructor(private _fb: FormBuilder) {}
ngOnInit() {
const formGroup = {};
for (let field of this.dataObject) {
if (field.type == "inputarray") {
console.log("THIS IS " + field.type)
formGroup[field.name] = this._fb.array([''])
} else {
console.log("THIS IS " + field.type)
formGroup[field.name] = new FormControl(field.data || '') //, this.mapValidators(field.validation));
}
}
this.form = new FormGroup(formGroup);
}
addFormInput(field) {
const form = new FormControl('');
( < FormArray > this.form.controls[field]).push(form);
}
removeFormInput(field, i) {
( < FormArray > this.form.controls[field]).removeAt(i);
}
}
<form *ngIf="form" novalidate (ngSubmit)="onSubmit(form.value)" [formGroup]="form">
<div *ngFor="let field of dataObject">
<h4>{{field.label}}</h4>
<div [ngSwitch]="field.type">
<input *ngSwitchCase="'text'" [formControlName]="field.name" [id]="field.name" [type]="field.type" class="form-control">
<div *ngSwitchCase="'inputarray'">
<div formArrayName="{{field.name}}" [id]="field.name">
<div *ngFor="let item of form.get(field.name).controls; let i = index;" class="array-line">
<div>
<input class="form-control" [formControlName]="i" [placeholder]="i">
</div>
<div>
<button id="btn-remove" type="button" class="btn" (click)="removeFormInput(field.name, i)">x</button>
</div>
</div>
</div>
<div>
<button id="btn-add" type="button" class="btn" (click)="addFormInput(field.name)">Add</button>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-danger btn-block" style="float: right; width:180px" [disabled]="!form.valid">Save</button>

This is not a great solution but I solved my problem by manipulating value and after removing the control.
I simply moved the item that I wanted to remove to the end of the array and then I removed the last item.
removeItem(index: number): void {
const value = this.formArray.value;
this.formArray.setValue(
value.slice(0, index).concat(
value.slice(index + 1),
).concat(value[index]),
);
this.formArray.removeAt(value.length - 1);
}
I hope it helps someone struggling with this issue in the future.

Maybe you can try to force change detection using the reference to application. To do that inject the ApplicationRef in the constructor an call the tick(); on your removeFormInput method.
constructor(private _fb: FormBuilder, private appRef: ApplicationRef) {}
And in removeFormInput
removeFormInput(field, i) {
(<FormArray>this.form.controls[field]).removeAt(i);
this.appRef.tick();
}
Take a look at angular documentation: API > #angular/core /ApplicationRef.tick()

replace below function, you are not removing the row object from 'dataObject'.
removeFormInput(field, i) {
( < FormArray > this.form.controls[field]).removeAt(i);
this.dataObject.splice(this.dataObject.indexOf(field),1);
}
Take a look here Add and Remove form items I build on stackblitz, for me its working fine, adding and removing items... Take a look.
working version

I was facing this problem as well. The solution for me was to get rid of/fix the trackBy function in NgFor*. I think you need to introduce a proper trackBy function and it might solve your error.
sauce: How to use `trackBy` with `ngFor`

Related

Angular Custom focus Directive. Focus a form's first invalid input

I have created a directive to focus an input if it's invalid
import { Directive, Input, Renderer2, ElementRef, OnChanges } from '#angular/core';
#Directive({
// tslint:disable-next-line:directive-selector
selector: '[focusOnError]'
})
export class HighlightDirective implements OnChanges {
#Input() submitted: string;
constructor(private renderer: Renderer2, private el: ElementRef) { }
ngOnChanges(): void {
const el = this.renderer.selectRootElement(this.el.nativeElement);
if (this.submitted && el && el.classList.contains('ng-invalid') && el.focus) {
setTimeout(() => el.focus());
}
}
}
I do have a reactive form with two inputs, and I've applied the directive to both inputs
<form>
...
<input type="text" id="familyName" focusOnError />
...
<input type="text" id="appointmentCode" focusOnError />
...
</form>
After submitting the form it works fine, but what I'm struggling to achieve is the following:
Expected result:
- After submitting the form if both inputs are invalid, only the first one should be focused.
Current result:
- After submitting the form if both inputs are invalid, the second one gets focused.
I don't know how to specify "only do this if it's the first child", I've tried with the directive's selector with no luck.
Any ideas?
Thanks a lot in advance.
To control the inputs of a Form, I think the better solution is use ViewChildren to get all elements. So, we can loop over this elements and focus the first.
So, we can has a auxiliar simple directive :
#Directive({
selector: '[focusOnError]'
})
export class FocusOnErrorDirective {
public get invalid()
{
return this.control?this.control.invalid:false;
}
public focus()
{
this.el.nativeElement.focus()
}
constructor(#Optional() private control: NgControl, private el: ElementRef) { }
}
And, in our component we has some like
#ViewChildren(FocusOnErrorDirective) fields:QueryList<FocusOnErrorDirective>
check() {
const fields=this.fields.toArray();
for (let field of fields)
{
if (field.invalid)
{
field.focus();
break;
}
}
}
You can see in action in the stackblitz
UPDATE always the things can improve:
Why not create a directive that applied to the form?
#Directive({
selector: '[focusOnError]'
})
export class FocusOnErrorDirective {
#ContentChildren(NgControl) fields: QueryList<NgControl>
#HostListener('submit')
check() {
const fields = this.fields.toArray();
for (let field of fields) {
if (field.invalid) {
(field.valueAccessor as any)._elementRef.nativeElement.focus();
break;
}
}
}
So, our .html it's like
<form [formGroup]="myForm" focusOnError>
<input type="text" formControlName="familyName" />
<input type="text" formControlName="appointmentCode" />
<button >click</button>
</form>
See the stackblitz
Even more, if we use as selector form
#Directive({
selector: 'form'
})
Even we can remove the focusOnError in the form
<form [formGroup]="myForm" (submit)="submit(myForm)">
..
</form>
Update 2 Problems with formGroup with formGroup. SOLVED
NgControl only take account the controls that has [(ngModel)], formControlName and [formControl], so. If we can use a form like
myForm = new FormGroup({
familyName: new FormControl('', Validators.required),
appointmentCode: new FormControl('', Validators.required),
group: new FormGroup({
subfamilyName: new FormControl('', Validators.required),
subappointmentCode: new FormControl('', Validators.required)
})
})
We can use a form like:
<form [formGroup]="myForm" focusOnError (submit)="submit(myForm)">
<input type="text" formControlName="familyName" />
<input type="text" formControlName="appointmentCode" />
<div >
<input type="text" [formControl]="group.get('subfamilyName')" />
<input type="text" [formControl]="group.get('subappointmentCode')" />
</div>
<button >click</button>
</form>
where in .ts we has
get group()
{
return this.myForm.get('group')
}
Update 3 with Angular 8 you can get the descendants of the children, so it's simply write
#ContentChildren(NgControl,{descendants:true}) fields: QueryList<NgControl>
well, just for funny stackblitz. If we has a formControl, we can inject ngControl that it's the control itself. So we can get the formGroup. I control the "submited" making a work-around in the app.component
<button (click)="check()">click</button>
check() {
this.submited = false;
setTimeout(() => {
this.submited = true;
})
}
The directive is like
export class FocusOnErrorDirective implements OnInit {
#HostListener('input')
onInput() {
this._submited = false;
}
//I used "set" to avoid ngChanges, but then I need the "ugly" work-around in app.component
#Input('focusOnError')
set submited(value) {
this._submited = value;
if (this._submited) { ((is submited is true
if (this.control && this.control.invalid) { //if the control is invalid
if (this.form) {
for (let key of this.keys) //I loop over all the
{ //controls ordered
if (this.form.get(key).invalid) { //If I find one invalid
if (key == this.control.name) { //If it's the own control
setTimeout(() => {
this.el.nativeElement.focus() //focus
});
}
break; //end of loop
}
}
}
else
this.el.nativeElement.focus()
}
}
}
private form: FormGroup;
private _submited: boolean;
private keys: string[];
constructor(#Optional() private control: NgControl, private el: ElementRef) { }
ngOnInit() {
//in this.form we has the formGroup.
this.form = this.control?this.control.control.parent as FormGroup:null;
//we need store the names of the control in an array "keys"
if (this.form)
this.keys = JSON.stringify(this.form.value)
.replace(/[&\/\\#+()$~%.'"*?<>{}]/g, '')
.split(',')
.map(x => x.split(':')[0]);
}
}

Create html element for every element in array (Angular)

I have Angular component
Here is code
#Component({
selector: 'app-testcomponent',
templateUrl: './testcomponent.component.html',
styleUrls: ['./testcomponent.component.scss']
})
export class TestcomponentComponent implements OnInit {
version: string = environment.version;
error: string;
searchForm: FormGroup;
constructor(
private formBuilder: FormBuilder,
private http: HttpClient
) {
this.createForm();
}
ngOnInit() {}
search() {
this.http.get('https://api.chucknorris.io/jokes/search?query='+this.searchForm.value.jokevalue ).subscribe(
data => [
console.log(data)
])
}
private createForm() {
this.searchForm = this.formBuilder.group({
jokevalue: ['', Validators.required],
});
}
}
Function search() is related to get values from API and make HTML markup for every element in the array on submit button. Here is template HTML
<div class="container-fluid">
<form class="form-inline" (ngSubmit)="search()" [formGroup]="searchForm" novalidate>
<label for="text">Enter value:</label>
<input formControlName="jokevalue" style="margin-left:20px;" type="text" class="form-control" id="email">
<button style="margin-left:20px;" type="submit" class="btn btn-primary">Submit</button>
</form>
Getting array is done and here is an array of response
{
"category": null,
"icon_url": "https://assets.chucknorris.host/img/avatar/chuck-norris.png",
"id": "cq6hLP0ETeW4VSrm7SYg5A",
"url": "https://api.chucknorris.io/jokes/cq6hLP0ETeW4VSrm7SYg5A",
"value": "Chuck Norris knows WAZZZUP!"
}
So now I need to loop from the array(it can have more than one element) and create HTML markup for every element
For example this
<div>
<p>Number of array element</p>
<p>"value"</p>
</div>
I try to do it like this
data => [
for (let i = 0; i < data.length; i++) {
}
])
But seems it not right.
How I can solve my problem.
Thank's
expose your data in your component source:
public jokes: any[]; //or create an interface instead of using "any" for "strong" typing support
search() {
this.http.get('https://api.chucknorris.io/jokes/search?query='+this.searchForm.value.jokevalue ).subscribe(
data => [
console.log(data)
this.jokes = data;
])
}
use an *ngFor in your component template to bind to your data:
<div *ngFor="let joke of jokes">
<p>{{joke.category}}</p>
<p>{{joke.value}}</p>
</div>
update for comments around not using an array:
expose your data in your component source:
public joke: any; //or create an interface instead of using "any" for "strong" typing support
search() {
this.http.get('https://api.chucknorris.io/jokes/search?query='+this.searchForm.value.jokevalue ).subscribe(
data => [
console.log(data)
this.joke = data;
])
}
component template:
<div>
<p>{{joke.category}}</p>
<p>{{joke.value}}</p>
</div>

How to get unique value in FormArray reactive form in Angular?

I have created a stackblitz app to demonstrate my question here: https://angular-ry1vc1.stackblitz.io/
I have formArray values on a select HTML list. Whenever a user changes the selection, it should display the selected value on the page. The problem is how can I display only the current selection and it should NOT overwrite the value selected previously. I wonder how to use the key to make the placeholder of the values unique. TIA
form.html
<form [formGroup]="heroForm" novalidate class="form">
<section formArrayName="league" class="col-md-12">
<h3>Heroes</h3>
<div class="form-inline" *ngFor="let h of heroForm.controls.league.controls; let i = index" [formGroupName]="i">
<label>Name</label>
<select (change)="onSelectingHero($event.target.value)">
<option *ngFor="let hero of heroes" [value]="hero.id" class="form-control">{{hero.name}}</option>
</select> <hr />
<div>
Hero detail: {{selectedHero.name}}
</div> <hr />
</div>
<button (click)="addMoreHeroes()" class="btn btn-sm btn-primary">Add more heroes</button>
</section>
</form>
component.ts
import { Component, OnInit } from '#angular/core';
import { FormArray, FormBuilder, FormControl, FormGroup, Validators, NgForm } from '#angular/forms';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {
heroes = [];
heroForm: FormGroup;
selectedHero;
constructor(
private fb: FormBuilder,
) {
this.heroForm = fb.group({
league: fb.array([
this.loadHeroes(),
this.loadHeroes(),
this.loadHeroes()
])
});
}
ngOnInit() {
this.listHeroes();
}
public addMoreHeroes() {
const control = this.heroForm.get('league') as FormArray;
control.push(this.loadHeroes());
}
public loadHeroes() {
return this.fb.group(
{
id: this.heroes[0],
name: '',
level: '',
skill: '',
}
);
}
public listHeroes() {
this.heroes = [
{
id: 1,
name: 'Superman'
},
{
id: 2,
name: 'Batman'
},
{
id: 3,
name: 'Aquaman'
},
{
id: 4,
name: 'Wonderwoman'
}
];
}
public onSelectingHero(heroId) {
this.heroes.forEach((hero) => {
if(hero.id === +heroId) {
this.selectedHero = hero;
}
});
}
}
If the aim of this is to show only the selected hero by array element instead of replace all the selected values then you can get some help using the array form elements.
A. The onSeletingHero and selectedHero are not necessary, I replaced that using the form value through formControlName attribute, in this example the id control is the select. The h.value.id is the way to get the selected value id.
<select formControlName="id">
<option *ngFor="let hero of heroes;" [value]="hero.id" class="form-control">{{hero.name}}</option>
</select> <hr />
<div>
Hero detail: {{getHeroById(h.value.id).name}}
</div> <hr />
</div>
B. In order to get the selected hero I added a getHeroById method.
getHeroById(id: number) {
return id ? this.heroes.find(x => x.id === +id) : {id: 0, name: ''};
}
Hope this information solve your question. Cheers

Angular2 how to remove 1 specific element from list with mouseclick

I am having a small issue in my code. Ive created an weather application using angular 2 and it works fine. Though do i have a small problem when clicking "delete this city button" where it delets only the last city in and not the one I wanted, how can i solve this? Here is my code:
clearWeatherItems() {
for(WEATHER_ITEMS, function(i)){
var city = WEATHER_ITEMS[i];
if(city == WEATHER_ITEMS[i]) {
city.splice(i, 1);
return false;
}
}
} }
Ive also tried doing it this way but still the same problem occurs:
clearWeatherItems() {
WEATHER_ITEMS.splice(-1);
}
here is my weather-item.component.ts:
import { Component, Input } from 'angular2/core';
import { WeatherItem } from "./weather-Item";
#Component({
selector: 'weather-item',
template: `
<div id="clear">
</div>
<article class="weather-element">
<div class="col-1">
<h3>{{ weatherItem.cityName }}</h3>
<p class="info">{{ weatherItem.description }}</p>
</div>
<div class="col-2">
<span class="temperature">{{ weatherItem.temprature }}°C</span>
</div>
<button class="delete" (click)="clearWeatherItems($event, weatherItem)">X</button>
</article>
`,
styleUrls: ['src/css/weather-item.css'],
// inputs: ['weatherItem: item']
})
export class WeatherItemComponent {
#Input('item') weatherItem: WeatherItem;
clearWeatherItems() {
// event.stopPropagation();
this.weatherItem.clearWeatherItems();
}
}
my weather.data.ts:
import { WeatherItem } from "./weather-Item";
export const WEATHER_ITEMS: WeatherItem[] = [];
and here is my weather-item.ts:
import { WEATHER_ITEMS } from "./weather.data";
export class WeatherItem {
constructor(public cityName, public description: string, public temprature: number) {
}
clearWeatherItems(item) {
WEATHER_ITEMS.splice(-1);
}
}
Somebody knows what to do?
Best regards from a programming noob :P
Just declare a method on ts file
clearWeatherItem(item:any){
let index: number = this.WEATHER_ITEMS.indexOf(item);
if (index !== -1) {
this.data.splice(index, 1);
}
}
call this method by passing the item to be removed from the HTML template

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