skip to Main Content

Here’s my code for a TablePresenter class that accepts an array of objects of the same structure and handles their sorting, filtering, and paging. In setting up a TablePresenter for use, for each of the properties in the objects in the targeted array, a client method uses the addProp method to add a TablePresenterPropConfig to specify how to sort on that property.

At line 9, in addProp, the code checker is telling me that

Property 'setCurrentOrder' is optional in type '{ propName: string; whatToSort: (x: any) => any; comparer: (a: any, b: any) => SortOrder; defaultOrder: SortOrder; currentOrder: SortOrder; ariaSort: AriaSort; propsToSort: string[]; isCurrentPrimary: boolean; setCurrentOrder?: (order: SortOrder) => void; }' but required in type 'TablePresenterPropConfig'.

Why isn’t it finding setCurrentOrder in the copy of TablePresenterPropConfig that I’ve instantiated? Or, perhaps the relevant question is, how is it finding that it’s "optional", and how do I fix that? This is part of an Angular 15 application and I’m developing in Visual Studio Code, in case that matters.

export type SortOrder = -1 | 0 | 1;
export type AriaSort = 'descending' | 'none' | 'ascending';

export class TablePresenter {    
    props: string[] = new Array<string>();
    propConfigs = {};

    addProp(partial: Partial<TablePresenterPropConfig>) {
        let propConfig: TablePresenterPropConfig = {...new TablePresenterPropConfig(), ...partial};
        this.propConfigs[propConfig.propName] = propConfig;
        this.props.push(propConfig.propName);
    }

    private sourceData: any[];
    private filter: ((row: any) => boolean) = (x => true);
    private filteredData: any[];
    private _pageSize: number = 100;
    public get pageSize(): number { return this._pageSize; };
    public setPageSize(size: number): void {
        this._pageSize = size;
        this.setDataPage(0);
    }
    private _currentPageNumber: number = 0;
    public get currentPageNumber(): number { return this._currentPageNumber; }
    public setPageNumber(pageNumber: number): void {
        this.setDataPage(pageNumber);
    }
    private _dataPage: any[];
    public get dataPage(): any { return this._dataPage }
    private set dataPage(data: any) { this._dataPage = data;  }

    setSourceData(data: any[]): void {
        this.sourceData = data;
        this.sort();
        this.filteredData = this.sourceData.filter(this.filter);
        this.setDataPage(0);
    }

    setFilter(filter: (row: any) => boolean) {
        this.filter = filter;
        this.filteredData = this.sourceData.filter(this.filter);
        this.setDataPage(0);
    }

    setDataPage(pageNumber?: number) {
        if (pageNumber) this._currentPageNumber = pageNumber;
        let start = this._currentPageNumber * this._pageSize;
        this.dataPage = this.filteredData.slice(start, start + this._pageSize);
    }

    sort(propName?: string) {
        if (propName === undefined) {
            let currentPrimaryKey = this.props.map(prop => this.propConfigs[prop]).find(config => config.isCurrentPrimary);
            propName = currentPrimaryKey ? currentPrimaryKey.propName : this.propConfigs[this.props[0]].propName;
        }
        // Set ordering data across the configs.
        for (let prop of this.props) {
            let config = this.propConfigs[prop];
            if (prop === propName) {
                config.setCurrentOrder(config.isCurrentPrimary ? -config.currentOrder : config.defaultOrder);
                if (config.currentOrder === 0) throw (`Non-sortable property ${prop} was set as the primary sort key`);
                config.isCurrentPrimary = true;
            }
            else {
                config.isCurrentPrimary = false;
                config.setCurrentOrder(0);
            }
        }

        // Do the sort if there's data.
        if (this.sourceData) {
            this.sourceData.sort((row1, row2) => {
                // Do the sort.
                for (let prop of this.propConfigs[propName].propsToSort) {
                    let config = this.propConfigs[prop];
                    let order = config.isCurrentPrimary ? config.currentOrder : config.defaultOrder;
                    if (order === 0) throw (`Non-sortable property ${prop} was given as a sort key for primary sort key ${propName}`);
                    let a = (config.currentOrder === 1) ? config.whatToSort(row1) : config.whatToSort(row2);
                    let b = (config.currentOrder === 1) ? config.whatToSort(row2) : config.whatToSort(row1);
                    let comparison = config.comparer(a, b);
                    if (comparison === -1 || comparison === 1) return comparison;
                }
                return 0;
            });
        }
    }

    public static basicComparer(a: any, b: any) {
        if (a < b) return -1;
        if (a > b) return 1;
        return 0;
    }

