Angular + Material - Как обновить источник данных (mat-table)


123

Я использую мат-таблицу для перечисления содержания языков, выбранных пользователем. Они также могут добавлять новые языки с помощью диалоговой панели. После добавили язык и вернули обратно. Я хочу, чтобы мой источник данных обновился, чтобы отобразить внесенные изменения.

Я инициализирую хранилище данных, получая пользовательские данные из службы и передавая их в источник данных в методе обновления.

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

язык-данные-источник.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
  }

}

Поэтому я попытался вызвать метод обновления, в котором я снова получаю пользователя из серверной части, а затем повторно инициализирую источник данных. Однако это не работает, никаких изменений не происходит.


1
Если вы хотите инициировать изменение «из источника данных», пожалуйста, посмотрите stackoverflow.com/questions/47897694/…
Йеннифер

В этом случае можно использовать эмиттеры событий. stackoverflow.com/a/44858648/8300620
Rohit Parte

Ответы:


58

Запустите обнаружение изменений, используя ChangeDetectorRefв refresh()методе сразу после получения новых данных, введите ChangeDetectorRef в конструктор и используйте detectChanges следующим образом:

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
Кажется, это работает, это правильный способ сделать это? Кажется немного взломанным ...
Кей

Какие еще есть способы? Не могли бы вы привести примеры в своем решении для полного ответа?
Кей


89

Я не знаю, ChangeDetectorRefтребовалось ли это при создании вопроса, но теперь этого достаточно:

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

// ...

dataSource = new MatTableDataSource<MyDataType>();

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

Пример:
StackBlitz


4
Хотя это решение действительно работает, оно действительно приводит к сбоям в разбивке на страницы, если элемент добавляется куда-то, кроме первой страницы результатов. Я понимаю, что это выходит за рамки этого вопроса, но, поскольку они связаны между собой, есть ли у вас быстрое решение этой проблемы, которое вы могли бы добавить к своему ответу?
Knight

5
@Knight Я думаю, вы должны назначить Paginator MatTableDataSource.paginatorсвойству после того, как представление Paginator было инициализировано. См. Описание paginatorсвойства здесь: material.angular.io/components/table/api#MatTableDataSource
Мартин Шнайдер,

Хорошая ссылка. Раньше я не заметил этого в документации. Спасибо!
Knight

2
@ MA-Maddin, можешь поподробнее рассказать, как это сделать? пример?
Nathanphan

@Nathanphan опоздал, но добавил пример;)
Мартин Шнайдер

46

Так что для меня никто не дал хорошего ответа на проблему, с которой я столкнулся, которая почти такая же, как @Kay. Для меня это про сортировку, в таблице сортировки не происходит изменений в мате. Я намерен ответить на этот вопрос, поскольку это единственная тема, которую я нахожу с помощью поиска в Google. Я использую Angular 6.

Как сказано здесь :

Поскольку таблица оптимизируется для производительности, она не будет автоматически проверять наличие изменений в массиве данных. Вместо этого, когда объекты добавляются, удаляются или перемещаются в массиве данных, вы можете инициировать обновление отображаемых строк таблицы, вызвав его метод renderRows ().

Поэтому вам просто нужно вызвать renderRows () в методе refresh (), чтобы изменения появились .

См. Здесь для интеграции.


1
Если источник данных таблицы изменен на клиенте, вы, вероятно, ищете этот ответ. Прекрасно работает!
Элвин Салдана,

Это правильный ответ для углового материала 8
Том

Спасибо. Но из какого объекта я должен вызывать «renderRows ()»? Это в "this.datasource"?
WitnessTruth

19

Поскольку вы используете MatPaginator, вам просто нужно внести какие-либо изменения в пагинатор, это приведет к перезагрузке данных.

Простой трюк:

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

Это обновляет размер страницы до текущего размера страницы, поэтому в основном ничего не меняется, кроме _emitPageEvent()вызова частной функции, запускающей перезагрузку таблицы.


Я пробовал ваш код, но он не работает (не действует). Однако nextPage и затем previousPage снова работают, но это не решение.
Ахмед Хасн.

_changePageSize()разве не общественное право? Насколько безопасно использовать? Подробнее на stackoverflow.com/questions/59093781/…
Джонс

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

Добавьте эту строку под действием добавления или удаления конкретной строки.

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

что это. элементы
Парват

8

Лучший способ сделать это - добавить дополнительную наблюдаемую в вашу реализацию Datasource.

В методе подключения вы уже должны использовать Observable.mergeдля подписки на массив наблюдаемых, которые включают paginator.page, sort.sortChange и т. Д. Вы можете добавить к нему новую тему и вызывать ее, когда вам нужно вызвать обновление.

что-то вроде этого:

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

А затем вы можете позвонить, recordChange$.next()чтобы инициировать обновление.

Естественно, я бы обернул вызов в метод refresh () и вызвал бы его из экземпляра источника данных с компонентом и другими подходящими методами.


Этот способ может быть верным. У меня отлично работает
Ману

как мне это реализовать, если я хочу расширить MatTableDataSource? Когда я пробую ваш пример кода, я получаю сообщение об ошибкеProperty '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>'.
Морис

1
@Maurice тип MatTableDataSource реализует метод подключения с другим типом возвращаемого значения. Он использует BehaviorSubject <t []>, что означает, что вам просто нужно изменить пример, чтобы вернуть это вместо Observable. Вы по-прежнему можете использовать DataSource, но если вам нужно использовать MatTableDataSource, тогда верните BehaviorSubject, который подписан на ваш наблюдаемый объект, при условии, что у вас есть один для начала. Надеюсь, это поможет. Вы можете обратиться к источнику MatTableDataSource для получения точного синтаксиса нового типа источника данных: github.com/angular/material2/blob/master/src/lib/table/…
jogi

