Anuglar2- model data jumps - javascript

I'm making a Anuglar2 application for people to log how many hours they put into each course, assignments, etc., per week (though there we be more advance options later on).
Right now i have a table which lists out how many hours you spent on each course per day. I want the user to be able to just edit and change any values as he/she goes along. So i have a two dimensional array ( named data), and i attach models to each element in the array, which i then represent as a input element.
Everything works fine, but there is a weird bug. Whenever you delete the value in the input box and re-enter new data, it jumps to the next input element. I cant figure out why. Anyone got any ideas ?
Example in GIF format (sorry for the quality had to use a converter)
gif link in case you cant see it on Stack
home.html
<!-- Table -->
<div class="row">
<div class="col-lg-12 col-sm-12">
<h1 class="page-header">Weekly Status Report</h1>
<table class="table table-responsive table-stripped table-bordered table-hover" *ngIf="courses">
<thead>
<tr class="btn-primary">
<th>Course</th>
<th>Time</th>
<th>SM #</th>
<th>Est</th>
<th *ngFor="let weekday of week">
{{weekday | date:'EEEE'}}
</th>
<th>Total</th>
</tr>
</thead>
<tbody *ngFor="let course of courses; let i = index;">
<tr>
<td >
{{course.name}}
</td>
</tr>
<tr class="alert-info">
<!-- Account for the title row -->
<td>
Date
</td>
<td >
Documentation Type
</td>
<td>
Documentation Text
</td>
</tr>
<tr *ngFor="let content of course.hours" class="alert-warning">
<td>
{{content.day| date}}
</td>
<td [attr.colspan]="3">
{{ title }}
</td>
<td [attr.colspan]="7">
</td>
</tr>
<tr class="alert-success">
<td></td>
<td></td>
<td></td>
<th></th>
<!-- DATA ARRAY -->
<th *ngFor="let d of data[i]; let j = index;">
<div>
<input maxlength="3" size="3" [(ngModel)]="data[i][j]" />
</div>
</th>
<td></td>
</tr>
</tbody>
<tfoot class="btn-primary">
<tr>
<td>Report Totals(all courses)</td>
<td></td>
<td></td>
<td></td>
<td *ngFor="let pivot of [0,1,2,3,4,5,6]">
{{ getSum(pivot) }}
</td>
<td>
{{getTotal()}}
</td>
</tr>
</tfoot>
</table>
</div>
</div>
home.component.ts
week: Array<any> = new Array();
data: Array<any> = new Array();
constructor(private hm: HomeService) {
let subscription = this.hm.getFormat(this.courses, this.week)
.subscribe(
value => {
this.data.push(value);
},
error => {
console.log(error)
},
() => {
console.log("Formated");
}
);
}
home.service.ts
getFormat(courses: any, weekdays: any): Observable<any> {
return new Observable(observer => {
// For all courses
courses.forEach(c => {
// temp week
let week: Array<any> = new Array();
// For each weekday
weekdays.forEach(w => {
let found: boolean = false;
// get hours spent on course
c.hours.forEach(h => {
let hour:Date = new Date (h.day);
// if the hours spent match up on the day push it to the week array
if (w.day.getTime() === hour.getTime()) {
week.push(h.duration);
found = true
}
});
// If no time was found on this take, push a empty string.
if (!found) {
week.push(0);
}
});
// push that week to the component
observer.next(week);
});
observer.complete();
});
}

