Angular checkbox gets updated even though ngModel doesn't - html

I'm working on a shift arrangement app. In it I'm trying to create two tables that show which possible shifts each user has selected.
Both tables display the same data, but arrange it differently. Each table cell has a number of check-boxes that display the possible shifts for each person (in table 1) or the possible people for a shift (in table 2). A checkbox from table 1 that displays shift A option for person X will have the same data-bind as its equivalent checkbox in table 2, which displays person X option for shift A.
The purpose of this is to update the equivalent data in both tables simultaneously when the user couples a person with a shift. The problem: when a checkbox in table 1 is checked/unchecked, all of the check-boxes in table 2 gets checked/unchecked, as shown below:
Here is my template:
<div class="table-container" dir="ltr">
<h3>People</h3>
<table>
<thead>
<th>Name</th>
<th>Options</th>
</thead>
<tbody>
<tr *ngFor="let user of userPreferences">
<td>{{user.name}}</td>
<td>
<div *ngFor="let selection of userYesses[user.name]">
<mat-checkbox class="option-checkbox" dir="ltr" [(ngModel)]="selection.isSelected" name="usc">{{selection.option}}</mat-checkbox>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="table-container" dir="ltr">
<h3>Shifts</h3>
<table>
<thead>
<th>Time</th>
<th>Options</th>
</thead>
<tbody>
<tr *ngFor="let shift of totalShifts">
<td dir="ltr">{{shift.time}}</td>
<td>
<div *ngFor="let selection of shiftYesses[shift.time]">
<mat-checkbox class="option-checkbox" [(ngModel)]="selection.isSelected" name="syc">{{selection.name}}</mat-checkbox>
</div>
</td>
</tr>
</tbody>
</table>
</div>
And here is relevant component code:
this.userPreferences.forEach(u => {
this.userYesses[u.name] = [];
u.preferences.shifts.forEach(week => {
week.forEach(day => {
if (!day.shifts) return;
day.shifts.forEach(shift => {
if (!this.shiftYesses[`${day.date} ${shift.time}`]) this.shiftYesses[`${day.date} ${shift.time}`] = [];
if (shift.isSelected) {
let selection = new Selection(`${day.date} ${shift.time}`, u.name);
this.userYesses[u.name].push(selection);
this.shiftYesses[`${day.date} ${shift.time}`].push(selection);
}
});
});
});
});
The code seems alright to me, am I missing anything? Maybe it's a bug in Angular?
Thanks in advance!

In case anyone else experiences this issue -
After a few days of struggling with this, I stumbled upon this issue from Angular's git - https://github.com/angular/angular/issues/9230
I've read the following in kara's answer:
In the case that you don't want to register a form control, you currently have a few options:
1 - Use ngModel outside the context of a form tag. This will never throw.
<input [(ngModel)]="person.food">
After reading this, I switched the <form> tag into a <div> and everything works as expected now.

Related

Filling all input fields

