Search code examples
angularangularjstypescriptsortingmaterial-ui

Add Sort in Angular with FormArray


Newbie here on Angular. I have an input class Foo with a list of books with Book class having Id, Title and Description property. I display it in a table with input so user can add, edit or delete book list. There is a button somewhere that adds and deletes a book. This is working fine.

I wanted to add a sort but it's not working. I added MatSort and Sort in the appmodule. I put the sort code block from Angular itself. What am I doing wrong?

Should I change this in a MatTable instead of looping through a form array? If so how can I do that with input instead of displaying per data as variable {{element.title}} etc?

Appreciate all the help.

TS

@Input() foo: Foo;
@ViewChild(MatSort, {static: true}) sort: MatSort;
bookForm: FormArray;
orderForm: FormGroup;
bookList !: Book[];
bookSorted : Book[];
initForm() {
  this.orderForm= this._formBuilder.group( {
    customerForm: this._formBuilder.array( [] ),
    bookForm: this._formBuilder.array( [] )
  } );

  this.addedBooks()
  this.bookList = this.foo.Books;
}

addedBooks() {
  this.bookForm= this.orderForm.get( 'bookForm' ) as FormArray;
  this.bookForm.clear();
  let _bookForm = this.foo.books?.map( _book => this.addBook( _book ) );
  _bookForm?.forEach( _addBook => this.bookForm.push( _addBook ) );

}

addBook( _book) {
  return this._formBuilder.group( {
    title: new FormControl( _book?.title),
    description: new FormControl( _book?.description ),
    id: new FormControl( _book?.id ?? Guid.EMPTY ),
  } );
}

get bookFormControls() {
  return ( this.orderForm.get( 'bookForm' ) as FormArray ).controls;
}

sortBook(sort: Sort) {
  const data = this.bookList.slice();
  if (!sort.active || sort.direction == '') {
    this.bookSorted = data;
    return;
  }

  this.bookSorted = data.sort((a, b) => {
    let isAsc = sort.direction == 'asc';
    switch (sort.active) {
      case 'title': return this.compare(a.title, b.title, isAsc);
      case 'description': return this.compare(+a.description, +b.description, isAsc);
      default: return 0;
    }
  });
}

compare(a, b, isAsc) {
  return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
}

removeBooksAt( index ) {
  this.dialogName = "Book"
  this.modalRef = this.dialog.open( this.deleteBook, {
    width: '600px',
  } );
  this.modalRef.afterClosed().subscribe( res => {
    if ( res ) this.bookForm.removeAt( index );
  } );

}

addNewBook() {
  let formValue = this.orderForm.controls['bookForm'] as FormArray;
  formValue.status == 'VALID' ? this.createBooksForm() : this.showToast();
}

createBooksForm(data?: any) {
  this.booksForm = this.orderForm.get( 'booksForm ' ) as FormArray;
  this.booksForm .push( this.addBooksControls(data) );
}

addBooksControls(data?: any): FormGroup {
  return this._formBuilder.group( {
    title: [data?.title ??'', Validators.required],
    description: [data?.description ??'', Validators.required],
    id: [data?.id ??Guid.EMPTY]
  } );
}

HTML

<!--Mat Sort Test-->
<fieldset>
  <div>
    <legend>Books</legend>
    <table matSort (matSortChange)="sortBook($event)" class="card-table">
      <thead class="primary-color">
      <tr>
        <th mat-sort-header="title">
          Book Title
        </th>
        <th mat-sort-header="description">
          Description
        </th>
        <th class="colums-name">
          Actions
        </th>
      </tr>
      </thead>
      <tbody>
      <tr class="margin-1" formArrayName="bookForm"
          *ngFor="let group of bookFormControls; let _i = index;">
        <td [formGroupName]="_i">
          <input type="text" formControlName="title" class="margin-1 readonly" placeholder="Add title">
        </td>
        <td [formGroupName]="_i">
          <input type="text" formControlName="description" class="margin-1 readonly"
                 placeholder="Add description">
          <input type="hidden" formControlName="id">
        </td>
        <td style="text-align: center;">
          <i (click)="removeBooksAt(_i, 'Title')" class="fa fa-trash margin-right-mini"
             style="color:darkgrey; font-size: xx-large;;" aria-hidden="true"></i>
        </td>
      </tr>
      </tbody>
    </table>
  </div>
