So I am trying to add a new line in a table which is populated from an API but it looks that I miss something.
Here is my code:
HTML:
<button mat-flat-button color="primary" class="btn-add"
(click)="addRow()"
[disabled]="isLoading">ADD</button>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z2" *ngIf="show">
<ng-container *ngFor="let column of displayedColumns" [matColumnDef]="column">
<th mat-header-cell *matHeaderCellDef>{{column}}</th>
<td mat-cell *matCellDef="let element; index as i;">
<span *ngIf="element.editing; then editBlock else displayBlock"></span>
<ng-template #displayBlock>{{element[column]}}</ng-template>
<ng-template #editBlock>
<mat-form-field appearance="fill">
<input matInput [(ngModel)]="element[column]">
</mat-form-field>
</ng-template>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
TS file:
displayedColumns: string[] = [];
dataSource: any;
addRow() {
console.log("Adding new row");
let newRow = {
editing: true
};
this.displayedColumns.forEach(x => newRow[x] = "");
this.dataSource.push(newRow);
}
Reference to the variable dataSource is not updated. Angular change detector not always can (while sometimes can) recognize such changes. Creating new reference (new array in this case) should solve this issue:
addRow() {
let newRow = { editing: true };
this.displayedColumns.forEach(x => newRow[x]="");
this.dataSource = [...dataSource, newRow];
}
You are missing the table.renderRows() call
Documentation:
If a data array is provided, the table must be notified when the array's objects are added, removed, or moved. This can be done by calling the renderRows() function which will render the diff since the last table render. If the data array reference is changed, the table will automatically trigger an update to the rows.
HTML
<table #elementTable mat-table class="mat-elevation-z2"
*ngIf="show"
[dataSource]="dataSource">
TS
#ViewChild('elementTable') matTable: MatTable<any>;
And in addRow() function
addRow() {
console.log("Adding new row");
const newRow: any = {
column: '',
editing: true
};
this.dataSource.push(newRow);
this.table.renderRows();
}
Related
I've got that Angular Material Table :
<table mat-table [dataSource]="data" [trackBy]="trackBy">
<ng-container
[matColumnDef]="column.value"
*ngFor="let column of columns; let i = index"
>
<th mat-header-cell *matHeaderCellDef>{{ column.label }}</th>
<td mat-cell *matCellDef="let element">
<input
matInput
[value]="element[column.value].value"
type="text"
(input)="updatedFn($event.target.value)"
/>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columnsToDisplay"></tr>
<tr mat-row *matRowDef="let row; columns: columnsToDisplay"></tr>
</table>
And this ts file :
#Component({
...
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StuffComponent implements OnInit, OnChanges {
#Input() data!: Data[];
#Input() columns!: Column[];
#Output() updated = new EventEmitter<Updated>();
columnsToDisplay!: string[];
constructor(private cdr: ChangeDetectorRef) {}
trackBy(index: number) {
return index;
}
ngOnChanges() {
this.cdr.markForCheck();
}
ngOnInit() {
this.columnsToDisplay = this._columns.map((item: Column) => item.value);
}
updatedFn(value: string) {
this.updated.emit(value);
}
}
I added the trackBy because I was losing focus on input whenever I'm typing. Now with trackBy, I can freely type, it dispatches the action, then I use the reducer to make the changes to the state, then it correctly send back the fresh data to the table component : the #Input() data/colums are corrects. However, it does not update the DOM. I assume this is because of the trackBy, but if I use something like this :
trackBy(index: number, stuff) {
return stuff.value;
}
...it loses focus again on the input.
Also, I tried to use this.cdr.markForCheck() but it did not do anything.
How can I update the DOM despite the trackyBy?
Edit: to be more precise on how I know it does not update the DOM -> in my reducer, I change the data value of another table cell but it does not appear on the table despite the #Input data contains the fresh data.
I found a solution: I added the same trackyBy to the ngFor itself :
<ng-container [matColumnDef]="column.value" *ngFor="let column of columns; let i = index; trackBy: trackBy"
I also removed the ngOnChanges and markForCheck()
I am using a mat table with paginator and sort within an *ngIf. The table data is retrieved using an API.
This is the component's typescript code:
export class UserPermissionAdminComponent implements AfterViewInit, OnInit {
dataSource: MatTableDataSource<UserPermission> = new MatTableDataSource<UserPermission>();
columnsToDisplay: string[] = ['id', 'name'];
#ViewChild(MatSort, { static: false }) sort!: MatSort;
#ViewChild(MatPaginator, { static: false }) paginator!: MatPaginator;
constructor(private userPermService: UserPermissionService) {
}
ngOnInit(): void {
this.userPermService.getAllUserPermissions()
.subscribe(recvPerms => this.dataSource.data = recvPerms);
}
ngAfterViewInit(): void {
this.dataSource.sort = this.sort;
this.dataSource.paginator = this.paginator;
console.log(this.dataSource.sort);
}
applyFilter(event: Event): void {
const filterValue = (event.target as HTMLInputElement).value;
this.dataSource.filter = filterValue.trim().toLowerCase();
}
}
And this is the component's template:
<div class="container">
<h2>Permisiuni utilizatori</h2>
<ng-container *ngIf="dataSource.data.length; then existingPermissions; else noPermissions">
</ng-container>
<ng-template #existingPermissions>
<div>
<div class="filter-container">
<mat-form-field>
<mat-label>Cautare</mat-label>
<input matInput (keyup)="applyFilter($event)" placeholder="Ex: READ" #input />
</mat-form-field>
</div>
<div class="table-container mat-elevation-z8">
<table mat-table [dataSource]="dataSource" matSort>
<ng-container matColumnDef="id">
<th mat-header-cell *matHeaderCellDef mat-sort-header> ID </th>
<td mat-cell *matCellDef="let permission"> {{permission.id}} </td>
</ng-container>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header> Denumire permisiune </th>
<td mat-cell *matCellDef="let permission"> {{permission.name}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columnsToDisplay"></tr>
<tr mat-row *matRowDef="let rowData; columns: columnsToDisplay"></tr>
</table>
<mat-paginator [pageSizeOptions]="[5, 10, 20]"></mat-paginator>
</div>
</div>
</ng-template>
<ng-template #noPermissions>
<div>
<span class="notification">Nu a fost gasita nicio permisiune utilizator.</span>
</div>
</ng-template>
</div>
The issue is that sorting and pagination do not work (the console.log also lists that this.sort is undefined). The table contains all the data on a single page. Filtering also works.
I have managed to fix the issue by adding setters to the paginator and sort:
#ViewChild(MatSort, { static: false }) set sort(val: MatSort) {
if (val) {
this.dataSource.sort = val;
}
}
#ViewChild(MatPaginator, { static: false }) set paginator(val: MatPaginator) {
if (val) {
this.dataSource.paginator = val;
}
}
However, the code now works even without ngAfterViewInit. Can someone please explain how and why this works?
I'm creating web application using Angular 6 about an online quiz which contain some questions that its option look like a table. I've used MatTableSourcedata to display the content of rows and columns, I've some columns that are a check boxes to be checked for choosing.
Here's column def code :
<!-- Checkbox Column -->
<ng-container matColumnDef="select5">
<th mat-header-cell *matHeaderCellDef> Always </th>
<td mat-cell *matCellDef="let row ;let i = index;">
<div *ngFor="let number of question.options">
<mat-checkbox [(ngModel)]="option.number" (change)="onSelect(question,
option);">
</mat-checkbox>
</div>
</td>
</ng-container>
And this is the onSelect method :
onSelect(question: Question, option: Option) {
if (question.questionTypeId === 3) {
this.config.requiredAll = true;
question.options.forEach((x) => { if (x.id !== option.id) x.selected =
false; });
this.option.number = this.selection.selected.length;
}
else {
this.config.requiredAll = false;
}
console.log(question.options);
}
If you need more code or more explaination let me know please, I've to know how to assign the selected cell index to the option number.
I have this UI
And my goal is to update table after clicking Update button based on Id and Description parameters
I have following code
Html
<table class="table table-bordered table-striped table-hover">
<thead>
<tr>
<th>Id</th>
<th>Description</th>
</tr>
</thead>
<tbody>
#foreach (var item in Model.Products)
{
<tr>
<td>#item.Id</td>
<td>#item.Description</td>
</tr>
}
</tbody>
</table>
<label>Id</label>
<input data-bind="value: productId" type="text" class="form-control" />
<br />
<label>Description</label>
<input data-bind="value: productDescription" type="text" class="form-control" />
<br />
<button class="btn btn-primary" data-bind="click: update">Update</button>
JavaScript
<script type='text/javascript'>
// Plain javascript alternative to jQuery.ready.
(function () {
// Convert server model to json object.
var json = {"products":[{"id":1,"description":"Product A"},{"id":2,"description":"Product B"},{"id":3,"description":"Product C"}],"categoryName":"chocolates"};
function viewModel()
{
var index;
// Store default this to self, to be able use self in subfunctions.
var self = this;
// Product array from server.
self.products = json.products;
// Automatic refreshed parameter in UI.
self.categoryName = ko.observable(json.categoryName);
// Update form parameters.
self.productId = null;
self.productDescription = null;
// Update function.
self.update = function () {
// Get products collection index.
index = self.products.findIndex(function (product) {
return product.Id == self.productId
});
// Throws -1, index was not found !
alert(index);
// Assign new value.
// self.products[index].description(self.productDescription);
};
}
// Apply viewModel to UI
ko.applyBindings(new viewModel());
})();
</script>
I need help with two things
Update knockout viewmodel (self.products collection based on Id and Description inputs in viewModel.Update function)
Update table (I don't know how to bind the rows, when using razor foreach)
EDIT:
When I change Razor loop with Knockout foreach
<tbody data-bind="foreach: products">
<tr>
<td data-bind="text: id"></td>
<td data-bind="text: description"></td>
</tr>
</tbody>
it does not work either :/ I tried some changes with observables
<script type='text/javascript'>
// Plain javascript alternative to jQuery.ready.
(function () {
// Convert server model to json object.
var json = {"products":[{"id":1,"description":"Product A"},{"id":2,"description":"Product B"},{"id":3,"description":"Product C"}],"categoryName":"chocolates"};
function viewModel()
{
var index;
// Store default this to self, to be able use self in subfunctions.
var self = this;
// Product array from server.
self.products = ko.observable(json.products);
// Automatic refreshed parameter in UI.
self.categoryName = ko.observable(json.categoryName);
// Update form parameters.
self.productId = ko.observable();
self.productDescription = ko.observable();
// Update function.
self.update = function () {
self.products()[self.productId()-1].description = self.productDescription()
};
}
// Apply viewModel to UI
ko.applyBindings(new viewModel());
})();
</script>
I do not get an error, but the value is not changed in the html table.
Your knockout code looks mostly correct but your UI isn't updating because the values that you're changing aren't observable. If you want the changes to the items to reflect in the table you need to make each field that will update an observable instead of having a single observable property for the entire json object.
The following code will only update if the entire object reference changes. IE if you fill self.products with a new object. It will not update if you change a property on the existing object.
// Product array from server.
self.products = ko.observable(json.products);
You'll need to change your code to individually apply observable properties to your json object when it comes from the server.
// Product array from server.
self.products = ko.observableArray([]); //Changed to array type
for(var i = 0; i < json.products.length; i++){
self.products.push({
id: json.products[i].id,
description: ko.observable(json.products[i].description)
});
}
And then you'll have to update the value using the function style getter/setter.
// Update function.
self.update = function () {
self.products()[Number(self.productId())-1].description(self.productDescription());
};
// Convert server model to json object.
var json = {"products":[{"id":1,"description":"Product A"},{"id":2,"description":"Product B"},{"id":3,"description":"Product C"}],"categoryName":"chocolates"};
function viewModel()
{
var index;
// Store default this to self, to be able use self in subfunctions.
var self = this;
// Product array from server.
self.products = ko.observableArray([]); //Changed to array type
for(var i = 0; i < json.products.length; i++){
self.products.push({
id: json.products[i].id,
description: ko.observable(json.products[i].description)
});
}
// Automatic refreshed parameter in UI.
self.categoryName = ko.observable(json.categoryName);
// Update form parameters.
self.productId = ko.observable();
self.productDescription = ko.observable();
// Update function.
// Update function.
self.update = function () {
self.products()[Number(self.productId())-1].description(self.productDescription());
};
}
// Apply viewModel to UI
ko.applyBindings(new viewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<table class="table table-bordered table-striped table-hover">
<thead>
<tr>
<th>Id</th>
<th>Description</th>
</tr>
</thead>
<tbody data-bind="foreach: products">
<tr>
<td data-bind="text: id"></td>
<td data-bind="text: description"></td>
</tr>
</tbody>
</table>
<br/>
<label>Id: </label>
<input data-bind="value: productId" type="text" class="form-control" />
<br />
<label>Description: </label>
<input data-bind="value: productDescription" type="text" class="form-control" />
<br />
<button class="btn btn-primary" data-bind="click: update">Update</button>
Long day and I'm still beating my head against the desk over this one. I think I've read every jQuery ui autocomplete post here on stack overflow as well as most of the jQuery ui documentation.
The Problem:
I have a form in an asp.net mvc 4 application. The form is for submitting a timesheet/work ticket. The user selects a project and based upon that project an autocomplete field for work order number is enabled. If the user chooses and existing work order number for the project, I populate two additional inputs with information about the work order. I need to allow the user to type in a completely new work order with out selecting an autocomplete suggested work order and still have the form validate. The problem is that validation fails if I do not select a suggestion from the autocomplete.
The jQuery-ui documentation does show that the methods close, disable, and destroy available to be used but I'm not finding a good example of how to use them and which would actually be best to use in my situation. Also it might be worth noting that I am using jQuery-ui 1.9
The error I get:
System.Data.Entity.Validation.DbEntityValidationException: Validation failed for one or more entities. See 'EntityValidationErrors' property for more details.
The error points to the line of code on my controller where I call _db.SaveChanges();
if ( ModelState.IsValid )
{
_db.WorkTickets.Add(workticket);
_db.SaveChanges();
}
The model state is valid and I have no nulls trying to be saved. My model does allow nulls on a few fields but in this case all fields are complete.
The form works great if I choose an existing work order from the autocomplete widget or if I leave it blank. It only fails if I type in a new work order. I have set a break point and verified that the model does have the value I typed in for work ticket.WONumber before calling save changes.
Here is my jQuery.
$(document).ready(function () {
$("#ProjectID").change(function () {
var id = $(this).val();
var results = "result";
var source1 = "Project/QuickCCSearch?project=" + id;
$("#ChargeCode").autocomplete({ source: source1 });
$("#ChargeCode").attr("disabled", false)
var source2 = "Project/QuickPOSearch?project=" + id;
$("#PONumber").autocomplete({ source: source2 });
$("#PONumber").attr("disabled", false)
var source3 = "Project/QuickWOSearch?project=" + id;
$("#WONumber").attr("disabled", false)
$("#WONumber").autocomplete({
source: function (request, response) {
$.ajax({
url: source3,
data: request,
success: function (data) {
response(data);
if (data.length === 0) {
$("#WONumber").attr("check", false);
}
}
});
},
change: function () {
if ($("#WONumber").attr("check") != false) {
$.getJSON("Project/JobLocation", { wo: $("#WONumber").val() },
function (data) {
$("#JobLocation").val(data);
});
$.getJSON("Project/JobDescription", { wo: $("#WONumber").val() },
function (data) {
$("#JobDescription").val(data);
});
}
}
});
$.getJSON("WorkTicket/GetClient/", { id: id },
function (data) {
$("#Client").html(data);
});
$.getJSON("WorkTicket/GetClientRep/", { id: id },
function (data) {
$("#ClientRep").html(data);
});
$.getJSON("WorkTicket/GetManager/", { id: id },
function (data) {
$("#Manager").html(data);
});
});
})
Edit:
Here is my Html
#model WorkTicket
#{
ViewBag.Title = "Create";
}
<article>
<div class="linearBg1">
Create Daily Work Ticket
</div>
<br />
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<div class="linearBg1">
General Information
</div>
<div class="section-span-body">
<table>
<tr class="empTableRowBody2">
<th class="empTableRowBody2">
#Html.LabelFor(Model => Model.DateWorked)
</th>
<th class="empTableRowBody2" colspan="2">
#Html.LabelFor(Model => Model.ProjectName)
</th>
</tr>
<tr>
<td>
#Html.EditorFor(Model => Model.DateWorked)
</td>
<td colspan="2">
#Html.DropDownList("ProjectID")
</td>
</tr>
<tr class="empTableRowBody2">
<th class="empTableRowBody2">
Client
</th>
<th class="empTableRowBody2">
Client Rep
</th>
<th class="empTableRowBody2">
Manager
</th>
</tr>
<tr>
<td>
<div id="Client"></div>
</td>
<td>
<div id="ClientRep"></div>
</td>
<td>
<div id="Manager"></div>
</td>
</tr>
<tr class="empTableRowBody2">
<th class="empTableRowBody2">
Charge Code
</th>
<th class="empTableRowBody2">
PO Number
</th>
<th class="empTableRowBody2">
Work Order
</th>
</tr>
<tr>
<td>
#Html.TextBoxFor(Model => Model.ChargeCode, new { disabled = true })
</td>
<td>
#Html.TextBoxFor(Model => Model.PONumber, new { disabled = true })
</td>
<td>
#Html.TextBoxFor(Model => Model.WONumber, new { disabled = true, check = true })
</td>
</tr>
<tr class="empTableRowBody2">
<th class="empTableRowBody2" colspan="3">
Job Location
</th>
</tr>
<tr>
<td colspan="3">
#Html.TextBoxFor(Model => Model.JobLocation, new { #class = "inputWidth500" })
</td>
</tr>
<tr class="empTableRowBody2">
<th class="empTableRowBody2" colspan="3">
Job Description
</th>
</tr>
<tr>
<td colspan="3">
#Html.TextBoxFor(Model => Model.JobDescription, new { #class = "inputWidth500" })
</td>
</tr>
</table>
</div>
<div class="section-span-footer"></div>
<br />
<div>
<input type="submit" value="Next" />
</div>
}
</article>
#section Menu{
#Html.Partial("_MainMenu")
#Html.Partial("_MenuFooter")
}
#section scripts{
#Scripts.Render("~/bundles/jqueryval")
#Scripts.Render("~/Scripts/WorkTicket.js")
}
I'm sure I'm doing many things wrong as I am new to jQuery and still learning. Is there a way to disable the autocomplete if the user does not choose an option from the autocomplete widget or if the result of the widget is null? Thank you in advance!
T.
Ok, I finally solved it. It has nothing to do with autocomplete. Just goes to show how much I need to learn about jQuery. I had a bad line of code in my controller where I was saving information about the new work order back to the project. I was missing one required item and I was just over looking it. Sorry for being an idiot and ignorant about jQuery. Thanks anyone that looked. Can't believe that took me so long to see.......