Show template in loop when value matches template property - javascript

I am trying to make a table component, which I have completed, I want to add a feature to the component where I can customize individual cells in the component. I am not exactly sure how to do this.
So I have seen this implemented like the following:
model is the data related to the table (headers, rows, pagination, etc.)
matches is the column name to match (in the headers id what is match, in rows it is the property key).
let-content is the data associated with the cell for that row.
<ui-table [model]="tableModel">
<ng-template matches="columnA" let-content="content">
{{content | commaSeparate}}
</ng-template>
</ui-table>
public tableModel = {
headers: [
{ title: 'Column A', id: 'columnA' },
{ title: 'Column B', id: 'columnB' }
],
rows: [
{ columnA: ['A', 'B'], columnB: 'Column B' },
{ columnA: ['C', 'D'], columnB: 'Column B2' }
]
}
In my ui-table component I have the following table body:
<tbody #tBody class="uitk-c-table__body">
<tr *ngFor="let row of rowItems" class="uitk-c-table__row">
<!-- If "matches === model.headers[x].id" show the custom cell -->
<td *ngFor="let cell of model.headers; index as c">{{row[model.headers[c].id] || ''}}</td>
</tr>
</tbody>
What I am not sure of, is how do I show the custom cell ng-template if matches === model.headers[x].id?

Inside your ui-table component, let's define a input property:
#Input() passedTemplate: TemplateRef<any>;
#Input() displayContent: any // to display variable inside ng-template
And define a input property in order to pass your ng-template to
Like this:
<ui-table [model]="tableModel" [passedTemplate]='template' [displayContent]='content'>
</ui-table>
<ng-template #template matches="columnA" let-content="content">
{{content | commaSeparate}}
</ng-template>
Then try to use ng-container in your ui-table html file:
<ng-container *ngIf="matches === model.headers[x].id"
[ngTemplateOutlet]="passedTemplate"
[ngTemplateOutletContext]="{content: displayContent}">
</ng-container>
Hope this help...

I was able to figure it out. First I create a directive which contains a match property which is the column I want to match on, and a tableCell property which is the template within the directive.
#Directive({ selector: 'ui-table-cell' })
export class TableCellDirective {
#Input() public match: string = '';
#ContentChild('tableCell', { static: false }) public tableCell!: any;
}
Next when the table loads I load all of the templates into an object where the key is the cell id and the value is the template.
export class TableComponent implements AfterContentInit {
#ContentChildren(TableCellDirective)
public cellDirectives!: QueryList<TableCellDirective>;
public columnMeta: { [key: string]: object; } = {};
public ngAfterContentInit() {
this.cellDirectives.toArray().forEach(colData => this.assignColumnMetaInfo(colData));
}
private assignColumnMetaInfo(colData: TableCellDirective) {
const columnMetaInfo: { [key: string]: object; } = {};
columnMetaInfo['tableCell'] = colData.tableCell;
this.columnMeta[colData.match] = columnMetaInfo;
}
}
In the HTML, I then check if the current cell has a template saved if so, I display the template. If not I display the original data.
<tr *ngFor="let row of rowItems" class="uitk-c-table__row">
<td *ngFor="let cell of model.headers">
<ng-container *ngIf="columnMeta[cell.id] && columnMeta[cell.id].tableCell" [ngTemplateOutlet]="columnMeta[cell.id].tableCell" [ngTemplateOutletContext]="{content: row[cell.id]}"></ng-container>
<ng-container *ngIf="!columnMeta[cell.id]">{{row[cell.id] || ''}}</ng-container>
</td>
</tr>
Finally, to use it I just do the following:
<ui-table *ngIf="tableDataModel.rows?.length>0" [model]="tableDataModel" (onRequestEdit)="onOpenEdit($event)">
<ui-table-cell match="permissions">
<ng-template #tableCell let-content="content">
{{content|arrayToList}}
</ng-template>
</ui-table-cell>
</ui-table>

Related

Why is ngOnChanges executed when modifying a local property of a class?

