Skip to content

Commit 2db8c8a

Browse files
committed
refactor(material-icons-extended): improve search and rendering
1 parent 0f88943 commit 2db8c8a

4 files changed

Lines changed: 121 additions & 46 deletions

File tree

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
"dompurify": "^3.2.7",
7272
"express": "^4.18.2",
7373
"file-saver": "^2.0.2",
74+
"fuse.js": "^7.1.0",
7475
"hammerjs": "^2.0.8",
7576
"igniteui-angular": "^21.0.0-rc.0",
7677
"igniteui-angular-charts": "^20.2.0",

src/app/data-display/icon/material-icons-extended/material-icons-extended.component.html

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
[value]="selectedCategory"
88
>
99
<label igxLabel>Select category</label>
10-
@for (option of categories; track option) {
10+
@for (option of categories; track option.category) {
1111
<igx-select-item
1212
[value]="option.category"
1313
>
@@ -17,29 +17,29 @@
1717
</igx-select>
1818
</div>
1919
<igx-input-group class="sample__header-search" type="search">
20-
<input #input igxInput placeholder="Search by icon name or keyword" />
20+
<input #input igxInput placeholder="Search by icon name or keyword" (input)="onSearchInput(input.value)" />
2121
<igx-prefix>
2222
<igx-icon>search</igx-icon>
2323
</igx-prefix>
2424
@if (input.value.length > 0) {
2525
<igx-suffix
26-
(click)="input.value = ''"
26+
(click)="input.value = ''; clearSearch()"
2727
>
2828
<igx-icon>clear</igx-icon>
2929
</igx-suffix>
3030
}
3131
</igx-input-group>
3232
</div>
3333
<div class="sample__body">
34-
@if ((allIcons | filterByName: input.value | categoriesFilter: selectedCategory); as fResults) {
35-
@for (group of fResults; track group) {
34+
@if ((allIcons | filterByName: searchTerm() | categoriesFilter: selectedCategory); as fResults) {
35+
@for (group of fResults; track trackByCategory($index, group)) {
3636
<article class="sample__body-inner">
3737
<header class="sample__body-title">
3838
{{ group.category }}
3939
</header>
4040
<section class="sample__body-section">
4141
<div class="sample__grid">
42-
@for (icon of group.icons; track icon) {
42+
@for (icon of group.icons; track trackByIcon($index, icon)) {
4343
<div
4444
class="sample__grid-item"
4545
>

src/app/data-display/icon/material-icons-extended/material-icons-extended.component.ts

Lines changed: 104 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,29 @@
11
/* eslint-disable @typescript-eslint/member-ordering */
2-
import { Component, OnInit, Pipe, PipeTransform, Renderer2, forwardRef, DOCUMENT, inject } from '@angular/core';
3-
import * as fileSaver from 'file-saver';
2+
import {
3+
Component,
4+
OnInit,
5+
Pipe,
6+
PipeTransform,
7+
Renderer2,
8+
forwardRef,
9+
inject
10+
} from '@angular/core';
11+
import fileSaver from 'file-saver';
12+
import Fuse from 'fuse.js';
13+
import { Subject } from 'rxjs';
14+
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
15+
import { toSignal } from '@angular/core/rxjs-interop';
416

517
import { IgxIconComponent, IgxIconService } from 'igniteui-angular/icon';
618
import { ISelectionEventArgs } from 'igniteui-angular/drop-down';
719
import { IgxSelectComponent, IgxSelectItemComponent } from 'igniteui-angular/select';
8-
import { IgxInputDirective, IgxInputGroupComponent, IgxLabelDirective, IgxPrefixDirective, IgxSuffixDirective } from 'igniteui-angular/input-group';
20+
import {
21+
IgxInputDirective,
22+
IgxInputGroupComponent,
23+
IgxLabelDirective,
24+
IgxPrefixDirective,
25+
IgxSuffixDirective
26+
} from 'igniteui-angular/input-group';
927
import { IgxButtonDirective } from 'igniteui-angular/directives';
1028

1129
import {
@@ -23,13 +41,32 @@ interface ICategoryOption {
2341
selector: 'app-material-icons-extended',
2442
templateUrl: './material-icons-extended.component.html',
2543
styleUrls: ['./material-icons-extended.component.scss'],
26-
imports: [IgxSelectComponent, IgxLabelDirective, IgxSelectItemComponent, IgxInputGroupComponent, IgxInputDirective, IgxPrefixDirective, IgxIconComponent, IgxSuffixDirective, IgxButtonDirective, forwardRef(() => CategoriesFilterPipe), forwardRef(() => FilterByName)]
44+
imports: [
45+
IgxSelectComponent,
46+
IgxLabelDirective,
47+
IgxSelectItemComponent,
48+
IgxInputGroupComponent,
49+
IgxInputDirective,
50+
IgxPrefixDirective,
51+
IgxIconComponent,
52+
IgxSuffixDirective,
53+
IgxButtonDirective,
54+
forwardRef(() => CategoriesFilterPipe), forwardRef(() => FilterByName)
55+
]
2756
})
2857
export class MaterialIconsExtendedComponent implements OnInit {
2958
private iconService = inject(IgxIconService);
30-
private document = inject<Document>(DOCUMENT);
3159
private renderer = inject(Renderer2);
3260

61+
// Search with debounce using signals
62+
private searchInput$ = new Subject<string>();
63+
public searchTerm = toSignal(
64+
this.searchInput$.pipe(
65+
debounceTime(300),
66+
distinctUntilChanged()
67+
),
68+
{ initialValue: '' }
69+
);
3370

3471
public categories: ICategoryOption[] = [
3572
{
@@ -38,6 +75,14 @@ export class MaterialIconsExtendedComponent implements OnInit {
3875
}
3976
];
4077

78+
onSearchInput(value: string) {
79+
this.searchInput$.next(value);
80+
}
81+
82+
clearSearch() {
83+
this.searchInput$.next('');
84+
}
85+
4186
public setCategories() {
4287
const categories = IconCategory.values().map(
4388
(category) =>
@@ -65,6 +110,14 @@ export class MaterialIconsExtendedComponent implements OnInit {
65110
this.selectedCategory = 'all';
66111
}
67112

113+
trackByIcon(_index: number, icon: IMXIcon): string {
114+
return icon.name;
115+
}
116+
117+
trackByCategory(_index: number, group: IIconsGroup): string {
118+
return group.category;
119+
}
120+
68121
addIcons() {
69122
for (const icon of imxIcons) {
70123
this.iconService.addSvgIconFromText(
@@ -80,36 +133,30 @@ export class MaterialIconsExtendedComponent implements OnInit {
80133
fileSaver.saveAs(blob, icon.name);
81134
}
82135

83-
copyValue(event: Event, val: string) {
136+
async copyValue(event: Event, val: string) {
84137
const target = event.currentTarget as HTMLButtonElement;
85138
const element = target.childNodes[0] as HTMLElement;
86-
const tempField = this.renderer.createElement('textarea');
87-
88-
this.renderer.setStyle(tempField, 'position', 'fixed');
89-
this.renderer.setStyle(tempField, 'opacity', '0');
90-
this.renderer.setProperty(tempField, 'value', val);
91-
this.renderer.appendChild(this.document.body, tempField);
92139

93-
tempField.focus();
94-
tempField.select();
95-
96-
this.document.execCommand('copy');
97-
this.renderer.removeChild(this.document.body, tempField);
98-
99-
if (element.innerText !== 'done') {
100-
this.renderer.setProperty(element, 'innerText', 'done');
101-
this.renderer.addClass(
102-
target,
103-
'sample__grid-item-clipboard--success'
104-
);
140+
try {
141+
await navigator.clipboard.writeText(val);
105142

106-
setTimeout(() => {
107-
this.renderer.setProperty(element, 'innerText', 'content_copy');
108-
this.renderer.removeClass(
143+
if (element.innerText !== 'done') {
144+
this.renderer.setProperty(element, 'innerText', 'done');
145+
this.renderer.addClass(
109146
target,
110147
'sample__grid-item-clipboard--success'
111148
);
112-
}, 1500);
149+
150+
setTimeout(() => {
151+
this.renderer.setProperty(element, 'innerText', 'content_copy');
152+
this.renderer.removeClass(
153+
target,
154+
'sample__grid-item-clipboard--success'
155+
);
156+
}, 1500);
157+
}
158+
} catch (err) {
159+
console.error('Failed to copy text: ', err);
113160
}
114161
}
115162

@@ -133,7 +180,11 @@ export class CategoriesFilterPipe implements PipeTransform {
133180
const index = acc.findIndex((group) => group.category === category);
134181

135182
if (index !== -1) {
136-
acc[index].icons.push(icon);
183+
const exists = acc[index].icons.some(existingIcon => existingIcon.name === icon.name);
184+
185+
if (!exists) {
186+
acc[index].icons.push(icon);
187+
}
137188
} else {
138189
acc.push({
139190
category,
@@ -167,17 +218,30 @@ export class CategoriesFilterPipe implements PipeTransform {
167218
name: 'filterByName'
168219
})
169220
export class FilterByName implements PipeTransform {
221+
private fuse: Fuse<IMXIcon> | null = null;
222+
private lastCollection: IMXIcon[] = [];
223+
170224
transform(icons: IMXIcon[], keyword: string): IMXIcon[] {
171-
return icons.filter((icon) => {
172-
const keywords = [...(icon.keywords || []), icon.name];
173-
const index = keywords.indexOf(keyword.toLowerCase());
174-
if (keyword !== '') {
175-
if (index !== -1) {
176-
return keywords;
177-
}
178-
} else {
179-
return icons;
180-
}
181-
});
225+
if (!keyword || keyword.trim() === '') {
226+
return icons;
227+
}
228+
229+
// Initialize Fuse only if collection changed
230+
if (this.lastCollection !== icons) {
231+
this.fuse = new Fuse(icons, {
232+
keys: [
233+
{ name: 'name', weight: 0.7 },
234+
{ name: 'keywords', weight: 0.3 }
235+
],
236+
threshold: 0.3,
237+
distance: 100,
238+
ignoreLocation: true,
239+
minMatchCharLength: 1
240+
});
241+
this.lastCollection = icons;
242+
}
243+
244+
const results = this.fuse!.search(keyword.toLowerCase());
245+
return results.map(result => result.item);
182246
}
183247
}

0 commit comments

Comments
 (0)