Angular - Таблица материалов, можно ли обновлять строки без обновления всей таблицы?

После нескольких недель поиска в Google и только одного вопроса с Stackoverflow, я, наконец, сумел создать свое приложение Angular CRUD с использованием компонента Material Table. Он показывает данные из бэкэнда (JSON) и для операций CRUD я использую диалоги, подобные показанному на картинке (это редактирование, извините за хорватский). Диалоги могут быть не лучшим способом, встроенное редактирование может быть лучше. Но все же, для добавления нового элемента вам нужно что-то вроде диалога.

enter image description here

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

В любом случае здесь код:

Табличный компонент:

export class BazaComponent implements OnInit {
  ....
  constructor(public httpClient: HttpClient, public dialog: MatDialog) {
  }

  ngOnInit() {
    this.loadData();
  }

  // TODO: Simplfy this...
  addNew(ident: number, naziv: string, mt: number, kutija: number,
         komada: number, jm: string, orginal: number, lokacija: number, napomena: string) {
    console.log('add new clicked');
    const dialogRef = this.dialog.open(AddDialogComponent, {
      data: {ident: ident, naziv: naziv, mt: mt, kutija: kutija,
        komada: komada, jm: jm, orginal: orginal, lokacija: lokacija, napomena: napomena }
    });

    dialogRef.afterClosed().subscribe(result => {
      console.log(result);
      if (result === 1) {
        this.loadData();  // --> This is a temp workaround, every time when I do CRUD operation just redraw whole thing again
      }
    });
  }

  startEdit(id: number, ident: number, naziv: string, mt: number, kutija: number,
            komada: number, jm: string, orginal: number, lokacija: number, napomena: string) {

    const dialogRef = this.dialog.open(EditDialogComponent, {
      data: {id: id, ident: ident, naziv: naziv, mt: mt, kutija: kutija,
        komada: komada, jm: jm, orginal: orginal, lokacija: lokacija, napomena: napomena}
    });

    dialogRef.afterClosed().subscribe(result => {
      if (result === 1) {
        this.loadData(); // --> This is a temp workaround, every time when I do CRUD operation just redraw whole thing again
      }
    });
  }

  deleteItem(id: number, ident: number, naziv: string, mt: number) {
    const dialogRef = this.dialog.open(DeleteDialogComponent, {
      data: {id: id, ident: ident, naziv: naziv, mt: mt}
    });

    dialogRef.afterClosed().subscribe(result => {
      if (result === 1) {
        this.loadData();
      }
    });
  }


  public loadData() {
    this.exampleDatabase = new DataService(this.httpClient);
    this.dataSource = new ExampleDataSource(this.exampleDatabase, this.paginator, this.sort);
    Observable.fromEvent(this.filter.nativeElement, 'keyup')
      .debounceTime(150)
      .distinctUntilChanged()
      .subscribe(() => {
        if (!this.dataSource) {
          return;
        }
        this.dataSource.filter = this.filter.nativeElement.value;
      });
  }
}


export class ExampleDataSource extends DataSource<Baza> {
  _filterChange = new BehaviorSubject('');

  get filter(): string {
    return this._filterChange.value;
  }

  set filter(filter: string) {
    this._filterChange.next(filter);
  }

  filteredData: Baza[] = [];
  renderedData: Baza[] = [];

  constructor(private _exampleDatabase: DataService,
              private _paginator: MatPaginator,
              private _sort: MatSort) {
    super();
    // Reset to the first page when the user changes the filter.
    this._filterChange.subscribe(() => this._paginator.pageIndex = 0);
  }

  /** Connect function called by the table to retrieve one stream containing the data to render. */
  connect(): Observable<Baza[]> {
    // Listen for any changes in the base data, sorting, filtering, or pagination
    const displayDataChanges = [
      this._exampleDatabase.dataChange,
      this._sort.sortChange,
      this._filterChange,
      this._paginator.page,
    ];

    this._exampleDatabase.getAllItems();

    return Observable.merge(...displayDataChanges).map(() => {
      // Filter data
      this.filteredData = this._exampleDatabase.data.slice().filter((item: Baza) => {
        const searchStr = (item.ident + item.naziv + item.mt + item.lokacija + item.napomena).toLowerCase();
        return searchStr.indexOf(this.filter.toLowerCase()) !== -1;
      });

      // Sort filtered data
      const sortedData = this.sortData(this.filteredData.slice());

      // Grab the page slice of the filtered sorted data.
      const startIndex = this._paginator.pageIndex * this._paginator.pageSize;
      this.renderedData = sortedData.splice(startIndex, this._paginator.pageSize);
      return this.renderedData;
    });
  }