</fieldset>

Solution

  • This is the summary of changes made.

    1. We need to add [formGroup] at the top of the table, since it's the root location. It might not be needed for you since you have it somewhere where you have not shared the code, so use if needed

    2. I moved the formArrayName to the tbody since it should be the parent element of the *ngFor

    3. I moved the [formGroupName] to the *ngFor line since its should be the parent of the form elements

    4. Make sure you imported MatSortModule to the child component

    5. Make sure you imported provideAnimations() into the providers array of bootstrapApplication

    6. You are using Books and books interchangeably which is not correct, I renamed all to books

    7. The main problem, was that, we are sorting the data instead of the form controls, since we are using the form controls to create the for loop, we should use the same formGroup array for the sort also.

    Sort code change:

      sortBook(sort: Sort) {
        debugger;
        if (!sort.active || sort.direction == '') {
          return;
        }
        (<Array<FormGroup>>this.bookFormControls).sort(
          (a: FormGroup, b: FormGroup) => {
            let isAsc = sort.direction == 'asc';
            switch (sort.active) {
              case 'title':
                return this.compare(
                  a?.controls?.['title']?.value,
                  a?.controls?.['title']?.value,
                  isAsc
                );
              case 'description':
                return this.compare(
                  a?.controls?.['description']?.value,
                  b?.controls?.['description']?.value,
                  isAsc
                );
              default:
                return 0;
            }
          }
        );
      }
    

    I may have missed few points, please go through the code and let me know if any doubts, please find below full code and stackblitz

    FULL CODE:

    CHILD TS

    import { CommonModule } from '@angular/common';
    import { Component, Input, ViewChild } from '@angular/core';
    import {
      FormArray,
      FormBuilder,
      FormControl,
      FormGroup,
      ReactiveFormsModule,
      Validators,
    } from '@angular/forms';
    import { MatSort, MatSortModule, Sort } from '@angular/material/sort';
    import { MatTableModule } from '@angular/material/table';
    
    @Component({
      selector: 'app-child',
      standalone: true,
      imports: [MatTableModule, CommonModule, ReactiveFormsModule, MatSortModule],
      templateUrl: './child.component.html',
      styleUrl: './child.component.css',
    })
    export class ChildComponent {
      @Input() foo: any;
      @ViewChild(MatSort, { static: true }) sort!: MatSort;
      bookForm!: FormArray;
      orderForm!: FormGroup;
      bookList!: any[];
      bookSorted!: any[];
    
      constructor(private _formBuilder: FormBuilder) {}
    
      ngOnInit() {
        this.initForm();
      }
    
      initForm() {
        this.orderForm = this._formBuilder.group({
          customerForm: this._formBuilder.array([]),
          bookForm: this._formBuilder.array([]),
        });
    
        this.addedBooks();
        this.bookList = this.foo.books;
      }
    
      addedBooks() {
        this.bookForm = this.orderForm.get('bookForm') as FormArray;
        this.bookForm.clear();
        let _bookForm = this.foo.books?.map((_book: any) => this.addBook(_book));
        _bookForm?.forEach((_addBook: any) => this.bookForm.push(_addBook));
      }
    
      addBook(_book: any) {
        return this._formBuilder.group({
          title: new FormControl(_book?.title),
          description: new FormControl(_book?.description),
          id: new FormControl(_book?.id ?? Math.random()),
        });
      }
    
      get bookFormControls() {
        return (this.orderForm.get('bookForm') as FormArray).controls;
      }
    
      sortBook(sort: Sort) {
        debugger;
        if (!sort.active || sort.direction == '') {
          return;
        }
        (<Array<FormGroup>>this.bookFormControls).sort(
          (a: FormGroup, b: FormGroup) => {
            let isAsc = sort.direction == 'asc';
            switch (sort.active) {
              case 'title':
                return this.compare(
                  a?.controls?.['title']?.value,
                  a?.controls?.['title']?.value,
                  isAsc
                );
              case 'description':
                return this.compare(
                  a?.controls?.['description']?.value,
                  b?.controls?.['description']?.value,
                  isAsc
                );
              default:
                return 0;
            }
          }
        );
      }
    
      compare(a: any, b: any, isAsc: any) {
        return (a < b ? -1 : 1) * (isAsc ? 1 : -1);
      }
    
      removeBooksAt(index: number) {
        // this.dialogName = "Book"
        // this.modalRef = this.dialog.open( this.deleteBook, {
        //   width: '600px',
        // } );
        // this.modalRef.afterClosed().subscribe( res => {
        // if ( res )
        this.bookForm.removeAt(index);
        // } );
      }
    
      addNewBook() {
        let formValue = this.orderForm.controls['bookForm'] as FormArray;
        formValue.status == 'VALID' ? this.createBooksForm() : this.showToast();
      }
    
      showToast() {
        alert('show status');
      }
    
      createBooksForm(data?: any) {
        this.bookForm = this.orderForm.get('booksForm') as FormArray;
        this.bookForm.push(this.addBooksControls(data));
      }
    
      addBooksControls(data?: any): FormGroup {
        return this._formBuilder.group({
          role: [data?.title ?? '', Validators.required],
          description: [data?.description ?? '', Validators.required],
          id: [data?.id ?? ''],
        });
      }
    }
    

    CHILD HTML

    <table
      matSort
      (matSortChange)="sortBook($event)"
      class="card-table"
      [formGroup]="orderForm"
    >
      <thead class="primary-color">
        <tr>
          <th mat-sort-header="title">Book Title</th>
          <th mat-sort-header="description">Description</th>
          <th class="colums-name">Actions</th>
        </tr>
      </thead>
      <tbody formArrayName="bookForm">
        <tr
          class="margin-1"
          *ngFor="let group of bookFormControls; let _i = index"
          [formGroupName]="_i"
        >
          <td>
            <input
              type="text"
              formControlName="title"
              class="margin-1 readonly"
              placeholder="Add title"
            />
          </td>
          <td>
            <input
              type="text"
              formControlName="description"
              class="margin-1 readonly"
              placeholder="Add description"
            />
            <input type="hidden" formControlName="id" />
          </td>
          <td style="text-align: center;">
            <i
              (click)="removeBooksAt(_i)"
              class="fa fa-trash margin-right-mini"
              style="color:darkgrey; font-size: xx-large;;"
              aria-hidden="true"
            ></i>
          </td>
        </tr>
      </tbody>
    </table>
    

    PARENT

    import { Component } from '@angular/core';
    import { bootstrapApplication } from '@angular/platform-browser';
    import 'zone.js';
    import { ChildComponent } from './app/child/child.component';
    import { provideAnimations } from '@angular/platform-browser/animations';
    
    @Component({
      selector: 'app-root',
      standalone: true,
      imports: [ChildComponent],
      template: `
        <app-child [foo]="foo"></app-child>
      `,
    })
    export class App {
      foo = {
        books: [
          { id: 1, title: 'test', description: 'test' },
          { id: 2, title: 'test2', description: 'test2' },
          { id: 3, title: 'test3', description: 'test3' },
        ],
      };
      name = 'Angular';
    }
    
    bootstrapApplication(App, {
      providers: [provideAnimations()],
    });
    

    Stackblitz Demo