angular / components

Component infrastructure and Material Design components for Angular
https://material.angular.io
MIT License
24.23k stars 6.7k forks source link

[Sort] Programatically setting active/direction does not update UI #10242

Open shahmirn opened 6 years ago

shahmirn commented 6 years ago

Bug, feature request, or proposal:

I'm programmatically setting the active and direction on matSort, but it's not updating the UI

What is the expected behavior?

programmatically setting the active and direction on matSort updates the UI

What is the current behavior?

programmatically setting the active and direction on matSort, but it's not updating the UI

What are the steps to reproduce?

https://stackblitz.com/edit/angular-material2-issue-mc4cve?file=app/app.component.ts

What is the use-case or motivation for changing an existing behavior?

I believe this is a regression. This was working in 5.1.0

Which versions of Angular, Material, OS, TypeScript, browsers are affected?

Is there anything else we should know?

rsenna commented 6 years ago

I'm seeing the same issue. Works in 5.1.0, does not in 5.2.*.

Hotell commented 6 years ago

Same issue here, it's kinda deal breaker, as you cannot set values dynamically or imperatively. I'm currently implementing a data table which reflects state to URL via query params, everything works ( even pagination set dynamically, cannot say the same for sort )

doesn't work with 5.2.5 nor 6.x

demo: https://stackblitz.com/edit/github-issue-ng-material-sorting-10242

Ian5015 commented 6 years ago

Encountering this as well in 6.0.0.

ericbae commented 6 years ago

Has there been any update to this?

sergidt commented 6 years ago

same here!!

Is a very important feature and is not working. I attach and example where I change the sort field dynamically after two seconds and you can see what undesired behavior is shown:

https://stackblitz.com/edit/angular-owjp47?file=app/sort-overview-example.html

jansivans commented 6 years ago

6.3.3 - bug still here: https://stackblitz.com/edit/angular-a4yakd

ThierryLehoux commented 5 years ago

Could be usefull when reflect state to URL via query params.

abbasharoon commented 5 years ago

Any update on this?

adgoncal commented 5 years ago

I spent some time debugging this. Here are my observations:

When you click on a mat-sort-header that is not currently active, eventually it will call _setAnimationTransitionState({fromState: '<direction>', toState: 'active'}) on the clicked mat-sort-header.

However, when you programmatically call matSort.sort({...}), the mat-sort-header that should become active never calls _setAnimationTransitionState().

I believe this is an issue on _rerenderSubscription().

I managed to make it work with the following ugly hack:

    this.matSort.sort({
      id: <value>,
      start: <direction>,
      disableClear: true,
    });

    // ugly hack!
    const activeSortHeader = this.matSort.sortables.get(value);
    activeSortHeader['_setAnimationTransitionState']({
      fromState: <direction>,
      toState: 'active',
    });

https://stackblitz.com/edit/angular-vuvckf?file=app%2Ftable-overview-example.ts


References:

https://github.com/angular/material2/blob/9ab2c905e2e81999347742f880bfd851f9e32022/src/lib/sort/sort-header.ts#L230

https://github.com/angular/material2/blob/9ab2c905e2e81999347742f880bfd851f9e32022/src/lib/sort/sort-header.ts#L145-L159

https://github.com/angular/material2/blob/9ab2c905e2e81999347742f880bfd851f9e32022/src/lib/sort/sort-header.ts#L204-L212

ms-dosx86 commented 5 years ago

Does not work with v7.0.0

ms-dosx86 commented 5 years ago

v7.0.3 works perfectly

literalpie commented 5 years ago

I believe the original intent of this issue was that mat-sort does not respond to programatic changes in active and direction. The discussion on this issue seems to be assuming that the sorting is done by calling the sort function, not changing the two attributes.

stackblitz that shows the issue I am talking about

has this issue been fully changed to just be about the header not updating? If so, I will create a new issue for updating sort via active and direction

marcusadrian commented 5 years ago

Still not working in version 7.1.0, arrow doesn't update showing the wrong sort column. Strangely disappears on wrong column when hovering over without appearing on right column.

rzubrycki commented 5 years ago

Hi guys, any update on this one?

soldierdi commented 5 years ago

Hello everyone,

does anyone know how the input properties "matSortActive" and "matSortDirection" for the matSort directive work. When I set them the respective sort arrow is not updated. It would be nice to update the sort arrows with the help of observables using these two input properties.

DmitryEfimenko commented 5 years ago

v7.1.1 - still not working.

LautaroLorenz commented 5 years ago

not working y.y

Nikkio commented 5 years ago
this.sort.active = ''; this.sort.direction = 'asc' as SortDirection;
this.sort.sortChange.emit();

