Skip to content

Commit

Permalink
Merge pull request #227 from gund/deferred-rendering
Browse files Browse the repository at this point in the history
Deferred rendering further optimizations
  • Loading branch information
softsimon authored Jul 14, 2017
2 parents 34e8065 + a987686 commit 395da56
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 10 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ myOptions: IMultiSelectOption[] = [
| maxHeight | The maximum height for the dropdown (including unit) | '300px' |
| displayAllSelectedText | Display the `allSelected` text when all options are selected | false |
| 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 |
| searchRenderAfter | Amount of characters to trigger rendering of items | 3 |
| searchRenderAfter | Amount of characters to trigger rendering of items | 1 |
| searchMaxLimit | If more than zero will render only first N options in search results | 0 |
| searchMaxRenderedItems | Used with searchMaxLimit to further limit rendering for optimization. Should be less than searchMaxLimit to take effect | 0 |
| displayAllSelectedText | Display the `allSelected` text when all options are selected | false |

### Texts
Expand Down
2 changes: 1 addition & 1 deletion src/dropdown/dropdown.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
</li>
<li *ngIf="settings.showCheckAll || settings.showUncheckAll" class="dropdown-divider divider"></li>
<ng-template [ngIf]="renderItems" [ngIfElse]="noRenderBlock">
<ng-template [ngIf]="options | searchFilter:filterControl.value" let-filteredOptions>
<ng-template [ngIf]="options | searchFilter:filterControl.value:settings.searchMaxLimit:settings.searchMaxRenderedItems" let-filteredOptions>
<li *ngIf="!filteredOptions.length" class="dropdown-item empty">{{ texts.saerchEmptyResult }}</li>
<li class="dropdown-item" [ngStyle]="getItemStyle(option)" *ngFor="let option of filteredOptions" (click)="!option.isLabel && setSelected($event, option)"
[class.dropdown-header]="option.isLabel">
Expand Down
4 changes: 3 additions & 1 deletion src/dropdown/dropdown.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ export class MultiselectDropdown implements OnInit, OnChanges, DoCheck, OnDestro
pullRight: false,
enableSearch: false,
searchRenderLimit: 0,
searchRenderAfter: 3,
searchRenderAfter: 1,
searchMaxLimit: 0,
searchMaxRenderedItems: 0,
checkedStyle: 'checkboxes',
buttonClasses: 'btn btn-default btn-secondary',
containerClasses: 'dropdown-inline',
Expand Down
93 changes: 86 additions & 7 deletions src/dropdown/search-filter.pipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,93 @@ import { IMultiSelectOption } from './types';
name: 'searchFilter'
})
export class MultiSelectSearchFilter implements PipeTransform {
transform(options: Array<IMultiSelectOption>, args: string): Array<IMultiSelectOption> {
const matchPredicate = (option: IMultiSelectOption) => option.name.toLowerCase().indexOf((args || '').toLowerCase()) > -1,

private _lastOptions: IMultiSelectOption[];
private _searchCache: { [k: string]: IMultiSelectOption[] } = {};
private _searchCacheInclusive: { [k: string]: boolean | number } = {};

transform(options: Array<IMultiSelectOption>, str: string, limit = 0, renderLimit = 0): Array<IMultiSelectOption> {
str = (str || '').toLowerCase();

// Drop cache because options were updated
if (options !== this._lastOptions) {
this._lastOptions = options;
this._searchCache = {};
this._searchCacheInclusive = {};
}

if (this._searchCache[str]) {
return this._limitRenderedItems(this._searchCache[str], renderLimit);
}

const prevStr = str.slice(0, -1);
const prevResults = this._searchCache[prevStr];

if (prevResults) {
const prevInclusiveOrIdx = this._searchCacheInclusive[prevStr];

if (prevInclusiveOrIdx === true) {
// If have previous results and it was inclusive, do only subsearch
options = prevResults;
} else if (typeof prevInclusiveOrIdx === 'number') {
// Or reuse prev results with unchecked ones
options = [...prevResults, ...options.slice(prevInclusiveOrIdx)];
}
}

const optsLength = options.length;
const maxFound = limit > 0 ? Math.min(limit, optsLength) : optsLength;
const filteredOpts = [];

const regexp = new RegExp(this._escapeRegExp(str), 'i');

const matchPredicate = (option: IMultiSelectOption) => regexp.test(option.name),
getChildren = (option: IMultiSelectOption) => options.filter(child => child.parentId === option.id),
getParent = (option: IMultiSelectOption) => options.find(parent => option.parentId === parent.id);
return options.filter((option: IMultiSelectOption) => {
return matchPredicate(option) ||
(typeof (option.parentId) === 'undefined' && getChildren(option).some(matchPredicate)) ||
(typeof (option.parentId) !== 'undefined' && matchPredicate(getParent(option)));
});

let i = 0, founded = 0;
for (; i < optsLength && founded < maxFound; ++i) {
const option = options[i];
const directMatch = regexp.test(option.name);

if (directMatch) {
filteredOpts.push(option);
founded++;
continue;
}

if (typeof (option.parentId) === 'undefined') {
const childrenMatch = getChildren(option).some(matchPredicate);

if (childrenMatch) {
filteredOpts.push(option);
founded++;
continue;
}
}

if (typeof (option.parentId) !== 'undefined') {
const parentMatch = matchPredicate(getParent(option));

if (parentMatch) {
filteredOpts.push(option);
founded++;
continue;
}
}
}

this._searchCache[str] = filteredOpts;
this._searchCacheInclusive[str] = i === optsLength || i + 1;

return this._limitRenderedItems(filteredOpts, renderLimit);
}

private _limitRenderedItems<T>(items: T[], limit: number): T[] {
return items.length > limit ? items.slice(0, limit) : items;
}

private _escapeRegExp(str: string): string {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}
}
11 changes: 11 additions & 0 deletions src/dropdown/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ export interface IMultiSelectSettings {
* 3 - By default
*/
searchRenderAfter?: number;
/**
* 0 - By default
* If >0 will render only N first items
*/
searchMaxLimit?: number;
/**
* 0 - By default
* Used with searchMaxLimit to further limit rendering for optimization
* Should be less than searchMaxLimit to take effect
*/
searchMaxRenderedItems?: number;
checkedStyle?: 'checkboxes' | 'glyphicon' | 'fontawesome';
buttonClasses?: string;
itemClasses?: string;
Expand Down

0 comments on commit 395da56

Please sign in to comment.