This is happening because of how data is tracked.
When you change the value from the array it will become another value, thus not being able to track it as it will keep track of the older value.
Your case is similar to the following bad code:
#Component({
selector: 'my-app',
template: `
<div>
<table>
<tr *ngFor="let dim1 of values; let dim1Index = index">
<td *ngFor="let dim2 of dim1; let dim2Index = index">
<input type="text" [(ngModel)]="values[dim1Index][dim2Index]" />
</td>
</tr>
</table>
</div>
`,
})
export class App {
values: string[][];
constructor() {
this.values = [
['a1', 'b1', 'c1'],
['a2', 'b2', 'c2'],
['a3', 'b3', 'c3']
];
}
}
There are multiple solutions:
Solution 1: Using an object to wrap the value
Look at the values array.
#Component({
selector: 'my-app',
template: `
<div>
<table>
<tr *ngFor="let dim1 of values; let dim1Index = index">
<td *ngFor="let dim2 of dim1; let dim2Index = index">
<input type="text" [(ngModel)]="values[dim1Index][dim2Index].value" />
</td>
</tr>
</table>
<br/>
{{ values | json }}
</div>
`,
})
export class App {
values: string[][];
constructor() {
this.values = [
[{ value: 'a1' }, { value: 'b1' }, { value: 'c1' }],
[{ value: 'a2' }, { value: 'b2' }, { value: 'c2' }],
[{ value: 'a3' }, { value: 'b3' }, { value: 'c3' }]
];
}
}
Solution 2: Using a custom trackBy function for *ngFor
Note that this solution is based on trackByIndex function which returns the index of the item, thus the item being located by its index instead of its value.
#Component({
selector: 'my-app',
template: `
<div>
<table>
<tr *ngFor="let dim1 of values; let dim1Index = index; trackBy: trackByIndex">
<td *ngFor="let dim2 of dim1; let dim2Index = index; trackBy: trackByIndex">
<input type="text" [(ngModel)]="values[dim1Index][dim2Index]" />
</td>
</tr>
</table>
<br/>
{{ values | json }}
</div>
`,
})
export class App {
values: string[][];
constructor() {
this.values = [
['a1', 'b1', 'c1'],
['a2', 'b2', 'c2'],
['a3', 'b3', 'c3']
];
}
public trackByIndex(index: number, item) {
return index;
}
}
Therefore in your code you can use:
<tbody *ngFor="let course of courses; let i = index; trackBy: trackByIndex">
next
<th *ngFor="let d of data[i]; let j = index; trackBy: trackByIndex">
and finally define trackByIndex in your component:
public trackByIndex(index: number, item) {
return index;
}

Related

Fill Json table with checkbox in react and check them according to JSON data

I am creating a table in React from a JSON like this:
[
{
"Id_side": 123,
"Name_side": "R4",
"Name_cycle": "C1"
},
{
"Id_side": 345,
"Name_side": "M1",
"Name_cycle": "C2"
},
{
"Id_side": 567,
"Name_side": "V5",
"Name_cycle": "C3"
},
{
"Id_side": 45,
"Name_side": "U4",
"Name_cycle": "C4"
}
]
The table, I am rendering it like this:
import tableData from "./actions/tableData.json"
const BrandTable = () => {
let tb_headers = tableData.map((item)=>{
return(
<td key={item.Id_side}>{item.Name_cycle}</td>
)
})
// this function is only for testing, I know it does not achieve anything.
function renderChecks() {
console.log("checkbox")
for (var i = 0; i < tableData.length; i++){
return <td><input type="checkbox" value="Test" /></td>
} }
let tb_data = tableData.map((item)=>{
return(
<tr key={item.Id_side}>
<td>{item.Name_side}</td>
{renderChecks()}
</tr>
)
})
return(
<table id="table">
<thead>
<tr>
<td></td>
{tb_headers}
</tr>
</thead>
<tbody>
{tb_data}
</tbody>
</table>
)
};
export default BrandTable;
For now I am only able to get a table like this:
Table1
but what I'm looking to do is to make a table with checkboxes in all the cells and have them checked as they come in the JSON.
For example, according to the JSON I show above the table should look like this:
table2
I need that each header has checkboxes in each of the cycles that exist and that these are activated or not depending on whether they come together in the json.
You can do it like below :
const cells = ['C1', 'C2', 'C3', 'C4'];
export default function App() {
return (
<div>
<table border="1" style={{ width: '100%' }}>
<thead>
<th></th>
<th> C1</th>
<th> C2</th>
<th> C3</th>
<th> C4</th>
</thead>
<tbody>
{data.map((item) => {
return <TableRow data={item} key={item.id} />;
})}
</tbody>
</table>
</div>
);
}
const TableRow = ({ data }) => {
return (
<tr>
<td> {data.Name_side} </td>{' '}
{cells.map((cell) => {
return (
<td key={cell}>
<input
type="checkbox"
checked={data.Name_cycle === cell}
// add event listener
/>
</td>
);
})}
</tr>
);
};
Example : Working demo
First step would be to create a basic map of Name_side & Name_cycles
var map = data.reduce((a,b) => {
a[b.Name_side] = a[b.Name_side] || [];
a[b.Name_side].push(b.Name_cycle);
return a;
}, {});
This would give an output like
[object Object] {
M1: ["C2"],
R4: ["C1"],
U4: ["C4", "C2"],
V5: ["C3", "C1", "C2"]
}
Now we have unique Name_side mapped to the Name_cycle values. We can now iterate on the object keys & create the rows.
<tbody>
{Object.keys(map).map((row) => (
<tr>
<td>{row}</td>
{cycleNames.map((cell) => {
const checked = map[row].includes(cell);
return (
<td>
<input checked={checked} type="checkbox" value="Bike" />{" "}
{cell}
</td>
);
})}
</tr>
))}
</tbody>