  disconnect() {
  }

  /** Returns a sorted copy of the database data. */
  sortData(data: Baza[]): Baza[] {
  ... sort stuff
}

Вот DataService, где я думаю, что я должен делать обновления поля:

import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse, HttpHeaders} from '@angular/common/http';
import { Baza } from '../models/kanban.baza';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';

    @Injectable()
    export class DataService {
      private readonly API_URL = 'http://localhost/api/'

      /** Stream that emits whenever the data has been modified. */
      dataChange: BehaviorSubject<Baza[]> = new BehaviorSubject<Baza[]>([]);

      constructor(private httpClient: HttpClient) {
      }

      get data(): Baza[] {
        return this.dataChange.value;
      }

      getAllItems(): void {
        this.httpClient.get<Baza[]>(this.API_URL).subscribe(data => {
          this.dataChange.next(data['items']);
        });
      }

    addItem(baza: Baza): void {
      this.httpClient.post(this.API_URL, Baza).subscribe(data => {
          //THIS WAS MY BEST TRY BUT IT DOESN'T WORK :(
          const copiedData = this.data.slice();
          copiedData.push(baza);
          console.log(copiedData);
          this.dataChange.next(copiedData);
      });
    }


      updateItem(baza: Baza): void {
        this.httpClient.put(this.API_URL + baza.id, baza).subscribe();
      }

      deleteItem(id: number): void {
        this.httpClient.delete(this.API_URL + id, {headers: new HttpHeaders().set('Access-Control-Allow-Origin', '*')} ).subscribe();
    }
}

ОБНОВЛЕНИЕ 27.11.2017:

Хорошо, я наконец понял, как вызвать добавление новой строки. Мне пришлось вызвать dataChange.value внутри компонента таблицы. Как только вы загрузите его с некоторыми данными, новая строка появится мгновенно.

const data = {id: 208, ident: 233, naziv: 'test', mt: 291, komada: 2, jm: 'a', orginal: 100, lokacija: 3, napomena: 'pls work'};
this.exampleDatabase.dataChange.value.push(data);

То же самое в DataService не будет работать:

this.dataChange.value.push(data); 

Плункер здесь:

https://plnkr.co/edit/IWCVsBRl54F7ylGNIJJ3?p=info

РЕДАКТИРОВАТЬ 28.11.2017:

Теперь осталось только построить логику для добавления, редактирования и удаления. Добавить просто, это просто "value.push(data)". Спасибо за помощь всем.

Ответ 1

Это заняло у меня некоторое время, но я наконец все заработало. Ваши ответы и разные подходы помогли. Итак, вот моя реализация CRUD, если у кого-то возникнут проблемы с этим:

https://github.com/marinantonio/angular-mat-table-crud

Снимок экрана: Alt Text

Или вы можете проверить демо проекта: https://marinantonio.github.io/angular-mat-table-crud/

Ключевые части находятся в файле table.ts:

