Two way binding on Array elements using <form> - javascript

I have an array of objects in a component. which I will iterate in the template.
app.component.ts
import {Component, OnInit} from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'sample-app';
classesData = [];
constructor() {
}
ngOnInit() {
this.classesData = [
{title: 'Hello0'}, {title: 'Hello1'}, {title: 'Hello2'}
];
}
duplicate() {
const newData = JSON.parse(JSON.stringify(this.classesData[1]));
newData.title += 'Copy';
this.classesData.splice(1, 0, newData);
}
}
app.template.html
<form #testingFrom="ngForm">
<p>{{classesData | json}}</p>
<div *ngFor="let classData of classesData; let i=index">
<input [(ngModel)]="classData.title" name="{{'title-' + i}}" type="text">
</div>
<button (click)="duplicate()">Duplicate</button>
</form>
My aim is when a user clicks on the duplicate button I simply add a new element at index 1 in an array. My initial state looks like (before user clicks)
And my state after a user clicks duplicate button
In the image above at 3rd input field, we are getting Hello1Copy instead of Hello1.

I completely suspect that this behavior is happening because of conflict in the name attribute value. For this case only, if you splice the newItem at first location, it only adds that's variable and other DOM's doesn't re-render. For cross verification you can try replacing input element with simple binding like {{classData.title}} and everything works fine.
This behavior can easily be solved by not conflicting name attribute value for all time. What that means is to assign a unique id variable with each collection item and use it.
this.classesData = [
{ id: 1, title: 'Hello0' },
{ id: 2, title: 'Hello1' },
{ id: 3, title: 'Hello2' }
];
duplicate() {
const newData = JSON.parse(JSON.stringify(this.classesData[1]));
newData.title += 'Copy';
newData.id = Date.now()
this.classesData.splice(1, 0, newData);
}
Template
<div *ngFor="let classData of classesData;let i=index">
<input [(ngModel)]="classData.title" [name]="'title_'+classData.id" type="text">
</div>
Stackblitz
You can also verify the same by removing name attribute from each input field. But that would not suffice, it would throw
ERROR Error: If ngModel is used within a form tag, either the name
attribute must be set or the form control must be defined as
'standalone' in ngModelOptions.
So add [ngModelOptions]="{standalone: true}" on each input field to make input working without name attribute. As suggested in another answer by #briosheje, you can also re-enforce rendering using trackBy.
PS: I'm investigating why this works differently when there is a combination of name and input, I suspect about form API wiring with input element. I'll update the answer as soon as I get something.

The problem is that you are using a form. Because you're using a form, you need to specify how angular should track the changes for your form items, if you're planning to alter the existing source. You can do such using the trackBy pipe:
<form #testingFrom="ngForm">
<p>{{classesData | json}}</p>
<div *ngFor="let classData of classesData; let i=index; trackBy: trackByFn">
<input [(ngModel)]="classData.title" [name]="'title-' + i" type="text">
</div>
<button (click)="duplicate()">Duplicate</button>
</form>
Typescript relevant part:
trackByFn(index: any) {
return index;
}
Please note that adding elements to the collection will work in your original example.
Working stackblitz: https://stackblitz.com/edit/angular-uabuya

Make another variable and iterate that variable to crate inputs
import { Component,OnInit } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {
title = 'sample-app';
originalData=[];
classesData = [];
constructor() {
}
ngOnInit() {
this.classesData = [
{title: 'Hello0'}, {title: 'Hello1'}, {title: 'Hello2'}
];
this.originalData=[...this.classesData]; // changed here
}
duplicate() {
const newData = JSON.parse(JSON.stringify(this.classesData[1]));
newData.title += 'Copy';
this.classesData.splice(1, 0, newData);
}
}
Working Demo