this resets matSort UI without triggering server requests.

PhilippMeissner commented 5 years ago
this.sort.active = ''; this.sort.direction = 'asc' as SortDirection;
this.sort.sortChange.emit();

this resets matSort UI without triggering server requests.

Well, this is basically the same solution as @austin5456 provided but remains a workaround and should not be necessary in a perfect world.

Anderman commented 5 years ago

The ArrowIcon is set in the clickhandler of matSortHeader. Is see only two ugly hacks. add a stylesheet entry

::ng-deep .mat-sort-header-sorted .mat-sort-header-arrow {
     opacity: 1 !important;
}

or call this.matSortHeader._setAnimationTransitionState({ toState: 'active' });

You can use something like this.

@ViewChild(MatSort) public matSort: MatSort;

and

   public setSort(id: string, start?: 'asc' | 'desc') {
    start = start || 'asc';
    const matSort = this.dataSource.sort;
    const toState = 'active';
    const disableClear = false;

    //reset state so that start is the first sort direction that you will see
    matSort.sort({ id: null, start, disableClear });
    matSort.sort({ id, start, disableClear });

    //ugly hack
    (matSort.sortables.get(id) as MatSortHeader)._setAnimationTransitionState({ toState });
  }
adgoncal commented 5 years ago

@Anderman that is very similar to what I have been using for a few months:

import { Component, AfterViewInit, OnDestroy } from '@angular/core';
import { Sort, MatSort, MatSortHeader } from '@angular/material';

import { Subject, Observable, of } from 'rxjs';
import {
  catchError,
  distinctUntilChanged,
  map,
  takeUntil,
  withLatestFrom,
} from 'rxjs/operators';

@Component({
  // ...
})
export class MyComponent implements AfterViewInit, OnDestroy {
  @ViewChild(MatSort) matSort: MatSort;

  private destroyed$ = new Subject<void>();

  private sort$: Observable<Sort>;

  ngAfterViewInit() {
    // HACK: fix sort header arrow after programmatically changing sort order
    // https://github.com/angular/material2/issues/10242
    // TODO: Remove this hack when the bug is fixed on @angular/material
    this.matSort._stateChanges
      .pipe(
        withLatestFrom(this.sort$),
        map(([_, sort]) => sort),
        takeUntil(this.destroyed$),
        catchError(error => {
          return of(error);
        })
      )
      .subscribe((sort: Sort) => {
        const sortables = this.matSort.sortables;
        const activeSortHeader = <MatSortHeader>(
          sortables.get(sort.active)
        );

        activeSortHeader._setAnimationTransitionState({
          fromState: activeSortHeader._arrowDirection,
          toState: 'active',
        });
      });
  }

  ngOnDestroy() {
    this.destroyed$.next();
    this.destroyed$.complete();
  }
}
Anderman commented 5 years ago

@adgoncal In some messages the problem seems to be solved or suggests other solutions that do not work. I spent to much time on this problem by trying some of those solutions. At least it was not clear for me how to make a workarond.

iamFujiYama commented 5 years ago

Hi all, sorry for my poor English skill , but I wanted to sharing my solution.

I also encountered this bug and tried use _setAnimationTransitionState Func, But I found the UI transition is not smooth, ( U need mouse focus then the arrow will change ...)

and then , I tried use _handleClick() function ( it seems can trigger click target column's click event )

so , i provider another dirty hack : The point is set "opposite direction" which you want the arrow be

           this.sort.active = _TARGET_HEADER_NAME_
           // I want set the arrow as 'asc', so  set the direction 'desc' once, 
           // then call _handleClick to trigger click event
           this.sort.direction  = 'desc' as SortDirection
           const _SortHeader =  this.sort.sortables.get('_TARGET_HEADER_NAME_') as MatSortHeader
           _SortHeader._handleClick()

If your sort using server , need add a flag to control server communication like this :

// _doNotRequestToServer is the flag 

 this.sort.sortChange.subscribe(
        () => {
          return doSort([ {
            [this.sort.active]: this.sort.direction
          } ], this._doNotRequestToServer);
        }
    );

I wish maybe it can help u :)

keatkeat87 commented 5 years ago

any update on this issue ? 8.0 problem same...

devkrish94 commented 4 years ago

using 8.1.1 still getting the same issue

sharan-zweck commented 4 years ago

The ArrowIcon is set in the clickhandler of matSortHeader. Is see only two ugly hacks. add a stylesheet entry

::ng-deep .mat-sort-header-sorted .mat-sort-header-arrow {
     opacity: 1 !important;
}

or call this.matSortHeader._setAnimationTransitionState({ toState: 'active' });

