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>
This is the summary of changes made.
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
I moved the formArrayName
to the tbody
since it should be the parent element of the *ngFor
I moved the [formGroupName]
to the *ngFor
line since its should be the parent of the form elements
Make sure you imported MatSortModule
to the child component
Make sure you imported provideAnimations()
into the providers array of bootstrapApplication
You are using Books
and books
interchangeably which is not correct, I renamed all to books
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
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()],
});