Angular + Material - Como atualizar uma fonte de dados (esteira)


120

Estou usando um tapete para listar o conteúdo dos idiomas escolhidos pelos usuários. Eles também podem adicionar novos idiomas usando o painel de diálogo. Depois eles adicionaram um idioma e voltaram. Quero que minha fonte de dados seja atualizada para mostrar as alterações que eles fizeram.

Eu inicializo o armazenamento de dados obtendo dados do usuário de um serviço e passando-os para uma fonte de dados no método de atualização.

Language.component.ts

import { Component, OnInit } from '@angular/core';
import { LanguageModel, LANGUAGE_DATA } from '../../../../models/language.model';
import { LanguageAddComponent } from './language-add/language-add.component';
import { AuthService } from '../../../../services/auth.service';
import { LanguageDataSource } from './language-data-source';
import { LevelbarComponent } from '../../../../directives/levelbar/levelbar.component';
import { DataSource } from '@angular/cdk/collections';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { MatSnackBar, MatDialog } from '@angular/material';

@Component({
  selector: 'app-language',
  templateUrl: './language.component.html',
  styleUrls: ['./language.component.scss']
})
export class LanguageComponent implements OnInit {

  displayedColumns = ['name', 'native', 'code', 'level'];
  teachDS: any;
  user: any;

  constructor(private authService: AuthService, private dialog: MatDialog) { }

  ngOnInit() {
    this.refresh();
  }

  add() {
    this.dialog.open(LanguageAddComponent, {
      data: { user: this.user },
    }).afterClosed().subscribe(result => {
      this.refresh();
    });
  }

  refresh() {
    this.authService.getAuthenticatedUser().subscribe((res) => {
      this.user = res;
      this.teachDS = new LanguageDataSource(this.user.profile.languages.teach);   
    });
  }
}

language-data-source.ts

import {MatPaginator, MatSort} from '@angular/material';
import {DataSource} from '@angular/cdk/collections';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/observable/merge';
import 'rxjs/add/operator/map';

export class LanguageDataSource extends DataSource<any> {

  constructor(private languages) {
    super();
  }

  connect(): Observable<any> {
    return Observable.of(this.languages);
  }

  disconnect() {
    // No-op
  }

}

Tentei chamar um método de atualização em que obtenho o usuário do back-end novamente e reinicializo a fonte de dados. No entanto, isso não funciona, nenhuma alteração está ocorrendo.


1
Se você deseja acionar a mudança "da fonte de dados", dê uma olhada em stackoverflow.com/questions/47897694/…
Yennefer

Emissores de eventos podem ser usados ​​neste caso. stackoverflow.com/a/44858648/8300620
Rohit Parte

Respostas:


58

Acione uma detecção de mudança usando ChangeDetectorRefo refresh()método logo após receber os novos dados, injete ChangeDetectorRef no construtor e use detectChanges como este:

import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
import { LanguageModel, LANGUAGE_DATA } from '../../../../models/language.model';
import { LanguageAddComponent } from './language-add/language-add.component';
import { AuthService } from '../../../../services/auth.service';
import { LanguageDataSource } from './language-data-source';
import { LevelbarComponent } from '../../../../directives/levelbar/levelbar.component';
import { DataSource } from '@angular/cdk/collections';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { MatSnackBar, MatDialog } from '@angular/material';

@Component({
  selector: 'app-language',
  templateUrl: './language.component.html',
  styleUrls: ['./language.component.scss']
})
export class LanguageComponent implements OnInit {
  displayedColumns = ['name', 'native', 'code', 'level'];
  teachDS: any;

  user: any;

  constructor(private authService: AuthService, private dialog: MatDialog,
              private changeDetectorRefs: ChangeDetectorRef) { }

  ngOnInit() {
    this.refresh();
  }

  add() {
    this.dialog.open(LanguageAddComponent, {
      data: { user: this.user },
    }).afterClosed().subscribe(result => {
      this.refresh();
    });
  }

