Guest User

ListView Component

a guest
Feb 23rd, 2024
152
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
TypeScript 7.80 KB | Source Code | 0 0
  1. import { AfterViewInit, ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
  2. import { CommonModule } from '@angular/common';
  3. import { ListViewSelectionMode } from './enums/list-view-selection-mode.enum';
  4. import { ListViewColumn } from './interfaces/list-view-column.interface';
  5. import { SearchTextboxComponent } from '../search-textbox/search-textbox.component';
  6. import { FilterService, SortingService } from 'Common';
  7. import { ListViewRecordAction } from './interfaces/list-view-record-action.interface';
  8. import { BehaviorSubject, Observable, Subject, take, takeUntil } from 'rxjs';
  9.  
  10. @Component({
  11.   selector: 'list-view',
  12.   standalone: true,
  13.   imports: [
  14.     CommonModule,
  15.     SearchTextboxComponent
  16.   ],
  17.   templateUrl: './list-view.component.html',
  18.   styleUrls: ['./list-view.component.css']
  19. })
  20. export class ListViewComponent<T> implements OnInit, OnDestroy, AfterViewInit {
  21.  
  22.   // Enumerations
  23.   public ListViewSelectionMode = ListViewSelectionMode;
  24.  
  25.   // Initialization properties (required)
  26.   @Input({ required: true }) public columns!: ListViewColumn<T>[];
  27.   @Input({ required: true }) public source!:  Observable<T[]>;
  28.  
  29.   // View/layout properties
  30.   @Input() public hover:          boolean = true;
  31.   @Input() public small:          boolean = true;
  32.   @Input() public showHeaderRow:  boolean = true;
  33.   @Input() public showSearch:     boolean = true;
  34.   @Input() public showStatusBar:  boolean = true;
  35.   @Input() public showSummary:    boolean = true;
  36.   @Input() public showToolbar:    boolean = true;
  37.  
  38.   // Functional properties
  39.   @Input() public actions?:             ListViewRecordAction<T>[];
  40.   @Input() public selectionMode?:       ListViewSelectionMode;
  41.  
  42.   // Selection properties
  43.   @Input()  public selectionModel?:      Observable<Map<T, boolean>>;
  44.   @Input()  public getRecordSelectable?: (record: T)                => boolean;
  45.   @Input()  public onRecordSelecting?:   (record: T, next: boolean) => Observable<boolean>;
  46.   @Output() public onRecordClicked:      EventEmitter<T>            = new EventEmitter<T>();
  47.   @Output() public onRecordSelected:     EventEmitter<T>            = new EventEmitter<T>();
  48.   @Output() public onSelectionChanged:   EventEmitter<T[]>          = new EventEmitter<T[]>();
  49.  
  50.   // Record management
  51.   public  displayedRecords$: BehaviorSubject<T[]> = new BehaviorSubject<T[]>([]);
  52.   private selectedRecords:   Map<T, boolean>      = new Map<T, boolean>();
  53.   private records:    T[] = [];
  54.   private filtered?:  T[];
  55.  
  56.   // Private
  57.   private readonly searchProperties:  string[]      = [];
  58.   private readonly onDestroying$:     Subject<void> = new Subject<void>();
  59.  
  60.   constructor(
  61.     private changeDetector: ChangeDetectorRef,
  62.     private filterService: FilterService,
  63.     private sortingService: SortingService
  64.   ) { }
  65.  
  66.   public get allDisplayedRecordsAreSelected(): boolean {
  67.     const displayedRecords = this.displayedRecords$.getValue();
  68.     for (let i = 0; i < displayedRecords.length; i++) {
  69.       const record = displayedRecords[i];
  70.       if (!this.selectedRecords.get(record)) {
  71.         return false;
  72.       }
  73.     }
  74.     return true;
  75.   }
  76.  
  77.   public get displayedColumns(): ListViewColumn<T>[] {
  78.     return this.columns;
  79.   }
  80.  
  81.   public get displayedRecordCount(): number {
  82.     return this.displayedRecords$.getValue().length;
  83.   }
  84.  
  85.   public get totalRecordCount(): number {
  86.     return this.records.length;
  87.   }
  88.  
  89.   public get isMultiSelect(): boolean {
  90.     return this.selectionMode === ListViewSelectionMode.Multiple;
  91.   }
  92.  
  93.   public ngOnInit(): void {
  94.  
  95.     // Cache column search properties
  96.     this.columns.filter(column => column.property !== null && column.isSearchable !== false).map(column => column.property!).forEach(property => {
  97.       this.searchProperties.push(property);
  98.     });
  99.  
  100.     // Subscribe to source stream changes
  101.     this.source.pipe(
  102.       takeUntil(this.onDestroying$)
  103.     ).subscribe({
  104.       next: (records: T[]) => {
  105.         this.records = records;
  106.         if (this.filtered) {
  107.           this.filtered = this.filtered.filter(record => this.records.includes(record));
  108.         }
  109.         this.updateDisplayedRecords();
  110.       }
  111.     });
  112.  
  113.     // Subscribe to provided selection model
  114.     if (this.selectionModel) {
  115.       this.selectionModel.pipe(
  116.         takeUntil(this.onDestroying$)
  117.       ).subscribe({
  118.         next: (selectionMap: Map<T, boolean>) => {
  119.           this.selectedRecords = selectionMap;
  120.         }
  121.       })
  122.     }
  123.    
  124.   }
  125.  
  126.   public ngOnDestroy(): void {
  127.     this.onDestroying$.next();
  128.     this.onDestroying$.complete();
  129.   }
  130.  
  131.   public ngAfterViewInit(): void {
  132.     this.changeDetector.detectChanges();
  133.   }
  134.  
  135.   public onSearch(keyword: string|undefined): void {
  136.     if (keyword) {
  137.       this.filtered = this.filterService.filter<T>(this.records, keyword, this.searchProperties);
  138.     } else {
  139.       this.filtered = undefined;
  140.     }
  141.     this.updateDisplayedRecords();
  142.   }
  143.  
  144.   public onSort(column: ListViewColumn<T>): void {
  145.     // TODO needs to be fixed
  146.     this.records = this.sortingService.sort(this.records, column.property);
  147.     if (this.filtered && this.filtered.length > 0) {
  148.       this.filtered = this.sortingService.sort(this.filtered, column.property);
  149.     }
  150.     this.updateDisplayedRecords();
  151.   }
  152.  
  153.   public isRecordSelectable(record: T): boolean {
  154.     return this.getRecordSelectable !== undefined ? this.getRecordSelectable(record) : true;
  155.   }
  156.  
  157.   public isRecordSelected(record: T): boolean {
  158.     return this.selectedRecords.get(record) as boolean;
  159.   }
  160.  
  161.   public async toggleRecordSelected(record: T, event: Event): Promise<void> {
  162.     if (event) {
  163.       event.stopPropagation();
  164.       //event.preventDefault();
  165.     }
  166.     if (!this.isRecordSelectable(record)) {
  167.       return;
  168.     }
  169.     const next: boolean = !this.isRecordSelected(record);
  170.     await this.setRecordSelected(record, next, true);
  171.   }
  172.  
  173.   public toggleAllDisplayedRecordsSelected(): void {
  174.     let allSelected: boolean = this.getCurrentlySelectedRecords().length === this.displayedRecordCount;
  175.     const selected = allSelected ? !allSelected : true;
  176.     this.displayedRecords$.getValue().forEach((displayedRecord: T) => {
  177.       this.setRecordSelected(displayedRecord, selected, false, false);
  178.     });
  179.     const currentlySelectedRecords = this.getCurrentlySelectedRecords();
  180.     this.onSelectionChanged.next(currentlySelectedRecords);
  181.   }
  182.  
  183.   private getCurrentlySelectedRecords(): T[] {
  184.     let records: T[] = [];
  185.     this.selectedRecords.forEach((isSelected: boolean, record: T) => {
  186.       if (isSelected) {
  187.         records.push(record);
  188.       }
  189.     });
  190.     return records;
  191.   }
  192.  
  193.   private async setRecordSelected(record: T, next: boolean, shouldPrompt: boolean = true, fireEvent: boolean = true): Promise<void> {
  194.  
  195.     if (this.isRecordSelected(record) == next) {
  196.       // No change
  197.       return;
  198.     }
  199.  
  200.     let shouldProceed: boolean = false;
  201.     if (shouldPrompt && this.onRecordSelecting !== undefined) {
  202.       await this.onRecordSelecting(record, next).pipe(
  203.         take(1)
  204.       ).subscribe({
  205.         next: (proceed: boolean) => {
  206.           shouldProceed = proceed;
  207.         }
  208.       });
  209.     } else {
  210.       shouldProceed = true;
  211.     }
  212.  
  213.     if (!shouldProceed) {
  214.       // Update not desired
  215.       return;
  216.     }
  217.  
  218.     if (!this.isMultiSelect) {
  219.       this.selectedRecords.forEach((value: boolean, key: T) => {
  220.         this.selectedRecords.set(key, key === record);
  221.       });
  222.     } else {
  223.       this.selectedRecords.set(record, next);
  224.     }
  225.  
  226.     if (fireEvent) {
  227.       if (this.isMultiSelect) {
  228.         this.onSelectionChanged.next(this.getCurrentlySelectedRecords());
  229.       } else {
  230.         this.onRecordSelected.next(record);
  231.       }
  232.     }
  233.  
  234.   }
  235.  
  236.   private updateDisplayedRecords(): void {
  237.     this.displayedRecords$.next(this.filtered ?? this.records);
  238.   }
  239.  
  240. }
Advertisement
Add Comment
Please, Sign In to add comment