....
addNew(issue: Issue) {
    const dialogRef = this.dialog.open(AddDialogComponent, {
      data: {issue: issue }
    });

    dialogRef.afterClosed().subscribe(result => {
      if (result === 1) {
        this.exampleDatabase.dataChange.value.push(this.dataService.getDialogData());
        this.refreshTable();
      }
    });
  }

  startEdit(i: number, id: number, title: string, state: string, url: string, created_at: string, updated_at: string) {
    this.index = i;
    this.id2 = id;
    console.log(this.index);
    const dialogRef = this.dialog.open(EditDialogComponent, {
      data: {id: id, title: title, state: state, url: url, created_at: created_at, updated_at: updated_at}
    });

    dialogRef.afterClosed().subscribe(result => {
      if (result === 1) {
        // Part where we do frontend update, first you need to find record using id
        const foundIndex = this.exampleDatabase.dataChange.value.findIndex(x => x.id === this.id2);
        // Then you update that record using dialogData
        this.exampleDatabase.dataChange.value[foundIndex] = this.dataService.getDialogData();
        // And lastly refresh table
        this.refreshTable();
      }
    });
  }

  deleteItem(i: number, id: number, title: string, state: string, url: string) {
    this.index = i;
    this.id2 = id;
    const dialogRef = this.dialog.open(DeleteDialogComponent, {
      data: {id: id, title: title, state: state, url: url}
    });

    dialogRef.afterClosed().subscribe(result => {
      if (result === 1) {
        const foundIndex = this.exampleDatabase.dataChange.value.findIndex(x => x.id === this.id2);
        this.exampleDatabase.dataChange.value.splice(foundIndex, 1);
        this.refreshTable();
      }
    });
  }


  private refreshTable() {
    // If there no data in filter we do update using pagination, next page or previous page
    if (this.dataSource._filterChange.getValue() === '') {
      if (this.dataSource._paginator.pageIndex === 0) {
        this.dataSource._paginator.nextPage();
        this.dataSource._paginator.previousPage();
      } else {
        this.dataSource._paginator.previousPage();
        this.dataSource._paginator.nextPage();
      }
      // If there something in filter, we reset it to 0 and then put back old value
    } else {
      this.dataSource.filter = '';
      this.dataSource.filter = this.filter.nativeElement.value;
    }
}
....

Ответ 2

Как я вижу из вашего кода, что вы используете разбивку на страницы, вы можете сделать следующее после операции crud:

this.dataSource.paginator = this.paginator;

Это обновит текущую страницу. И, рад, что кто-то из Хорватии использует материал angular.

Здесь важная часть моего кода:

dialogRef.afterClosed().subscribe(result => {
    if (result === null) { return; }
    switch (mode) {               // add new
        case 'C': {
            data.push(result.vendor);
            this.refreshTable();
            break;
        }
        case 'U': {               // update
            const index = data.findIndex((item) => item.buFmisVendorId === result.vendor.buFmisVendorId);
            if (index > -1) {
                data[index] = vendor;
                this.refreshTable();
            }
            break;
        }

    }
});

private refreshTable() {
    this.dataSource.paginator = this.paginator;
}

Ответ 3

У меня есть некоторые обходные пути в редактировании данных в таблице без использования модальных окон.

Вы можете взглянуть на мою реализацию CRUD с Angular 6 и Material

Служба данных

import {Injectable} from '@angular/core';
import {HttpClient, HttpParams, HttpHeaders} from '@angular/common/http';
import {User} from './user';

@Injectable()
export class UserService{
private url = "http://localhost:51120";

constructor(private http: HttpClient){ }
getUsers(){
    let getUrl = this.url + "/api/all/";
    return this.http.get(getUrl);
}
createUser(user: User){
    let saveUrl = this.url + "/api/Users";
    return this.http.post(saveUrl, user); 
}
updateUser(id: number, user: User) {
    const urlParams = new HttpParams().set("id", id.toString());
    return this.http.post(this.url + "/api/update", user);
}
deleteUser(id: number){
    const urlParams = new HttpParams().set("id", id.toString());
    return this.http.delete(this.url + "/api/delete/" + id);
 }
}

