MatSort 破坏了 MatTable 细节行动画
MatSort breaks MatTable detail row animations
在我来到这里之前,我已经为这个问题苦苦思索了好一阵子。本质上,我有一个 Angular Material table 使用动画来创建详细信息行。当 table 排序时,它会重新排列数据。在该过程中,一些详细信息行会过渡到无效。之后,详细信息行停止播放动画,即使动画事件正在触发。我怀疑 MatSort 以某种方式破坏了动画,但我不确定是如何破坏的。
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>
详情展开动画:
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)'))
])
];
我的组件,以备不时之需:
@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. 在这个之后,动画停止工作。另外,我已经测试过是否可以创建一个无效的过渡动画,但是那个动画也没有播放。
现在,我知道 table 数据正常工作,因为 table 显示正常。此外,在从排序中触发该事件之前,动画可以完美运行。事实上,排序有效并且 "detailRow.done" 事件即使在动画未播放时也会继续触发。所以,我知道这一定与 MatSort 和动画交互有关:我只是不知道是什么。
这是我尝试过的方法:
- 删除 [ngStyle] 和 [ngClass]
- 删除 table 及其容器的宽度和高度样式
- 删除 ngDoCheck 生命周期挂钩
- 更改 mat-sort-header 以使用 matColumnDef 并使 matColumnDef 匹配排序 属性 名称
- 使用 setTimeout 将排序设置为 tableData
- "Bouncing" table进出DOM排序后变化
- 在排序更改后 table 上强制执行 renderRows
更新 1
我尝试在 stackblitz 中重现该问题,但未能成功。似乎 MatSort 和 Angular 动画彼此配合得很好,而且这里还有其他事情发生。这给了我一些方向。
更新 2
所以,我找到了问题所在,尽管它是一个问题很奇怪。我用一些辅助函数扩展了 MatTableDataSource,这是我获得 "tableData.checkExpanded" 和 "tableData.toggleExpanded" 函数的地方。当我使用组件中的布尔值数组来检查扩展时,组件工作正常。当我使用这些功能时,我遇到了这个问题。这是 class 的代码。我可能会更新 stackblitz,看看我是否可以使用它来重现它。
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;
});
}
}
更新 3
我已经更新了 stackblitz 来演示这个问题。请注意,只有当我在 'More' 列的 p 标签上使用两个 *ngIf 时才会发生这种情况。如果我使用插值,则不会发生错误。
注意:这适用于 Angular < 10,请参阅 了解 Angular >= 10 解决方案
我遇到了同样的问题并通过更改动画
添加额外的 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 的解决方案 here。
Angular10个解
export const detailExpand = trigger('detailExpand',
[
state('collapsed, void', style({ height: '0px'})),
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)'))
]);
使用 Flex 布局时(没有 'table'、'tr'、'td' 元素),我无法让动画触发器可靠地进行排序。对 table 进行排序后,有些行只是随机死亡。我正在使用 Angular 10.
经过四个小时的调试和测试,我转向 [ngClass] 和 css 动画,效果完美。
> mat-row.detail-row {
overflow: hidden;
border-style: none;
min-height: auto;
&.detail-row-collapsed {
max-height: 0;
transition: max-height .4s ease-out;
}
&.detail-row-expanded {
max-height: 1000px;
transition: max-height .4s ease-in;
}
}
在我来到这里之前,我已经为这个问题苦苦思索了好一阵子。本质上,我有一个 Angular Material table 使用动画来创建详细信息行。当 table 排序时,它会重新排列数据。在该过程中,一些详细信息行会过渡到无效。之后,详细信息行停止播放动画,即使动画事件正在触发。我怀疑 MatSort 以某种方式破坏了动画,但我不确定是如何破坏的。
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>
详情展开动画:
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)'))
])
];
我的组件,以备不时之需:
@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. 在这个之后,动画停止工作。另外,我已经测试过是否可以创建一个无效的过渡动画,但是那个动画也没有播放。
现在,我知道 table 数据正常工作,因为 table 显示正常。此外,在从排序中触发该事件之前,动画可以完美运行。事实上,排序有效并且 "detailRow.done" 事件即使在动画未播放时也会继续触发。所以,我知道这一定与 MatSort 和动画交互有关:我只是不知道是什么。
这是我尝试过的方法:
- 删除 [ngStyle] 和 [ngClass]
- 删除 table 及其容器的宽度和高度样式
- 删除 ngDoCheck 生命周期挂钩
- 更改 mat-sort-header 以使用 matColumnDef 并使 matColumnDef 匹配排序 属性 名称
- 使用 setTimeout 将排序设置为 tableData
- "Bouncing" table进出DOM排序后变化
- 在排序更改后 table 上强制执行 renderRows
更新 1
我尝试在 stackblitz 中重现该问题,但未能成功。似乎 MatSort 和 Angular 动画彼此配合得很好,而且这里还有其他事情发生。这给了我一些方向。
更新 2
所以,我找到了问题所在,尽管它是一个问题很奇怪。我用一些辅助函数扩展了 MatTableDataSource,这是我获得 "tableData.checkExpanded" 和 "tableData.toggleExpanded" 函数的地方。当我使用组件中的布尔值数组来检查扩展时,组件工作正常。当我使用这些功能时,我遇到了这个问题。这是 class 的代码。我可能会更新 stackblitz,看看我是否可以使用它来重现它。
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;
});
}
}
更新 3
我已经更新了 stackblitz 来演示这个问题。请注意,只有当我在 'More' 列的 p 标签上使用两个 *ngIf 时才会发生这种情况。如果我使用插值,则不会发生错误。
注意:这适用于 Angular < 10,请参阅
我遇到了同样的问题并通过更改动画
添加额外的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 的解决方案 here。
Angular10个解
export const detailExpand = trigger('detailExpand',
[
state('collapsed, void', style({ height: '0px'})),
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)'))
]);
使用 Flex 布局时(没有 'table'、'tr'、'td' 元素),我无法让动画触发器可靠地进行排序。对 table 进行排序后,有些行只是随机死亡。我正在使用 Angular 10.
经过四个小时的调试和测试,我转向 [ngClass] 和 css 动画,效果完美。
> mat-row.detail-row {
overflow: hidden;
border-style: none;
min-height: auto;
&.detail-row-collapsed {
max-height: 0;
transition: max-height .4s ease-out;
}
&.detail-row-expanded {
max-height: 1000px;
transition: max-height .4s ease-in;
}
}