Angular + Material: cómo actualizar una fuente de datos (tabla de mat)

120

Estoy usando una tabla de mat para enumerar el contenido de los idiomas elegidos por los usuarios. También pueden agregar nuevos idiomas usando el panel de diálogo. Después agregaron un idioma y regresaron. Quiero que mi fuente de datos se actualice para mostrar los cambios que realizaron.

Inicializo el almacén de datos obteniendo datos de usuario de un servicio y pasándolos a una fuente de datos en el método de actualización.

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
  }

}

Así que intenté llamar a un método de actualización donde obtengo el usuario del backend nuevamente y luego reinicializo la fuente de datos. Sin embargo, esto no funciona, no se están produciendo cambios.

Kay
fuente
1
Si desea activar el cambio "desde la fuente de datos", eche un vistazo a stackoverflow.com/questions/47897694/…
Yennefer
En este caso, se pueden utilizar emisores de eventos. stackoverflow.com/a/44858648/8300620
Rohit Parte

Respuestas:

58

Active una detección de cambios usando ChangeDetectorRefen el refresh()método justo después de recibir los nuevos datos, inyecte ChangeDetectorRef en el constructor y 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();
    });
  }
}
HDJEMAI
fuente
9
Eso parece funcionar, ¿es esta la forma correcta de hacerlo? Parece un poco hacky ...
Kay
¿Cuáles son las otras formas? ¿Podría proporcionar ejemplos en su solución para obtener una respuesta completa?
Kay
88

No sé si ChangeDetectorRefse requirió cuando se creó la pregunta, pero ahora esto es suficiente:

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

// ...

dataSource = new MatTableDataSource<MyDataType>();

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

Ejemplo:
StackBlitz

Martin Schneider
fuente
4
Si bien esta solución de hecho funciona, estropea el paginador de material si el elemento se agrega en otro lugar que no sea la primera página de resultados. Entiendo que eso está más allá del alcance de esta pregunta, pero dado que los dos están relacionados, ¿tiene una solución rápida a ese problema que podría agregar a su respuesta?
Knight
5
@Knight Creo que tienes que asignar el Paginador a la MatTableDataSource.paginatorpropiedad después de que se haya inicializado la vista del Paginador. Vea la descripción de la paginatorpropiedad aquí: material.angular.io/components/table/api#MatTableDataSource
Martin Schneider
Buena referencia. No había visto eso en los documentos antes. ¡Gracias!
Knight
2
@ MA-Maddin, ¿puedes ser más específico sobre cómo hacerlo? ¿ejemplo?
Nathanphan
@Nathanphan tarde, pero agregó un ejemplo;)
Martin Schneider
46

Entonces, para mí, nadie dio una buena respuesta al problema que encontré, que es casi lo mismo que @Kay. Para mí se trata de ordenar, ordenar la tabla no ocurre cambios en el tapete. Propuse esta respuesta ya que es el único tema que encuentro al buscar en Google. Estoy usando Angular 6.

Como se dice aquí :

Dado que la tabla optimiza el rendimiento, no comprobará automáticamente los cambios en la matriz de datos. En cambio, cuando se agregan, eliminan o mueven objetos en la matriz de datos, puede activar una actualización de las filas representadas de la tabla llamando a su método renderRows ().

Así que solo tienes que llamar a renderRows () en tu método refresh () para que aparezcan tus cambios.

Consulte aquí la integración.

Los 4x10m
fuente
1
Si la fuente de datos de la tabla se modifica en el cliente, esta respuesta es probablemente lo que está buscando. ¡Funciona genial!
Alvin Saldanha
Esta es la respuesta correcta a partir del material angular 8
Tom
Gracias. Pero, ¿desde qué objeto debo llamar a "renderRows ()"? ¿Está en 'this.datasource'?
WitnessTruth
19

Ya que está usando MatPaginator, solo necesita hacer cualquier cambio en el paginador, esto activa la recarga de datos.

Truco simple:

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

Esto actualiza el tamaño de la página al tamaño de la página actual, por lo que básicamente nada cambia, excepto que _emitPageEvent()también se llama a la función privada , lo que activa la recarga de la tabla.

tomando
fuente
Probé su código y no funciona (no tiene ningún efecto). Sin embargo nextPage y luego previousPage nuevamente funcionan, pero no es una solución.
Ahmed Hasn.
_changePageSize()no es público, ¿verdad? ¿Es seguro de usar? Más en stackoverflow.com/questions/59093781/…
Jones
9
this.dataSource = new MatTableDataSource<Element>(this.elements);

