ng-matero / extensions

Angular Material Extensions Library.
https://ng-matero.github.io/extensions/
MIT License
393 stars 48 forks source link

[Data Grid] Allow for styling each td individually #178

Closed marcelimati closed 1 year ago

marcelimati commented 1 year ago

In angular-material table you can style your <td> by using [style] or [ngStyle] or [class] to add styles/classes dynamically to each rendered row in <ng-container>.

Custom template for cell is cool but it does only render content inside <td>, so for now I can only change <td> for example via javascript after rendering content or atleast that was my first thought how to achieve this using that workaround.

Would you add this functionallity to the component? The component is really great, thanks for your hard work!

nzbin commented 1 year ago

Please check the example custom class for row and column.

You can add a special class for a row (e.g. success, danger). In addition, every cell has a special class made with column's field key (e.g. mat-column-name, mat-column-weight). So you can use the following way to set styles for a cell.

.success .mat-column-name {
...
}
marcelimati commented 1 year ago

This will style all rows/columns. I want to style each cell in column differently.

marcelimati commented 1 year ago

The thing is: rowClassFormatter is adding the class name to the <tr> not to <td>, but I don't really use it because of performance issues, because it's a function it gets called with every scroll or data change which is not I'm looking for. When it would add class to only specific td with given condition I would be able to achieve styling each <td> differently by combining class resulted in rowClassFormatter and columnName. For the column classes below:

columns: MtxGridColumn[] = [
    { header: 'Name', field: 'name' },
    { header: 'Weight', field: 'weight' },
    { header: 'Gender', field: 'gender' },
    { header: 'Mobile', field: 'mobile', class: 'warning' },
    { header: 'City', field: 'city' },
  ];

By inserting class name in columns object, this it will insert the same class to every <td> in current column which is literally the same as .mat-column-name. With basic angular-material table i can do following thing in .html file:

  <ng-container matColumnDef="name">
            <th mat-header-cell *matHeaderCellDef mat-sort-header> Name </th>
            <td mat-cell *matCellDef="let element" class="{{ element.color }}">{{ element.name }} </td>
  </ng-container>

This will generate <td> in each row depending on what was in the row in color object property only once.

Another thing I'm also using is condition based class which is similar to given example above, I could achieve it with:

  <ng-container matColumnDef="name">
            <th mat-header-cell *matHeaderCellDef mat-sort-header> Name </th>
            <td mat-cell *matCellDef="let element" [ngClass]="{'young': element.age < 18, 'adult': element.age > 18 && element.age < 60, 'old': element.age > 60}">{{ element.name }} </td>
  </ng-container>

With this i can apply different colors for each row in given column. Template in Data Grid component would be perfect for it if it would allow for styling cells like I styled in example above but it just changes rendered template inside <td>.

marcelimati commented 1 year ago

Hello, i checked what changes did you implement. This will work, but calling functions in html file will heavilly affect performance. Because everytime user will move mouse / scroll through the component, function will be executed. You can read about this for example in here: https://medium.com/showpad-engineering/why-you-should-never-use-function-calls-in-angular-template-expressions-e1a50f9c0496 Best solution for it would be the pipes, because you can achieve same thing with it and it won't call the function hundreds of times.

nzbin commented 1 year ago

15.1.0 has released, please have a try.

marcelimati commented 1 year ago

I've tested it and it works nice, but I've run into small issue. Class didn't change even if condition in the row was changed, so ChangeDetector should re-assign the values again. It only happened when I reassigned all values again. Manually calling detectChanges() from grid component didn't work either. Other than that, thank you for making this functionality so quickly! Hope it will come in handy for other people as well!

nzbin commented 1 year ago

I've tested it and it works nice, but I've run into small issue. Class didn't change even if condition in the row was changed, so ChangeDetector should re-assign the values again. It only happened when I reassigned all values again. Manually calling detectChanges() from grid component didn't work either. Other than that, thank you for making this functionality so quickly! Hope it will come in handy for other people as well!

I know this issue because it use a pipe and it can't detect object changed. Do you have any suggestions? https://github.com/ng-matero/extensions/blob/9bec47efb82a41b4b0f918b8402bade63bdec6b6/projects/extensions/grid/grid.html#L121

marcelimati commented 1 year ago