Компонент

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [UserService]
})
export class AppComponent implements OnInit {

@ViewChild(MatPaginator) paginator: MatPaginator;

addNewUser: User[] = [
    { Id: 0, Name: null, Age: null, Email: null, Surname: null }
];

users: Array<User>;
showTable: boolean;
statusMessage: string;
isLoaded: boolean = true;
displayedColumnsUsers: string[] = ['Id', 'Name', 'Surname', 'Age', 'Email', 'Change', 'Delete'];
displayedColumnsAddUser: string[] = ['Name', 'Surname', 'Age', 'Email', 'Save', 'Cancel'];
dataSourceUsers: any;
dataSourceAddUser: any;
newUser : User;

constructor(private serv: UserService, public dialog: MatDialog, public snackBar: MatSnackBar) {
    this.users = new Array<User>();
}

@ViewChild(MatSort) sort: MatSort;

ngOnInit() {
    this.loadUsers();
    this.dataSourceAddUser = new MatTableDataSource();
}

applyFilter(filterValue: string) {
    this.dataSourceUsers.filter = filterValue.trim().toLowerCase();

    if (this.dataSourceUsers.paginator) {
        this.dataSourceUsers.paginator.firstPage();
    }
}

private loadUsers() {
    this.isLoaded = true;
    this.serv.getUsers().subscribe((data: User[]) => {
        this.users = data;
        this.users.sort(function (obj1, obj2) {
            // Descending: first id less than the previous
            return obj2.Id - obj1.Id;
        });
        this.isLoaded = false;
        this.dataSourceUsers = new MatTableDataSource(this.users);
        this.dataSourceAddUser = new MatTableDataSource(this.addNewUser);
        this.dataSourceUsers.sort = this.sort;
        this.dataSourceUsers.paginator = this.paginator;
    },
        error => {
            alert("Error: " + error.name);
            this.isLoaded = false;
        }
    );
}

deleteUserForDialog(user: User) {
    this.serv.deleteUser(user.Id).subscribe(data => {
        this.statusMessage = 'User ' + user.Name + ' is deleted',
            this.openSnackBar(this.statusMessage, "Success");
        this.loadUsers();
    })
}

editUser(user: User) {
    this.serv.updateUser(user.Id, user).subscribe(data => {
        this.statusMessage = 'User ' + user.Name + ' is updated',
        this.openSnackBar(this.statusMessage, "Success");
        this.loadUsers();
    },
        error => {
            this.openSnackBar(error.statusText, "Error");
        }
    );
}

saveUser(user: User) {
    if (user.Age != null && user.Name != null && user.Name != "" && user.Age != 0) {
        this.serv.createUser(user).subscribe(data => {
            this.statusMessage = 'User ' + user.Name + ' is added',
            this.showTable = false;
            this.openSnackBar(this.statusMessage, "Success");
            this.loadUsers();
        },
            error => {
                this.showTable = false;
                this.openSnackBar(error.statusText, "Error");
            }
        );
    }
    else {
        this.openSnackBar("Please enter correct data", "Error")
    }
}

show() {
    this.showTable = true;
    this.addNewUser = [{ Id: 0, Name: null, Age: null, Email: null, Surname: null }];

}
cancel() {
    this.showTable = false;
}

//snackBar
openSnackBar(message: string, action: string) {
    this.snackBar.open(message, action, {
        duration: 3000,
    });
}

//material dialog
openDialog(element): void {
    const dialogRef = this.dialog.open(DialogOverviewExampleDialogComponent, 
{
        width: '250px',
        data: element,
    });

    dialogRef.afterClosed().subscribe(result => {
        console.log('The dialog was closed');
        if (result == "Confirm") {
            this.deleteUserForDialog(element);
        }
    });
}

//   Form field with error messages 
name = new FormControl('', [Validators.required]);

getErrorMessage() {
    return this.name.hasError('required') ? 'You must enter a value' :
        this.name.hasError('name') ? 'Not a valid name' : '';
}

age = new FormControl('', [Validators.required]);

email = new FormControl('', [Validators.required, Validators.email]);
surnameFormControl= new FormControl('', [Validators.required]);
emailGetErrorMessage() {
    return this.email.hasError('required') ? 'You must enter a value' :
        this.email.hasError('email') ? 'Not a valid email' :
            '';
}

onSubmit(newUser:User){
    this.newUser = new User(0,"",0,"","");
}
}

https://github.com/AleksandrChuikov/Angular6MaterialCRUD

Вот ссылка на демо: https://crud-angular6.azurewebsites.net

Обновлено до Angular 8

Click here to see screenshot

Ответ 4

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

