angular - MatSort breaks MatTable detail row animations
问题描述
I've been beating my head against this problem for quite a while before I got here. Essentially, I have an Angular Material table that uses animations to create a detail row. When the table sorts, it rearranges the data. Some of the detail rows have a transition to void during that process. Afterwards, the detail rows stop playing the animation, even though the animation events are firing. I suspect that MatSort is breaking the animations somehow, but I'm not sure how.
Angular Material table:
<mat-table matSort
[dataSource]="tableData"
multiTemplateDataRows>
<!-- More Column -->
<ng-container matColumnDef="more">
<mat-header-cell *matHeaderCellDef
translate>
More
</mat-header-cell>
<mat-cell *matCellDef="let scheduleCourse">
<p class="fa fa-angle-right" *ngIf="!tableData.checkExpanded(scheduleCourse)"></p>
<p class="fa fa-angle-down" *ngIf="tableData.checkExpanded(scheduleCourse)"></p>
</mat-cell>
</ng-container>
<!-- Meets Column -->
<ng-container matColumnDef="meets">
<mat-header-cell *matHeaderCellDef
mat-sort-header="Meets"
translate>
Meets
<filter [data]="tableData" columnName="Meets" dataType="string"></filter>
</mat-header-cell>
<mat-cell *matCellDef="let scheduleCourse">
{{scheduleCourse.Meets}}
</mat-cell>
</ng-container>
<!-- Term Column -->
<ng-container matColumnDef="term">
<mat-header-cell *matHeaderCellDef
mat-sort-header="Term"
translate>
Term
<filter [data]="tableData" columnName="Term" dataType="string"></filter>
</mat-header-cell>
<mat-cell *matCellDef="let scheduleCourse">
{{scheduleCourse.Term}}
</mat-cell>
</ng-container>
<!-- Course Name Column -->
<ng-container matColumnDef="course">
<mat-header-cell *matHeaderCellDef
mat-sort-header="Course"
translate>
Course Name
<filter [data]="tableData" columnName="Course" dataType="string"></filter>
</mat-header-cell>
<mat-cell *matCellDef="let scheduleCourse">
{{scheduleCourse.Course}}
</mat-cell>
</ng-container>
<!-- Teacher Column -->
<ng-container matColumnDef="teacher">
<mat-header-cell *matHeaderCellDef
mat-sort-header="Teacher"
translate>
Teacher
<filter [data]="tableData" columnName="Teacher" dataType="string"></filter>
</mat-header-cell>
<mat-cell *matCellDef="let scheduleCourse">
{{scheduleCourse.Teacher}}
</mat-cell>
</ng-container>
<!-- Room Column -->
<ng-container matColumnDef="room">
<mat-header-cell *matHeaderCellDef
mat-sort-header="Room"
translate>
Room
<filter [data]="tableData" columnName="Room" dataType="string"></filter>
</mat-header-cell>
<mat-cell *matCellDef="let scheduleCourse">
{{scheduleCourse.Room}}
</mat-cell>
</ng-container>
<!-- Entry Date Column -->
<ng-container matColumnDef="entry date">
<mat-header-cell *matHeaderCellDef
mat-sort-header="EntryDate"
translate>
Entry Date
<filter [data]="tableData" columnName="EntryDate" dataType="date"></filter>
</mat-header-cell>
<mat-cell *matCellDef="let scheduleCourse">
{{scheduleCourse.EntryDate.toString() != junkDate.toString() ? scheduleCourse.EntryDate.toLocaleDateString() : ''}}
</mat-cell>
</ng-container>
<!-- Dropped Date Column -->
<ng-container matColumnDef="dropped date">
<mat-header-cell *matHeaderCellDef
mat-sort-header="DroppedDate"
translate>
Dropped Date
<filter [data]="tableData" columnName="DroppedDate" dataType="date"></filter>
</mat-header-cell>
<mat-cell *matCellDef="let scheduleCourse">
{{scheduleCourse.DroppedDate.toString() != junkDate.toString() ? scheduleCourse.DroppedDate.toLocaleDateString() : ''}}
</mat-cell>
</ng-container>
<!-- Team Column -->
<ng-container matColumnDef="team">
<mat-header-cell *matHeaderCellDef
mat-sort-header="TeamCode"
translate>
Team
<filter [data]="tableData" columnName="TeamCode" dataType="string"></filter>
</mat-header-cell>
<mat-cell *matCellDef="let scheduleCourse">
{{scheduleCourse.TeamCode}}
</mat-cell>
</ng-container>
<!-- Expand Row 1 -->
<ng-container matColumnDef="expandedRow">
<td mat-cell
*matCellDef="let scheduleCourse"
[attr.colspan]="columns.length"
style="width: 100%">
<!-- Links and Actions -->
<div class="detailRow">
<div class="detailItem">
<label style="color: #595959" translate>Course-Section</label>
{{scheduleCourse.SubjectCode}}-{{scheduleCourse.Section}}
</div>
<a class="detailItem"
(click)="assignmentClick(scheduleCourse)"
translate>
Assignments
</a>
<a class="detailItem"
(click)="attendanceClick(scheduleCourse)"
translate>
Attendance
</a>
<a class="detailItem"
(click)="emailTeacherClick(scheduleCourse)"
translate>
Email Teacher
</a>
<a class="detailItem"
(click)="gradesClick(scheduleCourse)"
translate>
Grades
</a>
<!-- Menu Button -->
<button class="detailItem"
*ngIf="showProfiles"
style="cursor: pointer; border: none; background-color: inherit;"
[matMenuTriggerFor]="actionMenu"
[matMenuTriggerData]="{'scheduleCourse': scheduleCourse}">
<img src="./assets/images/actions.png"
alt="actions">
</button>
</div>
<!-- School Indicator -->
<div *ngIf="showSchool(scheduleCourse)"
class="detailRow">
<div class="detailItem">
<label style="color: #595959" translate>
School
</label>
{{scheduleCourse.SchoolName}}
</div>
</div>
</td>
</ng-container>
<!-- Row definitions -->
<mat-header-row *matHeaderRowDef="columns"></mat-header-row>
<mat-row *matRowDef="let row; columns: columns;"
matRipple
tabindex="0"
style="cursor: pointer"
[ngStyle]="{'background-color': selectedRow == row ? 'whitesmoke' : ''}"
[ngClass]="{'detailRowOpened': tableData.checkExpanded(row)}"
(click)="tableData.toggleExpanded(row); selectedRow = row;"></mat-row>
<mat-row *matRowDef="let row; columns: ['expandedRow']"
matRipple
(click)="selectedRow = row;"
[ngClass]="{'selectedRow': selectedRow == row}"
(@detailExpand.done)="animation($event)"
[@detailExpand]="tableData.checkExpanded(row) ? 'expanded' : 'collapsed'"
style="overflow: hidden"></mat-row>
</mat-table>
The detailExpand animation:
export const detailExpand = [
trigger('detailExpand', [
state('collapsed', style({
paddingTop: '0px',
height: '0px',
minHeight: '0',
paddingBottom: '0px'
})),
state('expanded', style({
paddingTop: '*',
height: 'auto',
paddingBottom: '25px'
})),
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)'))
])
];
My component, in case you need it:
@Component({
selector: 'student-schedule',
templateUrl: './student-schedule.component.html',
styleUrls: [
'./student-schedule.component.css'
],
animations: [
detailExpand
]
})
export class StudentScheduleComponent implements OnInit, DoCheck, OnDestroy {
// Properties
private _viewOption = 1;
private _includeDropped = false;
schedule: ScheduleCourse[] = [];
subscriptions: Subscription[] = [];
tableData = new TylerMatTableDataSource();
junkDate = System.junkDate;
V10: boolean;
columns = ['more', 'meets', 'term', 'course', 'teacher', 'room', 'entry date', 'dropped date', 'team'];
selectedRow: ScheduleCourse;
expandEmitter = new EventEmitter<boolean>();
tableHeight: number;
minTableWidth: number;
@ViewChild('tableContainer', {read: ElementRef}) tableContainer: ElementRef;
showProfiles: boolean;
studentEnrollment: Enrollment;
_sort: MatSort;
// Class Functions
constructor(
private studentScheduleService: StudentScheduleService,
private loginService: LoginService,
private router: Router,
private dialog: MatDialog,
private studentService: StudentService,
private sendEmailService: SendEmailService
) { }
get viewOption(): number {
return this._viewOption;
}
set viewOption(value: number) {
this._viewOption = value;
this.getSchedule();
}
get includeDropped(): boolean {
return this._includeDropped;
}
set includeDropped(value: boolean) {
this._includeDropped = value;
this.checkColumns();
}
@ViewChild(MatSort) set sort(value: MatSort) {
this._sort = value;
this.tableData.sort = this._sort;
}
get sort(): MatSort {
return this._sort;
}
// Event Functions
ngOnInit() {
// POST: initializes the data
this.V10 = this.loginService.LoginSettings.V10;
this.showProfiles = this.loginService.LoginSettings.ParentPortalCourseScheduleProfiles;
this.checkColumns();
this.subscriptions.push(
this.expandEmitter.subscribe(expand => {
this.tableData.expandAll(expand);
}),
this.studentService.selectedStudentStream$.subscribe(() => {
this.studentEnrollment = this.studentService.studentEnrollment;
this.getSchedule();
})
);
}
ngDoCheck() {
// POST: determines the height and width of the table container
if (this.tableContainer) {
this.tableHeight = System.getTableHeight(this.tableContainer);
}
}
ngOnDestroy() {
// POST: unsubscribes to all observables
this.subscriptions.forEach(subscription => {
subscription.unsubscribe();
});
}
assignmentClick(scheduleCourse: ScheduleCourse) {
// PRE: the user clicks on an assignment link under a course
// POST: routes the user to that assignment page
// TODO: Ensure it links to the proper class
this.router.navigateByUrl('/student360/assignments');
}
attendanceClick(scheduleCourse: ScheduleCourse) {
// PRE: the user clicks on an attendance link under a course
// POST: routes the user to that attendance page
this.router.navigateByUrl('/student360/attendance');
}
emailTeacherClick(scheduleCourse: ScheduleCourse) {
// PRE: the user clicks on an attendance link under a course
// POST: routes the user to the email page
// TODO: Ensure it links to the proper teacher
this.sendEmailService.teacherName = scheduleCourse.TeacherName;
this.sendEmailService.teacherEmailAddress = scheduleCourse.TeacherEmail;
this.router.navigateByUrl('/student360/sendEmail');
}
gradesClick(scheduleCourse: ScheduleCourse) {
// PRE: the user clicks on a grade link under a course
// POST: routes the user to the grade page
this.router.navigateByUrl('/student360/reportcardgrades');
}
courseDescriptionClick(scheduleCourse: ScheduleCourse) {
// PRE: the user clicks on a course description link under a course
// POST: shows a modal for the course's description
this.dialog.open(CourseDescriptionDialogComponent, {
data: {
course: scheduleCourse.Course,
section: scheduleCourse.Section,
teacherName: scheduleCourse.TeacherName,
schoolName: scheduleCourse.SchoolName,
curriculum: scheduleCourse.Curriculum,
description: scheduleCourse.Description
}
});
}
classInformationClick(scheduleCourse: ScheduleCourse) {
// PRE: the user clicks on a class information link under a course
// POST: shows a modal for that class' profile
this.dialog.open(ProfileViewerDialogComponent, {
data: {
courseSSEC_ID: scheduleCourse.Id,
courseName: scheduleCourse.Course,
courseSection: scheduleCourse.Section,
teacherName: scheduleCourse.TeacherName,
school: scheduleCourse.SchoolName
}
});
}
teacherProfileClick(scheduleCourse: ScheduleCourse) {
// PRE: the user clicks on a teacher profile link under a couse
// POST: shows a modal for that teacher's profile
this.dialog.open(ProfileViewerDialogComponent, {
data: {
teacherId: scheduleCourse.TeacherId,
teacherName: scheduleCourse.TeacherName,
school: scheduleCourse.SchoolName
}
});
}
animation(event) {
console.log(event);
}
// Methods
showSchool(scheduleCourse: ScheduleCourse): boolean {
return this.studentEnrollment.SchoolName &&
scheduleCourse.SchoolName &&
this.studentEnrollment.SchoolName.trim().toUpperCase() != scheduleCourse.SchoolName.trim().toUpperCase();
}
getSchedule() {
// POST: obtains the schedule from the server
this.subscriptions.push(
this.studentScheduleService.getStudentSchedule(this.viewOption).subscribe(schedule => {
this.schedule = schedule;
for (let i = 0; i < this.schedule.length; i++) {
this.schedule[i] = System.convert<ScheduleCourse>(this.schedule[i], new ScheduleCourse());
}
this.tableData = new TylerMatTableDataSource(this.schedule);
if (this.sort) {
this.tableData.sort = this.sort;
}
})
);
}
checkColumns() {
// POST: checks the columns for ones that shouldn't be there
// Team is a V9 only column
if (this.V10 && this.columns.includes('team')) {
this.columns.splice(this.columns.indexOf('team'), 1);
} else if (!this.V10 && !this.columns.includes('team')) {
this.columns.push('team'); // Team is always on the end
}
// Entry date and dropped date are only there if include dropped
if (this.includeDropped) {
if (!this.columns.includes('entry date')) {
this.columns.splice(5, 0, 'entry date');
}
if (!this.columns.includes('dropped date')) {
this.columns.splice(6, 0, 'dropped date');
}
this.minTableWidth = 1000;
} else {
if (this.columns.includes('dropped date')) {
this.columns.splice(this.columns.indexOf('dropped date'), 1);
}
if (this.columns.includes('entry date')) {
this.columns.splice(this.columns.indexOf('entry date'), 1);
}
this.minTableWidth = 750;
}
}
}
This is the animation event to void that I'm talking about. After this one, the animation stops working. Also, I've tested to see if I can create a void transition animation, but that animation doesn't play either.
Now, I know that the tableData works properly because the table displays fine. Further, the animations work perfectly before that event is fired from sorting. In fact, the sorting works and the "detailRow.done" event keeps firing even when the animation isn't playing. So, I know it must be something to do with MatSort and Animation interaction: I just don't know what.
Here's what I've tried:
- Removing [ngStyle] and [ngClass]
- Removing the width and height styling on the table and its container
- Removing the ngDoCheck lifecycle hook
- Changing mat-sort-header to use the matColumnDef and making the matColumnDef match the sort property name
- Using a setTimeout to set the sort to the tableData
- "Bouncing" the table in and out of the DOM after the sort changes
- Forcing a renderRows on the table after sort changes
UPDATE 1
I tried reproducing the problem in a stackblitz, but I couldn't do so successfully. It appears that MatSort and Angular Animations play well with each other and that something else is going on here. That gives me some direction.
UPDATE 2
So, I've found the problem, although it's odd that it is a problem. I've extended the MatTableDataSource with a few helper functions, which is where I get the "tableData.checkExpanded" and "tableData.toggleExpanded" functions. When I use an array of booleans from the component to check for expansion, the component works fine. When I used those functions, I end up with this problem. This is the code for that class. I may update the stackblitz to see if I can reproduce it using this.
export class TylerMatTableDataSource extends MatTableDataSource<any>{
filterNumber:number = 0;
filterTestValue:string = '';
filters:FilterModel[] = [];
expandedElements:number[] = [];
constructor(initialData?: any[]){
super(initialData);
this.filterPredicate = this.genericFilter;
}
toggleExpanded(row: any) {
if (row != undefined) {
if(row.detailRow == undefined || row.detailRow == false){
row.detailRow = true;
}
else{
row.detailRow = false;
}
}
}
checkExpanded(row:any):boolean{
if(row.detailRow == undefined){
row.detailRow = false;
}
return row.detailRow;
}
expandAll(expand: boolean) {
this.data.forEach(element => {
element.detailRow = expand;
});
}
}
UPDATE 3
I've updated the stackblitz to demonstrate the problem. Note that this only happens when I use two *ngIf's on the p tags in the 'More' column. If I use interpolation, the error does not occur.
解决方案
我遇到了同样的问题,并void
通过更改动画添加了一个额外的状态来解决
trigger('detailExpand', [
state('collapsed', style({ height: '0px', minHeight: '0', display: 'none' })),
state('expanded', style({ height: '*' })),
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
])
至
trigger('detailExpand', [
state('collapsed, void', style({ height: '0px', minHeight: '0', display: 'none' })),
state('expanded', style({ height: '*' })),
transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')),
transition('expanded <=> void', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)'))
])
只有更改是第一state('collapsed'
行state('collapsed, void'
和最后transition(...)
一行。
现在排序和扩展行都按预期工作。
感谢 pabloFuente 的解决方案。
推荐阅读
- java - JAVA:如何获取目录中的前 N 个 SORTED 条目?
- zip - 如何提取 Jester 数据集文件?
- python - 使用 python3 解析 json 元素
- angular - 如何在构造函数中使用可观察订阅对角度防护进行单元测试
- powershell - Powershell - 更改返回的 SQL 数据变量的值失败
- wpf - 如何使用 WPF 在 Xamarin.Forms 中检测鼠标单击位置?
- php - PHP用多个LIKE条件准备SQL
- graphql - ApolloServer 使用 mergeSchemas,错误:Apollo Server 需要现有架构、模块或 typeDefs
- macos - Filebeat:从 Mac OS 上正在运行的 docker 映像读取日志
- firebase - Firebase 应用程序仅适用于本地主机,初始化和部署成功