    /*
        createListBasedComparer is a comparison function factory. Its output is a comparison function that takes two
        parameters that are members of a list and return -1 if the first one is earlier in the list, 1 if it's later,
        and 0 if they're the same item.

        The list is supplied to the factory as the array *list*. For example, if the value supplied for *list* is
        ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], the comparer that the factory returns can be used 
        to sort an array of days of the week in Sun-through-Sat order.

        If the parameter *allowNonMembers* is set to true, the generate comparer will accept parameters not on 
        the list, treating them as greater than all items in the list, so that in an ascending sort they will all
        be sorted to the end. If *allowNonMembers is false (the default), then an exception is thrown when a
        value that isn't on the list is supplied to the comparer.

        TODO: add parameter *isCaseSensitive*, default false. If false, the comparer should check whether
        the two values have a *toLowerCase* method and, if so, convert them to lower case before comparing.
    */
    public static createListBasedComparer<T>(list: T[], allowNonMembers: boolean = false) {
        let func = function(a: T, b: T) {
            let aIndex = list.indexOf(a);
            let bIndex = list.indexOf(b);
   
            if (allowNonMembers) {
                if (aIndex === -1 && bIndex === -1) return 0;
                if (aIndex === -1) return 1;
                if (bIndex === -1) return -1
            }
            else {
                if (aIndex === -1 || bIndex === -1) throw("Attempted to sort an array containing nonmembers of the list");
            }
   
            if (aIndex < bIndex) return -1;
            if (aIndex > bIndex) return 1;
            return 0;
        }
        return func;
   }
   
}

export class TablePresenterPropConfig{
    static readonly ariaSorts: AriaSort[] = ['descending', 'none', 'ascending'];
    propName: string;
    whatToSort: ((x: any ) => any) = row => row;
    comparer: ((a: any, b: any) => SortOrder) = TablePresenter.basicComparer;
    defaultOrder: SortOrder = 1;
    currentOrder: SortOrder = 0;
    ariaSort: AriaSort = 'none';
    propsToSort: string[] = null;
    isCurrentPrimary: boolean = false;
    setCurrentOrder(order: SortOrder) {
        this.currentOrder = order;
        this.ariaSort = TablePresenterPropConfig.ariaSorts[1 + order];
    }
}

2

