web-dev-qa-db-fra.com

Angular - Table de matériaux, est-il possible de mettre à jour des lignes sans actualisation complète de la table?

Après quelques semaines de recherches sur Google et une seule question de Stackoverflown jusqu'à présent, j'ai finalement réussi à construire mon Angular CRUD App à l'aide de Material Table Component. Il affiche les données du backend (JSON) et pour les opérations CRUD, j'utilise des boîtes de dialogue telles que celle affichée sur la photo (il s'agit de la modification, désolé pour le croate). Les dialogues pourraient ne pas être la meilleure solution, l'édition en ligne pourrait être meilleure. Néanmoins, pour ajouter un nouvel élément, vous avez besoin de quelque chose comme un dialogue.

 enter image description here

La dernière chose que je suis coincé avec est de savoir comment mettre à jour les champs dans la table en conséquence. Ainsi, lorsque vous appuyez sur "Enregistrer" dans la boîte de dialogue, les données sont mises à jour dans le backend (dans la table MySQL) mais pas dans la partie avant. Pour le moment, j'ai une solution de contournement moche pour cela. Chaque fois que vous effectuez une mise à jour, la table entière est également actualisée.

En tout cas voici le code:

Composant de la table:

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

Voici DataService où je suppose que je devrais faire des mises à jour sur le terrain:

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

UPDATE 27.11.2017:

D'accord, j'ai enfin compris comment déclencher l'ajout d'une nouvelle ligne. J'ai dû appeler dataChange.value à l'intérieur du composant de table. Une fois que vous avez chargé certaines données, la nouvelle ligne apparaît instantanément.

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

La même chose dans DataService ne fonctionnera pas:

this.dataChange.value.Push(data); 

Plunker est ici:

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

EDIT du 28.11.2017:

Il ne reste maintenant que la logique de construction pour ajouter, éditer et supprimer. Pour ajouter, rien de plus simple, c’est simplement «value.Push (data)». Merci pour l'aide à tous.

10
besthiroeu

Cela m'a pris du temps mais j'ai finalement tout fait fonctionner. Vos réponses et vos différentes approches ont également aidé. Alors, voici mon implémentation de CRUD si quelqu'un a des problèmes avec ceci:

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

Capture d'écran:Alt Text

Ou vous pouvez consulter la démonstration du projet: https://marinantonio.github.io/angular-mat-table-crud/

Les parties clés sont dans le fichier 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's 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's 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;
    }
}
....
15
besthiroeu

Comme je vois dans votre code que vous utilisez la pagination, vous pouvez procéder comme suit après l'opération crud: 

this.dataSource.paginator = this.paginator;

Cela actualisera la page actuelle. Et, heureux que quelqu'un de Croatie utilise un matériau angulaire.

Voici la partie importante de mon code:

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;
}
6
zszep

Une approche légèrement différente pour supprimer un élément et actualiser la table de données. Il appelle à nouveau l'API, mais cela pourrait fonctionner pour des ensembles de données plus petits.

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

Ceci s’appelle bien sûr en haut du composant mais est inclus ici pour référence.

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

Cette solution utilise mon code de suppression existant, mais identique pour le code de mise à jour. Le problème clé est de trouver l'index de tableau pour l'élément modifié ou supprimé. Notez qu’une fois le résultat obtenu, j’appelle un modal de réussite pour informer l’utilisateur, puis appelle une fonction pour supprimer la ligne de la table de données. Vous pouvez également mettre à jour les données de cette ligne avec un code légèrement différent, tel que l'insertion des données dans le tableau d'objets. De cette façon, nous n'avons pas à télécharger toutes les données à nouveau.

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

Permet maintenant de supprimer ou de mettre à jour cette ligne supprimée ou modifiée.

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;
  }
3
Preston

J'ai une solution de contournement dans l'édition de données dans la table sans utiliser les fenêtres modales. 

Vous pouvez jeter un oeil à ma mise en œuvre CRUD avec Angular 6 et Material 

Service de données

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

Composant 

@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

Voici le lien vers la démo: https://crud-angular6.azurewebsites.net

Click here to see screenshot

3
Aleksandr Chuikov

Ma réponse est dans Angular 6 Material 2 .

J'ai utilisé la fonction splice qui prend comme argument l'index de la ligne modifiée, puis le nombre de lignes à supprimer (dans votre cas 1) et enfin la nouvelle version de la ligne modifiée qui sera insérée à cet index:

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

Pouvez-vous jeter un oeil à 

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

La demande POST fonctionne-t-elle et envoie-t-elle les données? Vous faites référence à Baza dans la demande POST qui doit être 'baza' (minuscule B). Peut-être que la requête échoue à cause de cela et que l'abonnement observable n'est jamais rempli ... vous pouvez vérifier cette théorie avec un gestionnaire d'erreurs sur l'abonnement.

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

Enfin, en ce qui concerne les modifications, mon approche serait légèrement différente. Injectez la même instance de DataService dans le composant et transmettez cette même référence à la table DataSource plutôt qu'à une nouvelle instance. Ensuite, passez tout l'objet baza à la boîte de dialogue d'édition, pas seulement à ses propriétés. Ensuite, à la fermeture du dialogue, passez l'original (objet non modifié) ainsi que les nouvelles propriétés (ou, mieux, un nouvel objet de la classe Baza avec les champs modifiés). Envoyez-les à notre service de données avec une méthode "édition/mise à jour". La méthode edit/update va filtrer l'ensemble existant de tableau de données en recherchant les entrées qui correspondent à notre objet non édité et en les définissant comme égales à notre nouvel objet. Exemple légèrement résumé donné ci-dessous

// par exemple. Composant

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

// par exemple. un service

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;
    });
  }
2
NJ.

En fait, vous n'avez pas à actualiser la table après l'édition, si vous avez le code HTML suivant:

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

Et, dans le .ts vous avez:

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

Vous pouvez voir que automatiquement la ligne lorsque vous appuyez sur Modifier le changement de nom d'utilisateur (dans ce cas à "user123")

1
Juan Calderon