|
| 1 | +/** |
| 2 | + * @license |
| 3 | + * Copyright Google LLC All Rights Reserved. |
| 4 | + * |
| 5 | + * Use of this source code is governed by an MIT-style license that can be |
| 6 | + * found in the LICENSE file at https://angular.io/license |
| 7 | + */ |
| 8 | + |
| 9 | +// NOTE: This file is a direct copy of src/material/table/table-data-source.ts, but it imports |
| 10 | +// `MatPaginator` from `@angular/material-experimental/mdc-paginator` rather than |
| 11 | +// `@angular/material/paginator`. |
| 12 | +// TODO(mmalerba): Refactor this into a common base class with the non-MDC version. |
| 13 | + |
| 14 | +import {_isNumberValue} from '@angular/cdk/coercion'; |
| 15 | +import {DataSource} from '@angular/cdk/table'; |
| 16 | +import { |
| 17 | + BehaviorSubject, |
| 18 | + combineLatest, |
| 19 | + merge, |
| 20 | + Observable, |
| 21 | + of as observableOf, |
| 22 | + Subscription, |
| 23 | + Subject, |
| 24 | +} from 'rxjs'; |
| 25 | +import {MatPaginator, PageEvent} from '@angular/material-experimental/mdc-paginator'; |
| 26 | +import {MatSort, Sort} from '@angular/material/sort'; |
| 27 | +import {map} from 'rxjs/operators'; |
| 28 | + |
| 29 | +/** |
| 30 | + * Corresponds to `Number.MAX_SAFE_INTEGER`. Moved out into a variable here due to |
| 31 | + * flaky browser support and the value not being defined in Closure's typings. |
| 32 | + */ |
| 33 | +const MAX_SAFE_INTEGER = 9007199254740991; |
| 34 | + |
| 35 | +/** |
| 36 | + * Data source that accepts a client-side data array and includes native support of filtering, |
| 37 | + * sorting (using MatSort), and pagination (using MatPaginator). |
| 38 | + * |
| 39 | + * Allows for sort customization by overriding sortingDataAccessor, which defines how data |
| 40 | + * properties are accessed. Also allows for filter customization by overriding filterTermAccessor, |
| 41 | + * which defines how row data is converted to a string for filter matching. |
| 42 | + * |
| 43 | + * **Note:** This class is meant to be a simple data source to help you get started. As such |
| 44 | + * it isn't equipped to handle some more advanced cases like robust i18n support or server-side |
| 45 | + * interactions. If your app needs to support more advanced use cases, consider implementing your |
| 46 | + * own `DataSource`. |
| 47 | + */ |
| 48 | +export class MatTableDataSource<T> extends DataSource<T> { |
| 49 | + /** Stream that emits when a new data array is set on the data source. */ |
| 50 | + private readonly _data: BehaviorSubject<T[]>; |
| 51 | + |
| 52 | + /** Stream emitting render data to the table (depends on ordered data changes). */ |
| 53 | + private readonly _renderData = new BehaviorSubject<T[]>([]); |
| 54 | + |
| 55 | + /** Stream that emits when a new filter string is set on the data source. */ |
| 56 | + private readonly _filter = new BehaviorSubject<string>(''); |
| 57 | + |
| 58 | + /** Used to react to internal changes of the paginator that are made by the data source itself. */ |
| 59 | + private readonly _internalPageChanges = new Subject<void>(); |
| 60 | + |
| 61 | + /** |
| 62 | + * Subscription to the changes that should trigger an update to the table's rendered rows, such |
| 63 | + * as filtering, sorting, pagination, or base data changes. |
| 64 | + */ |
| 65 | + _renderChangesSubscription = Subscription.EMPTY; |
| 66 | + |
| 67 | + /** |
| 68 | + * The filtered set of data that has been matched by the filter string, or all the data if there |
| 69 | + * is no filter. Useful for knowing the set of data the table represents. |
| 70 | + * For example, a 'selectAll()' function would likely want to select the set of filtered data |
| 71 | + * shown to the user rather than all the data. |
| 72 | + */ |
| 73 | + filteredData: T[]; |
| 74 | + |
| 75 | + /** Array of data that should be rendered by the table, where each object represents one row. */ |
| 76 | + get data() { return this._data.value; } |
| 77 | + set data(data: T[]) { this._data.next(data); } |
| 78 | + |
| 79 | + /** |
| 80 | + * Filter term that should be used to filter out objects from the data array. To override how |
| 81 | + * data objects match to this filter string, provide a custom function for filterPredicate. |
| 82 | + */ |
| 83 | + get filter(): string { return this._filter.value; } |
| 84 | + set filter(filter: string) { this._filter.next(filter); } |
| 85 | + |
| 86 | + /** |
| 87 | + * Instance of the MatSort directive used by the table to control its sorting. Sort changes |
| 88 | + * emitted by the MatSort will trigger an update to the table's rendered data. |
| 89 | + */ |
| 90 | + get sort(): MatSort | null { return this._sort; } |
| 91 | + set sort(sort: MatSort|null) { |
| 92 | + this._sort = sort; |
| 93 | + this._updateChangeSubscription(); |
| 94 | + } |
| 95 | + private _sort: MatSort|null; |
| 96 | + |
| 97 | + /** |
| 98 | + * Instance of the MatPaginator component used by the table to control what page of the data is |
| 99 | + * displayed. Page changes emitted by the MatPaginator will trigger an update to the |
| 100 | + * table's rendered data. |
| 101 | + * |
| 102 | + * Note that the data source uses the paginator's properties to calculate which page of data |
| 103 | + * should be displayed. If the paginator receives its properties as template inputs, |
| 104 | + * e.g. `[pageLength]=100` or `[pageIndex]=1`, then be sure that the paginator's view has been |
| 105 | + * initialized before assigning it to this data source. |
| 106 | + */ |
| 107 | + get paginator(): MatPaginator | null { return this._paginator; } |
| 108 | + set paginator(paginator: MatPaginator|null) { |
| 109 | + this._paginator = paginator; |
| 110 | + this._updateChangeSubscription(); |
| 111 | + } |
| 112 | + private _paginator: MatPaginator|null; |
| 113 | + |
| 114 | + /** |
| 115 | + * Data accessor function that is used for accessing data properties for sorting through |
| 116 | + * the default sortData function. |
| 117 | + * This default function assumes that the sort header IDs (which defaults to the column name) |
| 118 | + * matches the data's properties (e.g. column Xyz represents data['Xyz']). |
| 119 | + * May be set to a custom function for different behavior. |
| 120 | + * @param data Data object that is being accessed. |
| 121 | + * @param sortHeaderId The name of the column that represents the data. |
| 122 | + */ |
| 123 | + sortingDataAccessor: ((data: T, sortHeaderId: string) => string|number) = |
| 124 | + (data: T, sortHeaderId: string): string|number => { |
| 125 | + const value = (data as {[key: string]: any})[sortHeaderId]; |
| 126 | + |
| 127 | + if (_isNumberValue(value)) { |
| 128 | + const numberValue = Number(value); |
| 129 | + |
| 130 | + // Numbers beyond `MAX_SAFE_INTEGER` can't be compared reliably so we |
| 131 | + // leave them as strings. For more info: https://goo.gl/y5vbSg |
| 132 | + return numberValue < MAX_SAFE_INTEGER ? numberValue : value; |
| 133 | + } |
| 134 | + |
| 135 | + return value; |
| 136 | + } |
| 137 | + |
| 138 | + /** |
| 139 | + * Gets a sorted copy of the data array based on the state of the MatSort. Called |
| 140 | + * after changes are made to the filtered data or when sort changes are emitted from MatSort. |
| 141 | + * By default, the function retrieves the active sort and its direction and compares data |
| 142 | + * by retrieving data using the sortingDataAccessor. May be overridden for a custom implementation |
| 143 | + * of data ordering. |
| 144 | + * @param data The array of data that should be sorted. |
| 145 | + * @param sort The connected MatSort that holds the current sort state. |
| 146 | + */ |
| 147 | + sortData: ((data: T[], sort: MatSort) => T[]) = (data: T[], sort: MatSort): T[] => { |
| 148 | + const active = sort.active; |
| 149 | + const direction = sort.direction; |
| 150 | + if (!active || direction == '') { return data; } |
| 151 | + |
| 152 | + return data.sort((a, b) => { |
| 153 | + let valueA = this.sortingDataAccessor(a, active); |
| 154 | + let valueB = this.sortingDataAccessor(b, active); |
| 155 | + |
| 156 | + // If there are data in the column that can be converted to a number, |
| 157 | + // it must be ensured that the rest of the data |
| 158 | + // is of the same type so as not to order incorrectly. |
| 159 | + const valueAType = typeof valueA; |
| 160 | + const valueBType = typeof valueB; |
| 161 | + |
| 162 | + if (valueAType !== valueBType) { |
| 163 | + if (valueAType === 'number') { valueA += ''; } |
| 164 | + if (valueBType === 'number') { valueB += ''; } |
| 165 | + } |
| 166 | + |
| 167 | + // If both valueA and valueB exist (truthy), then compare the two. Otherwise, check if |
| 168 | + // one value exists while the other doesn't. In this case, existing value should come last. |
| 169 | + // This avoids inconsistent results when comparing values to undefined/null. |
| 170 | + // If neither value exists, return 0 (equal). |
| 171 | + let comparatorResult = 0; |
| 172 | + if (valueA != null && valueB != null) { |
| 173 | + // Check if one value is greater than the other; if equal, comparatorResult should remain 0. |
| 174 | + if (valueA > valueB) { |
| 175 | + comparatorResult = 1; |
| 176 | + } else if (valueA < valueB) { |
| 177 | + comparatorResult = -1; |
| 178 | + } |
| 179 | + } else if (valueA != null) { |
| 180 | + comparatorResult = 1; |
| 181 | + } else if (valueB != null) { |
| 182 | + comparatorResult = -1; |
| 183 | + } |
| 184 | + |
| 185 | + return comparatorResult * (direction == 'asc' ? 1 : -1); |
| 186 | + }); |
| 187 | + } |
| 188 | + |
| 189 | + /** |
| 190 | + * Checks if a data object matches the data source's filter string. By default, each data object |
| 191 | + * is converted to a string of its properties and returns true if the filter has |
| 192 | + * at least one occurrence in that string. By default, the filter string has its whitespace |
| 193 | + * trimmed and the match is case-insensitive. May be overridden for a custom implementation of |
| 194 | + * filter matching. |
| 195 | + * @param data Data object used to check against the filter. |
| 196 | + * @param filter Filter string that has been set on the data source. |
| 197 | + * @returns Whether the filter matches against the data |
| 198 | + */ |
| 199 | + filterPredicate: ((data: T, filter: string) => boolean) = (data: T, filter: string): boolean => { |
| 200 | + // Transform the data into a lowercase string of all property values. |
| 201 | + const dataStr = Object.keys(data).reduce((currentTerm: string, key: string) => { |
| 202 | + // Use an obscure Unicode character to delimit the words in the concatenated string. |
| 203 | + // This avoids matches where the values of two columns combined will match the user's query |
| 204 | + // (e.g. `Flute` and `Stop` will match `Test`). The character is intended to be something |
| 205 | + // that has a very low chance of being typed in by somebody in a text field. This one in |
| 206 | + // particular is "White up-pointing triangle with dot" from |
| 207 | + // https://en.wikipedia.org/wiki/List_of_Unicode_characters |
| 208 | + return currentTerm + (data as {[key: string]: any})[key] + '◬'; |
| 209 | + }, '').toLowerCase(); |
| 210 | + |
| 211 | + // Transform the filter by converting it to lowercase and removing whitespace. |
| 212 | + const transformedFilter = filter.trim().toLowerCase(); |
| 213 | + |
| 214 | + return dataStr.indexOf(transformedFilter) != -1; |
| 215 | + } |
| 216 | + |
| 217 | + constructor(initialData: T[] = []) { |
| 218 | + super(); |
| 219 | + this._data = new BehaviorSubject<T[]>(initialData); |
| 220 | + this._updateChangeSubscription(); |
| 221 | + } |
| 222 | + |
| 223 | + /** |
| 224 | + * Subscribe to changes that should trigger an update to the table's rendered rows. When the |
| 225 | + * changes occur, process the current state of the filter, sort, and pagination along with |
| 226 | + * the provided base data and send it to the table for rendering. |
| 227 | + */ |
| 228 | + _updateChangeSubscription() { |
| 229 | + // Sorting and/or pagination should be watched if MatSort and/or MatPaginator are provided. |
| 230 | + // The events should emit whenever the component emits a change or initializes, or if no |
| 231 | + // component is provided, a stream with just a null event should be provided. |
| 232 | + // The `sortChange` and `pageChange` acts as a signal to the combineLatests below so that the |
| 233 | + // pipeline can progress to the next step. Note that the value from these streams are not used, |
| 234 | + // they purely act as a signal to progress in the pipeline. |
| 235 | + const sortChange: Observable<Sort|null|void> = this._sort ? |
| 236 | + merge(this._sort.sortChange, this._sort.initialized) as Observable<Sort|void> : |
| 237 | + observableOf(null); |
| 238 | + const pageChange: Observable<PageEvent|null|void> = this._paginator ? |
| 239 | + merge( |
| 240 | + this._paginator.page, |
| 241 | + this._internalPageChanges, |
| 242 | + this._paginator.initialized |
| 243 | + ) as Observable<PageEvent|void> : |
| 244 | + observableOf(null); |
| 245 | + const dataStream = this._data; |
| 246 | + // Watch for base data or filter changes to provide a filtered set of data. |
| 247 | + const filteredData = combineLatest([dataStream, this._filter]) |
| 248 | + .pipe(map(([data]) => this._filterData(data))); |
| 249 | + // Watch for filtered data or sort changes to provide an ordered set of data. |
| 250 | + const orderedData = combineLatest([filteredData, sortChange]) |
| 251 | + .pipe(map(([data]) => this._orderData(data))); |
| 252 | + // Watch for ordered data or page changes to provide a paged set of data. |
| 253 | + const paginatedData = combineLatest([orderedData, pageChange]) |
| 254 | + .pipe(map(([data]) => this._pageData(data))); |
| 255 | + // Watched for paged data changes and send the result to the table to render. |
| 256 | + this._renderChangesSubscription.unsubscribe(); |
| 257 | + this._renderChangesSubscription = paginatedData.subscribe(data => this._renderData.next(data)); |
| 258 | + } |
| 259 | + |
| 260 | + /** |
| 261 | + * Returns a filtered data array where each filter object contains the filter string within |
| 262 | + * the result of the filterTermAccessor function. If no filter is set, returns the data array |
| 263 | + * as provided. |
| 264 | + */ |
| 265 | + _filterData(data: T[]) { |
| 266 | + // If there is a filter string, filter out data that does not contain it. |
| 267 | + // Each data object is converted to a string using the function defined by filterTermAccessor. |
| 268 | + // May be overridden for customization. |
| 269 | + this.filteredData = |
| 270 | + !this.filter ? data : data.filter(obj => this.filterPredicate(obj, this.filter)); |
| 271 | + |
| 272 | + if (this.paginator) { this._updatePaginator(this.filteredData.length); } |
| 273 | + |
| 274 | + return this.filteredData; |
| 275 | + } |
| 276 | + |
| 277 | + /** |
| 278 | + * Returns a sorted copy of the data if MatSort has a sort applied, otherwise just returns the |
| 279 | + * data array as provided. Uses the default data accessor for data lookup, unless a |
| 280 | + * sortDataAccessor function is defined. |
| 281 | + */ |
| 282 | + _orderData(data: T[]): T[] { |
| 283 | + // If there is no active sort or direction, return the data without trying to sort. |
| 284 | + if (!this.sort) { return data; } |
| 285 | + |
| 286 | + return this.sortData(data.slice(), this.sort); |
| 287 | + } |
| 288 | + |
| 289 | + /** |
| 290 | + * Returns a paged slice of the provided data array according to the provided MatPaginator's page |
| 291 | + * index and length. If there is no paginator provided, returns the data array as provided. |
| 292 | + */ |
| 293 | + _pageData(data: T[]): T[] { |
| 294 | + if (!this.paginator) { return data; } |
| 295 | + |
| 296 | + const startIndex = this.paginator.pageIndex * this.paginator.pageSize; |
| 297 | + return data.slice(startIndex, startIndex + this.paginator.pageSize); |
| 298 | + } |
| 299 | + |
| 300 | + /** |
| 301 | + * Updates the paginator to reflect the length of the filtered data, and makes sure that the page |
| 302 | + * index does not exceed the paginator's last page. Values are changed in a resolved promise to |
| 303 | + * guard against making property changes within a round of change detection. |
| 304 | + */ |
| 305 | + _updatePaginator(filteredDataLength: number) { |
| 306 | + Promise.resolve().then(() => { |
| 307 | + const paginator = this.paginator; |
| 308 | + |
| 309 | + if (!paginator) { return; } |
| 310 | + |
| 311 | + paginator.length = filteredDataLength; |
| 312 | + |
| 313 | + // If the page index is set beyond the page, reduce it to the last page. |
| 314 | + if (paginator.pageIndex > 0) { |
| 315 | + const lastPageIndex = Math.ceil(paginator.length / paginator.pageSize) - 1 || 0; |
| 316 | + const newPageIndex = Math.min(paginator.pageIndex, lastPageIndex); |
| 317 | + |
| 318 | + if (newPageIndex !== paginator.pageIndex) { |
| 319 | + paginator.pageIndex = newPageIndex; |
| 320 | + |
| 321 | + // Since the paginator only emits after user-generated changes, |
| 322 | + // we need our own stream so we know to should re-render the data. |
| 323 | + this._internalPageChanges.next(); |
| 324 | + } |
| 325 | + } |
| 326 | + }); |
| 327 | + } |
| 328 | + |
| 329 | + /** |
| 330 | + * Used by the MatTable. Called when it connects to the data source. |
| 331 | + * @docs-private |
| 332 | + */ |
| 333 | + connect() { return this._renderData; } |
| 334 | + |
| 335 | + /** |
| 336 | + * Used by the MatTable. Called when it is destroyed. No-op. |
| 337 | + * @docs-private |
| 338 | + */ |
| 339 | + disconnect() { } |
| 340 | +} |
0 commit comments