1

I want to implement highly dynamic table with column level filters applied to it and editable rows with validation applied to each cell of table.

I have implemented Dynamic table display with editable rows and dynamic validations. But struggling with Column level filters.

My Problem statement:

  • UI will receive table headers to display and corrosponding table rows data. e.g. headers = ['name','age'] and data [{name:'abc',age:'xyz'},{name:'pqr',age:'xyz'}, ..]

with above setup I have implemented reactive form using formArray.

sample setup is created in stackblitz

here is my form :

<form [formGroup]="data_form">
  <table class="table table-border">
    <thead>
      <tr>
        <th>
          name
        </th>
        <th>
          age
        </th>
        <th><button class="btn btn-primary ">Save</button></th>
      </tr>
      <tr>
        <th *ngFor="let th of rowKeys">
          <ng-container *ngIf="th !=='isEditable'">
            <input type="text" formControlName="{{th}}" />
          </ng-container>
        </th>

        <th></th>
      </tr>
    </thead>
    <tbody formArrayName="persons">
      <ng-container *ngFor="let item of persons.controls;let j = index">
        <tr [formGroupName]="j">
          <ng-container *ngIf="!item.value.isEditable; else editable">
            <td>{{ item.value.name }}</td>
            <td>{{ item.value.age }}</td>
          </ng-container>
          <ng-template #editable>
            <td><input formControlName="name" /></td>
            <td><input formControlName="age" /></td>
          </ng-template>
          <td>
            <button (click)="toggleEdit(j)">
              {{ !item.value.isEditable ? "Edit": "Cancel"}}
            </button>
          </td>
        </tr>
      </ng-container>
    </tbody>
  </table>
</form>
<h2>
  {{data_form.status}}
</h2>

and ts:

import { Component } from "@angular/core";
import {
  FormArray,
  FormBuilder,
  FormControl,
  FormGroup,
  Validators
} from "@angular/forms";