Reactive Form is always invalid when using dynamic patterns validation

Reactive Form having dynamic patten validation is always invalid on first load.
Even after I edit it still invalid.
This is happening only when I am adding dynamic pattern matching validation.
Here is my use case :
On UI there is table and edit button in last row
Initially No formcontrols are visible
User Clicks on edit button then form control gets appear.
Refer this stackblitz
Below is code
<form [formGroup]="data_form">
<table class="table table-border">
<thead>
<th>
name
</th>
<th>
age
</th>
<th><button class="btn btn-primary ">Save</button></th>
</thead>
<tbody>
<ng-container *ngFor='let item of data;let j = index'>
<tr>
<ng-container *ngFor="let td of keys;let i = index">
<ng-container>
<td *ngIf="td !=='isEditable'">
{{item[td]}}
<input [formControlName]="getControlName(j,i)" *ngIf="item.isEditable" type="text" name="" id="">
</td>
</ng-container>
</ng-container>
<td>
<button (click)="item.isEditable = true"> Edit</button>
</td>
</tr>
</ng-container>
</tbody>
</table>
</form>
ts code :
import { Component } from "#angular/core";
import { FormControl, FormGroup, Validators } from "#angular/forms";
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
name = "Angular";
data_form = new FormGroup({});
public data;
keys;
ngOnInit() {
this.data = [
{
name: "Sachin",
age: 27,
isEditable: false
},
{
name: "Gopal",
age: 27,
isEditable: false
},
{
name: "Pankaj",
age: 24,
isEditable: false
}
];
this.keys = Object.keys(this.data[0]);
this.data.forEach((element, j) => {
this.keys.forEach((k, i) => {
this.data_form.addControl(
"name_" + j + "_" + i,
new FormControl(element[k], [Validators.required, Validators.pattern(/^[.\d]+$/)])
);
});
});
}
log() {
console.log(this.data);
}
getControlName(j, i) {
return "name_" + j + "_" + i;
}
}
Thanks in Advance.
EDIT:
Various Patterns that I have used :
Validators.pattern("^[a-zA-Z0-9 _/]+$")
Validators.pattern(/^[.\d]+$/)
Validators.pattern(/^(yes|no)$/i)
Validators.pattern(/^(0[1-9]|1[0-2])\/(0[1-9]|1\d|2\d|3[01])\/(19|20)\d{2}$/)
The problem is actually because the pattern is being used for all the controls. I have refactored your code using FormArray and it seems your pattern work fine
Below is my approach
TS File
constructor(private fb: FormBuilder) {}
patterns = [
/^[.\d]+$/,
/^(yes|no)$/i,
/^[a-zA-Z0-9 _/]+$/,
/^(0[1-9]|1[0-2])\/(0[1-9]|1\d|2\d|3[01])\/(19|20)\d{2}$/
];
data = [
{
name: "Sachin",
age: 27,
isEditable: false
},
{
name: "Gopal",
age: 27,
isEditable: false
},
{
name: "Pankaj",
age: 24,
isEditable: false
}
];
keys = [...new Set(this.data.map(item => Object.keys(item)).flat())];
keyPattern = this.keys.map(item => ({
key: item,
pattern: this.patterns.find(pattern =>
this.data.every(i => pattern.test(i[item]))
)
}));
data_form = this.fb.group({
persons: this.fb.array(
this.data.map(item =>
this.fb.group(
this.keyPattern.reduce(
(prev, { key, pattern }) => ({
...prev,
[key]: [
item[key],
[Validators.required, Validators.pattern(pattern)]
]
}),
{}
)
)
)
)
});
get persons(): FormArray {
return this.data_form.get("persons") as FormArray;
}
toggleEdit(j) {
const currentEditStatus = this.persons.controls[j].get("isEditable").value;
this.persons.controls[j].get("isEditable").setValue(!currentEditStatus);
}
HTML
<form [formGroup]="data_form">
<table class="table table-border">
<thead>
<tr>
<th> name </th>
<th>age </th>
<th><button class="btn btn-primary ">Save</button></th>
</tr>
</thead>
<tbody formArrayName='persons'>
<ng-container *ngFor='let item of persons.controls;let j = index'>
<tr [formGroupName]='j'>
<ng-container *ngIf="!item.value.isEditable; else editable">
<td>{{ item.value.name }}</td>
<td>{{ item.value.age }}</td>
</ng-container>
<ng-template #editable>
<td><input formControlName='name'></td>
<td><input formControlName='age'></td>
</ng-template>
<td>
<button (click)="toggleEdit(j)">
{{ !item.value.isEditable ? "Edit": "Cancel"}}
</button>
</td>
</tr>
</ng-container>
</tbody>
</table>
</form>
See below fork Demo