  refresh() {
    this.authService.getAuthenticatedUser().subscribe((res) => {
      this.user = res;
      this.teachDS = new LanguageDataSource(this.user.profile.languages.teach);
      this.changeDetectorRefs.detectChanges();
    });
  }
}

9
Isso parece funcionar, é a maneira correta de fazer isso? Parece um pouco hacky ...
Kay

Quais são as outras maneiras? Você poderia fornecer exemplos em sua solução para uma resposta completa?
Kay


88

Não sei se ChangeDetectorRefera necessário quando a pergunta foi criada, mas agora basta:

import { MatTableDataSource } from '@angular/material/table';

// ...

dataSource = new MatTableDataSource<MyDataType>();

refresh() {
  this.myService.doSomething().subscribe((data: MyDataType[]) => {
    this.dataSource.data = data;
  }
}

Exemplo:
StackBlitz


4
Embora essa solução funcione de fato, ela bagunça o paginador de material se o elemento for adicionado em algum lugar diferente da primeira página de resultados. Eu entendo que isso está além do escopo desta pergunta, mas como os dois estão relacionados, você tem uma solução rápida para esse problema que você poderia adicionar à sua resposta?
Cavaleiro de

5
@Knight Acho que você deve atribuir o Paginator à MatTableDataSource.paginatorpropriedade depois que a visualização do Paginator for inicializada. Veja a descrição da paginatorpropriedade aqui: material.angular.io/components/table/api#MatTableDataSource
Martin Schneider

Boa referência. Eu não tinha notado isso nos documentos antes. Obrigado!
Cavaleiro de

2
@ MA-Maddin você pode ser mais específico sobre como fazer isso? exemplo?
Nathanphan de

@Nathanphan atrasado, mas adicionou um exemplo;)
Martin Schneider

46

Então, para mim, ninguém deu uma boa resposta para o problema que encontrei que é quase o mesmo que @Kay. Para mim trata-se de classificar, a classificação da mesa não ocorre mudanças no tapete. Proponho esta resposta, pois é o único tópico que encontro pesquisando no google. Estou usando o Angular 6.

Como disse aqui :

Como a tabela é otimizada para desempenho, ela não verificará automaticamente se há alterações na matriz de dados. Em vez disso, quando os objetos são adicionados, removidos ou movidos no array de dados, você pode acionar uma atualização nas linhas renderizadas da tabela chamando seu método renderRows ().

Portanto, você só precisa chamar renderRows () em seu método refresh () para fazer suas alterações aparecerem.

Veja aqui para integração.


1
Se a fonte de dados da tabela for alterada no cliente, essa resposta é provavelmente o que você está procurando. Funciona bem!
Alvin Saldanha

Esta é a resposta correta a partir do material angular 8
Tom

Obrigado. Mas de qual objeto devo chamar o "renderRows ()"? Está em 'this.datasource'?
WitnessTruth

19

Já que você está usando MatPaginator, você só precisa fazer qualquer alteração no paginator, isso aciona a recarga dos dados.

Truque simples:

this.paginator._changePageSize(this.paginator.pageSize); 

Isso atualiza o tamanho da página para o tamanho da página atual, então basicamente nada muda, exceto a _emitPageEvent()função privada também é chamada, iniciando o recarregamento da tabela.


Tentei seu código e não funciona (não tem efeito). No entanto, nextPage e, em seguida, previousPage funcionam novamente, mas não é a solução.
Ahmed Hasn.

_changePageSize()não é público certo? É seguro usar? Mais em stackoverflow.com/questions/59093781/…
Jones

9
this.dataSource = new MatTableDataSource<Element>(this.elements);

Adicione esta linha abaixo de sua ação de adicionar ou excluir a linha particular.

refresh() {
  this.authService.getAuthenticatedUser().subscribe((res) => {
    this.user = new MatTableDataSource<Element>(res);   
  });
}

o que é
isso.elementos

8

A melhor maneira de fazer isso é adicionando um observável adicional à implementação da fonte de dados.

No método de conexão, você já deve estar usando Observable.mergepara assinar uma matriz de observáveis ​​que incluem paginator.page, sort.sortChange, etc. Você pode adicionar um novo assunto a isso e chamar next quando precisar causar uma atualização.