Agregue esta línea debajo de su acción de agregar o eliminar la fila en particular.

refresh() {
  this.authService.getAuthenticatedUser().subscribe((res) => {
    this.user = new MatTableDataSource<Element>(res);   
  });
}
Pooja Paigude
fuente
lo que es this.elements
Parvat
8

La mejor manera de hacer esto es agregando un observable adicional a su implementación de Datasource.

En el método de conexión ya debería estar usando Observable.mergepara suscribirse a una matriz de observables que incluyen paginator.page, sort.sortChange, etc. Puede agregar un nuevo sujeto a esto y llamar a next cuando necesite causar una actualización.

algo como esto:

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
    }
}

Y luego puede llamar recordChange$.next()para iniciar una actualización.

Naturalmente, envolvería la llamada en un método refresh () y la llamaría fuera de la instancia de la fuente de datos con el componente y otras técnicas adecuadas.

jogi
fuente
Este método puede ser el correcto. Me funciona bien
Manu
¿Cómo implemento esto cuando quiero extender MatTableDataSource? Cuando intento su ejemplo de código, aparece el errorProperty '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 el tipo MatTableDataSource implementa el método de conexión con un tipo de retorno diferente. Utiliza BehaviorSubject <t []> lo que significa que solo necesita alterar el ejemplo para devolver esto en lugar de un Observable. Aún debería poder usar DataSource, pero si tiene que usar MatTableDataSource, devuelva un BehaviorSubject que esté suscrito a su observable, asumiendo que tiene uno para comenzar. Espero que ayude. Puede consultar la fuente de MatTableDataSource para obtener la sintaxis exacta del nuevo tipo de fuente de
jogi
7

Puede usar la función de conexión de la fuente de datos

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

al igual que. 'datos' son los nuevos valores para la tabla de datos

cozmik05
fuente
Tenía potencial pero no parece funcionar. Si después accede a this.datasource.data, no está actualizado.
Rui Marques
4

Puede actualizar fácilmente los datos de la tabla usando "concat":

por ejemplo:

language.component.ts

teachDS: any[] = [];

language.component.html

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

Y, cuando actualiza los datos (language.component.ts):

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

Cuando estás usando "concat" angular, detecta los cambios del objeto (this.teachDS) y no necesitas usar otra cosa.

PD: Me funciona en los ángulos 6 y 7, no probé otra versión.

Mayra Rodríguez
fuente
2
Sí, me funciona, es un problema sobre la referencia y el valor var, la detección de cambios no ve los nuevos cambios, para eso necesitas actualizarlo.
Mayra Rodriguez
Esto podría funcionar si dataSource es solo una matriz, pero no cuando dataSource es un objeto MatTableDataSource.
Rui Marques
3

Bueno, me encontré con un problema similar en el que agregué algo a la fuente de datos y no se está recargando.

La forma más fácil que encontré fue simplemente reasignar los datos

let dataSource = ['a','b','c']
dataSource.push('d')
let cloned = dataSource.slice()
// OR IN ES6 // let cloned = [...dataSource]
dataSource = cloned
Mordy Stern
fuente
¡¡Perfecto!! ¡Funciona! Gracias :)
Nicolas
3

En Angular 9, el secreto es this.dataSource.data = this.dataSource.data;

Ejemplo:

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);
}
Nicolas
fuente
2

Logré una buena solución usando dos recursos:

actualizando tanto la fuente de datos como el paginador:

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

donde, por ejemplo, dataSource se define aquí:

    users: User[];
    ...
    dataSource = new MatTableDataSource(this.users);
    ...
    this.dataSource.paginator = this.paginator;
    ...
danilo
fuente
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);   
    });
  }
}
Machhindra Neupane
fuente
8
Agregue una explicación a su respuesta, solo publicar el código no es muy útil y puede resultar en que se elimine su respuesta.
TJ Wolschon
1

en mi caso (Angular 6+), heredé de MatTableDataSourcecrear MyDataSource. Sin llamar despuésthis.data = someArray

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

datos donde no se muestran

clase 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 
Pipo
fuente
1