Are there a way to overwrite increment/decrement-handling for a input of type number?

What i would like to do is manipulate the built-in html input buttons for increment and decrement of numbers. If there is a "vue-way" of doing this, that would of course be preferred.
First of all i'm working on a small vue-app that i've created for learning Vue, what i got now is a Vuex store which contains the state and methods for a shopping cart. I have bound the value item.itemCount seen in the image, to the value of the inputfield. But i would like the increment/decrement buttons to actually update the vuex-state in a proper way.
<input
class="slim"
type="number"
v-model.number="item.itemCount"
/>
I understand that i can just stop using a input-field, and create my own "count-view" + two buttons, but i'm curious if it's possible to do something like this.
UPDATE
Shoppingcart.vue
<template>
<div class="sliding-panel">
<span class="header">Shopping Cart</span>
<table>
<thead>
<th>Item Name</th>
<th>Count</th>
<th>Remove</th>
</thead>
<transition-group name="fade">
<tr v-for="item in items" :key="item.id">
<td>{{ item.name }}</td>
<td>
<input class="slim" type="number" v-model.number="item.itemCount" />
</td>
<td><button #click="removeProductFromCart(item)">Remove</button></td>
</tr>
</transition-group>
<tr>
Current sum:
{{
sum
}}
of
{{
count
}}
products.
</tr>
</table>
</div>
</template>
<script>
import { mapState, mapActions } from "vuex";
export default {
computed: mapState({
items: (state) => state.cart.items,
count: (state) => state.cart.count,
sum: (state) => state.cart.sum,
}),
methods: mapActions("cart", ["removeProductFromCart"]),
};
</script>
<style>
</style>
First you don't need to "overwrite increment/decrement handling" in any way. You have the <input> so you need to handle all user inputs changing value - be it inc/dec buttons or user typing value directly...
Proper way of updating Vuex state is by using mutations. So even it's technically possible to bind v-model to some property of object stored in Vuex (as you do), it's not correct "Vuex way"
If there is only single value, you can use computed prop like this:
computed: {
myValue: {
get() { return this.$store.state.myValue },
set(value) { this.$store.commit('changemyvalue', value )} // "changemyvalue" is defined mutation in the store
}
}
...and bind it to input
<input type="number" v-model="myValue" />
But because you are working with array of values, it is more practical to skip v-model entirely - in the end v-model is just syntactic sugar for :value="myValue" #input="myValue = $event.target.value"
In this case
<input type="number" :value="item.itemCount" min="1" #input="setItemCount({ id: item.id, count: $event.target.value})"/>
...where setItemCount is mutation created to change item count in the cart
Working example:
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
items: [
{ id: 1, name: 'Socks', itemCount: 2},
{ id: 2, name: 'Trousers', itemCount: 1}
]
},
mutations: {
setItemCount(state, { id, count }) {
const index = state.items.findIndex((item) => item.id === id);
if(index > -1) {
const item = state.items[index]
item.itemCount = count;
console.log(`Changing count of item '${item.name}' to ${count}`)
}
}
}
})
const app = new Vue({
store,
template: `
<div>
<span>Shopping Cart</span>
<table>
<thead>
<th>Item Name</th>
<th>Count</th>
<th>Remove</th>
</thead>
<transition-group name="fade">
<tr v-for="item in items" :key="item.id">
<td>{{ item.name }}</td>
<td>
<input type="number" :value="item.itemCount" min="1" #input="setItemCount({ id: item.id, count: $event.target.value})"/>
</td>
<td><button>Remove</button></td>
</tr>
</transition-group>
</table>
</div>
`,
computed: {
...Vuex.mapState({
items: (state) => state.items,
})
},
methods: {
...Vuex.mapMutations(['setItemCount'])
}
})
app.$mount("#app")
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vuex/3.5.1/vuex.min.js"></script>
<div id="app"> </div>

