Angular MatSort打断MatTable细节行动画
在我到这里之前,我已经为这个问题绞尽脑汁很久了。基本上,我有一个角度材质表,它使用动画创建细节行。当表格排序时,它会重新排列数据。在该过程中,某些详图行会转换为void。之后,细节行停止播放动画,即使动画事件正在触发。我怀疑MatSort以某种方式破坏了动画,但我不确定如何破坏 角材料表:Angular MatSort打断MatTable细节行动画,angular,animation,angular-animations,Angular,Animation,Angular Animations,在我到这里之前,我已经为这个问题绞尽脑汁很久了。基本上,我有一个角度材质表,它使用动画创建细节行。当表格排序时,它会重新排列数据。在该过程中,某些详图行会转换为void。之后,细节行停止播放动画,即使动画事件正在触发。我怀疑MatSort以某种方式破坏了动画,但我不确定如何破坏 角材料表: <mat-table matSort [dataSource]="tableData" multiTemplateDataRows> <!-
<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>
更多
相遇
{{scheduleCourse.Meets}
学期
{{scheduleCourse.Term}
课程名称
{{scheduleCourse.Course}
老师
{{scheduleCourse.Teacher}}
房间
{{scheduleCourse.Room}
参赛日期
{{scheduleCourse.EntryDate.toString()!=junkDate.toString()?scheduleCourse.EntryDate.toLocaleDateString():''}}
删除日期
{{scheduleCourse.DroppedDate.toString()!=junkDate.toString()?scheduleCourse.DroppedDate.toLocaleDateString():''}
团队
{{scheduleCourse.TeamCode}
课程组
{{scheduleCourse.SubjectCode}}-{{scheduleCourse.Section}
在此之后,动画停止工作。此外,我还测试了是否可以创建空洞过渡动画,但该动画也无法播放
现在,我知道tableData工作正常,因为该表显示良好。此外,在从排序触发该事件之前,动画可以完美地工作。事实上,排序工作正常,“detailRow.done”事件即使在动画未播放时也会持续触发。所以,我知道这一定与MatSort和动画交互有关:我只是不知道是什么
以下是我尝试过的:
- 删除[ngStyle]和[ngClass]
- 删除表格及其容器上的宽度和高度样式
- 卸下ngDoCheck生命周期挂钩
- 更改mat sort标头以使用matColumnDef,并使matColumnDef与排序属性名称匹配
- 使用setTimeout将排序设置为tableData
- 在排序更改后,将表“跳出”到DOM中
- 排序更改后强制在表上显示renderRows
更新1
我试图在stackblitz中重现这个问题,但没有成功。看起来MatSort和Angular动画彼此配合得很好,而且这里还发生了其他一些事情。这给了我一些方向
更新2
所以,我发现了问题,尽管这是一个奇怪的问题。我用几个辅助函数扩展了MatTableDataSource,在这里我得到了“tableData.checkExpanded”和“tableData.toggleExpanded”函数。当我使用组件中的布尔数组检查扩展时,组件工作正常。当我使用这些函数时,我最终遇到了这个问题。这是该类的代码。我可能会更新stackblitz,看看是否可以用这个复制它
导出类TylerMatTableDataSource扩展了MatTableDataSource我也遇到了同样的问题,通过将动画从
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)')),
])
触发器('detailExpand'[
状态('collapped',样式({height:'0px',minHeight:'0',display:'none'})),
状态('expanded',样式({height:'*'})),
过渡(“展开折叠”,动画(“225ms立方贝塞尔(0.4,0.0,0.2,1)”,
])
到
触发器('detailExpand'[
状态('collapped,void',style({height:'0px',minHeight:'0',display:'none'})),
状态('expanded',样式({height:'*'})),
过渡(“展开折叠”,动画(“225ms立方贝塞尔(0.4,0.0,0.2,1)”,
过渡(“展开的空心”,动画(“225ms立方贝塞尔(0.4,0.0,0.2,1))
])
只有第一个状态('collapsed')
到状态('collapsed,void'
和最后一个转换(…)
行的更改
现在,排序和扩展行都可以按预期工作
解决方案归功于pabloFuente。Angular 10解决方案
export const detailExpand=触发器('detailExpand',
[
状态('折叠,无效',样式({height:'0px'})),
状态('expanded',样式({height:'*'})),
过渡(“展开折叠”,动画(“225ms立方贝塞尔(0.4,0.0,0.2,1)”,
过渡(“展开的空心”,动画(“225ms立方贝塞尔(0.4,0.0,0.2,1))
]);
使用Flex布局(无“表”、“tr”、“td”元素)时,我无法获得动画效果
@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;
}
}
}
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;
});
}
}
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)'))
])
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)'))
]);
> 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;
}
}