import { DataSource } from '@angular/cdk/collections' ; import { BehaviorSubject , Subject , Observable } from 'rxjs' ; import { TableElementFactory } from './table-element.factory' ; import { ValidatorService } from './validator.service' ; import { TableElement } from './table-element' ; import { DefaultValidatorService } from './default-validator.service' ; export class TableDataSource < T > extends DataSource < TableElement < T > > { protected rowsSubject : BehaviorSubject < TableElement < T > [ ] > ; datasourceSubject : Subject < T [ ] > ; protected dataConstructor : new ( ) => T ; protected dataKeys : any [ ] ; protected currentData : any ; param data param dataType param validatorService param config constructor ( data , dataType ? , private validatorService ? , private config = { prependNewElements : false , suppressErrors : false } ) { super ( ) ; if ( ! validatorService ) this . validatorService = new DefaultValidatorService ( ) ; if ( dataType ) { this . dataConstructor = dataType ; } else { if ( data && data . length > 0 ) this . dataKeys = Object . keys ( data [ 0 ] ) ; else throw new Error ( 'You must define either a non empty array, or an associated class to build the table.' ) ; } this . checkValidatorFields ( this . validatorService ) ; this . rowsSubject = new BehaviorSubject ( this . getRowsFromData ( data ) ) ; this . datasourceSubject = new Subject < T [ ] > ( ) ; } protected checkValidatorFields ( validatorService ) { const formGroup = validatorService . getRowValidator ( ) ; if ( formGroup != null ) { const rowKeys = Object . keys ( this . createNewObject ( ) ) ; Object . keys ( formGroup . controls ) . forEach ( key => { if ( rowKeys . some ( x => x === key ) ) { this . logError ( 'Validator form control keys must match row object keys.' ) ; } } ) ; } } protected logError ( message ) { if ( ! this . config . suppressErrors ) console . error ( message ) ; } /**
     * Start the creation of a new element, pushing an empty-data row in the table.
     */ createNew ( ) { const source = this . rowsSubject . getValue ( ) ; if ( ! this . existsNewElement ( source ) ) { const newElement = TableElementFactory . createTableElement ( { id : - 1 , editing : true , currentData : this . createNewObject ( ) , source : this , validator : this . validatorService . getRowValidator ( ) , } ) ; if ( this . config . prependNewElements ) { this . rowsSubject . next ( [ newElement ] . concat ( source ) ) ; } else { source . push ( newElement ) ; this . rowsSubject . next ( source ) ; } } } param row confirmCreate ( row ) { if ( ! row . isValid ( ) ) { return false ; } const source = this . rowsSubject . getValue ( ) ; row . id = source . length - 1 ; this . rowsSubject . next ( source ) ; row . editing = false ; this . updateDatasourceFromRows ( source ) ; return true ; } param row confirmEdit ( row ) { if ( ! row . isValid ( ) ) { return false ; } const source = this . rowsSubject . getValue ( ) ; const index = this . getIndexFromRowId ( row . id , source ) ; source [ index ] = row ; this . rowsSubject . next ( source ) ; row . originalData = undefined ; row . editing = false ; this . updateDatasourceFromRows ( source ) ; return true ; } /**
     * Delete the row with the index specified.
     */ delete ( id ) { const source = this . rowsSubject . getValue ( ) ; const index = this . getIndexFromRowId ( id , source ) ; source . splice ( index , 1 ) ; this . updateRowIds ( index , source ) ; this . rowsSubject . next ( source ) ; if ( id != - 1 ) this . updateDatasourceFromRows ( source ) ; } param id getRow ( id ) { const source = this . rowsSubject . getValue ( ) ; const index = this . getIndexFromRowId ( id , source ) ; return ( index >= 0 && index < source . length ) ? source [ index ] : null ; } param data param options updateDatasource ( data , options = { emitEvent : true } ) { if ( this . currentData !== data ) { this . currentData = data ; this . rowsSubject . next ( this . getRowsFromData ( data ) ) ; if ( options . emitEvent ) this . datasourceSubject . next ( data ) ; } } param source protected existsNewElement ( source ) { return ! ( source . length == 0 || source [ this . getNewRowIndex ( source ) ] . id > - 1 ) ; } param source protected getNewRowIndex ( source ) { if ( this . config . prependNewElements ) return 0 ; else return source . length - 1 ; } param index param count protected getRowIdFromIndex ( index , count ) { if ( this . config . prependNewElements ) return count - 1 - index ; else return index ; } param id param source protected getIndexFromRowId ( id , source ) { if ( id == - 1 ) { return this . existsNewElement ( source ) ? this . getNewRowIndex ( source ) : - 1 ; } else { if ( this . config . prependNewElements ) return source . length - 1 - id ; else return id ; } } param initialIndex param source protected updateRowIds ( initialIndex , source ) { const delta = this . config . prependNewElements ? - 1 : 1 ; for ( let index = initialIndex ; index < source . length && index >= 0 ; index += delta ) { if ( source [ index ] . id != - 1 ) source [ index ] . id = this . getRowIdFromIndex ( index , source . length ) ; } } param rows protected getDataFromRows ( rows ) { return rows . filter ( row => row . id != - 1 ) . map < T > ( ( row ) => { return row . originalData ? row . originalData : row . currentData ; } ) ; } param rows protected updateDatasourceFromRows ( rows ) { this . currentData = this . getDataFromRows ( rows ) ; this . datasourceSubject . next ( this . currentData ) ; } param arrayData protected getRowsFromData ( arrayData ) { return arrayData . map < TableElement < T > > ( ( data , index ) => { return TableElementFactory . createTableElement ( { id : this . getRowIdFromIndex ( index , arrayData . length ) , editing : false , currentData : data , source : this , validator : this . validatorService . getRowValidator ( ) , } ) ; } ) ; } /**
     * Create a new object with identical structure than the table source data.
     * It uses the object's type contructor if available, otherwise it creates
     * an object with the same keys of the first element contained in the original
     * datasource (used in the constructor).
     */ protected createNewObject ( ) { if ( this . dataConstructor ) { return new this . dataConstructor ( ) ; } else { return this . dataKeys . reduce ( ( obj , key ) => { obj [ key ] = undefined ; return obj ; } , { } ) ; } } /** Connect function called by the table to retrieve one stream containing
     *  the data to render. */ connect ( ) { return this . rowsSubject . asObservable ( ) ; } disconnect ( ) { } }