I have been dealing with this scenario for a while, I appreciate your advice in advance
ngOnChanges runs in a context that I understand it shouldn't run. When modifying a property of a class which was initially set through #Input. This modification causes ngOnchanges hook to be executed in one context and not in another. I describe my scenario below
I have the following parent component that contains a list of customers that is passed to a child component,
Parent controller
export class AppComponent {
customers: ICustomer[];
currentlySelected: Option = 'All';
constructor() {
this.customers = [
{
id: 1,
name: 'Task1',
status: 'Pending',
},
{
id: 2,
name: 'Task2',
status: 'Pending',
},
{
id: 3,
name: 'Task3',
status: 'Progress',
},
{
id: 4,
name: 'Task4',
status: 'Closed',
},
];
}
selectBy(option: Option): void {
this.currentlySelected = option;
}
filterBy(): ICustomer[] {
if (this.currentlySelected === 'All') {
return this.customers;
}
return this.customers.filter(
(customer) => customer.status === this.currentlySelected
);
}
}
Parent template
<nav>
<ul>
<li (click)="selectBy('All')">All</li>
<li (click)="selectBy('Pending')">Pending</li>
<li (click)="selectBy('Progress')">Progress</li>
<li (click)="selectBy('Closed')">Closed</li>
</ul>
</nav>
<app-list [customers]="filterBy()"></app-list>
Before passing customer to the child component they are filtered according to the customer status property, that is the purpose of the filterBy function.
The child component in the hook ngOnChanges modifies each customer by adding the showDetail property and assigns it the value false
export class ListComponent implements OnInit, OnChanges {
#Input() customers: ICustomer[] = [];
constructor() {}
ngOnChanges(changes: SimpleChanges): void {
this.customers = changes.customers.currentValue.map(
(customer: ICustomer) => ({
...customer,
showDetail: false,
})
);
}
ngOnInit(): void {
console.log('init');
}
toggleDetail(current: ICustomer): void {
this.customers = this.customers.map((customer: ICustomer) =>
customer.id === current.id
? { ...customer, showDetail: !customer.showDetail }
: { ...customer }
);
}
}
Calling the toggleDetail method changes the value of the showDetail property to show the customer's detail
child template
<table>
<thead>
<tr>
<th>Name</th>
<th></th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let customer of customers">
<tr>
<td>{{ customer.name }}</td>
<td>
<button (click)="toggleDetail(customer)">Detail</button>
</td>
</tr>
<tr *ngIf="customer.showDetail">
<td colspan="2">
<pre>
{{ customer | json }}
</pre>
</td>
</tr>
</ng-container>
</tbody>
</table>
The behavior that occurs is the following, when all clients are listed and click on detail it works as expected, but if I change to another state and the list is updated and I click on detail it does not show the detail. The reason is that the ngOnchanges hook is re-executed causing the showDetail property to be set to false again, thus defeating my intention.
Why is ngOnChanges executed in this context? What alternative is there to solve it?
Update 1
I have added sample app: https://stackblitz.com/edit/angular-ivy-dkvvgt?file=src/list/list.component.html
You has in your code
<app-list [customers]="filterBy()"></app-list>
Angular can not know the result of the function "filterBy" until executed, so it's executed each time check. This is the reason we should avoid use functions in our .html (We can use, of course, but we need take account this has a penalty). Use an auxiliar variable
customerFilter:ICustomer[];
In constructor you add
constructor() {
this.customers = [...]
this.customerFilter=this.customers //<--add this line
}
And in selectBy
selectBy(option: Option): void {
this.currentlySelected = option;
this.customerFilter=this.filterBy() //<--this line
}
Now pass as argument the customerFilter
<app-list [customers]="customerFilter"></app-list>
Your forked stackblitz
Angular runs ngOnChanges when any of the inputs change. When you use an object as an input parameter Angular compares references. As Eliseo said Angular calls your filterBy function on each change detection, and it's not a problem when the currentlySelected is All, beacuse you return the same array reference and it won't trigger change detection in your list component. However when it's not, that causes an issue. You filter your array on each change detection and that results in a new array every time. Now Angular detects that the #Input() changed and runs ngOnChanges.
You can do as Eliseo said, that's a solution too. My suggestion is to create a pipe, it's makes the component.ts less bloated.
#Pipe({
name: 'filterCustomers',
})
export class FilterCustomersPipe implements PipeTransform {
transform(customers: ICustomer[] | null | undefined, filter: Option | undefined | undefined): ICustomer[] | undefined {
if (!customers) {
return undefined;
}
if (!filter || filter === 'All') {
return customers;
}
return customers.filter((customer) => customer.status === filter);
}
}
I prefere writing out null | undefined too, so it's safer with strictTemplates.
You can use this pipe like this:
<app-list [customers]="customers | filterCustomers : currentlySelected"></app-list>
Here you can read more about Angular pipes.
Another suggestion:
Your nav doesn't have button elements, you bind your (click) events on li elements. That's a really bad practice as it not focusable by keyboard. More about HTML Accessibility.

