@query

The @query decorator provides a convenient way to query (and optionally cache) references to elements within your host DOM. It creates a getter that performs the query on every access by default, or once and caches the result if you pass cache: true. The default is uncached in both standard and legacy decorator mode.

It works on both RadiantElement and RadiantController.

Usage

import { RadiantElement, customElement, query } from '@ecopages/radiant';
 
@customElement('user-profile')
export class UserProfile extends RadiantElement {
	// Query by data-ref (recommended)
	@query({ ref: 'avatar' }) avatar!: HTMLImageElement | null;
	
	// Query by CSS selector
	@query({ selector: 'button.action' }) submitButton!: HTMLButtonElement | null;
	
	// Query all matching elements
	@query({ ref: 'item', all: true }) items!: HTMLElement[];
}

Parameters

ParameterTypeRequiredDescription
selectorstringOne of selector/refCSS selector to match elements.
refstringOne of selector/refValue of data-ref attribute to match.
alltrue or falseNoQuery for all matching elements (default: false). Must be a literal, because it picks the field type.
cachebooleanNoCache the query result (default: false). A query that finds nothing is not cached: it runs again on the next read.

Feature Highlight

Caching Behavior

By default, queries are not cached — each access re-runs the lookup. For a stable reference that never needs to be re-resolved, pass cache: true so repeated access skips the query. A result of null, or an empty array with all: true, is not kept, so the query runs again until it finds something.

@query({ ref: 'stable-el', cache: true })
stableEl!: HTMLElement | null;

Type Safety

A single-element query is T | null and an all: true query is T[], empty when nothing matches. Under standard decorators, any other field type does not compile: see Typing Decorated Fields.

@query({ ref: 'avatar' }) avatar!: HTMLImageElement | null;
@query({ ref: 'item', all: true }) items!: HTMLElement[];
 
focusAvatar() {
	this.avatar?.focus();
}

@query vs JSX Bindings

@query is for reading DOM you do not directly control from render() — content projected into a slot by a consumer, DOM enhanced by a RadiantController, or a live Element handle needed by a third-party imperative API (positioning libraries, focus management, observers).

If the host does not override render(), copy the field with @bindTo instead of querying a node only to write into it.

If the element is one your own render() JSX produces, prefer a binding instead of querying it back out to write into it:

// Avoid — re-fetching a host-rendered node just to write into it
@query({ ref: 'count' }) countEl!: HTMLElement | null;
 
@onUpdated('count')
syncCount() {
  if (this.countEl) this.countEl.textContent = String(this.count);
}
 
override render() {
  return <span data-ref="count"></span>;
}
// Prefer — bind the value directly at the JSX position
override render() {
  return <span>{this.$.count}</span>;
}

The binding patches just that text node when count changes; the query-and-write version does the same job with more code, an extra DOM read, and no benefit. Reach for @query when there's no reactive value to bind — you need the node itself, not a value at that position.

Learn More

  • Use @query({ all: true }) when you want every matching element instead of only the first one.
  • @querySlot - Query projected elements assigned to a render-owning RadiantElement slot.
  • RadiantElement - Learn about getRef and other utility methods.