@Component({
  selector: "my-app",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent {
  name = "Angular";
  constructor(private fb: FormBuilder) {}
  patterns = [
    /^[.\d]+$/,
    /^(yes|no)$/i,
    /^[a-zA-Z0-9 _/]+$/,
    /^(0[1-9]|1[0-2])\/(0[1-9]|1\d|2\d|3[01])\/(19|20)\d{2}$/
  ];
  data = [
    {
      name: "Sachin",
      age: 27,
      isEditable: false
    },
    {
      name: "Gopal",
      age: 27,

      isEditable: false
    },
    {
      name: "Pankaj",
      age: 24,

      isEditable: false
    }
  ];
  rowKeys = Object.keys(this.data[0]);
  keys = [...new Set(this.data.map(item => Object.keys(item)).flat())];
  keyPattern = this.keys.map(item => ({
    key: item,
    pattern: this.patterns.find(pattern =>
      this.data.every(i => pattern.test(i[item]))
    )
  }));
  data_form = this.fb.group({
    persons: this.fb.array(
      this.data.map(item =>
        this.fb.group(
          this.keyPattern.reduce(
            (prev, { key, pattern }) => ({
              ...prev,
              [key]: [
                item[key],
                [Validators.required, Validators.pattern(pattern)]
              ]
            }),
            {}
          )
        )
      )
    )
  });
  get persons(): FormArray {
    return this.data_form.get("persons") as FormArray;
  }

  toggleEdit(j) {
    const currentEditStatus = this.persons.controls[j].get("isEditable").value;
    this.persons.controls[j].get("isEditable").setValue(!currentEditStatus);
  }
  ngOnInit(){
     this.rowKeys.forEach((num) => {
        if (num == "isEditable") return;
        const fc = new FormControl('');
        this.data_form.addControl(num, fc)
      });

      /**
       * How to filter formsArray ?
       */

      // this.data_form.get('cuisp').valueChanges.pipe(
      //   debounceTime(100),
      //   distinctUntilChanged(),
      // ).subscribe(val => {
      //   console.log(val)
      //   const result = this.persons.value.filter(res => {
      //     if (res['cuisp'] === val) {
      //       return res
      //     }
      //   });
      //   this.persons.patchValue(result)
      //   console.log(result)
      // });

  }

}

How to implement column level search so then when I search in Name column then respective name should get displayed.

1 Answer 1

1

Consider below approach using reactive programing.

The Steps are as below

  • Convert all your inputs to observables
  • Set up a Subject to use as a trigger for filtering
  • Combine the data and the subject using the combineLatest([...]) operator from rxjs

Below is a working code, See this demo on stackblitz

  constructor(private fb: FormBuilder) {}
  patterns = [
    /^[.\d]+$/,
    /^(yes|no)$/i,
    /^[a-zA-Z0-9 _/]+$/,
    /^(0[1-9]|1[0-2])\/(0[1-9]|1\d|2\d|3[01])\/(19|20)\d{2}$/
  ];
  data$ = of([
    {
      name: "Sachin",
      age: 27,
      isEditable: false
    },
    {
      name: "Gopal",
      age: 27,

      isEditable: false
    },
    {
      name: "Pankaj",
      age: 24,

      isEditable: false
    }
  ]);
  filterStringSubject$ = new BehaviorSubject({});
  filterStringAction$ = this.filterStringSubject$.asObservable();
  filterString$ = this.filterStringAction$.pipe(
    map(stringObject =>
      Object.entries(stringObject).map(item => ({
        key: item[0],
        value: item[1]
      }))
    )
  );
  rowKeys$ = this.data$.pipe(
    map(data => Object.keys(data[0])),
    tap(rowKeys => {
      rowKeys.forEach(num => {
        if (num == "isEditable") return;
        this.filterStringSubject$.next({
          ...this.filterStringSubject$.value,
          [num]: ""
        });
      });
    })
  );
  keys$ = this.data$.pipe(
    map(data => [...new Set(data.map(item => Object.keys(item)).flat())])
  );
  keyPattern$ = combineLatest(this.keys$, this.data$).pipe(
    map(([keys, data]) => {
      return keys.map(item => ({
        key: item,
        pattern: this.patterns.find(pattern =>
          data.every(i => pattern.test(i[item]))
        )
      }));
    })
  );
  data_form: FormGroup;
  dataFiltered$ = combineLatest([this.data$, this.filterString$]).pipe(
    map(([data, filterString]) =>
      this.persons?.value.filter(item =>
        filterString.every(a => `${item[a.key]}`.includes(`${a.value}`))
      )
    )
  );
  dataForm$ = combineLatest([ this.data$,
    this.keyPattern$]).pipe(
tap(([data, keyPattern]) => { 
      this.data_form = this.fb.group({
        persons: this.fb.array(
          data.map(item =>
            this.fb.group(
              keyPattern.reduce(
                (prev, { key, pattern }) => ({
                  ...prev,
                  [key]: [
                    item[key],
                    [Validators.required, Validators.pattern(pattern)]
                  ]
                }),
                {}
              )
            )
          )
        )
      });
    }),
    )
  v$ = combineLatest([
    this.dataForm$,
    this.rowKeys$,
    this.filterString$,
    this.dataFiltered$
  ]).pipe(
    
    map(([, rowKeys, filterString, dataFiltered]) => ({
      rowKeys,
      filterString,
      dataFiltered
    }))
  );
  get persons(): FormArray {
    return this.data_form?.get("persons") as FormArray;
  }

  toggleEdit(j) {
    
    const currentEditStatus = this.persons.controls[j].get("isEditable").value;
    this.persons.controls[j].get("isEditable").setValue(!currentEditStatus);
  }
  filterBy(item, value) {
    this.filterStringSubject$.next({
      ...this.filterStringSubject$.value,
      [item]: value
    });
  }
  ngOnInit() { }

In your HTML

<form [formGroup]="data_form" *ngIf='v$ | async as v'>
    <table class="table table-border">

        <thead>
            <tr>
                <th>
                    name
                </th>
                <th>
                    age
                </th>
                <th><button class="btn btn-primary ">Save</button></th>
            </tr>
            <tr>
                <td *ngFor="let item of v.rowKeys">
                    <input *ngIf='item != "isEditable"' type="text"
          (input)="filterBy(item, $event.target.value)" />
        </td>

                <th></th>
            </tr>
        </thead>
        <tbody formArrayName="persons">
            <ng-container *ngFor="let item of v.dataFiltered;let j = index">
                <tr [formGroupName]="j">
                    <ng-container *ngIf="!persons.controls[j]?.get('isEditable').value; else editable">
                        <td>{{ item.name }}</td>
                        <td>{{ item.age }}</td>
                    </ng-container>
                    <ng-template #editable>
                        <td><input formControlName="name" /></td>
                        <td><input formControlName="age" /></td>
                    </ng-template>
                    <td>
                        <button (click)="toggleEdit(j)">
              {{ !persons.controls[j]?.get('isEditable').value ? "Edit": "Cancel"}}
            </button>
                    </td>
                </tr>
            </ng-container>
        </tbody>
    </table>
</form>
<h2>
    {{data_form?.status}}
</h2>
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.