Tengo una directiva que está en el nivel superior de la aplicación y consulta elementos a través de document.querySelector y los entrega a fromEvent . El problema es que cuando se ejecuta el código en ngAfterViewInit de la directiva, el DOM aún no se representa por completo, por lo que la consulta del elemento devuelve un valor nulo.
Podría ejecutar el código en setTimeout con una cantidad de tiempo arbitraria, pero eso no parece una solución sostenible y flexible.
¿Alguna idea de cómo se puede manejar esto con elegancia?
En este momento, mi código parece una variación de esto:
ngAfterViewInit() { const trackedEl = document.querySelector('#myInput'); const input$ = fromEvent(trackedEl, 'input') .pipe( map((inputEvent: any) => inputEvent.target.value) ) .subscribe((v) => { console.log(`Got to the observable:: ${v}`, count++); }) }Creo que su mejor enfoque aquí sería usar algo como ViewChild .
Entonces, suponiendo que su querySelector apunta a una etiqueta <input> , su html sería como (observe el decorador #myInput):
<input #myInput ....>Su archivo .ts sería:
@ViewChild('myInput', { static: true }) input?: ElementRef<HTMLElement>;Entonces porque solo queremos resolver el evento cuando está suscrito a:
const input$ = defer(() => fromEvent(this.input, 'input').pipe( map((inputEvent: any) => inputEvent.target.value) ) .subscribe((v) => { console.log(`Got to the observable:: ${v}`, count++); }); Algo en este sentido, pero ciertamente ViewChild es clave aquí.
El gancho de ciclo de vida ngAfterViewInit que angular llama después de crear todas las vistas secundarias del componente. Por lo tanto, debe implementarlo en el AppComponent raíz. Luego mantenga un observable en un servicio o estado y simplemente suscríbase a él en cualquier lugar que desee verificar.
Componente raíz de la aplicación:
export class AppComponent implements AfterViewInit { title = "Angular App"; constructor(private data: DataService){} ngAfterViewInit() { this.data.rendered.next(true); } }suscribiéndolo en cualquier lugar:
export class HighlightDirective { constructor(private data: DataService) { this.data.rendered.subscribe(rendered => { // all components rendered here, write you code }); } }Creo que esto podría ser un enfoque:
ngAfterViewInit() { defer(() => fromEvent(document.querySelector('#myInput'), 'input')) .pipe( subscribeOn(asyncScheduler), map((inputEvent: any) => inputEvent.target.value) ) .subscribe((v) => { console.log(`Got to the observable:: ${v}`, count++); }) } Este enfoque sigue recurriendo a algo como setTimeout , pero de una manera más elegante . Esto se logra usando defer + subscribeOn .
Al proporcionar asyncScheduler para subscribeOn , obtenemos una funcionalidad similar a setTimeout . El operador defer se ha utilizado porque queremos evaluar esa función que consulta el DOM cuando la fuente está suscrita, y recordar que el tiempo de suscripción es retrasado por el operador subscribeOn .
Entonces, en el momento en que se suscribe la fuente, el DOM debería estar listo para ser consultado.
Sé que esto es solo una solución para setTimeout , pero pensé que se vería más elegante con algunos operadores RxJS interesantes.
Terminé encontrando la solución que estaba buscando.
Los elementos secundarios no se procesaron a tiempo probablemente porque estaban respondiendo a algún evento http pendiente que aún no había devuelto una respuesta. Después de leer la documentación para la detección de cambios y ngZone , parecía que los eventos destacados de http cuentan como "microtareas".
El siguiente código es lo que funcionó para mí.
ngOnInit() { this.ngZone.onMicrotaskEmpty.pipe(take(1)).subscribe((a) => { this.initiateElementEventListeners(); }); }