public deleteMember(memberId) {
      // Call the confirm dialog component
      this.confirmService.confirm('Confirm Delete', 'This action is final. Gone forever!')
          .switchMap(res => {if (res === true) {
              return this.appService.deleteItem(this.dbTable, memberId);
          }})
          .subscribe(
              result => {
                this.success();
                // Refresh DataTable to remove row.  This solution calls the db and is a hack.
                this.ngAfterViewInit();
              },
              (err: HttpErrorResponse) => {
                  console.log(err.error);
                  console.log(err.message);
                this.messagesService.openDialog('Error', 'Delete did not happen.');
              }
          );
  }

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

private dbTable = 'members';
dataSource = new MatTableDataSource();

ngAfterViewInit() {
    this.appService = new AppService(this.http);
    this.dataSource.sort = this.sort;
    this.dataSource.paginator = this.paginator;


    // Populate the Material2 DataTable.
    Observable.merge(this.paginator.page)
      .startWith(null)  // Delete this and no data is downloaded.
      .switchMap(() => {
        return this.appService.getItems( this.dbTable,
          this.paginator.pageIndex);
      })
      .map(data => {
        return data.resource;
      })
      .subscribe(data => {
        this.dataLength = data.length;
        this.dataSource.data = data;
      });
  }

Ответ 5

Это решение использует мой существующий код удаления, но тот же для кода обновления. Ключевой проблемой является поиск индекса массива для отредактированного или удаляемого элемента. Обратите внимание, что как только результат будет успешным, я вызываю успешную модификацию, чтобы уведомить пользователя, а затем вызвать функцию, чтобы удалить строку из таблицы данных. Или вы можете обновить данные в этой строке с помощью небольшого разного кода, например, нажимать данные в массив объектов. Таким образом, нам не нужно снова загружать все данные.

public deleteMember(memberId) {
      // Call the confirm dialog component
      this.confirmService.confirm('Confirm Delete', 'This action is final. Gone forever!')
          .switchMap(res => {if (res === true) {
              return this.appService.deleteItem(this.dbTable, memberId);
          }})
          .subscribe(
              result => {
                this.success();
                // Refresh DataTable to remove row.
                this.updateDataTable (memberId);
              },
              (err: HttpErrorResponse) => {
                  console.log(err.error);
                  console.log(err.message);
                this.messagesService.openDialog('Error', 'Delete did not happen.');
              }
          );
  }

Теперь можно удалить или обновить удаленные или отредактированные строки.

private dsData: any;
  // Remove the deleted row from the data table. Need to remove from the downloaded data first.
  private updateDataTable (itemId) {
    this.dsData = this.dataSource.data;
    if (this.dsData.length > 0) {
      for (let i = 0; i < this.dsData.length; i++ ) {
        if (this.dsData[i].member_id === itemId) {
          this.dataSource.data.splice(i, 1);
        }
      }
    }
    this.dataSource.paginator = this.paginator;
  }

Ответ 6

Мой ответ в Angular 6 Материал 2.

Я использовал функцию splice которая принимает в качестве аргументов индекс отредактированной строки, затем количество удаляемых строк (в вашем случае 1) и, в-третьих, новую версию отредактированной строки, которая будет вставлена в этот индекс:

dialogRef.afterClosed().subscribe(result => {
  if(result !== '' && result !== null) {
    const idx_editedRow = this.mattabledatasource.data.indexOf(row);
    this.mattabledatasource.data.splice(idx_editedRow, 1, result);
    loadData();
  }
});

Ответ 7

На самом деле вам не нужно обновлять таблицу после редактирования, если у вас есть следующий HTML:

<mat-table [dataSource]="dataSource" matSort>
      <ng-container matColumnDef="userName">
        <mat-header-cell mat-sort-header> UserName </mat-header-cell>
        <mat-cell *matCellDef="let row"> {{row.userName}} </mat-cell>
      </ng-container>
      <ng-container matColumnDef="actions">
        <mat-cell *matCellDef="let user">
          <button mat-icon-button matTooltip="Edit" (click)="editUser(user)">
            <mat-icon>edit</mat-icon>
          </button>
        </mat-cell>
      </ng-container>
      <mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
      <mat-row *matRowDef="let row; columns: displayedColumns;">
      </mat-row>