Angular ag-Grid: how to autogenerate default template for cell but with different name of field?

I have ag-grid when a lot of columns have specific templates. But some of data that I put into table are nothing more than just text until now.. I want to put something more for default case:
<ng-template #defaultRecord let-record>
ADDITIONAL THINGS HERE
<!-- CHOOSEN FIELD HERE -->
</ng-template>
So I have method for autogenerating columns:
private generateColumn(headerNameKey: string, colId: string, ngTemplate: TemplateRef<any>, filter = true, sortable = true, field?: string) {
const headerName = headerNameKey ? this.translateService.instant(headerNameKey) : '';
return {
headerName,
field,
sortable,
filter,
colId,
cellRendererFramework: CellRendererComponent,
cellRendererParams: {
ngTemplate
}
};
}
And I don't know how to use unspecified field in template. For example I get from api data when field is called "XYZ", how to make display it in my default template?
In this case:
<ng-template #defaultRecord let-record>
ADDITIONAL THINGS HERE
<span> Value of XYZ param</span>
</ng-template>
Can someone help me? Thanks!
EDIT 1:
Custom CellRenderer Component:
export class CellRendererComponent implements ICellRendererAngularComp {
template: TemplateRef<any>;
templateContext: { $implicit: any, params: ICellRendererParams };
agInit(params: ICellRendererParams) {
this.template = params['ngTemplate'];
this.refresh(params);
}
refresh(params: ICellRendererParams): boolean {
this.templateContext = {
$implicit: params.data,
params
};
return true;
}
}
And how params in agInit method looks:
And as You can see in the picture, in the template I want to display value of Fields.Date param that is defined field. How I suppose to use it in the template above?
if I understand correctly what you want to do.
Then here is one way.
You add context parameter to the template context.
<ng-template #defaultRecord let-record let-value="value">
ADDITIONAL THINGS HERE
<span> {{value}} </span>
</ng-template>
And you calculate this value in renderer component.
refresh(params: ICellRendererParams): boolean {
const valuePath = params.colDef.field;
const value = _.get(params.data, valuePath); // using lodash
this.templateContext = {
$implicit: params.data,
params,
value
};
return true;
}
I am using lodash function, but you can use other library or write simple method to get value from the object using its path.

Filter search on specific attributes of an object