algo assim:

export class LanguageDataSource extends DataSource<any> {

    recordChange$ = new Subject();

    constructor(private languages) {
      super();
    }

    connect(): Observable<any> {

      const changes = [
        this.recordChange$
      ];

      return Observable.merge(...changes)
        .switchMap(() => return Observable.of(this.languages));
    }

    disconnect() {
      // No-op
    }
}

E então você pode ligar recordChange$.next()para iniciar uma atualização.

Naturalmente, eu envolveria a chamada em um método refresh () e a chamaria da instância da fonte de dados com o componente e outras técnicas adequadas.


Este método pode ser o caminho certo. Funciona bem para mim
Manu

como faço para implementar isso quando quero estender MatTableDataSource? Quando tento seu exemplo de código, recebo o erroProperty 'connect' in type 'customDataSource<T>' is not assignable to the same property in base type 'MatTableDataSource<T>'. Type '() => Observable<any>' is not assignable to type '() => BehaviorSubject<T[]>'. Type 'Observable<any>' is not assignable to type 'BehaviorSubject<T[]>'. Property '_value' is missing in type 'Observable<any>'.
Maurice

1
@Maurice o tipo MatTableDataSource implementa o método de conexão com um tipo de retorno diferente. Ele usa BehaviorSubject <t []> o que significa que você só precisa alterar o exemplo para retornar este em vez de um Observable. Você ainda deve ser capaz de usar DataSource, mas se tiver que usar MatTableDataSource, retorne um BehaviorSubject que está inscrito em seu observável, supondo que você tenha um para começar. Espero que ajude. Você pode consultar a fonte de MatTableDataSource para sintaxe exata do novo tipo de fonte de dados: github.com/angular/material2/blob/master/src/lib/table/…
jogi

7

Você pode apenas usar a função de conexão da fonte de dados

this.datasource.connect().next(data);

igual a. 'dados' sendo os novos valores para a tabela de dados


Tinha potencial, mas não parece funcionar. Se depois disso você acessar this.datasource.data, não é atualizado.
Rui Marques de

4

Você pode atualizar facilmente os dados da tabela usando "concat":

por exemplo:

language.component.ts

teachDS: any[] = [];

language.component.html

<table mat-table [dataSource]="teachDS" class="list">

E, quando você atualizar os dados (language.component.ts):

addItem() {
    // newItem is the object added to the list using a form or other way
    this.teachDS = this.teachDS.concat([newItem]);
 }

Quando você estiver usando o angular "concat" detecte as alterações do objeto (this.teachDS) e não precise usar outra coisa.

PD: Funcionou para mim no angular 6 e 7, não tentei outra versão.


2
Sim, funciona para mim, é um problema de referência e valor var, a detecção de alterações não vê as novas alterações, para isso é necessário atualizá-lo.
Mayra Rodriguez

Isso pode funcionar se dataSource for apenas uma matriz, mas não quando dataSource for um objeto MatTableDataSource.
Rui Marques


3

Bem, eu encontrei um problema semelhante em que adicionei algo à fonte de dados e ele não estava recarregando.

A maneira mais fácil que encontrei é simplesmente reatribuir os dados

let dataSource = ['a','b','c']
dataSource.push('d')
let cloned = dataSource.slice()
// OR IN ES6 // let cloned = [...dataSource]
dataSource = cloned

Perfeito!! Funciona !! Obrigado :)
Nicolas

3

No Angular 9, o segredo é this.dataSource.data = this.dataSource.data;

Exemplo:

import { MatTableDataSource } from '@angular/material/table';

dataSource: MatTableDataSource<MyObject>;

refresh(): void {
    this.applySomeModif();
    // Do what you want with dataSource

    this.dataSource.data = this.dataSource.data;
}

applySomeModif(): void {
    // add some data
    this.dataSource.data.push(new MyObject());
    // delete index number 4
    this.dataSource.data.splice(4, 0);
}

2

Consegui uma boa solução usando dois recursos:

atualizando dataSource e paginator:

this.dataSource.data = this.users;
this.dataSource.connect().next(this.users);
this.paginator._changePageSize(this.paginator.pageSize);

onde, por exemplo, dataSource é definido aqui:

    users: User[];
    ...
    dataSource = new MatTableDataSource(this.users);
    ...
    this.dataSource.paginator = this.paginator;
    ...

1
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';

export class LanguageComponent implemnts OnInit {
  displayedColumns = ['name', 'native', 'code', 'leavel'];
  user: any;
  private update = new Subject<void>();
  update$ = this.update.asObservable();

  constructor(private authService: AuthService, private dialog: MatDialog) {}

   ngOnInit() {
     this.update$.subscribe(() => { this.refresh()});
   }

   setUpdate() {
     this.update.next();
   }

   add() {
     this.dialog.open(LanguageAddComponent, {
     data: { user: this.user },
   }).afterClosed().subscribe(result => {
     this.setUpdate();
   });
 }

 refresh() {
   this.authService.getAuthenticatedUser().subscribe((res) => {
     this.user = res;
     this.teachDS = new LanguageDataSource(this.user.profile.languages.teach);   
    });
  }
}

8
Adicione uma explicação à sua resposta, apenas postar o código não é muito útil e pode resultar na remoção da sua resposta.
TJ Wolschon

1

no meu caso (Angular 6+), herdei de MatTableDataSourcepara criar MyDataSource. Sem ligar depoisthis.data = someArray

this.entitiesSubject.next(this.data as T[])

dados onde não são exibidos

classe MyDataSource

export class MyDataSource<T extends WhateverYouWant> extends MatTableDataSource<T> {

    private entitiesSubject = new BehaviorSubject<T[]>([]);


    loadDataSourceData(someArray: T[]){
        this.data = someArray //whenever it comes from an API asyncronously or not
        this.entitiesSubject.next(this.data as T[])// Otherwise data not displayed
    }

    public connect(): BehaviorSubject<T[]> {
        return this.entitiesSubject
    }

}//end Class 

1

Há duas maneiras de fazer isso porque o material angular é inconsistente e está muito mal documentado. A tabela de material angular não será atualizada quando uma nova linha chegar. Surpreendentemente, dizem que é por causa dos problemas de desempenho. Mas parece mais um problema de design, eles não podem mudar. Deve-se esperar que a tabela seja atualizada quando ocorrer uma nova linha. Se esse comportamento não deve ser ativado por padrão, deve haver um interruptor para desligá-lo.

De qualquer forma, não podemos alterar o material angular. Mas podemos basicamente usar um método muito mal documentado para fazer isso:

Um - se você usar um array diretamente como fonte:

call table.renderRows()

em que table é ViewChild da mesa-tapete

Segundo - se você usar classificação e outros recursos

table.renderRows () surpreendentemente não funcionará. Porque a mesa-tapete é inconsistente aqui. Você precisa usar um hack para dizer que a fonte mudou. Você faz isso com este método:

this.dataSource.data = yourDataSource;

onde dataSource é o wrapper MatTableDataSource usado para classificação e outros recursos.


0

Isso funcionou para mim:

refreshTableSorce() {
    this.dataSource = new MatTableDataSource<Element>(this.newSource);
}

Não é uma solução ideal, pois recria a fonte para a tabela. E usar isso com simplificado / soquete não é eficiente.
Maihan Nijat

0

Eu acho que o MatTableDataSource objeto está de alguma forma vinculado ao array de dados que você passa para o MatTableDataSourceconstrutor.

Por exemplo:

dataTable: string[];
tableDS: MatTableDataSource<string>;

ngOnInit(){
   // here your pass dataTable to the dataSource
   this.tableDS = new MatTableDataSource(this.dataTable); 
}

Então, quando você tiver que alterar os dados; mudança na lista original dataTablee, em seguida, reflete a mudança na mesa por chamada_updateChangeSubscription() método de em tableDS.

Por exemplo:

this.dataTable.push('testing');
this.tableDS._updateChangeSubscription();

Isso é trabalho comigo por meio do Angular 6.


