Compare commits

...

1 Commits

Author SHA1 Message Date
shamoon ab7a546366 Performance: optimize dropdown filtering 2026-07-07 11:20:54 -07:00
3 changed files with 80 additions and 10 deletions
@@ -61,12 +61,15 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
writeValue(newValue: number[]): void { writeValue(newValue: number[]): void {
this.value = newValue this.value = newValue
} }
registerOnChange(fn: any): void { registerOnChange(fn: any): void {
this.onChange = fn this.onChange = fn
} }
registerOnTouched(fn: any): void { registerOnTouched(fn: any): void {
this.onTouched = fn this.onTouched = fn
} }
setDisabledState?(isDisabled: boolean): void { setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled this.disabled = isDisabled
} }
@@ -74,6 +77,7 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
ngOnInit(): void { ngOnInit(): void {
this.tagService.listAll().subscribe((result) => { this.tagService.listAll().subscribe((result) => {
this.tags = result.results this.tags = result.results
this.tagSearchTextCache.clear()
}) })
} }
@@ -113,15 +117,12 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
tags: Tag[] = [] tags: Tag[] = []
private readonly tagSearchTextCache = new Map<number, string>()
public createTagRef: (name) => void public createTagRef: (name) => void
public searchFn = (term: string, tag: Tag): boolean => public searchFn = (term: string, tag: Tag): boolean =>
matchesSearchText( matchesSearchText(this.getTagSearchText(tag), term)
[this.getParentChain(tag?.id).map((parent) => parent.name), tag?.name]
.flat()
.join(' '),
term
)
getTag(id: number) { getTag(id: number) {
if (this.tags) { if (this.tags) {
@@ -152,8 +153,8 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
private removeChildren(tagIDs: number[], tag: Tag) { private removeChildren(tagIDs: number[], tag: Tag) {
if (tag.children?.length) { if (tag.children?.length) {
const childIDs = tag.children.map((child) => child.id) const childIDs = new Set(tag.children.map((child) => child.id))
tagIDs = tagIDs.filter((id) => !childIDs.includes(id)) tagIDs = tagIDs.filter((id) => !childIDs.has(id))
for (const child of tag.children) { for (const child of tag.children) {
tagIDs = this.removeChildren(tagIDs, child) tagIDs = this.removeChildren(tagIDs, child)
} }
@@ -188,6 +189,7 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
tap((newTag) => { tap((newTag) => {
this.tagService.listAll().subscribe((tags) => { this.tagService.listAll().subscribe((tags) => {
this.tags = tags.results this.tags = tags.results
this.tagSearchTextCache.clear()
add && this.addTag(newTag.id) add && this.addTag(newTag.id)
}) })
}) })
@@ -238,4 +240,22 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
} }
return chain return chain
} }
private getTagSearchText(tag: Tag): string {
if (!tag) {
return ''
}
const cachedSearchText = this.tagSearchTextCache.get(tag.id)
if (cachedSearchText !== undefined) {
return cachedSearchText
}
const searchText = [
...this.getParentChain(tag.id).map((parent) => parent.name),
tag.name,
].join(' ')
this.tagSearchTextCache.set(tag.id, searchText)
return searchText
}
} }
+19
View File
@@ -46,4 +46,23 @@ describe('FilterPipe', () => {
itemsReturned = pipe.transform(items, 'slug', 'slug') itemsReturned = pipe.transform(items, 'slug', 'slug')
expect(itemsReturned).toEqual([items[0]]) expect(itemsReturned).toEqual([items[0]])
}) })
it('should filter matchingmodel items by normalized independent terms', () => {
const pipe = new FilterPipe()
const items: MatchingModel[] = [
{
id: 1,
name: 'Financ\u00e9 Taxes 2026',
slug: 'finance-taxes',
},
{
id: 2,
name: 'Receipt Archive 2026',
slug: 'receipt-archive',
},
]
expect(pipe.transform(items, 'finance 26')).toEqual([items[0]])
expect(pipe.transform(items, 'finance receipt')).toEqual([])
})
}) })
+33 -2
View File
@@ -8,8 +8,39 @@ export type SearchTextValue =
| null | null
| undefined | undefined
const normalizedStringCache = new Map<string, string>()
const searchTermsCache = new Map<string, string[]>()
function normalizeString(value: string): string {
const cachedValue = normalizedStringCache.get(value)
if (cachedValue !== undefined) {
return cachedValue
}
const normalizedValue = normalizeSync(value).toLocaleLowerCase()
normalizedStringCache.set(value, normalizedValue)
return normalizedValue
}
export function normalizeSearchText(value: SearchTextValue): string { export function normalizeSearchText(value: SearchTextValue): string {
return normalizeSync(String(value ?? '')).toLocaleLowerCase() return normalizeString(String(value ?? ''))
}
function getSearchTerms(searchText: SearchTextValue): string[] {
const normalizedSearchText = normalizeSearchText(searchText).trim()
if (!normalizedSearchText) {
return []
}
const cachedTerms = searchTermsCache.get(normalizedSearchText)
if (cachedTerms !== undefined) {
return cachedTerms
}
const searchTerms = normalizedSearchText.split(/\s+/)
searchTermsCache.set(normalizedSearchText, searchTerms)
return searchTerms
} }
export function matchesSearchText( export function matchesSearchText(
@@ -17,7 +48,7 @@ export function matchesSearchText(
searchText: SearchTextValue searchText: SearchTextValue
): boolean { ): boolean {
const normalizedValue = normalizeSearchText(value) const normalizedValue = normalizeSearchText(value)
const searchTerms = normalizeSearchText(searchText).trim().split(/\s+/) const searchTerms = getSearchTerms(searchText)
return searchTerms.every((term) => normalizedValue.includes(term)) return searchTerms.every((term) => normalizedValue.includes(term))
} }