I'm trying to filter some tasks in a table according only to the attributes I'm giving in the table but the search bar also gives tasks that have other attributes matching with the input. Ie there's a boolean attribute and when I type "true" in the search bar, the tasks remain even though they don't have that string in the name i display. Do you know how I could restrict the search to only these attributes ?
<input type="text" ng-model="searchFilter">
<table>
<tr ng-repeat="task in taskList | filter: searchFilter">
<td>{{task.task_desc_1}}</td>
<td>{{task.task_desc_2}}</td>
<td>{{task.task_desc_3}}</td>
<td>{{task.task_bv_ref}}</td>
</tr>
</table>
You can create your own pipe (a class which implements PipeTransform with #Pipe annotation).
Overload transform() method like below:
#Pipe({
name: 'searchTask'
})
export class SearchTaskPipe implements PipeTransform {
transform(tasks: Task[], searchText: string): Task[] {
if(!tasks){
return [];
}
if(!searchText){
return tasks;
}
searchText=searchText.trim();
searchText=searchText.toLowerCase();
return tasks.filter(t=>{
if(t.task_desc_1.toLowerCase().includes(searchText)||
t.task_desc_2.toLowerCase().includes(searchText)||
t.task_desc_3.toLowerCase().includes(searchText)||
t.task_bv_ref.toLowerCase().includes(searchText)||
){
return true;
}
}
}
}
Add this pipe to your declarations of NgModule. Then in your HTML you can use it as below:
<tr ng-repeat="task in taskList | searchTask: searchFilter">

Filter on multiple columns in a table using one pipe in Angular

Hi everyone.
I want to make a custom filter for my table which intakes more than one argument
to search multiple columns .. in my case right now only one argument can be passed .
thanks in advance
component.html
<tr *ngFor = "let builder of builderDetailsArray[0] | filter :'groupName': searchString; let i = index" >
<td style="text-align: center;"><mat-checkbox></mat-checkbox></td>
<td>{{builder.builderId}}</td>
<td>{{builder.viewDateAdded}}</td>
<td>{{builder.viewLastEdit}}</td>
<td>{{builder.groupName}}</td>
<td>{{builder.companyPersonName}}</td>
<td style="text-align: center;"><button mat-button color="primary">UPDATE</button></td>
</tr>
pipe.ts
#Pipe({
name: "filter",
pure:false
})
export class FilterPipe implements PipeTransform {
transform(items: any[], field: string, value: string): any[] {
if (!items) {
return [];
}
if (!field || !value) {
return items;
}
return items.filter(singleItem =>
singleItem[field].toLowerCase().includes(value.toLowerCase()) );
}
Created multiple arguments pipe in angular 4
The code lets you search through multiple columns in your table.
Passed 2 arguments in the transform function
value: Which involves all the data inside the table, all columns
searchString: What you want to search inside the columns (inside the table).
Hence, you can define which columns to be searched inside the transform function.
In this case, the columns to be searched are builderId, groupName and companyPersonName
Pipe file
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({
name: "arrayFilter"
})
export class BuilderFilterPipe implements PipeTransform {
transform(value:any[],searchString:string ){
if(!searchString){
console.log('no search')
return value
}
return value.filter(it=>{
const builderId = it.builderId.toString().includes(searchString)
const groupName = it.groupName.toLowerCase().includes(searchString.toLowerCase())
const companyPersonName = it.companyPersonName.toLowerCase().includes(searchString.toLowerCase())
console.log( builderId + groupName + companyPersonName);
return (builderId + groupName + companyPersonName );
})
}
}
What does the transform function do?
builderId, groupName and companyPersonName are the three fields I searched
builderId converted to string because our searchString is in string format.
toLowerCase() is used to make search accurate irrespective of user search in lowercase or uppercase
Html:
<tr *ngFor = "let builder of newBuilderDetailsArray | arrayFilter:search" >
<td>{{builder.builderId}}</td>
<td>{{builder.groupName}}</td>
<td>{{builder.companyPersonName}}</td>
</tr>
Make sure your filter.ts file added to module.ts file
Below is the sample code where I have passed 10 and 2 as arguments to pipes similarly you can pass multiple arguments and get it through parameters in pipe component. increase the arguments in pipe component with respect to number of inputs. working demo
Template
<p>Super power boost: {{2 | exponentialStrength:10:2}}</p>
Pipe
import { Pipe, PipeTransform } from '#angular/core';
/*
* Raise the value exponentially
* Takes an exponent argument that defaults to 1.
* Usage:
* value | exponentialStrength:exponent
* Example:
* {{ 2 | exponentialStrength:10 }}
* formats to: 1024
*/
#Pipe({name: 'exponentialStrength'})
export class ExponentialStrengthPipe implements PipeTransform {
transform(value: number, exponent1: any,exponent2: any): number {
let exp = parseFloat(exponent2);
return Math.pow(value, isNaN(exp) ? 1 : exp);
}
}

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>

Categories

Resources