There is a markup:
<div class="scroll">
<table class="table table-striped">
<thead>
<tr>
<th>Image</th>
<th>Name</th>
<th>Author</th>
<th>Year</th>
<th>ISBN</th>
<th>Price</th>
<th>Count</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let book of bookService.bookList">
<td><img src="../../{{book.ImagePath}}" width="100" height="150"></td>
<td>{{book.Name}}</td>
<td>{{book.Author}}</td>
<td>{{book.Year}}</td>
<td>{{book.ISBN}}</td>
<td>{{book.Price}}</td>
<td>{{book.Count}}</td>
<td>
<input type="text" name="Count" [(ngModel)]="Count">
<button class="btn btn-block btn-outline-success" (click)="onAdd(book, Count)"><i class="fa fa-plus"></i></button>
</td>
</tr>
</tbody>
The last column looks like this:
The problem is the following: when filling one TextBox, all the TextBoxes in the column are filled.
How can I solve this problem? Tried to give unique names to text fields and to thrust this cell in form, but all the same did not work.
You need to give unique name to the input field using angular template
<input [name]="'count' + i" >
Where i is the index from the *ngFor
But I think the major issue you have is that you need to bind book.Count instead of just Count
In latter case you'll have one variable called Count and you bind the same variable to all of the input fields. You need to attach the variable to the book itself so it is unique.
All your inputs have the same [(ngModel)]="Count" and the same name so if you update one, all of them will be updated
You can fix that if you have an array of count instead. So it will be something like
<tr *ngFor="let book of bookService.bookList; let i = index">
...
<input type="text" [name]="'count' + i" [(ngModel)]="count[i]">
People are giving you the HTML way, I'm giving you the Angular way : trackBy functions.
*ngFor="let book of bookService.bookList; trackBy: book.Name"
This one should work but I've never tested it.
The one should work in any case :
*ngFor="let book of bookService.bookList; trackBy: customTB"
customTB(item, index) {
return `${index}-${item.Name}`;
}
You can't use the same name for your inputs.
To fix this, you can add an id populate by the index of the loop *ngFor
Try this :
<tr *ngFor="let book of bookService.bookList; let i = index">
<input type="text" name="Count_${i}" [(ngModel)]="count">

nested array rendering using ngFor angular

I have a table where tr is repeated using *ngFor a collection, the collection which have a child members which should be binded as related table.
Note This is similar to nested array but only difference the html is not inside the nested array. I have referenced ngFor deep nested array already
typescript class
export class CreateBooking{
id: number;
bookings: BookingItem[]; // This should repeat as parent rows,
}
export class BookingItem {
id: number;
bookingNumber: number;
relatedVehicles: BookingItem[]; //This should be repeated as child rows
}
Html
<table>
<tr *ngFor="let booking of createBooking.bookings;let bookingIndex = index;">
<td>
<input type="text"
[ngModel]="booking.serialType" name="serialType--{{bookingIndex}}" />
</td>
</tr>
<tr> *ngFor="let v of booking[parent].relatedVehicles;
let i = index;
let parent = booking[something];"
<td>
<input [(ngModel)]="v.bookingNumber"
(ngModelChange)="serialNumberChangedIndex(parentIndex,$event,i)"
name="bookingNumber--{{i}}"/>
</td>
</tr>
</table>
Question 1 - How would i get the index of parent row on the child row
Question 2 - As a alternative i tried a inside which contains another table which have child items, But even with td colspan="allcolumns" i couldn't achieve expected output.
Challenges
- if i design the like this
<tr *ngFor="let booking of createBooking.bookings;let bookingIndex = index;">
<td>
<input type="text"
[ngModel]="booking.serialType" name="serialType--{{bookingIndex}}" />
</td>
<td>
<table>
<tbody>
<tr> *ngFor="let v of booking.RelatedVehicles;let i = index;"
<td>
<input [(ngModel)]="v.bookingNumber"
(ngModelChange)="serialNumberChangedIndex(bookingIndex ,$event,i)"
name="bookingNumber--{{i}}"/>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
This way i could generate the Nested array but the child collection will start at the 2nd column, I need the child element as full row.
Working sample code stackblitz
Please help me with some
expected output
From your code above, I do not see any relation in the second tag to the first one, where as your data structure I see the need for a nested tag. I think you should use a tag for the outer loop and a tag for the inner loop.

Angular 2+ : Condition based on previous index date value ngIf NgFor