You can use something like this.

@ViewChild(MatSort) public matSort: MatSort;

and

   public setSort(id: string, start?: 'asc' | 'desc') {
    start = start || 'asc';
    const matSort = this.dataSource.sort;
    const toState = 'active';
    const disableClear = false;

    //reset state so that start is the first sort direction that you will see
    matSort.sort({ id: null, start, disableClear });
    matSort.sort({ id, start, disableClear });

    //ugly hack
    (matSort.sortables.get(id) as MatSortHeader)._setAnimationTransitionState({ toState });
  }

This is the best workaround for the timebeing

JayAhn2 commented 4 years ago

Hi guys,

Here is my implementation for this issue.

You can control the exact sorting behavior(ASC / DESC / CLEAR) with this code. I wrapped the logic with 'if statement' to prevent the change of 'matSort' after certain sorting behavior executed.

@ViewChild(MatSort, { static: false }) matSort: MatSort;

  sortData(id: string, start?: 'asc' | 'desc') {
    const disableClear = false;
    const currentDirection = this.matSort.direction;

    if (start === 'asc' && currentDirection !== 'asc') {
      this.matSort.sort({ id: null, start, disableClear });
      this.matSort.sort({ id, start, disableClear });
    } else if (start === 'desc' && currentDirection !== 'desc') {
      this.matSort.sort({ id: null, start, disableClear });
      this.matSort.sort({ id, start, disableClear });
    }
  }

  clearSort(id: string) {
    if (this.matSort.direction === 'asc') {
      this.matSort.sort({ id, start: 'desc', disableClear: false });
    } else if (this.matSort.direction === 'desc') {
      this.matSort.sort({ id, start: 'asc', disableClear: false });
    }
  }
<div mat-menu-item mat-filter-item [disableRipple]="true">
      <button mat-raised-button (click)="sortData('name', 'asc')">ASC</button>
      <button mat-raised-button (click)="sortData('name', 'desc')">DESC</button>
      <button mat-raised-button (click)="clearSort('name')">Clear</button>
</div>

For your info, check this out in StackBlitz

Thanks.

jakubnavratil commented 4 years ago

Hi Andrew @andrewseguin :) Can we prioritize this issue a little more? According to reactions, this is most wanted FIX for Sort component (https://github.com/angular/components/issues?utf8=%E2%9C%93&q=project%3Aangular%2Fcomponents%2F18+is%3Aopen+sort%3Areactions-%2B1-desc+)

API reveals sort method, which works fine, the only thing missing is call for updating animation state - so actual arrow is hidden after sorting programaticaly.

In SortHeaderComponent, there are two places, where UI is updated.

  1. click handler https://github.com/angular/components/blob/198911f5c0dd198f42ee0476a3291ec9858a0660/src/material/sort/sort-header.ts#L243
  2. sort change subscription - which is called also from click handler (1.) https://github.com/angular/components/blob/198911f5c0dd198f42ee0476a3291ec9858a0660/src/material/sort/sort-header.ts#L162

To fix this, we just need to move most of the click handler logic to subscription logic, as it is also called, becase click handler calls that sort method. This way everything is handled in subscription and click handler only calls sort method.

So we can move this logic from click handler:

   // Do not show the animation if the header was already shown in the right position.
    if (this._viewState.toState === 'hint' || this._viewState.toState === 'active') {
      this._disableViewStateAnimation = true;
    }

    // If the arrow is now sorted, animate the arrow into place. Otherwise, animate it away into
    // the direction it is facing.
    const viewState: ArrowViewStateTransition = this._isSorted() ?
        {fromState: this._arrowDirection, toState: 'active'} :
        {fromState: 'active', toState: this._arrowDirection};
    this._setAnimationTransitionState(viewState);

To subscription handler after these lines

   if (this._isSorted()) {
        this._updateArrowDirection();
   }

and run it only when condition this._sort.active == this.id is met.

There propably needs to be one more condition or something. but the main idea is that this is actually really easy fix for the most wanted issue of Sort component.

Thanks for considering this and thanks for your hard work at Angular team, this product is amazing! :)

joshcomley commented 4 years ago

This problem still persists. Pinging to get it attention.

WhiteStarLine commented 4 years ago

Just spent 2 days trying to get a server-side change to update the UI. It only worked if the user clicked on a sort arrow. JayAhn2's code describes the problem as I see it and provides a workaround that worked immediately. Hope this gets addressed at some stage. Cheers.

lukekroon commented 4 years ago

<table matSort (matSortChange)="sortData($event)" [matSortActive]="sortActive" [matSortDirection]="sortDirection" matSortDisableClear>