You can solve your issue using "trackBy" feature. Please see below code sample.
app.component.ts
import { Component, OnInit } from '#angular/core';
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
title = 'sample-app';
classesData = [];
constructor() {}
ngOnInit() {
this.classesData = [
{ title: 'Hello0' },
{ title: 'Hello1' },
{ title: 'Hello2' }
];
}
duplicate() {
const newData = JSON.parse(JSON.stringify(this.classesData[1]));
newData.title += 'Copy';
this.classesData.splice(1, 0, newData);
}
trackByIndex(index: number, obj: any): any {
return index;
}
}
app.component.html
<form>
<p>{{classesData | json}}</p>
<div *ngFor="let classData of classesData; let i=index;trackBy:trackByIndex;">
<input [(ngModel)]="classesData[i].title" name="{{'title-' + i}}" type="text" />
</div>
<button (click)="duplicate()">Duplicate</button>
</form>
Please let me know if this solution works for you!

Related

Angular change the state of a checkbox after user click

I have the following code that should set the checked value to false on click:
#Component({
template: `
<input type="checkbox" [checked]="checked" (change)="onChange()">
`
})
export class AppComponent {
checked = false;
onChange() {
setTimeout(() => {
this.checked = false;
}, 1000)
}
}
The problem is that if we click on the input and we wait for a second, it'll stay checked. Why is this happening? Why Angular doesn't change it to false again?
Long story short Angular checkboxes are just broken : Issue
If you however want to achive this effect i will recommend you to create your own custom component that will act the same way as a checkbox.
Here is one more fun example of Checkbox madnes try to interact with both "With track by" and "No track by" blocks and see what happens.
I think you can do that easily by using [(ngModel)]="checked". Your code is working in the stackblitz link. Please check that. Here is my code given below.
import { Component } from '#angular/core';
#Component({
selector: 'my-app',
template: `
<input type="checkbox" [checked]="checked" [(ngModel)]="checked" (change)="onChange()" name="horns">
<label for="horns">Horns</label>
`,
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
checked = false;
onChange() {
setTimeout(() => {
this.checked = false;
}, 2000)
}
}
in angular you can manipulate dom using view child and element ref
first of all you need to import viewchild and elementRef in your component
app.component.ts
import { Component, VERSION } from "#angular/core";
import { ViewChild, ElementRef } from "#angular/core";
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
#ViewChild("checkBox") el: ElementRef;
onChange() {
setTimeout(() => {
this.el.nativeElement.checked = false;
}, 100);
}
}
app.component.html
<input type="checkbox" #checkBox (click)="onChange()">
stackblitz

How to Render Child Components Dynamically using loops if possible? (Angular 8)