4
Esse método é prefixado com um sublinhado _e você o chama?
Stephane

0

Isso está funcionando para mim:

dataSource = new MatTableDataSource<Dict>([]);
    public search() {
        let url = `${Constants.API.COMMON}/dicts?page=${this.page.number}&` + 
        (this.name == '' ? '' : `name_like=${this.name}`);
    this._http.get<Dict>(url).subscribe((data)=> {
    // this.dataSource = data['_embedded'].dicts;
    this.dataSource.data =  data['_embedded'].dicts;
    this.page = data['page'];
    this.resetSelection();
  });
}

Portanto, você deve declarar sua instância da fonte de dados como MatTableDataSource


0

Eu fiz mais pesquisas e encontrei este lugar para me dar o que eu precisava - parece limpo e relacionado a atualização de dados quando atualizado do servidor: https://blog.angular-university.io/angular-material-data-table/

A maioria dos créditos para a página acima. Abaixo está um exemplo de como um seletor de esteira pode ser usado para atualizar uma tabela de esteira ligada a uma fonte de dados na mudança de seleção. Estou usando o Angular 7. Desculpe por ser extenso, tentando ser completo, mas conciso - rasguei o máximo possível de partes desnecessárias. Com isso na esperança de ajudar alguém a avançar mais rápido!

organization.model.ts:

export class Organization {
    id: number;
    name: String;
}

organization.service.ts:

import { Observable, empty } from 'rxjs';
import { of } from 'rxjs';

import { Organization } from './organization.model';

export class OrganizationService {
  getConstantOrganizations(filter: String): Observable<Organization[]> {
    if (filter === "All") {
      let Organizations: Organization[] = [
        { id: 1234, name: 'Some data' }
      ];
      return of(Organizations);
     } else {
       let Organizations: Organization[] = [
         { id: 5678, name: 'Some other data' }
       ];
     return of(Organizations);
  }

  // ...just a sample, other filterings would go here - and of course data instead fetched from server.
}

organizationdatasource.model.ts:

import { CollectionViewer, DataSource } from '@angular/cdk/collections';
import { Observable, BehaviorSubject, of } from 'rxjs';
import { catchError, finalize } from "rxjs/operators";

import { OrganizationService } from './organization.service';
import { Organization } from './organization.model';

export class OrganizationDataSource extends DataSource<Organization> {
  private organizationsSubject = new BehaviorSubject<Organization[]>([]);

  private loadingSubject = new BehaviorSubject<boolean>(false);

  public loading$ = this.loadingSubject.asObservable();

  constructor(private organizationService: OrganizationService, ) {
    super();
  }

  loadOrganizations(filter: String) {
    this.loadingSubject.next(true);

    return this.organizationService.getOrganizations(filter).pipe(
      catchError(() => of([])),
      finalize(() => this.loadingSubject.next(false))
    ).subscribe(organization => this.organizationsSubject.next(organization));
  }

  connect(collectionViewer: CollectionViewer): Observable<Organization[]> {
    return this.organizationsSubject.asObservable();
  }

  disconnect(collectionViewer: CollectionViewer): void {
    this.organizationsSubject.complete();
    this.loadingSubject.complete();
  }
}

organizações.component.html:

<div class="spinner-container" *ngIf="organizationDataSource.loading$ | async">
    <mat-spinner></mat-spinner>
</div>

<div>
  <form [formGroup]="formGroup">
    <mat-form-field fxAuto>
      <div fxLayout="row">
        <mat-select formControlName="organizationSelectionControl" (selectionChange)="updateOrganizationSelection()">
          <mat-option *ngFor="let organizationSelectionAlternative of organizationSelectionAlternatives"
            [value]="organizationSelectionAlternative">
            {{organizationSelectionAlternative.name}}
          </mat-option>
        </mat-select>
      </div>
    </mat-form-field>
  </form>
</div>

<mat-table fxLayout="column" [dataSource]="organizationDataSource">
  <ng-container matColumnDef="name">
    <mat-header-cell *matHeaderCellDef>Name</mat-header-cell>
    <mat-cell *matCellDef="let organization">{{organization.name}}</mat-cell>
  </ng-container>