</mat-table>

И, в .ts у вас есть:

private editUser(user?: User) {
    let userTest: User = user;
    userTest.userName = "user123";
  }

Вы можете видеть, что автоматически строка при нажатии Изменить имя пользователя (в данном случае "user123")

Ответ 8

Можете ли вы взглянуть на

addItem(baza: Baza): void {
  this.httpClient.post(this.API_URL, Baza).subscribe(data => {
      //THIS WAS MY BEST TRY BUT IT DOESN'T WORK :(
      const copiedData = this.data.slice();
      copiedData.push(baza);
      console.log(copiedData);
      this.dataChange.next(copiedData);
  });
}

Выполняется ли запрос POST и отправка данных? Вы ссылаетесь на Baza в запросе POST, который должен быть "база" (нижний регистр B). Возможно, из-за этого запрос терпит неудачу, и наблюдаемая подписка никогда не выполняется... вы можете дважды проверить эту теорию с обработчиком ошибок в подписке.

addItem(baza: Baza): void {
  this.httpClient.post(this.API_URL, baza).subscribe(data => {
      const copiedData = this.data.slice();
      copiedData.push(baza);
      console.log(copiedData);
      this.dataChange.next(copiedData);
  }, (errror) => {
    console.log(error);
  });
}

Наконец, что касается изменений, мой подход будет немного отличаться. Внесите тот же экземпляр DataService в компонент и передайте эту же ссылку на таблицу DataSource, а не на новый экземпляр. Затем передайте весь объект базы в диалог редактирования, а не только его свойства. Затем в диалоговом окне закройте исходный (неотредактированный объект), а также новые свойства (или, еще лучше, новый объект класса Baza с отредактированными полями). Отправьте их в нашу службу данных с помощью метода "edit/update". Метод edit/update будет фильтровать существующий набор массивов данных, ища любые записи, соответствующие нашему неотредактированному объекту, и устанавливать их в соответствии с нашим новым объектом. Немного абстрагированный пример, приведенный ниже

//например. Компонент

export class BazaComponent implements OnInit {
  ....
  constructor(
    public httpClient: HttpClient, 
    public dialog: MatDialog,
    public dataService: DataService
  ){}
  ....
  public loadData() {
    this.dataSource = new ExampleDataSource(this.dataService, this.paginator, this.sort);
    Observable.fromEvent(this.filter.nativeElement, 'keyup')
      .debounceTime(150)
      .distinctUntilChanged()
      .subscribe(() => {
        if (!this.dataSource) {
          return;
        }
        this.dataSource.filter = this.filter.nativeElement.value;
      });
  }
  ....
  startEdit(baza: Baza) {
    const dialogRef = this.dialog.open(EditDialogComponent, {
      data: {
        baza: baza
      }
    });

    dialogRef.afterClosed().subscribe(result => {
      // result will be simple array of our 'old' baza object that we passed in, and the 'new' baza object that contains the edits
      this.dataService.updateItem(result[0], result[1]);
    });
  }

  dialogRef.close(['close',editBaza,baza]);

//например. услуги

export class DataService {
  ....
  set data(data: Baza[]) {
    this.dataChange.next(data);
  }
  ....
  updateItem(oldBaza: Baza, newBaza: Baza){
    this.data = this.data.map((baza: Baza) => {
      if(baza === oldBaza) return newBaza;
      return baza;
    });
  }

Ответ 9

Структура файла jobposting.component.ts:

export class JobPostingComponent implements OnInit {
  values: JobPosting[];
  columns: string[] = ['title', 'vacancies','division.name'];
  displayedColumns: string[] = ['actions'].concat(this.columns);
  dataSource: MatTableDataSource<JobPosting>;

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

onEdit(data: JobPosting) {
  const dialogRef = this.dialog.open(AddJobPostingComponent, {
    data,
    width: '1000px'
  });

  dialogRef.afterClosed().subscribe(res => {
    if (res !== undefined) {
      const id = res.id;
      const index = this.values.findIndex(x => x.id === id);
      this.values[index] = res;
      this.dataSource.data = this.values;
    }
  });
}