Trying to format a table using Angular where:
If the day of the month changes, then you insert a new row which just contains the date (but also displays the data for that index value immediately below). If the day of the week is the same as before continue inserting the rows.
1) Clearly my code for accessing previous value in index is wrong but I can't seem to find anything clearly which helps.
2) I realise that my current code would compare the full datetime value and not just the day of the month (or week) but I am unsure how to do this in this scenario.
3) when the day changes and I try and insert the date line, I cannot get an additional new row formatted correctly which includes the data for that index value. I have tried playing around with various and combinations.
Please could someone help correct this code or point me in the right direction
Thanks
<table class="table-striped">
<thead>
<tr>
<td>day</td>
<td>time</td>
<td>region</td>
<td>event</td>
<td>period</td>
</tr>
</thead>
<tbody>
<tr *ngFor="let eco of eco_r ; let i = index">
<template [ngIf]="eco.date != eco[i-1].date">
<td colspan="5">{{eco.datetime| date:'longDate' }}</td>
<tr>
<td>{{eco.datetime | date:'EE'}}</td>
<td>{{eco.datetime | date:'shortTime'}}</td>
<td>{{eco.region}}</td>
<td>{{eco.event}}</td>
<td>{{eco.period}}</td>
</tr>
</template>
<template [ngIf]="eco.date == eco[i-1].date">
<td>{{eco.datetime | date:'EE'}}</td>
<td>{{eco.datetime | date:'shortTime'}}</td>
<td>{{eco.region}}</td>
<td>{{eco.event}}</td>
<td>{{eco.period}}</td>
</template>
</tr>
</tbody>
</table>
------------UPDATE ---------------
Following #Iancovici comments I have revised the following
i) corrected to reference eco_r[i-1].date
ii) Changed sql source to provide eco.day_of_month and eco.week_of_year to make it easier to reference and check conditions
iii) updated code to only check previous value when i > 0
I am still unable to get it to display a new line with the date AND also include the data for that value of i on a separate row where it formats correctly. Utilising as in my revised code puts all 5 values into the first column of the next row and messes put the table format. How should I resolve this please?
Thanks
<template [ngIf]="i >0 && eco.day_of_month != eco_r[i-1].day_of_month">
<tr><td colspan="5">{{eco.datetime| date:'longDate' }}</td>
</tr>
<tr> <td>{{eco.datetime | date:'EE'}}</td>
<td>{{eco.datetime | date:'shortTime'}}</td>
<td> {{eco.region}} </td>
<td>{{eco.event}}</td>
<td>{{eco.period}}</td>
</tr>
</template>
Should probbly be *ngIf not [ngIf] in general. But I see in your case it’s ok because you’re using an ng template which means instead of directive *ngIf it’s not a bonded property [ngIf]
Also you're accesing the same instance, should access index of array instead so..
change from
*ngIf="eco.date == eco[i-1].date">
to
*ngIf="eco.date == eco_r[i-1].date">
Update: Two more notes
Make sure you create a table, with tag
Don't be afraid to use paranthesis if conditonal expressions become more complex, so you can distinguish the two conditions.
Try this without the filters, then integrate the filters in.
<table>
<div *ngFor="let eco of eco_r; let i = index">
<div *ngIf="(i > 0) && (eco.day_of_month != eco_r[i-1].day_of_month)">
<tr>
<td colspan="5">{{eco.datetime }}</td>
</tr>
<tr>
<td>{{eco.datetime }}</td>
<td>{{eco.datetime }}</td>
<td> {{eco.region}} </td>
<td>{{eco.event}}</td>
<td>{{eco.period}}</td>
</tr>
</div>
</div>
</table>

Angular 2: how to hide a field in table and show when mouse hover?

I have this simple Angular2/html table:
<tr *ngFor="let item of mf.data; let i = index;">
<td><input type="checkbox" value="" [(ngModel)]="item.checked" ></td>
<td>{{i}}</td>
<td>{{item.name}}</td>
<td>{{item.password}}</td>
<td>{{item.number}}</td>
<td>Infos</td>
<td><button type="button" class="btn btn-success" [disabled]="!item.checked">start</button></td>
</tr>
I would like to hide field password with ***** or ....., but display clear text when mouse hovers in and hide when mouse hovers out. Basically, I is sensitive data and I don't want some one to be able to take a photo when I open my app. How can I do this ?
Use the mouseover and mouseout events and bind a display variable to them.
E.g.:
<td (mouseover)="displayPassword = true" (mouseout)="displayPassword = false">{{displayPassword? item.password : '****'}}</td>
maybe something like this could do the trick :
// not working <td title="{{item.password}}">{{item.password.split('').forEach(p => '*')}}</td>
<td title="{{item.password}}">******</td>
I have finally found something that work find. Fill free to improve this solution.
Basically I added an index to every of my rows.
<td align="center" (mouseover)="displayPassword(item.index)" (mouseout)="hiddePassword(item.index)">{{item.showpassword? item.password:'00000000000000000000000000000000000'}}</td>
In my app.component.ts, I added this two functions:
displayPassword(index){
console.log(index)
this.data[index].showpassword = true;
}
hiddePassword(index){
this.data[index].showpassword = false;
}
now it is working as expected ...