7

Вы можете просто использовать функцию подключения источника данных

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

вот так. 'data' - новые значения для таблицы данных


Был потенциал, но, похоже, не работает. Если после этого вы получите доступ к this.datasource.data, он не будет обновлен.
Rui Marques

4

Вы можете легко обновить данные таблицы с помощью "concat":

например:

language.component.ts

teachDS: any[] = [];

language.component.html

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

И, когда вы обновляете данные (language.component.ts):

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

Когда вы используете "concat" angular, обнаруживайте изменения объекта (this.teachDS), и вам не нужно использовать что-то еще.

PD: У меня работает в angular 6 и 7, другую версию не пробовал.


2
Да, у меня это работает, проблема связана с ссылкой и значением var, обнаружение изменений не видит новых изменений, для этого вам нужно обновить его.
Майра Родригес

Это может сработать, если dataSource является просто массивом, но не, когда dataSource является объектом MatTableDataSource.
Rui Marques

4

Ну, я столкнулся с аналогичной проблемой, когда я что-то добавил в источник данных, и он не перезагружается.

Самый простой способ, который я нашел, - это просто переназначить данные

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

1
Отлично!! Это работает !! Спасибо :)
Николас

4

В Angular 9 секрет в том, что this.dataSource.data = this.dataSource.data;

Пример:

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

Я нашел хорошее решение, используя два ресурса:

обновление как dataSource, так и paginator:

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

где, например , DataSource определяется здесь:

    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
Пожалуйста, добавьте пояснение к своему ответу, просто отправка кода не очень полезна и может привести к удалению вашего ответа.
TJ Wolschon

1

в моем случае (Angular 6+) я унаследовал от MatTableDataSourceto create MyDataSource. Без звонка послеthis.data = someArray

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

данные где не отображаются

класс 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

Есть два способа сделать это, потому что Angular Material несовместим, и это очень плохо документировано. Таблица материалов Angular не обновляется при поступлении новой строки. Удивительно, но это связано с проблемами производительности. Но это больше похоже на конструктивную проблему, они не могут измениться. Следует ожидать, что таблица обновится при появлении новой строки. Если это поведение не должно быть включено по умолчанию, должен быть переключатель, чтобы выключить его.

В любом случае, мы не можем изменить Angular Material. Но мы можем использовать для этого очень плохо документированный метод:

Один - если вы используете массив непосредственно в качестве источника:

call table.renderRows()

где table - это ViewChild мат-таблицы

Во-вторых, если вы используете сортировку и другие функции

table.renderRows () на удивление не работает. Потому что мат-таблица здесь несовместима. Вам нужно использовать хак, чтобы сообщить об изменении источника. Вы делаете это с помощью этого метода:

this.dataSource.data = yourDataSource;

где dataSource - это оболочка MatTableDataSource, используемая для сортировки и других функций.


0

Это сработало для меня:

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

Не идеальное решение, так как оно воссоздает исходный код для таблицы. И использование этого с оптимизированным / socket неэффективно.
Maihan Nijat

0

Я думаю, что MatTableDataSourceобъект каким-то образом связан с массивом данных, который вы передаетеMatTableDataSource конструктору.

Например:

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

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

Итак, когда вам нужно изменить данные; изменить исходный список, dataTableа затем отразить изменение в таблице методом вызова _updateChangeSubscription()on tableDS.

Например:

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

Это работает со мной через Angular 6.


4
Этот метод имеет префикс подчеркивания, _и вы его называете?
Стефан

0

Это работает для меня:

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

Итак, вы должны объявить свой экземпляр источника данных как MatTableDataSource


0

Я провел еще несколько исследований и нашел это место, чтобы дать мне то, что мне нужно - кажется чистым и связано с обновлением данных при обновлении с сервера: https://blog.angular-university.io/angular-material-data-table/

Большинство кредитов на странице выше. Ниже приведен пример того, как можно использовать селектор матов для обновления таблицы матов, привязанных к источнику данных, при изменении выбора. Я использую Angular 7. Извините за обширность, попытку быть полной, но краткой - я удалил как можно больше ненужных частей. С этим надеясь помочь кому-то еще быстрее продвигаться вперед!

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

organization.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

После прочтения таблицы материалов, не обновляющей данные публикации, обновление # 11638 Отчет об ошибке, я обнаружил, что лучшее (читай, самое простое решение) было предложено последним комментатором «shhdharmen» с предложением использовать EventEmitter.

Это включает в себя несколько простых изменений в сгенерированном классе источника данных.

т.е.) добавьте новую частную переменную в свой класс источника данных

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

и когда я помещаю новые данные во внутренний массив (this.data), я генерирую событие.

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

и, наконец, измените массив dataMutation в методе connect следующим образом

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

0

// это источник данных
this.gests = [];

this.gests.push ({id: 1, name: 'Ricardo'});

// обновляем источник данных this.gests = Array.from (this.guest);


0
npm install @matheo/datasource

Я выпустил библиотеку, призванную стать официальным источником данных материала в будущем, поддерживающую любые типы входных потоков (сортировка, разбиение на страницы, фильтры) и некоторую конфигурацию с отладкой, чтобы увидеть, как это работает, пока вы кодируете.

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

Вы можете найти демонстрацию StackBlitz и дополнительную информацию здесь:
https://medium.com/@matheo/reactive-datasource-for-angular-1d869b0155f6

Буду рад услышать ваше мнение и при необходимости поддержать ваши варианты использования.
Удачного кодирования!


0

Я пробовал ChangeDetectorRef, Subject и BehaviourSubject, но что у меня работает

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

что тут происходит? Я чувствую, что были допущены ошибки при именовании переменных.
ChumiestBucket
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.