  <ng-container matColumnDef="number">
    <mat-header-cell *matHeaderCellDef>Number</mat-header-cell>
    <mat-cell *matCellDef="let organization">{{organization.number}}</mat-cell>
  </ng-container>

  <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
  <mat-row *matRowDef="let row; columns: displayedColumns"></mat-row>
</mat-table>

organization.component.scss:

.spinner-container {
    height: 360px;
    width: 390px;
    position: fixed;
}

organization.component.ts:

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder } from '@angular/forms';
import { Observable } from 'rxjs';

import { OrganizationService } from './organization.service';
import { Organization } from './organization.model';
import { OrganizationDataSource } from './organizationdatasource.model';

@Component({
  selector: 'organizations',
  templateUrl: './organizations.component.html',
  styleUrls: ['./organizations.component.scss']
})
export class OrganizationsComponent implements OnInit {
  public displayedColumns: string[];
  public organizationDataSource: OrganizationDataSource;
  public formGroup: FormGroup;

  public organizationSelectionAlternatives = [{
    id: 1,
    name: 'All'
  }, {
    id: 2,
    name: 'With organization update requests'
  }, {
    id: 3,
    name: 'With contact update requests'
  }, {
    id: 4,
    name: 'With order requests'
  }]

  constructor(
    private formBuilder: FormBuilder,
    private organizationService: OrganizationService) { }

  ngOnInit() {
    this.formGroup = this.formBuilder.group({
      'organizationSelectionControl': []
    })

    const toSelect = this.organizationSelectionAlternatives.find(c => c.id == 1);
    this.formGroup.get('organizationSelectionControl').setValue(toSelect);

    this.organizationDataSource = new OrganizationDataSource(this.organizationService);
    this.displayedColumns = ['name', 'number' ];
    this.updateOrganizationSelection();
  }

  updateOrganizationSelection() {
    this.organizationDataSource.loadOrganizations(this.formGroup.get('organizationSelectionControl').value.name);
  }
}

0

Depois de ler a tabela de materiais não atualizando a atualização de dados de postagem # 11638, Relatório de bug eu achei que a melhor (leia, a solução mais fácil) foi sugerida pelo comentador final 'shhdharmen' com uma sugestão para usar um EventEmitter.

Isso envolve algumas mudanças simples na classe da fonte de dados gerada

ou seja) adicione uma nova variável privada à sua classe de fonte de dados

import { EventEmitter } from '@angular/core';
...
private tableDataUpdated = new EventEmitter<any>();

e onde coloco novos dados no array interno (this.data), emito um evento.

public addRow(row:myRowInterface) {
    this.data.push(row);
    this.tableDataUpdated.emit();
}

e, finalmente, altere a matriz 'dataMutation' no método 'conectar' - como segue

const dataMutations = [
    this.tableDataUpdated,
    this.paginator.page,
    this.sort.sortChange
];

0

// esta é a
fonte de dados this.guests = [];

this.guests.push ({id: 1, nome: 'Ricardo'});

// atualize o dataSource this.guests = Array.from (this.guest);


0
npm install @matheo/datasource

Lancei uma biblioteca com o objetivo de ser o Material DataSource oficial no futuro, suportando qualquer tipo de fluxo de entrada (classificação, paginação, filtros) e alguma configuração com depuração para ver como funciona enquanto você está codificando.

import { MatDataSourceModule } from '@matheo/datasource';

Você pode encontrar a demonstração StackBlitz e mais informações aqui:
https://medium.com/@matheo/reactive-datasource-for-angular-1d869b0155f6

Terei prazer em ouvir sua opinião e apoiar seus casos de uso, se necessário.
Boa codificação!


0

Eu tentei ChangeDetectorRef, Subject e BehaviourSubject, mas o que funciona para mim

dataSource = [];
this.dataSource = [];
 setTimeout(() =>{
     this.dataSource = this.tableData[data];
 },200)

O que está acontecendo aqui? Eu sinto que erros de nomenclatura de variáveis ​​foram cometidos.
ChumiestBucket
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.