Setting the variables sortActive and sortDirection in the component does the job for me. It updates the arrow as well.

https://github.com/angular/components/issues/7838#issuecomment-380380985

v9.1+

shahmirn commented 4 years ago

@lukekroon It doesn't work in the same scenario as the original bug, which is code in the callback of .subscribe()

https://stackblitz.com/edit/angular-material2-issue-qjaqm4?file=app%2Fapp.component.ts

NanduSurendranNair commented 4 years ago

I have tried with the below snippet and it is working as expected. Once the state of the material table is set, then executing it made the UI to update the sort arrow.

Using angular material version 7.3.7 Here id refers to the current active sort column name.

(matSort.sortables.get(id) as MatSortHeader)._updateArrowDirection();

binson1989 commented 3 years ago

Still facing this issue with angular material v10.0.1.

@andrewseguin any update on a possible fix?

ccamba commented 3 years ago

@Nikkio in my case only with using this.sort.sortChange.emit(); works. Thanks

codemile commented 3 years ago

Hi everyone,

I've found probably the easiest workaround for this issue. The only side effect is that it requires the table to be redrawn when sort changes, but it's extremely simple to implement and works in most cases.

<table *ngFor="let hack of [sort]"
            mat-table
            matSort
            [matSortActive]="sort?.active"
            (matSortChange)="setSort($event)"
            [matSortDirection]="sort?.direction">

When ever you change the sort: Sort property on the component in an immutable way, then it will trigger the <table> to be recreated and the matSort component renders the active sort header correctly, because it does render it correctly the first time.

I find this solution is better future proofed, because when they later fix this issue, then you can just remove the *ngFor and it should continue to work without the performance lag.

I tried many of the above hacks, but suffered from many other side effects in my source code from trying to handle both external and internal sort change events. The above hack solves those issues, because (matSortChange) does not emit a value for the first time the table is rendered. So it only fires when the user clicks on a header, and doesn't suffer from feedback loops some of the higher solutions introduced in my source code.

telb99 commented 3 years ago

Still an issue in 10.2.4 Wasted several hours before finding this issue...

Melmoth-the-Wanderer commented 3 years ago

Can someone confirm this works in v11? @andrewseguin please talk to us

edit: this is still an issue in v 11.2.0

rudzikdawid commented 3 years ago

angular 11.2.6 material 11.2.5

works better but still there is a little problem with refresh ui after model matSortActive, matSortDirection change:
https://stackblitz.com/edit/angular-i8z8ny

Issue:

  1. Open stackblitz example
  2. Click inside table header in column symbol after that: sort arrow shuld appears inside symbol column
  3. Click in reset filter button below the table
  4. sort arrow is still visible in symbol column header but shuld be in name column header

Description:
button reset filter triggers method onResetFilter from parent controller.

onResetFilter() {
  this.matSortActive = "name";
  this.matSortDirection = "asc";
}

calling this method sometimes triggers ui changes, sometimes not.

@pavelmarozau @andrewseguin

stnor commented 2 years ago

I had a repaint issue. This solved it for me:

const sortHeader: MatSortHeader = this.sort.sortables.get(this.sort.active) as MatSortHeader;
sortHeader._setAnimationTransitionState({fromState: sortHeader._arrowDirection, toState: 'active'});
this.sort.sortChange.emit(sort);
enekonieto commented 2 years ago

Still an issue in 13.1.3

erksmaas commented 2 years ago

Would be great if this could be fixed

PhilippMeissner commented 2 years ago

Hold your horses, it's only been 4 years.

BruneXX commented 2 years ago

Wow, this is still an issue.. :´(

technbuzz commented 2 years ago

I have a table with sorting, filtering, tabs, pagination with server where it is run by URL being source of truth. Everything works but this bug is ruining all it's potential. I wonder what's stopping its implementation.

chacabuk commented 1 year ago

setTimeout(() => { this.dataSource.sort = this.sort; }, 100);

PhilippMeissner commented 1 year ago

setTimeout(() => { this.dataSource.sort = this.sort; }, 100);

If you really want to rely on setTimeout you may as well set the delay to 0. The callback will still be put in the appropriate queue and called, but without the assumingly unneeded delay.

hunterlan commented 1 year ago

Guys, any plans for this bug? 👀

daviekr commented 1 year ago

bump

mgremm commented 4 months ago

Upgrading from Angular 7 to 15 broke my mat-table's programmatically determined sort. I tried many things, but in the end I fixed my programmatically generated sort by changing

 @ViewChild(MatSort)
 matSort: MatSort = new MatSort;

to

@ViewChild(MatSort, { static: true }) matSort: MatSort;

No this.sort.sortChange.emit(sort); needed.