Angular UI-Bootstrap typeahead in table

Simple question really, but how can I setup the typeahead to work in a table that works off of a different table than my typeahead?
For Example, I have a foreign key in a table and I want to let the users select this key based on the respective NAME value in the foreign key's primary table.
Code Example:
<table class="table table-bordered table-hover table-striped rwd-table" id="page-wrap">
<thead>
<tr>
<th>Primary ID</th>
<th>Foreign Key (As Name)</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="p in PrimaryTable" id="display">
<td data-th="ID">{{p.PrimaryID}}</td>
<td>
<input id="ForeignKeyName"
type="text"
ng-model="p.ForeignKeyID"
uib-typeahead="f.ForeignKeyID as f.ForeignKeyName for f in ForeignKeyTable | filter:$viewValue"
class="form-control">
</td>
</tr>
</tbody>
With this example, I would want the users to see the "Foreign Key (As Name)" As the Name value instead of the ID value. The trick is that I also want the underlying value to be the ID and have it mapped to match the original value, as notified by the ng-model.
UPDATE: Another question I had that was in line with the previous one is how do I setup my ng-model to show the ForeignKeyTable.ForeignKeyName in place of the PrimaryTable.ForeignKeyID?
This would be similar ( I imagine) to how the uib-typeahead matches the ForeignKeyTable.ForeignKeyID and ForeignKeyTable.ForeignKeyName but with the two seperate tables?
What I would desire is to be able to put ng-model: PrimaryTable.ForeignKeyID as ForeignKeyTable.ForeignKeyName
First thing would be updating PrimaryKeyTable rows every time user selects value in typeahead. You'll have to catch selected item and manually assign its ForeignKeyId value to the row of PrimaryTable.
The way to do it is to add typeahead-on-select directive to your typeahead and bind it to the function that assigns the values for you.
It would look like this:
HTML
<tr ng-repeat="p in PrimaryTable" id="display">
<td data-th="ID">{{p.PrimaryID}}</td>
<td>
<input id="ForeignKeyName"
type="text"
ng-model="selectedItem" <!-- could be anything really for we will not use it here -->
uib-typeahead="f.ForeignKeyID as f.ForeignKeyName for f in ForeignKeyTable | filter:$viewValue"
typeahead-on-select="onSelect($item, $model, $label, p)"
class="form-control">
</td>
</tr>
Inside your controller
$scope.onSelect = function($item, $model, $label, primaryKeyTableRow) {
primaryKeyTableRow.ForeignKeyId = $item.ForeignKeyId;
}
Next step is to display name property value of ForeignKeyTable row that corresponds to ForeignKeyId from PrimaryKeyTable in each row. Since we have to filter ForeignKeyTable to find suitable item it would be a good idea to put that logic inside the controller. For there are multiple rows in which we want to display corresponding name, we'll have to filter the ForeignKeyTable for each row separately. This is where ng-controller for ng-repeat comes in handy. What we'll do is bind new controller for each table row generated by ng-repeat and put some logic inside that controller. In HTML it would look like this:
<tr ng-repeat="p in primaryKeyTable" ng-controller="itemController">
<!-- ... -->
</tr>
And we'll have to define new controller in JS file:
app.controller('itemController', function() {
// ...
});
Now we can have separate logic for each row in the table. So that's a place to filter ForeignKeyTable to find corresponding item and display it's name.
Since whole code is kind of big I've put it in the plunker:
http://plnkr.co/edit/xccgnpxoPHg6vhXWPwZn?p=preview
See what you can do with it.