HTML Angular Material: Hide Columns in a dynamically forming table - html

I want to hide columns in my table based on an ID value that is sent to the page upon opening it. I've seen there is a method for tables whose columns are formed as needed... https://github.com/angular/components/issues/9940 this post outlines that. However my table is formed as such.
<table mat-table [dataSource]="dataSource" align="center" class="mat-elevation-z8">
<ng-container [matColumnDef]="column" *ngFor="let column of getDisplayedColumns()">
<th mat-header-cell *matHeaderCellDef > {{column}} </th>
<td mat-cell *matCellDef="let element" > {{element[column]}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns" ></tr>
<tr mat-row *matRowDef="let row; columns: getDisplayedColumns();" ></tr>
</table>
getDisplayedColumns() should return my array with only the elements I want to show
I'm confused on how I would implement the above method into my code given that my table is formed differently than the OP's.

Probably a lot of ways to do this, but I had a similar need so created a component and passed in all possible columns. The component does a lot of special formatting like booleans = "yes" | "no" or dates dates | date:'MM/dd/yyyy h:mm:ss a', but for your case isVisible is what you seem to need. A few key pieces:
export interface IPaginateRowAttributes {
attributeName: string;
listLabel: string;
listFormat: string;
isAttribute: boolean;
lookupIndex: number;
isVisible: boolean;
}
#Input() rowColumns: IPaginateRowAttributes [] = [];
#Input() yourId
<tr *ngFor="let dynamicListRowAttribute of dynamicListRowAttributes">
<ng-container *ngIf="dynamicListRowAttribute.listFormat=='number' && dynamicListRowAttribute.isVisible==true" matColumnDef={{dynamicListRowAttribute.compositeKey}}>
<th mat-header-cell *matHeaderCellDef mat-sort-header (click)="sort(dynamicListRowAttribute.attributeName, true, dynamicListRowAttribute.listFormat)"> {{dynamicListRowAttribute.listLabel}} </th>
<td mat-cell *matCellDef="let element"> {{element['attributes'][dynamicListRowAttribute.attributeName] | currency}} </td>
</ng-container>
<ng-container> more format options</ng-container>
ngOnChanges(changes: SimpleChanges) {
let returnHeaders: string[] = [];
for (let detail of displayColumns) {
returnHeaders.push(detail.dictionaryCompositeKey);
}
}
So right there include ngOnChanges, you could check your ID value and set isVisible per column based on whatever criteria you want to use and let the *ngIf filter the rest. This is one option anyway, there may be better solutions in terms of performance.

Related

Populate option set of multiple select statement using data from a table column in Angular Material

Here's the scenarios:
I have a table that displays dealer information, and one of the columns in the table is agents name.
I'm looking to create a table filter that filters the table based on agent name. For the filter I'm using a Select option set that looks like this.
UI:
HTML Code for select:
<div>
<button mat-raised-button class="filter"
(click)=select.open()
>
<mat-icon style="margin-right: 5px;">filter_list</mat-icon>
FIlter by Agent
</button>
<div class="mat-select-wrapper">
<mat-select #select [formControl]="agentsControl" multiple disableOptionCentering panelClass="myPanelClass">
<mat-option *ngFor="let agent of agentsList" [value]="agent">{{agent}}</mat-option>
</mat-select>
</div>
TS Code for Select drop down:
agentsControl = new FormControl();
agentsList: string[] = ['John Doe', 'Mary Jane', 'Johannah Kiffin', 'Eldin Astbery', 'Stephen Curry', 'Chris Smith'];
Here is the code for the table:
HTML Code:
<div flexLayout>
<mat-card>
<div class="table-container">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" aria-label="Dealer Queue table">
<ng-container matColumnDef="dealer_id">
<th scope="col" mat-header-cell *matHeaderCellDef>Dealer #</th>
<td mat-cell *matCellDef="let el"> {{el.dealer_id}} </td>
</ng-container>
<ng-container matColumnDef="dealership_name">
<th scope="col" mat-header-cell *matHeaderCellDef>Dealership</th>
<td mat-cell *matCellDef="let el"><a [routerLink]="['dreams-account-page',el.dealer_id]">{{el.dealership_name}} </a></td>
</ng-container>
<ng-container matColumnDef="agent_name">
<th scope="col" mat-header-cell *matHeaderCellDef>Agent</th>
<td mat-cell *matCellDef="let el"> {{el.agent_name}} </td>
</ng-container>
<ng-container matColumnDef="state">
<th scope="col" mat-header-cell *matHeaderCellDef>State</th>
<td mat-cell *matCellDef="let el"> {{el.state}} </td>
</ng-container>
<ng-container matColumnDef="phone_number">
<th scope="col" mat-header-cell *matHeaderCellDef>Phone</th>
<td mat-cell *matCellDef="let el"> {{el.phone_number}} </td>
</ng-container>
<ng-container matColumnDef="total_cancellations">
<th scope="col" mat-header-cell *matHeaderCellDef>Cancellations</th>
<td mat-cell *matCellDef="let el"> {{el.total_cancellations}} </td>
</ng-container>
<ng-container matColumnDef="cancellations_over_120_days">
<th scope="col" mat-header-cell *matHeaderCellDef>Cancellations > 120 Days</th>
<td mat-cell *matCellDef="let el"> {{el.cancellations_over_120_days}} </td>
</ng-container>
<ng-container matColumnDef="total_titles_outstanding">
<th scope="col" mat-header-cell *matHeaderCellDef>Titles</th>
<td mat-cell *matCellDef="let el"> {{el.total_titles_outstanding}} </td>
</ng-container>
<ng-container matColumnDef="titles_over_90_days">
<th scope="col" mat-header-cell *matHeaderCellDef>Titles > 90 Days</th>
<td mat-cell *matCellDef="let el"> {{el.titles_over_90_days}} </td>
</ng-container>
<ng-container matColumnDef="last_contact_date">
<th scope="col" mat-header-cell *matHeaderCellDef>Days Since Last Contact</th>
<td mat-cell *matCellDef="let el"> {{el.last_contact_date | daysSinceToday}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let dqrow; columns: displayedColumns" (click)="openBottomSheet(dqrow)"></tr>
</table>
</div>
<mat-paginator #paginator [pageSizeOptions]="[10, 20, 50, 100, 200, 500]" showFirstLastButtons></mat-paginator>
</mat-card>
</div>
TS for table:
data: DealerQueueTable[] = [];
displayedColumns: string[] = [
'dealer_id',
'dealership_name',
'agent_name',
'state',
'phone_number',
'total_cancellations',
'cancellations_over_120_days',
'total_titles_outstanding',
'titles_over_90_days',
'last_contact_date',
];
dataSource = new MatTableDataSource(this.data);
private subscriptionToDealerQueueTable : Subscription;
constructor(private dreamsService : DreamsService) { }
ngOnInit(): void {
this.subscriptionToDealerQueueTable = this.dreamsService.getDealerQueue().subscribe( dealerQueue => {
this.dataSource.data = dealerQueue;
})
}
#ViewChild('paginator') paginator: MatPaginator;
ngAfterViewInit() {
this.dataSource.paginator = this.paginator;
}
ngOnDestroy() {
this.subscriptionToDealerQueueTable?.unsubscribe();
}
}
Table Model Code:
export interface DealerQueueTable {
dealer_id : number,
dealership_name : string,
agent_name : string,
state : string,
phone_number : number,
total_cancellations : number,
cancellations_over_120_days : number,
total_titles_outstanding : number,
titles_over_90_days : number,
last_contact_date : Date,
market_manager : string,
market_manager_phone : string,
market_manager_email : string
}
Both are child components that share a common parent.
Here's an image of the folder structure:
So basically what I'm trying to do and can't figure out how to do is rather than populating the option set array "agentsList" manually, I'm trying to figure out how this can be done dynamically and programmatically? Where all the unique agent names present in table column agent are passed as values to the option set array.
Here's how I was able to solve this:
In the API call for the table data I retrieved the list of unique agent names and emitted it to the parent component:
Dealer Table TS:
#Output() agentData = new EventEmitter<string[]>();
ngOnInit(): void {
this.subscriptionToDealerQueueTable = this.dreamsService.getDealerQueue().subscribe( dealerQueue => {
//Set the dealerQueue value to this.unfilteredData so we can alter it and still access it later.
this.unfilteredData = dealerQueue;
//Call method to set the this.datasource.data based on given filters.
this.setDataSource(this.unfilteredData, this.FilterValues?.value || []);
let uniqueAgentList = [...new Set(dealerQueue.map(item => item.agent_name))];
this.agentData.emit(uniqueAgentList);
})
this.subscriptionToOptionSetValueChanges = this.FilterValues.valueChanges.subscribe( x => {
this.setDataSource(this.unfilteredData, x || []);
});
}
Using the spread (...) operator and Set operator in javascript we are able to get the unique set of values from the data returned by the api. I then emitted this value to the parent component.
Spread Operator: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
Set Operator: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set
Parent HomeComponent.html
<div fxFlexOffset="2%" >
<div class="element-header">Dealer Queue Table</div>
<app-filter #FilterDropdown [agentsListOptions]="agentList" ></app-filter>
<app-dealer-queue-table
*ngIf = "FilterDropdown?.agentsControl"
(agentData)="getUniqueAgentData($event)"
class="dealerqueuetable"
[FilterValues]="FilterDropdown?.agentsControl"
></app-dealer-queue-table>
</div>
Parent Home.Component.ts
export class HomeComponent implements OnInit {
agentList = [];
constructor() { }
ngOnInit(): void {
}
getUniqueAgentData(data : any) {
this.agentList = data;
}
}
The event binding, '(agentData)="getUniqueAgentData($event)', connects the event in the child, this.agentData.emit(uniqueAgentList), to the method in the parent, getUniqueAgentData(data : any) {
this.agentList = data;
}.
Now that we have a unique list of agents stored in agentList=[]; in our parent component we pass [agentsListOptions]="agentList" as in input back to the child FilterComponent.
Home.component.html
<app-filter #FilterDropdown [agentsListOptions]="agentList" ></app-filter>
Receive agentListOptions as an input in the filter component:
filter.component.ts
#Input() agentsListOptions: string[];
And finally use it to populate your dropdown options in the html
<mat-select [formControl]="agentsControl" multiple>
<mat-option *ngFor="let agent of agentsListOptions" [value]="agent">{{agent}
</mat-option>
</mat-select>

Angular sorter not working on specific column

I am trying to implement a sorting method in my angular project and I am having some really weird issues. There is 3 columns, 2 string columns and one number with an Id in the beginning. The string columns seems to be working fine with the implemented sorter but the Id is not sorting itself in numerous order.
What I have testet is to move one of the working columns to be in the first column to see if it is because its the first column in line. I have also tried to change the value of the column with id to one of the values working and it worked fine so what I wonder is.
Does anyone have any idea of what could be causing this, is it because it is a number and not a string? feels like numbers should be easier for the sorter to work with but idk.
I will leave the code down below.
//Typescript:
#ViewChild(MatSort) sort: MatSort;
ngAfterViewInit() {
this.dataSource.sort = this.sort;
}
<table [hidden]="loading || !this.dataSource.data.length" mat-table #table [#tableRow] [#fadeIn] [#fadeOut] [dataSource]="dataSource" matSort>
<ng-container sticky matColumnDef="id">
<th mat-header-cell class="small-width64 nowrap-text text-right pl-1 pr-1" mat-sort-header sortActionDescription="Sort by id" arrowPosition='before' *matHeaderCellDef>{{ 'Id' | translate }}</th>
<td mat-cell class="small-width64 nowrap-text text-right pl-1 pr-1" *matCellDef="let message">{{ message.messageId }}</td>
</ng-container>
<ng-container matColumnDef="message">
<th mat-header-cell class="nowrap-text pl-1 pr-1 " mat-sort-header sortActionDescription="Sort by message" *matHeaderCellDef>{{ 'Message_administration.Message' | translate }}</th>
<td mat-cell class="nowrap-text pl-1 pr-1" *matCellDef="let message">{{ message.message }}</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell class="nowrap-text pl-1 pr-1 " mat-sort-header sortActionDescription="Sort by description" *matHeaderCellDef>{{ 'Message_administration.Description' | translate }}</th>
<td mat-cell class="nowrap-text pl-1 pr-1" *matCellDef="let message">{{ message.description }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
As you're saying the "id" column has data in form of strings, formatted as Id 1, Id 20, Id 100. This means they are strings, and hence will be sorted as strings. So with the above examples, correct sorting would order would be:
Id 1, Id 100, Id 20.
I assume you would like to sort those as numbers. To do that, you would have to implement custom sorting logic where you strip the Id part, pars those as number and sort them as numbers.
To do that, you can override the sortData predicate on the datasource:
this.dataSource.sortData = (data: YourObjectType[], sort: MatSort) => {
return data.sort((a: YourObjectType, b: YourObjectType => {
//Your custom sorting logic here
});
}
You can look at the default implementation in the material code on github: https://github.com/angular/components/blob/c2a20c4a035ef57bf598fd78bc7284c180b34c78/src/material/table/table-data-source.ts#L168 for some guidance.

Angular - Array is not shown in mat-table

In the function, the string is split into an array. Each word gets send to the server, from there I get some information about the word back. This I save into an array. The problem I have is that if I go through the array with *ngFor in the HTML document I get all the entries back but in the mat-table, it isn't shown. I don't get an error just the data is not shown.
sendText(){
this.words=[];
this.stringArray = this.sentence.split(" ");
console.log(this.stringArray);
for(let i=0;i<this.stringArray.length ;i++){
this.httpService.sendText(this.stringArray[i]).subscribe((res) => {
this.result = res;
console.log(this.result.words);
this.b.wort = this.result.words[0].wort;
this.b.definition = this.result.words[0].definition;
this.b.wortart = this.result.words[0].wortart;
this.words.push(this.b);
this.b = this.getemptyObject();
});} console.log(this.words); this.sentence = "";}
HTML
<div *ngFor="let word of words">
{{word.wort}},{{word.definition}}, {{word.wortart}}
</div>
<table mat-table [dataSource]="words" class="mat-elevation-z8">
<!-- Word Column -->
<ng-container matColumnDef="wort">
<th mat-header-cell *matHeaderCellDef> Wort </th>
<td mat-cell *matCellDef="let word"> {{word.wort}} </td>
</ng-container>
<!-- Wortdtype Column -->
<ng-container matColumnDef="wortart">
<th mat-header-cell *matHeaderCellDef> Wortart </th>
<td mat-cell *matCellDef="let word"> {{word.wortart}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
I found a solution to this question on this Reddit Thread.
According to "#spacechimp" (the person who answered the question), you need to update the entire array for the MatTable to detect the changes.
Something like this:
this.words.push(this.b);
this.words = [...this.words]; // <--- Update the whole array with its own data
Even though this is a solution, it is a rather expensive way of doing this for large arrays, so I encourage you to find other ways to optimize this.

Is there any way to edit specific Column of table using mat-table in Angular material

In my project i am using Angular Material's table for displaying value in table format, As per new requirement i have to perform in-line editing for last 2 column over other along with that if user click 1st column the other column get highlighted automatically and everything has to be done using Angular material
This is the last 2 column i want to perform in-line editing
> <ng-container matColumnDef="weight">
> <th mat-header-cell *matHeaderCellDef> Weight </th>
> <td mat-cell *matCellDef="let element"> {{element.weight}} </td>
> </ng-container>
> <ng-container matColumnDef="symbol">
> <th mat-header-cell *matHeaderCellDef> Symbol </th>
> <td mat-cell *matCellDef="let element"> {{element.symbol}} </td>
> </ng-container>
>
>
> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> </table>
How i achieved is as follows :
<tbody>
<tr *ngFor="let data of dataSource">
<td >{{data.position}}</td>
<td >{{data.name}}</td>
<td *ngIf="data.position === editRowId"> <input matInput [(ngModel)]="data.weight"></td>
<td *ngIf="data.position !== editRowId" (click)="editTableRow(data.position)">{{data.weight}}</td>
<td *ngIf="data.position === editRowId"> <input matInput [(ngModel)]="data.symbol"></td>
<td *ngIf="data.position !== editRowId" (click)="editTableRow(data.position)">{{data.symbol}}</td>
</tr>
</tbody>
TS file for above code :
export class AppComponent {
showEditTable = false;
editRowId: any = '';
selectedRow;
displayedColumns: string[] = ['position', 'name', 'weight', 'symbol'];
dataSource = ELEMENT_DATA;
editTableRow(val) {
console.log('click event started val = ' + val);
this.editRowId = val;
console.log('click event ended with val = ' + val);
}
}
I expect the result as table where last 2 column can be edited inline and at the same time i can send modified data to backend
Naman, it's the same, you need use <ng-container>to avoid create extra tags, so your columns becomes like
<!-- Weight Column -->
<ng-container matColumnDef="weight">
<th mat-header-cell *matHeaderCellDef> Weight </th>
<td mat-cell *matCellDef="let element">
<ng-container *ngIf="element.position!==editRowId">
<span (click)="edit(element.position,'weigth')">{{element.weight}} </span>
</ng-container>
<ng-container *ngIf="element.position===editRowId">
<input matInput name="weigth" [(ngModel)]="element.weight">
</ng-container>
</td>
</ng-container>
Well, I call to function "edit" passign two arguments, the position and a string indicate the "name" of the input attributes. This allow us "focus" the input clicked. how?
We declare a ViewChildren of the MatInputs
#ViewChildren(MatInput,{read:ElementRef}) inputs:QueryList<ElementRef>;
See that we get not the MatInput else the "ElementRef". This allow us, in out function edit get the element with the attributes name equal the string that pass as argument, and focus it. See that we need "enclosed" all in a setTimeout to allow Angular to show the input
edit(row,element)
{
this.editRowId=row;
setTimeout(()=>{
this.inputs.find(x=>x.nativeElement.getAttribute('name')==element)
.nativeElement.focus()
})
}
You can see the full example in stackblitz
Well, in the example the data is hardcoded. Let's go to imagine that the data (and the structure) comes from a service data. The data is easy imagine because it's the same. The "structure" we can imagine as an array of object with three properties: name,head ad fixed. if fixed is true, we only show the data, else we can edit. So the only thing we need is create the columns in a *ngFor
First we are going to see how our schema can be defined. It's only an array
[
{name:'position',head:"No.",fixed:true},
{name:'name',head:"Name",fixed:true},
{name:'weight',head:"Weigth",fixed:false},
{name:'symbol',head:"Symbol",fixed:false},
]
Our table becomes like
<table #table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container *ngFor="let column of schema;let last=last">
<ng-container [matColumnDef]="column.name">
<th mat-header-cell *matHeaderCellDef> {{column.head}} </th>
<td mat-cell *matCellDef="let element">
<ng-container *ngIf="element[schema[0].name]!==editRowId || column.fixed">
<span
(click)="column.fixed?editRowId=-1:
edit(element[schema[0].name],column.name)">
{{element[column.name]}}
</span>
</ng-container>
<ng-container *ngIf="element[schema[0].name]===editRowId && !column.fixed">
<input matInput [id]="column.name"
[(ngModel)]="element[column.name]"
(blur)="last?editRowId=-1:null">
</ng-container>
</td>
</ng-container>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
See that we "replace" element.position by element[column[0].name] -I supouse the first element of the schema will be the "key" and how we using [(ngModel)]="elemen[column.name]". Yes, to refererd to element.position we can refered too as elemen["position"] and remember we are iterating over "schema"
Another thing is that we are going to use "id", not "name". this is because if we using name, really Angular put as attrib some like: ng-reflect-name, and this don't allow us focus the input.
Finally we are get the data in the ngOnInit. We are going to use forkJoin to get together the schema and the data. A forkJoin only call and array of observables (in this case this.dataService.getSchema() and this.dataServide.getData, and return in an array the response of all the observables. We use the way
([variable1,variable2]) to store in "variable1" the first result and in variable2 the second result
ngOnInit()
{
forkJoin([this.dataService.getSchema(),this.dataService.getData()])
.subscribe(([schema,data])=>{
this.dataSource=data;
this.displayedColumns=schema.map(x=>x.name)
this.schema=schema
})
}
The displayedColumns must be an array with the names of the columns, but we need to store in an array the "schema" too.
In the stackblitz I create a service and "simulate" the observable using the rxjs creation operator of, in a real application the data becomes from a httpClient.get(....)

Provided data source did not match an array, Observable, or DataSource at getTableUnknownDataSourceError

I am new to Angular and Angular Material ,
Without Angular Materail it is working fine
<table>
<tr>
<th>City</th>
<th>State</th>
</tr>
<tr>
<td>{{ varEmp?.address.city}}</td>
<td>{{ varEmp?.address.state}}</td>
</tr>
</table>
With Angular Material it is not Working
<table mat-table [dataSource]="varEmp" class="mat-elevation-z8">
<!-- Position Column -->
<ng-container matColumnDef="city">
<th mat-header-cell *matHeaderCellDef> City</th>
<td mat-cell *matCellDef="let element"> {{element?.address.city}} </td>
</ng-container>
<!-- Name Column -->
<ng-container matColumnDef="state">
<th mat-header-cell *matHeaderCellDef> State</th>
<td mat-cell *matCellDef="let element"> {{element?.address.state}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
In component I declared variable
displayedColumns: string[] = ['city','state'];
briefy described component code to reduce complexity
Json Object
{"address":
{"city": "Karwar", "state": "Karnataka"}
}
I stumbled upon this issue as I had a static data source of json format and I needed to dump it in a mat-table.
The most common reason for this behavior is poor format of json data. The json data expected by mat-table should be in the format [{...}, {...}, {...}]. If you cannot change the format of the json data, simply convert the data to Array like this
var dataArr = new Array(jsonData));
and it would work.
Note - Converting it to array would add an extra layer of square brackets to the data. You might need to change the [dataSource] property in mat-table to dataArr[0].
<table mat-table [dataSource]="dataArr[0]">
Define the datasource of table as ;
this.dataSource = new MatTableDataSource<Address>(this.addressList);
You could try edit the td tag and do not use ? after the element.
For example:
<td mat-cell *matCellDef="let element"> {{element.address.city}} </td>
You need to convert your data to an array.
[dataSource] for angular material mat-table, requires that an array of data is passed to it. Ensure that you have a well formatted array of your data before assigning to your data source