Hay dos formas de hacerlo porque Angular Material es inconsistente y esto está muy mal documentado. La tabla de material angular no se actualizará cuando llegue una nueva fila. Sorprendentemente, se dice que se debe a problemas de rendimiento. Pero parece más un problema de diseño, no pueden cambiar. Se debe esperar que la tabla se actualice cuando se produzca una nueva fila. Si este comportamiento no debe estar habilitado de forma predeterminada, debe haber un interruptor para apagarlo.

De todos modos, no podemos cambiar el material angular. Pero básicamente podemos usar un método muy pobremente documentado para hacerlo:

Uno: si usa una matriz directamente como fuente:

call table.renderRows()

donde tabla es ViewChild del mat-table

Segundo: si usa la clasificación y otras funciones

table.renderRows () sorprendentemente no funcionará. Porque mat-table es inconsistente aquí. Necesita usar un truco para decirle a la fuente que cambió. Lo haces con este método:

this.dataSource.data = yourDataSource;

donde dataSource es el contenedor MatTableDataSource que se utiliza para ordenar y otras características.

Tomasz Smykowski
fuente
0

Esto funcionó para mí:

refreshTableSorce() {
    this.dataSource = new MatTableDataSource<Element>(this.newSource);
}
Cristian Hernandez
fuente
No es una solución ideal, ya que recrea el código fuente para la tabla, y usar esto con / socket optimizado no es eficiente.
Maihan Nijat
0

Creo que el MatTableDataSourceobjeto está vinculado de alguna manera con la matriz de datos que pasa al MatTableDataSourceconstructor.

Por ejemplo:

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

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

Entonces, cuando tenga que cambiar datos; cambiar en la lista original dataTabley luego reflejar el cambio en la tabla mediante el _updateChangeSubscription()método de llamada en tableDS.

Por ejemplo:

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

Eso es trabajo conmigo a través de Angular 6.

nimeresam
fuente
4
Ese método tiene el prefijo de subrayado _y lo llama?
Stephane
0

Esto es trabajo para mí:

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();
  });
}

Por lo tanto, debe declarar su instancia de fuente de datos como MatTableDataSource

Pequeño rey
fuente
0

Investigué un poco más y encontré este lugar para darme lo que necesitaba: se siente limpio y se relaciona con los datos de actualización cuando se actualiza desde el servidor: https://blog.angular-university.io/angular-material-data-table/

La mayoría de los créditos corresponden a la página anterior. A continuación se muestra una muestra de cómo se puede usar un selector de mat para actualizar una tabla de mat enlazada a una fuente de datos en el cambio de selección. Estoy usando Angular 7. Perdón por ser extenso, tratando de ser completo pero conciso. He arrancado tantas partes no necesarias como sea posible. ¡Con esto esperando ayudar a alguien más a avanzar más rápido!

organización.modelos.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();
  }
}

organizaciones.componente.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>

organizaciones.componente.scss:

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

organisation.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);
  }
}
Henrik
fuente
0

Después de leer Material Table que no actualiza la actualización de datos de publicación # 11638 Informe de error , encontré que la mejor (lea, la solución más fácil) fue la sugerida por el comentario final o 'shhdharmen' con una sugerencia para usar un EventEmitter.

Esto implica algunos cambios simples en la clase de fuente de datos generada

es decir, agregue una nueva variable privada a su clase de fuente de datos

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

y donde envío nuevos datos a la matriz interna (this.data), emito un evento.

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

y finalmente, cambie la matriz 'dataMutation' en el método 'connect', de la siguiente manera

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

// esta es la fuente de datos
this.guests = [];

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

// actualiza el origen de datos this.guests = Array.from (this.guest);

Igor Faoro
fuente
0
npm install @matheo/datasource

Lancé una biblioteca destinada a ser la fuente de datos de material oficial en el futuro, que admite cualquier tipo de flujo de entrada (clasificación, paginación, filtros) y alguna configuración con depuración para ver cómo funciona mientras codifica.

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

Puede encontrar la demostración de StackBlitz y más información aquí:
https://medium.com/@matheo/reactive-datasource-for-angular-1d869b0155f6

Estaría encantado de escuchar su opinión y apoyar sus casos de uso si es necesario.
¡Feliz codificación!

Mateo Tibaquira
fuente
0

Probé ChangeDetectorRef, Subject y BehaviourSubject, pero lo que me funciona

dataSource = [];
this.dataSource = [];
 setTimeout(() =>{
     this.dataSource = this.tableData[data];
 },200)
Vishesh Mishra
fuente
¿Que está pasando aqui? Siento que se cometieron errores de nomenclatura de variables.
ChumiestBucket