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
Related
So i am passing an object having length of data and the data itself which is an array of objects
Now what is happening is that when I change the data in the child component and reload the page the data is still showing the edited property
To clarify more see the following screenshots:
When we navigate to the route for the first time there would be no modified data:
When I edit the data::
As you can see on the right side finalAmount property is added and its value is initialised
Now when renavigate to the same component finalAmount is set to 5
The property itself should not be present in the data as I am getting it from the parent
These are the relevant files:
stepper.component.html (Parent Component)
<mat-step [stepControl]="thirdFormGroup" >
<ng-template matStepLabel>Prepare Invoice</ng-template>
<ng-template matStepContent>
<div class="container-fluid">
<app-create-invoice [selectedCustomersInfo]="selectedCustomerData"
[selectedProductsData]="{lengthOfData:cartItems.length , selectedProducts:cartItems}"></app-create-invoice>
</div>
</ng-template>
</mat-step>
stepper.component.ts:
#Component({
selector: 'app-stepper',
templateUrl: './stepper.component.html',
styleUrls: ['./stepper.component.scss'],
providers: [
{
provide: STEPPER_GLOBAL_OPTIONS,
useValue: { showError: true }
}
]
})
export class StepperComponent implements OnInit, OnDestroy {
getProductsSubscription = new Subscription
cartItems: ProductDataModel[] = []
selectedCustomerData: any
products = this._formBuilder.group({
firstCtrl: ['', Validators.required],
});
thirdFormGroup = this._formBuilder.group({
thirdCtrl: ['', Validators.required],
});
stepperOrientation: Observable<StepperOrientation>;
constructor(private _formBuilder: FormBuilder, breakpointObserver: BreakpointObserver, private cartService: CartService) {
this.stepperOrientation = breakpointObserver
.observe('(min-width: 800px)')
.pipe(map(({ matches }) => (matches ? 'horizontal' : 'vertical')));
}
ngOnDestroy(): void {
console.log("Stepper destroyed");
this.getProductsSubscription.unsubscribe()
}
ngOnInit(): void {
this.getProductsSubscription = this.cartService.getProducts().subscribe((items) => {
this.cartItems = [...items]
console.log("Cart items::", this.cartItems)
})
}
setCustomerData(customerData: any) {
this.selectedCustomerData = customerData
}
}
create-invoice.component.html (Child Component):
<div class="table-responsive">
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Product Category</th>
<th scope="col">Sub Category</th>
<th scope="col">Master Category</th>
<th scope="col">Product Weight</th>
<th scope="col">Price</th>
<th scope="col">Labour</th>
<th scope="col">SGST (In %)</th>
<th scope="col">CGST (In %)</th>
<th scope="col">Discount</th>
<th scope="col">Final Amount</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of _selectedProductsData;index as i">
<th scope="row">{{i+1}}</th>
<td>{{item.productCategory}}</td>
<td>{{item.subCategory}}</td>
<td>{{item.masterCategory}}</td>
<td>{{item.productWeight}} gms</td>
<td>
<input class="form-control priceInput" type="number" id="{{item.productGuid}}-price"
min="0" (input)="item.price = getValue($event,item.id,'price')" #price
placeholder="Enter Price">
</td>
<td>
<input class="form-control labourInput" type="number" id="{{item.productGuid}}-labour"
min="0" (input)="item.labour = getValue($event,item.id,'labour')" #labor
placeholder="Enter Labour">
</td>
<td>
<input class="form-control sgstInput" type="number" id="{{item.productGuid}}-SGST"
min="0" (input)="item.SGST = getValue($event,item.id,'sgst')" #sgst
placeholder="Enter SGST">
</td>
<td>
<input class="form-control cgstInput" type="number" id="{{item.productGuid}}-CGST"
min="0" (input)="item.CGST = getValue($event,item.id,'cgst')" #cgst
placeholder="Enter CGST">
</td>
<td>
<input class="form-control discountInput" type="number" min="0"
(input)="item.discount = getValue($event,item.id,'discount')" #discount
id="{{item.productGuid}}-discount" placeholder="Enter Discount">
</td>
<td>
{{ item.finalAmount ?? 0 }}
<input class="form-control" type="hidden" [value]="item.finalAmount ?? 0">
</td>
</tr>
</tbody>
</table>
</div>
create-invoice.component.ts:
#Component({
selector: 'app-create-invoice',
templateUrl: './create-invoice.component.html',
styleUrls: ['./create-invoice.component.scss']
})
export class CreateInvoiceComponent implements OnInit,OnDestroy {
_selectedCustomersInfo:any
_selectedProductsData:InvoiceProductDataModel[] = []
totalWeight = 0
totalDiscount = 0
totalGST = 0
totalAmountWithGST = 0
currentDate:Date = new Date()
#Input() set selectedProductsData(productsData: {lengthOfData:number,selectedProducts:ProductDataModel[]}) {
this.totalWeight = 0
this.totalDiscount = 0
this.totalAmountWithGST = 0
this.totalGST = 0
var temp = Object.assign({},productsData)
this._selectedProductsData = []
this._selectedProductsData = [...temp.selectedProducts]
// this._selectedProductsData = [...productsData.selectedProducts]
this._selectedProductsData.forEach((product) => {
this.totalWeight += product.productWeight
})
console.log(this._selectedProductsData)
}
#Input() set selectedCustomersInfo(customerInfo: any) {
this._selectedCustomersInfo = customerInfo
}
constructor() { }
ngOnDestroy(): void {
console.log("Create Invoice Destroyed!!")
this._selectedProductsData = []
}
ngOnInit(): void {
}
getValue(event: Event, productId:number, valueOf:string): number {
let product = this._selectedProductsData.find(item => item.id === productId)
if (product) {
switch (valueOf) {
case 'labour':
product.labour = Number((event.target as HTMLInputElement).value)
this.setFinalAmountOfEachProduct(product)
this.setTotalAmountWithGST()
break
case 'price':
product.price = Number((event.target as HTMLInputElement).value)
this.setFinalAmountOfEachProduct(product)
this.setTotalAmountWithGST()
break
case 'sgst':
product.SGST = Number((event.target as HTMLInputElement).value)
this.setFinalAmountOfEachProduct(product)
this.setTotalAmountWithGST()
this.setTotalGST()
break
case 'cgst':
product.CGST = Number((event.target as HTMLInputElement).value)
this.setFinalAmountOfEachProduct(product)
this.setTotalAmountWithGST()
this.setTotalGST()
break
case 'discount':
product.discount = Number((event.target as HTMLInputElement).value)
this.setFinalAmountOfEachProduct(product)
this.setTotalDiscount()
this.setTotalAmountWithGST()
break
}
}
console.log(this._selectedProductsData.find(i => i.id == productId))
return Number((event.target as HTMLInputElement).value);
}
setFinalAmountOfEachProduct(product:InvoiceProductDataModel) {
product.finalAmount = 0
let partialSum = (product.labour ?? 0) + (product.price ?? 0) - (product.discount ?? 0)
let cgst = product.CGST ? partialSum * ((product.CGST ?? 100) / 100) : 0
let sgst = product.SGST ? partialSum * ((product.SGST ?? 100) / 100) : 0
product.totalGST = cgst + sgst
product.finalAmount = partialSum + cgst + sgst
}
setTotalDiscount() {
this.totalDiscount = 0
this._selectedProductsData.forEach((item)=> {
this.totalDiscount += item.discount ?? 0
})
}
setTotalAmountWithGST() {
this.totalAmountWithGST = 0
this._selectedProductsData.forEach((item)=> {
this.totalAmountWithGST += item.finalAmount ?? 0
})
}
setTotalGST() {
this.totalGST = 0
this._selectedProductsData.forEach((item)=> {
this.totalGST += item.totalGST ?? 0
})
}
}
And its not limited to finalAmount property it happens with other properties too like discount,sgst etc..
Any help will be appreciated
From what I've understood you've got a problem with some data that have been changed in parent but it's not updated in child component, that have received it thru #input(). If I am right, you should read about Angular lifecycle hooks: HERE. Basically what you need to do, is to implement change detection hooks
#Input() someInputData: any;
ngOnChanges(changes: SimpleChanges) {
this.doSomething(changes.someInputData.currentValue);
}
The alternative is to use getter-setter approach like that
private _someInputData: string;
#Input() set someInputData(value: any) {
this._someInputData = value;
this.doSomething(this._someInputData);
}
get someInputData(): any {
return this._someInputData;
}
Which approach is better? The short answer is I don't know :) I haven't measure performance difference, but:
ngOnChanges() will allow you to compare current and prev value
ngOnChanges() will track all inputs in comparison to getter-setter approach
However, there are some particular scenarios, usually with nested objects that tend to resist change detection in Angular, and for more elaborate solution see: SOLUTION
EDIT!
Based on your comments, now I understand your problem, however I do not feel competent enough to tell you, why exactly such thing is occurring, it's related to the fact how Objects are made. Basically while passing Object through #Input decorator, you're creating a copy of object itself with all of its references. TLDR: Object passed by #Input() still holds a reference to memory that holds some variable. In that case what you need to do, is to create a deep copy of object and all of Objects within this object, because otherwise the nested references are still there. I see, that you're trying to mitigate this behaviour by using Object.assign(), which to my knowledge should work. However instead of doing so, please try using another approach:
const tempClone = JSON.parse(JSON.stringify(productsData));
We can use structuredClone() to deep copy nested objects here in this case as my object having an array to a key
Refer the following screenshot as well:
Also JSON.parse(JSON.stringify(productsData)) has its own limitations
For more information refer these links:
https://stackoverflow.com/a/5344074/18480147
https://stackoverflow.com/a/10916838/18480147
I am trying to create a dialog box with a table inside of it. I already created it but then I was told to use mat-table instead of creating a table from scratch and ever since that, I have been stuck with this particular error. I'll share my code to make it more convenient. These are the files I have.
asm-prop.interface.ts
export interface Properties {
name: string;
value: number;
}
asm-prop.component.ts
import { Component, ViewChild } from '#angular/core';
import { ContextMenuComponent } from 'ngx-contextmenu';
import { MatDialogRef } from '#angular/material/dialog';
import { AssemblyFetchService } from 'src/app/assembly/assembly-fetch.service';
import { Properties } from './asm-prop.interface';
#Component({
selector: 'asm-prop',
templateUrl: 'asm-prop.component.html',
styleUrls: ['asm-prop.component.scss'],
providers: []
})
export class AsmPropComponent {
#ViewChild(ContextMenuComponent) public customContextMenu: ContextMenuComponent;
asmProps = null;
displayedColumns: string[] = ['Property Name', 'Value'];
storedProperties: Properties[];
public constructor(public asmPropDialogRef: MatDialogRef<AsmPropComponent>, public asmFetchServ: AssemblyFetchService) {
this.storedProperties = [
{ name: 'Number of Assemblies:', value: this.asmFetchServ.propertiesDialogComponentStatistics.propertiesDialogComponentStatistics.assembly_statistics.num_assemblies },
{ name: 'Number of Assembly Instances:', value: this.asmFetchServ.propertiesDialogComponentStatistics.assembly_statistics.num_assembly_instances },
{ name: 'Number of Part Instances:', value: this.asmFetchServ.propertiesDialogComponentStatistics.assembly_statistics.num_assembly_instances },
{ name: 'Number of Parts:', value: this.asmFetchServ.propertiesDialogComponentStatistics.assembly_statistics.num_parts },
{ name: 'Number of Contact Instances', value: this.asmFetchServ.propertiesDialogComponentStatistics.assembly_statistics.num_contacts },
{ name: 'Number of Contacts:', value: this.asmFetchServ.propertiesDialogComponentStatistics.assembly_statistics.num_contacts },
{ name: 'Nesting Level:', value: this.asmFetchServ.propertiesDialogComponentStatistics.assembly_statistics.nesting_level },
];
}
closeDialog(): void {
this.asmPropDialogRef.close();
}
}
asm-prop.component.html
<h1 mat-dialog-title> Properties of {{asmFetchServ.propertiesDialogComponentName}}</h1>
<div>
<table mat-table #table [dataSource]="storedProperties" class="mat-elevation-z8">
<!-- Position Column -->
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Property Name </th>
<td mat-cell *matCellDef="let element"> {{element.name}} </td>
</ng-container>
<!-- Name Column -->
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef> Value </th>
<td mat-cell *matCellDef="let element"> {{element.value}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef=" displayedColumns">
</tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<div mat-dialog-actions>
<button mat-button (click)="closeDialog()">Close</button>
</div>
For all of your information, I have already included MatTableModule in app.module.ts. What is the correct way to go about it?
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>
Let me start this off by saying I am very new to Angular2. So I am trying to load the list EMPS into my table. Here's my app.component.ts file which has the majority of my code so far
import { Component } from '#angular/core';
export class Emp {
name: string;
email: string;
office: string;
}
const EMPS: Emp[] = [
{ name: 'test1', email: 'test1#test.com', office: 'NY' },
{ name: 'test2', email: 'test2#test.com', office: 'LA' },
{ name: 'test3', email: 'test3#test.com', office: 'CHA' }
];
#Component({
selector: 'my-app',
template: `
<h1>{{ title }}</h1>
<div *ngFor="let d of data | async">
<table border=1>
<tr>
<td>
<h3>name: {{ d.name }}</h3>
</td>
<td>
<h3>email: {{ d.email }}</h3>
</td>
<td>
<h3>office: {{ d.office }}</h3>
</td>
<td>
</tr>
</table>
</div>
`,
styles: [`
`]
})
export class AppComponent {
title = 'TestTable';
emps = EMPS;
data = this.emps;
}
Here are the errors I am getting:
I am pretty lost at the moment and I have no clue what to do. Can anyone help me out?
Drop the use of the async pipe in your *ngFor. That should be used for iterating an Observable. You're using "static" content, not an Observable.
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;
}