I know pipe is detecting changes when variable used in pipe (in your example: "col" ) get changed. Maybe something like adding value which will be binded to cell, so maybe this will make angular call DetectChanges? Because now that object is static. Another thing i discovered is: when I call async method to fetch data again to the array variable which was used in mtx grid it works(and when I re-assigned data by removing dataSource and assigning it again without fetching data it didn't change, so only with async method). So I guess it only detect changes with async data fetching.

nzbin commented 1 year ago

I know pipe is detecting changes when variable used in pipe (in your example: "col" ) get changed. Maybe something like adding value which will be binded to cell, so maybe this will make angular call DetectChanges? Because now that object is static. Another thing i discovered is: when I call async method to fetch data again to the array variable which was used in mtx grid it works(and when I re-assigned data by removing dataSource and assigning it again without fetching data it didn't change, so only with async method). So I guess it only detect changes with async data fetching.

15.1.1 has fixed it, please have a try and give me some feedback.

marcelimati commented 1 year ago

I know pipe is detecting changes when variable used in pipe (in your example: "col" ) get changed. Maybe something like adding value which will be binded to cell, so maybe this will make angular call DetectChanges? Because now that object is static. Another thing i discovered is: when I call async method to fetch data again to the array variable which was used in mtx grid it works(and when I re-assigned data by removing dataSource and assigning it again without fetching data it didn't change, so only with async method). So I guess it only detect changes with async data fetching.

15.1.1 has fixed it, please have a try and give me some feedback.

Nothing happend when I changed column value in one record, or perhaps do I need to add something more to my code? at the moment i have something like this:

columns = [
  { header: 'Name', field: 'name'},
  { header: 'Value', field: 'value', 
    class: data => {
      return data?.value == 1 ? 'success' : '';
    } 
  }
];

changeValueFunc(id: number, value: number) {
  const record = this.records.find(x => x.id == id);
  record.value = value;
}

Above method was working in regular mat-table but cell class still stays the same as rendered initially in 15.1.1 update.

nzbin commented 1 year ago

With your use case, you should detect changes manually.

@ViewChild('grid', { static: true }) grid!: MtxGrid;

changeValueFunc(id: number, value: number) {
  const record = this.records.find(x => x.id == id);
  record.value = value;
  this.grid.detectChanges();
}
marcelimati commented 1 year ago

With your use case, you should detect changes manually.

@ViewChild('grid', { static: true }) grid!: MtxGrid;

changeValueFunc(id: number, value: number) {
  const record = this.records.find(x => x.id == id);
  record.value = value;
  this.grid.detectChanges();
}

In past replies I said that I called this.grid.detectChanges() manually and it didn't work either. Just tested with newest update, same problem.

I've tried this too:

 this.records = [...this.records];

The only way that I can update rows in table is refreshing all records array with async method that is re-assigning the variable that grid is refferencing to.

records = await backendService.getAll();

columns = [
  { header: 'Name', field: 'name'},
  { header: 'Value', field: 'value', 
    class: data => {
      return data?.value == 1 ? 'success' : '';
    } 
  }
];

changeValueFunc(id: number, value: number) {
  const record = this.records.find(x => x.id == id);
  record.value = value;
  await backendService.updateOne(record);
  this.records = await backendService.getAll();
}

Calling async function that way is triggering this function

 class: data => {
      return data?.value == 1 ? 'success' : '';
    } 

run again, so I can achieve same effect. Because technically it's okay to re-run above function but skipping this.records = await backendService.getAll(); it would be perfect.

Update:

changeValueFunc(id: number, value: number) {
  const record = this.records.find(x => x.id == id);
  record.value = value;
  this.grid._onRowDataChange(record);
}

I tried above and it changed the class as expected, but only once. When I changed it back to initial value, it didn't apply again.

nzbin commented 1 year ago

I created an online example. https://stackblitz.com/edit/angular-material-15-starter-x1xj4z-rsdmhq?file=src%2Fapp%2Fapp.component.ts

marcelimati commented 1 year ago

image This only worked when value got changed once. I changed value to 12, then to 5 and it's still with warning class

marcelimati commented 1 year ago

Okay, after hours of testing i finally noticed why it worked for you and didn't work for me. https://stackblitz.com/edit/angular-material-15-starter-x1xj4z-rsdmhq-i8prbz?file=src%2Fapp%2Fapp.component.ts I forked your project and made some comments.

The thing is when grid is using templates (but not always ?) i don't know what really causes it. For example with 4 templates it doesn't work, but with 3 it works. (But when there are 3: weightTemplate, genderTemplate, mobileTemplate - it doesn't work ) So this problem only occurs with templates.

Edit: When all columns are templates, it doesn't work at all So far, calling this.grid._onRowDataChange(record); manually after updating record is best, but it only refresh row once.

nzbin commented 1 year ago

Thanks for your test case, the new version 15.1.2 has released and the online demo works fine. https://stackblitz.com/edit/angular-material-15-starter-x1xj4z-rsdmhq-hh7f2p?file=src%2Fapp%2Fapp.component.ts

marcelimati commented 1 year ago

That version fixed case when there is atleast one column without template. But it still doesn't work when all columns are templates. In that situation rowDataChange event is not firing at all.

nzbin commented 1 year ago

That version fixed case when there is at least one column without template. But it still doesn't work when all columns are templates. In that situation rowDataChange event is not firing at all.

Because I use mtx-grid-cell to detect the changes of row data. There has no mtx-grid-cell if you make all field to template. So the rowDataChange event wont be firing. And also the colClass pipe wont execute as all its params doesn't change.

https://github.com/ng-matero/extensions/blob/6a136c81d26974cc0b9e9773e6ef89dd1c696c3e/projects/extensions/grid/grid.html#L121

In this way, you should manually change the rowChangeRecord to force update.

  changeValue(id: number, field: string, value: number) {
    const record = this.list.find((x) => x.id == id);
    record[field] = value;
    this.grid.rowChangeRecord = { ...record } as any; // hack!!!
  }

https://stackblitz.com/edit/angular-material-15-starter-x1xj4z-rsdmhq-hh7f2p?file=src%2Fapp%2Fapp.component.ts

There has many limitations to wrap mat-table and it's difficult to detect changes if you don't change the record's pointer. Maybe it's better to use mat-table instead of mtx-grid if your needs are very complex.

marcelimati commented 1 year ago

Thank you it works perfectly, I can now use this manual hack when I know my table will contain templates only, and in other cases it will just work!