I am trying to render my angular components dynamically using loops but I'm lost.
What I want to achieve is drag and re-arrange my components (I was able to achieve this using Dragula - ng2-dragula) but I want to save the arrangement to my local storage or session storage.
The tricky part is I am using child components
<section [dragula]="'VAMPIRES'">
<div class="first_element_class" id="first_element">
<my-first-component
[outputData]="outputData"></my-first-component>
</div>
<div class="second_element_class" id="second_element">
<my-second-component
[outputData]="outputData"></my-second-component>
</div>
</section>
I tried using the DOM sanitizer pipe to render them via for loops
import { Pipe, PipeTransform } from '#angular/core';
import { DomSanitizer, SafeHtml } from '#angular/platform-browser';
#Pipe({
name: 'trustHtml'
})
export class TrustHtmlPipe implements PipeTransform {
constructor(readonly sr: DomSanitizer){}
transform(html: string) : SafeHtml {
return this.sr.bypassSecurityTrustHtml(html);
}
}
Updated HTML Code (Let's assume I added my html elements to an array of object)
objectHTML: any[] = [{
toBeRender: `<div class="first_element_class" id="first_element">
<my-first-component
[outputData]="outputData"></my-first-component>
</div>`
}];
<section [dragula]="'VAMPIRES'">
<div *ngFor="let element of objectHTML">
<div [innerHTML]="element.toBeRender | trustHtml" ></div>
</div>
</section>
My question is is it really possible to render the html child components/elements using ngFor? I tried using the DOMSanitizer but I'm only getting a blank page (No error on console)
I have a similar solution for you. Firstly, in your <my-first-component> ts file declare "outputData" as #Input and use ngFor in HTML. Here is my sample code below.
CUSTOM Control
HTML: event.component.html
<h2>Event {{ event.id }}</h2>
<button (click)="event.hidden = true">Close me</button>
TS:event.component.ts
import { Component, Input } from '#angular/core';
import { Event } from './event';
#Component({
selector: 'my-event',
templateUrl: './event.component.html'
})
export class EventComponent {
#Input() event: Event;
}
export interface Event
{
id: number;
hidden: boolean;
}
**HTML:** app.component.html (You can use that code where you need to.)
<button (click)="addEvent()">Add event</button>
<div *ngFor="let event of events">
<my-event *ngIf="!event.hidden" [event]="event"></my-event>
</div>
Note: Here [event] is the #Input for your custom control and *ngFor is the loop.
TS: app.component.ts
import { Component } from '#angular/core';
import { Event } from './event';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
events: Array<Event> = [1, 2, 3, 4, 5].map(i => ({id: i, hidden: true}));
addEvent() {
const event = this.events.find(e => e.hidden);
if (event) {
event.hidden = false;
}
}
}
Note: Please check the code and let me know. This code is also available in Stackblitz
LINK .
You can iterate with ngFor and render many times the child component by his selector.
components = [1, 2, 3, 4, 5];
<ng-container *ngFor="let component of components">
<child-component></child-component>
</ng-container>

execute a function evertime ngFor is finished displaying data

I am trying to call a function everytime my ngFor is done loading data from my API.
but the callback is only triggering on first load of the ngFor.
how can I execute the callback everytime my ngFor is changed;
I used this answer: https://stackoverflow.com/a/38214091/6647448
here is what I have so far...
HTML
<button class="btn" (click)="changeDate()"></button>
<div *ngFor="item of items; let last = last">
<div>{{item}}{{last ? ngForAfterInit() : ''}}</div>
</div>
TS
this.ngForIsFinished = true;
ngForAfterInit() {
if (this.ngForIsFinished) {
this.ngForIsFinished = false;
}
}
changeDate() {
// this is where i trigger my ngFor to change its content
}
The person who answered said that you just need to set the ngForIsFinished back to true but I am having a hard time where to set it on my code.
Try to use ChangeDetectionStrategy.OnPush and last variable of ngFor. Because ChangeDetectionStrategy.Default always checks methods, so fooMethod(...) will be called multiple items:
<div *ngFor = "let title of iterated; let i=index; let last=last">
{{last ? fooMethod(last) : '' }}
</div>
yourComponent.ts:
import { Component, ChangeDetectionStrategy } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
name = 'Angular 4';
iteratedData = [
{"title":"test1","description":"foo","name":"name1"},
{"title":"test2","description":"foo","name":"name2"},
{"title":"test3","description":"foo","name":"name3"},
{"title":"test4","description":"foo","name":"name4"}
];
fooMethod(v) {
if (v)
console.log(`This is from the method ${v}`);
}
}
UPDATE:
After you've loaded data you should call slice method as Angular 2 change detection strategy doesn't check the contents of arrays or object. So try to create a copy of the array after mutation:
this.iteratedData.push(newItem);
this.iteratedData = this.iteratedData.slice();
OR:
constructor(private changeDetectorRef: ChangeDetectorRef)
And you can call a method markForCheck to trigger a changeDetection:
this.iteratedData.push(newItem);
this.changeDetectorRef.markForCheck();
Here is the example:
Component:
import { Component } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
name = 'Angular 5';
data:any = [];
ngAfterViewInit(){
this.changeDate();
}
ngForAfterInit(valuue) {
alert('FOR LOOP COMPLETED : '+ valuue);
}
ChangeDate(){
let i = 0;
setInterval(()=>{
this.data=[];
for(var j=0; j<10; j++){
this.data.push("TEST DATA "+ j+" SET AT : "+i);
};
i+=1;
},7000);
}
}
html:
<div *ngFor="let item of data; let last=last;">
{{item}} {{last? ngForAfterInit(item) :''}}
</div>
SetInterval is like your API which will assign data.This is working for me
https://stackblitz.com/edit/pokemon-app-4n2shk

