Skip to content

Commit 395da56

Browse files
authored
Merge pull request #227 from gund/deferred-rendering
Deferred rendering further optimizations
2 parents 34e8065 + a987686 commit 395da56

File tree

5 files changed

+104
-10
lines changed

5 files changed

+104
-10
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,9 @@ myOptions: IMultiSelectOption[] = [
124124
| maxHeight | The maximum height for the dropdown (including unit) | '300px' |
125125
| displayAllSelectedText | Display the `allSelected` text when all options are selected | false |
126126
| searchRenderLimit | If `enableSearch=true` and total amount of items more then `searchRenderLimit` (0 - No limit) then render items only when user typed more then or equal `searchRenderAfter` charachters | 0 |
127-
| searchRenderAfter | Amount of characters to trigger rendering of items | 3 |
127+
| searchRenderAfter | Amount of characters to trigger rendering of items | 1 |
128+
| searchMaxLimit | If more than zero will render only first N options in search results | 0 |
129+
| searchMaxRenderedItems | Used with searchMaxLimit to further limit rendering for optimization. Should be less than searchMaxLimit to take effect | 0 |
128130
| displayAllSelectedText | Display the `allSelected` text when all options are selected | false |
129131

130132
### Texts

src/dropdown/dropdown.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
</li>
2727
<li *ngIf="settings.showCheckAll || settings.showUncheckAll" class="dropdown-divider divider"></li>
2828
<ng-template [ngIf]="renderItems" [ngIfElse]="noRenderBlock">
29-
<ng-template [ngIf]="options | searchFilter:filterControl.value" let-filteredOptions>
29+
<ng-template [ngIf]="options | searchFilter:filterControl.value:settings.searchMaxLimit:settings.searchMaxRenderedItems" let-filteredOptions>
3030
<li *ngIf="!filteredOptions.length" class="dropdown-item empty">{{ texts.saerchEmptyResult }}</li>
3131
<li class="dropdown-item" [ngStyle]="getItemStyle(option)" *ngFor="let option of filteredOptions" (click)="!option.isLabel && setSelected($event, option)"
3232
[class.dropdown-header]="option.isLabel">

src/dropdown/dropdown.component.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@ export class MultiselectDropdown implements OnInit, OnChanges, DoCheck, OnDestro
7878
pullRight: false,
7979
enableSearch: false,
8080
searchRenderLimit: 0,
81-
searchRenderAfter: 3,
81+
searchRenderAfter: 1,
82+
searchMaxLimit: 0,
83+
searchMaxRenderedItems: 0,
8284
checkedStyle: 'checkboxes',
8385
buttonClasses: 'btn btn-default btn-secondary',
8486
containerClasses: 'dropdown-inline',

src/dropdown/search-filter.pipe.ts

Lines changed: 86 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,93 @@ import { IMultiSelectOption } from './types';
55
name: 'searchFilter'
66
})
77
export class MultiSelectSearchFilter implements PipeTransform {
8-
transform(options: Array<IMultiSelectOption>, args: string): Array<IMultiSelectOption> {
9-
const matchPredicate = (option: IMultiSelectOption) => option.name.toLowerCase().indexOf((args || '').toLowerCase()) > -1,
8+
9+
private _lastOptions: IMultiSelectOption[];
10+
private _searchCache: { [k: string]: IMultiSelectOption[] } = {};
11+
private _searchCacheInclusive: { [k: string]: boolean | number } = {};
12+
13+
transform(options: Array<IMultiSelectOption>, str: string, limit = 0, renderLimit = 0): Array<IMultiSelectOption> {
14+
str = (str || '').toLowerCase();
15+
16+
// Drop cache because options were updated
17+
if (options !== this._lastOptions) {
18+
this._lastOptions = options;
19+
this._searchCache = {};
20+
this._searchCacheInclusive = {};
21+
}
22+
23+
if (this._searchCache[str]) {
24+
return this._limitRenderedItems(this._searchCache[str], renderLimit);
25+
}
26+
27+
const prevStr = str.slice(0, -1);
28+
const prevResults = this._searchCache[prevStr];
29+
30+
if (prevResults) {
31+
const prevInclusiveOrIdx = this._searchCacheInclusive[prevStr];
32+
33+
if (prevInclusiveOrIdx === true) {
34+
// If have previous results and it was inclusive, do only subsearch
35+
options = prevResults;
36+
} else if (typeof prevInclusiveOrIdx === 'number') {
37+
// Or reuse prev results with unchecked ones
38+
options = [...prevResults, ...options.slice(prevInclusiveOrIdx)];
39+
}
40+
}
41+
42+
const optsLength = options.length;
43+
const maxFound = limit > 0 ? Math.min(limit, optsLength) : optsLength;
44+
const filteredOpts = [];
45+
46+
const regexp = new RegExp(this._escapeRegExp(str), 'i');
47+
48+
const matchPredicate = (option: IMultiSelectOption) => regexp.test(option.name),
1049
getChildren = (option: IMultiSelectOption) => options.filter(child => child.parentId === option.id),
1150
getParent = (option: IMultiSelectOption) => options.find(parent => option.parentId === parent.id);
12-
return options.filter((option: IMultiSelectOption) => {
13-
return matchPredicate(option) ||
14-
(typeof (option.parentId) === 'undefined' && getChildren(option).some(matchPredicate)) ||
15-
(typeof (option.parentId) !== 'undefined' && matchPredicate(getParent(option)));
16-
});
51+
52+
let i = 0, founded = 0;
53+
for (; i < optsLength && founded < maxFound; ++i) {
54+
const option = options[i];
55+
const directMatch = regexp.test(option.name);
56+
57+
if (directMatch) {
58+
filteredOpts.push(option);
59+
founded++;
60+
continue;
61+
}
62+
63+
if (typeof (option.parentId) === 'undefined') {
64+
const childrenMatch = getChildren(option).some(matchPredicate);
65+
66+
if (childrenMatch) {
67+
filteredOpts.push(option);
68+
founded++;
69+
continue;
70+
}
71+
}
72+
73+
if (typeof (option.parentId) !== 'undefined') {
74+
const parentMatch = matchPredicate(getParent(option));
75+
76+
if (parentMatch) {
77+
filteredOpts.push(option);
78+
founded++;
79+
continue;
80+
}
81+
}
82+
}
83+
84+
this._searchCache[str] = filteredOpts;
85+
this._searchCacheInclusive[str] = i === optsLength || i + 1;
86+
87+
return this._limitRenderedItems(filteredOpts, renderLimit);
88+
}
89+
90+
private _limitRenderedItems<T>(items: T[], limit: number): T[] {
91+
return items.length > limit ? items.slice(0, limit) : items;
92+
}
93+
94+
private _escapeRegExp(str: string): string {
95+
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
1796
}
1897
}

src/dropdown/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,17 @@ export interface IMultiSelectSettings {
1919
* 3 - By default
2020
*/
2121
searchRenderAfter?: number;
22+
/**
23+
* 0 - By default
24+
* If >0 will render only N first items
25+
*/
26+
searchMaxLimit?: number;
27+
/**
28+
* 0 - By default
29+
* Used with searchMaxLimit to further limit rendering for optimization
30+
* Should be less than searchMaxLimit to take effect
31+
*/
32+
searchMaxRenderedItems?: number;
2233
checkedStyle?: 'checkboxes' | 'glyphicon' | 'fontawesome';
2334
buttonClasses?: string;
2435
itemClasses?: string;

0 commit comments

Comments
 (0)