Using an ngFor to traverse a 2 dimensional array

I've been beating my head up against the wall on this one for a while but I finally feel close. What I'm trying to do is read my test data, which goes to a two dimensional array, and print its contents to a table in the html, but I can't figure out how to use an ngfor to loop though that dataset
Here is my typescript file
import { Component } from '#angular/core';
import { Http } from '#angular/http';
#Component({
selector: 'fetchdata',
template: require('./fetchdata.component.html')
})
export class FetchDataComponent {
public tableData: any[][];
constructor(http: Http) {
http.get('/api/SampleData/DatatableData').subscribe(result => {
//This is test data only, could dynamically change
var arr = [
{ ID: 1, Name: "foo", Email: "foo#foo.com" },
{ ID: 2, Name: "bar", Email: "bar#bar.com" },
{ ID: 3, Name: "bar", Email: "bar#bar.com" }
]
var res = arr.map(function (obj) {
return Object.keys(obj).map(function (key) {
return obj[key];
});
});
this.tableData = res;
console.log("Table Data")
console.log(this.tableData)
});
}
}
Here is my html which does not work at the moment
<p *ngIf="!tableData"><em>Loading...</em></p>
<table class='table' *ngIf="tableData">
<tbody>
<tr *ngFor="let data of tableData; let i = index">
<td>
{{ tableData[data][i] }}
</td>
</tr>
</tbody>
</table>
Here is the output from my console.log(this.tableData)
My goal is to have it formatted like this in the table
1 | foo | bar#foo.com
2 | bar | foo#bar.com
Preferably I'd like to not use a model or an interface because the data is dynamic, it could change at any time. Does anyone know how to use the ngfor to loop through a two dimensional array and print its contents in the table?
Like Marco Luzzara said, you have to use another *ngFor for the nested arrays.
I answer this just to give you a code example:
<table class='table' *ngIf="tableData">
<tbody>
<tr *ngFor="let data of tableData; let i = index">
<td *ngFor="let cell of data">
{{ cell }}
</td>
</tr>
</tbody>
</table>

Cannot update template in angular 2 when data is changed

I have a form to insert data to a table. When i delete data from table , the data is removed but the table row is not deleted.
It appears that the data is two way binding so data is removed but the html structure remains same.
Component
export class HomeComponent implements OnInit {
studentform = new FormGroup({
id: new FormControl(),
name: new FormControl(),
address: new FormControl()
});
student: Student[]=[];
std: Student= new Student();
constructor(public homeService: HomeService){ }
OnInit(){
this.getData();
}
getData(){
this.student = this.homeService.GetData();
}
onEdit(id:number){
console.log("Edit:" + id);
}
onDelete(id:number){
this.homeService.delete(id);
this.getData();
}
Save(model:Student){
this.homeService.SaveData(model);
this.studentform.reset();
this.getData();
}
}
Service
#Injectable()
export class HomeService{
student:Student[]=[];
SaveData(model:Student){
this.student.push(model);
}
GetData(){
return this.student;
}
delete(id:number){
for(var i=0;i<this.student.length;i++){
if(this.student[i].id==id){
delete this.student[i]
}
}
}
}
Template
div class="col-md-6">
<h5> Lists </h5>
<table>
<th>ID </th>
<th>Name </th>
<th>Address </th>
<th>Edit </th>
<th>Delete </th>
<tr *ngFor="let x of student">
<td> {{ x.id }} </td>
<td> {{ x.name }} </td>
<td> {{ x.address }} </td>
<td (click)="onEdit(x.id)"> Edit </td>
<td (click)="onDelete(x.id)"> Delete </td>
</tr>
</table>
Help me update the html (template) when data changes.
This is the result after I click table : data is gone but row remains
You are actually deleting the object but it's reference remain in the primary array. Try this instead :
delete(id:number){
for(var i=0;i<this.student.length;i++){
if(this.student[i].id==id){
this.student.splice(i, 1); //delete this.student[i]
break;
}
}
}
delete this.student[i] is not the correct way to remove an element from an array in this situation. You need to use splice().
this.student.splice(i, 1);
Also you should do a truthy check when displaying object fields in the template. Otherwise you will get errors like that. Usually safe-navigation operator(?) will do the trick.
Example:
<td> {{ x?.id }} </td>

Categories

Resources