Answers


  1. Chosen as BEST ANSWER

    Thanks to Dimava. Final, functional result, and tightened up, with the partial class moved to the constructor:

    export type SortOrder = -1 | 0 | 1;
    export type AriaSort = 'descending' | 'none' | 'ascending';
    
    export class TablePresenter {    
        props: string[] = new Array<string>();
        propConfigs = {};
    
        addProp(init: Partial<TablePresenterPropConfig>) {
            let propConfig: TablePresenterPropConfig = new TablePresenterPropConfig(init);
            this.propConfigs[propConfig.propName] = propConfig;
            this.props.push(propConfig.propName);
        }
    
        private sourceData: any[];
        public get sourceDataCount(): number { return this.sourceData.length; }
        private filter: ((row: any) => boolean) = (x => true);
        private get filteredData(): any { return this.sourceData.filter(this.filter); };
        public get filteredDataCount(): number { return this.filteredData.length; }
        private _pageSize: number = 100;
        public get pageSize(): number { return this._pageSize; };
        public setPageSize(size: number): void {
            this._pageSize = size;
        }
        private _currentPageNumber: number = 0;
        public get currentPageNumber(): number { return this._currentPageNumber; }
        public setPageNumber(pageNumber: number): void {
            this._currentPageNumber = pageNumber;
        }
        public get dataPage(): any {
            let start = this._currentPageNumber * this._pageSize;
            return this.filteredData.slice(start, start + this._pageSize);
        }
    
        setSourceData(data: any[]): void {
            this.sourceData = data;
            this.sort();
        }
    
        setFilter(filter: (row: any) => boolean) {
            this.filter = filter;
        }
    
        sort(propName?: string) {
            if (propName === undefined) {
                let currentPrimaryKey = this.props.map(prop => this.propConfigs[prop]).find(config => config.isCurrentPrimary);
                propName = currentPrimaryKey ? currentPrimaryKey.propName : this.propConfigs[this.props[0]].propName;
            }
            // Set ordering data across the configs.
            for (let prop of this.props) {
                let config = this.propConfigs[prop];
                if (prop === propName) {
                    config.setCurrentOrder(config.isCurrentPrimary ? -config.currentOrder : config.defaultOrder);
                    if (config.currentOrder === 0) throw (`Non-sortable property ${prop} was set as the primary sort key`);
                    config.isCurrentPrimary = true;
                }
                else {
                    config.isCurrentPrimary = false;
                    config.setCurrentOrder(0);
                }
            }
    
            // Do the sort if there's data.
            if (this.sourceData) {
                this.sourceData.sort((row1, row2) => {
                    // Do the sort.
                    for (let prop of this.propConfigs[propName].propsToSort) {
                        let config = this.propConfigs[prop];
                        let order = config.isCurrentPrimary ? config.currentOrder : config.defaultOrder;
                        if (order === 0) throw (`Non-sortable property ${prop} was given as a sort key for primary sort key ${propName}`);
                        let a = (order === 1) ? config.whatToSort(row1) : config.whatToSort(row2);
                        let b = (order === 1) ? config.whatToSort(row2) : config.whatToSort(row1);
                        let comparison = config.comparer(a, b);
                        if (comparison === -1 || comparison === 1) return comparison;
                    }
                    return 0;
                });
            }
        }
    
        public static basicComparer(a: any, b: any) {
            if (a < b) return -1;
            if (a > b) return 1;
            return 0;
        }
    
        /*
            createListBasedComparer is a comparison function factory. Its output is a comparison function that takes two
            parameters that are members of a list and return -1 if the first one is earlier in the list, 1 if it's later,
            and 0 if they're the same item.
    
            The list is supplied to the factory as the array *list*. For example, if the value supplied for *list* is
            ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], the comparer that the factory returns can be used 
            to sort an array of days of the week in Sun-through-Sat order.
    
            If the parameter *allowNonMembers* is set to true, the generate comparer will accept parameters not on 
            the list, treating them as greater than all items in the list, so that in an ascending sort they will all
            be sorted to the end. If *allowNonMembers is false (the default), then an exception is thrown when a
            value that isn't on the list is supplied to the comparer.
    
            TODO: add parameter *isCaseSensitive*, default false. If false, the comparer should check whether
            the two values have a *toLowerCase* method and, if so, convert them to lower case before comparing.
        */
        public static createListBasedComparer<T>(list: T[], allowNonMembers: boolean = false) {
            let func = function(a: T, b: T) {
                let aIndex = list.indexOf(a);
                let bIndex = list.indexOf(b);
       
                if (allowNonMembers) {
                    if (aIndex === -1 && bIndex === -1) return 0;
                    if (aIndex === -1) return 1;
                    if (bIndex === -1) return -1
                }
                else {
                    if (aIndex === -1 || bIndex === -1) throw("Attempted to sort an array containing nonmembers of the list");
                }
       
                if (aIndex < bIndex) return -1;
                if (aIndex > bIndex) return 1;
                return 0;
            }
            return func;
       }
       
    }
    
    export class TablePresenterPropConfig{
        static readonly ariaSorts: AriaSort[] = ['descending', 'none', 'ascending'];
        propName: string;
        whatToSort: ((x: any ) => any) = row => row;
        comparer: ((a: any, b: any) => SortOrder) = TablePresenter.basicComparer;
        defaultOrder: SortOrder = 1;
        currentOrder: SortOrder = 0;
        get ariaSort(): AriaSort { return TablePresenterPropConfig.ariaSorts[1 + this.currentOrder]; }
        propsToSort: string[] = null;
        isCurrentPrimary: boolean = false;
        setCurrentOrder(order: SortOrder) {
            this.currentOrder = order;
        }
    
        constructor(init: Partial<TablePresenterPropConfig>) {
            if (!init.hasOwnProperty('propName') || !init.propName) {
                throw ('The propName for a new TablePresenter property configuration is missing');
            }
            Object.assign(this, init);
            let propName = this.propName;
    
            // whatToSort: If not specified, the default  is a map from the row to this property of the row
            if (!init.hasOwnProperty('whatToSort')) {
                this.whatToSort = (row) => row[propName];
            }
            // propsToSort: If not specified, the default is to sort only on this property (that is,
            // no secondary, tertiary, etc. sort keys).
            if (!init.hasOwnProperty('propsToSort')) {
                this.propsToSort = [propName];
            }
    
        }
    }```
    

  2. let o = {...new TablePresenterPropConfig(), ...partial}
    console.log(o) // Object { propName: "foo" }
    

    does not create an TablePresenterPropConfig instance – it create Object instance with its properties

    You need

    let o = Object.assign(new TablePresenterPropConfig(), partial)
    console.log(o) // TablePresenterPropConfig { ..., propName: "foo" }
    

    instead to override properties of TablePresenterPropConfig instance

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search