How to check if two strings are almost equal with Angular

I have a quiz app made with Ionic and Angular 4. User have to submit answer, I check if it's the same as the good answer or not.
I would like to check string correspondence, and handle event according to the correspondence between good answer and user answer.
In Exemple :
If the answer is 'azerty', and he wrote 'mzerty', I would like to allow him to continue.
If user wrote 'qwerty', or something too different, he failes.
A simple demo with Levenstein distance would be like that:
Typescript
import { Component, OnInit } from '#angular/core';
import { FormBuilder, FormGroup } from '#angular/forms';
import { Observable } from 'rxjs/Observable';
import { map } from 'rxjs/operators';
import levenshtein from 'fast-levenshtein';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent implements OnInit {
form: FormGroup;
score$: Observable<number>;
constructor(private fb: FormBuilder) { }
ngOnInit() {
this.initForm();
this.initScore();
}
private initForm() {
this.form = this.fb.group({
str1: '',
str2: '',
});
}
private initScore() {
this.score$ = this.form
.valueChanges
.pipe(
map(({str1, str2}) => levenshtein.get(str1, str2))
);
}
}
HTML
<form [formGroup]="form">
<input type="text" formControlName="str1">
<br>
<br>
<input type="text" formControlName="str2">
</form>
<br>
<div>
Levenshtein score: {{ score$ | async }}
</div>
Stackblitz live demo: https://stackblitz.com/edit/angular-usydyu
You can simply create a method which will return you how many characters are matched. so on basis of matched characters and the length of string you can decide weather its a good answer or not.
function checkEq(str1, str2){
var arr1 = str1.split('');
var arr2 = str2.split('');
var counter = 0;
for(var i=0;i<arr1.length;i++){
if(arr1[i]==arr2[i]){
counter++;
}
}
return counter;
}

Force update of a property binding in angular 2

Consider the following component
import {Component} from 'angular2/core'
#Component({
selector: 'my-app',
providers: [],
template: `
<div>
<h3>Input with two decimals</h3>
<input type="text"
[value]="formattedN"
(change)="nChanged($event.target.value)"/>
</div>
`,
directives: []
})
export class App {
private n: number = 0;
private formattedN: string = "0.00"
public nChanged(value) {
this.n = parseFloat(value);
this.formattedN = this.n.toFixed(2);
}
}
The input should always be a number with two decimals. That is however not always the case. Try removing the last zero, the field is not changed to 0.00 which is what I want. I understand it doesn't work because the formattedN value is not updated, which means that the property binding is not updated, hence the value of the input is not updated.
Run the example in plunker: http://plnkr.co/edit/Vyi4RKladslrdZslZQhm
Does anyone have a nice solution for this problem?
Is this Answer what you're trying to accomplish????
//our root app component
import {Component} from 'angular2/core'
#Component({
selector: 'my-app',
providers: [],
template: `
<div>
<input type="text" [(ngModel)]="formattedN" (change)="nChanged($event.target.value)"/>
</div>
`,
directives: []
})
export class App {
private n: number = 0;
private formattedN: string = "0.00"
constructor() {
this.name = 'Angular2'
}
public nChanged(value) {
console.log(value);
this.n = parseFloat(value);
console.log(this.n);
this.formattedN = this.n.toFixed(2)
console.log(this.formattedN);
}
}

Categories

Resources