|
7 | 7 | */ |
8 | 8 |
|
9 | 9 | import { |
10 | | - AfterViewInit, |
11 | 10 | ChangeDetectionStrategy, |
12 | 11 | Component, |
13 | | - DestroyRef, |
14 | 12 | ElementRef, |
15 | | - EventEmitter, |
16 | | - NgZone, |
| 13 | + Injector, |
17 | 14 | OnDestroy, |
18 | | - OnInit, |
19 | | - Output, |
20 | | - QueryList, |
21 | | - ViewChild, |
22 | | - ViewChildren, |
| 15 | + Signal, |
| 16 | + afterNextRender, |
| 17 | + effect, |
23 | 18 | inject, |
| 19 | + output, |
| 20 | + viewChild, |
| 21 | + viewChildren, |
24 | 22 | } from '@angular/core'; |
25 | 23 |
|
26 | 24 | import {WINDOW} from '../../providers/index'; |
@@ -53,95 +51,82 @@ import {RelativeLink} from '../../pipes/relative-link.pipe'; |
53 | 51 | templateUrl: './search-dialog.component.html', |
54 | 52 | styleUrls: ['./search-dialog.component.scss'], |
55 | 53 | }) |
56 | | -export class SearchDialog implements OnInit, AfterViewInit, OnDestroy { |
57 | | - @Output() onClose = new EventEmitter<void>(); |
58 | | - @ViewChild('searchDialog') dialog?: ElementRef<HTMLDialogElement>; |
59 | | - @ViewChildren(SearchItem) items?: QueryList<SearchItem>; |
| 54 | +export class SearchDialog implements OnDestroy { |
| 55 | + onClose = output(); |
| 56 | + dialog = viewChild.required('searchDialog', {read: ElementRef}) as Signal< |
| 57 | + ElementRef<HTMLDialogElement> |
| 58 | + >; |
| 59 | + items = viewChildren(SearchItem); |
60 | 60 |
|
61 | | - private readonly destroyRef = inject(DestroyRef); |
62 | | - private readonly ngZone = inject(NgZone); |
63 | 61 | private readonly search = inject(Search); |
64 | 62 | private readonly relativeLink = new RelativeLink(); |
65 | 63 | private readonly router = inject(Router); |
66 | 64 | private readonly window = inject(WINDOW); |
67 | | - |
68 | | - private keyManager?: ActiveDescendantKeyManager<SearchItem>; |
| 65 | + private readonly injector = inject(Injector); |
| 66 | + private readonly keyManager = new ActiveDescendantKeyManager( |
| 67 | + this.items, |
| 68 | + this.injector, |
| 69 | + ).withWrap(); |
69 | 70 |
|
70 | 71 | searchQuery = this.search.searchQuery; |
71 | 72 | searchResults = this.search.searchResults; |
72 | 73 |
|
73 | | - ngOnInit(): void { |
74 | | - this.ngZone.runOutsideAngular(() => { |
75 | | - fromEvent<KeyboardEvent>(this.window, 'keydown') |
76 | | - .pipe( |
77 | | - filter((_) => !!this.keyManager), |
78 | | - takeUntilDestroyed(this.destroyRef), |
79 | | - ) |
80 | | - .subscribe((event) => { |
81 | | - // When user presses Enter we can navigate to currently selected item in the search result list. |
82 | | - if (event.key === 'Enter') { |
83 | | - this.navigateToTheActiveItem(); |
84 | | - } else { |
85 | | - this.ngZone.run(() => { |
86 | | - this.keyManager?.onKeydown(event); |
87 | | - }); |
88 | | - } |
89 | | - }); |
| 74 | + constructor() { |
| 75 | + effect(() => { |
| 76 | + this.items(); |
| 77 | + afterNextRender( |
| 78 | + { |
| 79 | + write: () => this.keyManager.setFirstItemActive(), |
| 80 | + }, |
| 81 | + {injector: this.injector}, |
| 82 | + ); |
90 | 83 | }); |
91 | | - } |
92 | | - |
93 | | - ngAfterViewInit() { |
94 | | - if (!this.dialog?.nativeElement.open) { |
95 | | - this.dialog?.nativeElement.showModal?.(); |
96 | | - } |
97 | 84 |
|
98 | | - if (!this.items) { |
99 | | - return; |
100 | | - } |
| 85 | + this.keyManager.change.pipe(takeUntilDestroyed()).subscribe(() => { |
| 86 | + this.keyManager.activeItem?.scrollIntoView(); |
| 87 | + }); |
101 | 88 |
|
102 | | - this.keyManager = new ActiveDescendantKeyManager(this.items).withWrap(); |
103 | | - this.keyManager?.setFirstItemActive(); |
| 89 | + afterNextRender({ |
| 90 | + write: () => { |
| 91 | + if (!this.dialog().nativeElement.open) { |
| 92 | + this.dialog().nativeElement.showModal?.(); |
| 93 | + } |
| 94 | + }, |
| 95 | + }); |
104 | 96 |
|
105 | | - this.updateActiveItemWhenResultsChanged(); |
106 | | - this.scrollToActiveItem(); |
| 97 | + fromEvent<KeyboardEvent>(this.window, 'keydown') |
| 98 | + .pipe(takeUntilDestroyed()) |
| 99 | + .subscribe((event) => { |
| 100 | + // When user presses Enter we can navigate to currently selected item in the search result list. |
| 101 | + if (event.key === 'Enter') { |
| 102 | + this.navigateToTheActiveItem(); |
| 103 | + } else { |
| 104 | + this.keyManager.onKeydown(event); |
| 105 | + } |
| 106 | + }); |
107 | 107 | } |
108 | 108 |
|
109 | 109 | ngOnDestroy(): void { |
110 | | - this.keyManager?.destroy(); |
| 110 | + this.keyManager.destroy(); |
111 | 111 | } |
112 | 112 |
|
113 | 113 | closeSearchDialog() { |
114 | | - this.dialog?.nativeElement.close(); |
115 | | - this.onClose.next(); |
| 114 | + this.dialog().nativeElement.close(); |
| 115 | + this.onClose.emit(); |
116 | 116 | } |
117 | 117 |
|
118 | 118 | updateSearchQuery(query: string) { |
119 | 119 | this.search.updateSearchQuery(query); |
120 | 120 | } |
121 | 121 |
|
122 | | - private updateActiveItemWhenResultsChanged(): void { |
123 | | - this.items?.changes.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { |
124 | | - // Change detection should be run before execute `setFirstItemActive`. |
125 | | - Promise.resolve().then(() => { |
126 | | - this.keyManager?.setFirstItemActive(); |
127 | | - }); |
128 | | - }); |
129 | | - } |
130 | | - |
131 | 122 | private navigateToTheActiveItem(): void { |
132 | | - const activeItemLink: string | undefined = this.keyManager?.activeItem?.item?.url; |
| 123 | + const activeItemLink: string | undefined = this.keyManager.activeItem?.item?.url; |
133 | 124 |
|
134 | 125 | if (!activeItemLink) { |
135 | 126 | return; |
136 | 127 | } |
137 | 128 |
|
138 | 129 | this.router.navigateByUrl(this.relativeLink.transform(activeItemLink)); |
139 | | - this.onClose.next(); |
140 | | - } |
141 | | - |
142 | | - private scrollToActiveItem(): void { |
143 | | - this.keyManager?.change.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { |
144 | | - this.keyManager?.activeItem?.scrollIntoView(); |
145 | | - }); |
| 130 | + this.onClose.emit(); |
146 | 131 | } |
147 | 132 | } |
0 commit comments