@QuerySelector()
is a property decorator which can be used to query for a specific element in the component's
template. It is inspired by the @ViewChild()
and @ContentChild()
decorators in Angular. It accepts a CSS selector as
an argument which will be used to query for the element. This is alternative approach to the getting reference to a DOM
element in the component (see: https://stenciljs.com/docs/templating-jsx#getting-a-reference-to-a-dom-element for more
details). Using this decorator, you are able to apply declarative style of programming when working with DOM elements.
@QuerySelector()
decorator accepts two arguments:
selector
, string, required - CSS selector which will be used to query for the element. It can be any valid CSS selector. Search will start from the component element, or shadow root if component is using shadow DOM and parameteroptions.shadowRoot
is set totrue
.options
,QuerySelectorOptions
, optional, defaultundefined
which means that default options will be used. This is an object with following properties:shadowRoot
, boolean, optional, defaultfalse
- if set totrue
, search will start from shadow root of the component, instead of the component element itself. If component does not use shadow DOM, exception will be thrown.mutationObserver
, boolean, optional, defaultfalse
- if set totrue
, decorator will use MutationObserver to observe changes in the DOM subtree of the component. This is more reliable approach, but it is also more expensive. It should be used when you are querying for element which is projected into the component through<slot>
element, or if subtree of the component is changed by some other means, not via Stencil's reactivity. By default, decorator will monitor execution of therender()
function of the component and will query for the element after each execution.
@QuerySelector()
decorator must be used on any property of the component, and type of the property is Observable
from rxjs
library. Observable will be used to emit reference to queried element, or null
if element does not exist.
Note that you should not set initial value for the property, decorator will do that for you. Example:
class MyCmp {
@QuerySelector('div')
private element$: Observable<HTMLElement | null>;
}
After each execution of the render()
function of the component (or mutation of DOM subtree), decorator will query for
the element and emit its reference, if it can be found, or null
. This value will be emitted only if it is different
from the previous one and on next micro-task (i.e. after Promise.resolve()
).
A fair warning: in theory, you are able to create infinite loop without noticing because value is emitted on next micro-task, main browser thread will not be blocked.
Let's say that your component has a template which consist of an input field for search and a list of results. You want for user to be able to type term in the input field and to see results in the list. However, you would like to debounce search as well as not to send same search term twice. Imperative approach would require a lot of code for this task, however, with RxJS this can be done in a very elegant way.
import {
Component,
ComponentInterface,
State,
h,
} from '@stencil/core';
import {
debounceTime,
distinctUntilChanged,
from,
fromEvent,
map,
Observable,
switchMap,
} from 'rxjs';
import { fromFetch } from 'rxjs/internal/observable/dom/fetch';
import { untilDisconnected } from '@runopencode/rx-stencil';
@Component({
tag: 'app-search',
shadow: true,
})
class AppSearch implements ComponentInterface {
@QuerySelector('input[type="search"]', true)
private readonly input$: Observable<HTMLInputElement>;
@State()
private result: string[] = [];
public connectedCallback(): void {
this.input$.pipe(
// when input is ready, we can start listening for `input` events
switchMap((input: HTMLInputElement): Observable<string> => {
return fromEvent(input, 'input').pipe(
map((): string => input.value.trim()),
);
}),
// debounce input events to avoid sending too many requests
// and filter out same search terms sent in a row
debounceTime(300),
distinctUntilChanged(),
// send search request
switchMap((term: string): Observable<string[]> => {
return fromFetch(`https://api.example.com/search?term=${term}`).pipe(
switchMap((response: Response): Observable<string[]> => response.json()),
);
}),
// unsubscribe when component is disconnected
untilDisconnected(this),
).subscribe((result: string[]): void => {
this.result = result;
});
}
public render(): any {
return (
<Host>
<input type='search' />
{this.result.map((item): any => <div>{item}</div>)}
</Host>
);
}
}