Every listings component renders a built-in layout by default. To use your own HTML
instead, put a <template> with {{ ... }} placeholders inside the component — the SDK
fills it with live listing data and keeps it in sync.
If you don't add a template, the built-in layout is used, so this is fully opt-in.
<rechat-map-listings-grid>
<template rechat-each="listings">
<a class="card" href="/listings/{{ id }}">
<img src="{{ cover_image_url }}" alt="">
<div class="price" data-format="currency">{{ price }}</div>
<div>{{ formatted.street_address.text }}</div>
<span data-format="pluralize:bed">{{ property.bedroom_count }}</span>
</a>
</template>
<template rechat-state="empty"><p>No listings match your search.</p></template>
</rechat-map-listings-grid>
rechat-each="listings" repeats its content once per listing. rechat-state="empty" (or
"loading") shows content only in that state.
Write {{ path }} in text or attributes. Paths are dotted property names only — no
logic, function calls, or expressions. A missing value renders as empty.
<h2>{{ formatted.price.text }}</h2>
<img src="{{ cover_image_url }}">
<a href="/p/{{ id }}">Details</a>
Commonly used fields on a listing:
| Path | Example |
|---|---|
id, status |
…, Active |
price |
1250000 |
cover_image_url |
image URL |
formatted.price.text |
$1,250,000 |
formatted.street_address.text |
123 Main St |
formatted.bedroom_count.value |
3 |
formatted.total_bathroom_count.value |
2.5 |
formatted.square_feet.text_no_label |
2,500 |
property.address.city |
Dallas |
formatted.* fields are ready-to-display strings; use them for prices, counts, and
addresses.
Anywhere under <rechat-listings> the scope also exposes the result totals and the
active sort, so you can render your own results header:
| Path | Example |
|---|---|
total |
2048 |
count |
listings on the current page |
pagination.from / pagination.to |
1 / 20 |
sort.label |
Listed Date |
sort.value |
-list_date |
sort.field / sort.direction |
list_date / DESC |
<div class="results">
<span>{{ total }} results</span>
<span>Sorted by {{ sort.label }}</span>
</div>
Add data-format="name" to an element that wraps a single {{ }} value:
| Formatter | Example |
|---|---|
currency |
1250000 → $1,250,000 |
number |
2500 → 2,500 |
date |
Mar 5, 2024 |
pluralize:bed / pluralize:bath |
3 beds / 1 bath |
uppercase / lowercase |
case change |
<span data-format="currency">{{ price }}</span>
data-if="path" removes an element when the value is empty/false:
<span class="badge" data-if="status">{{ status }}</span>
data-if-number="path" removes an element unless the value is a non-zero number.
Counts in formatted.* are strings, so a listing with no bathrooms gives "0" —
truthy to data-if, which would leave an empty "0 Baths" stat behind. Grouped
digits like "2,921" are read as numbers, and anything non-numeric is removed.
<span data-if-number="formatted.total_bathroom_count.value">
{{ formatted.total_bathroom_count.value }} Baths
</span>
Placeholders are static, so paging through a listing's photos needs a little help.
rechat-gallery="path" marks an element as a gallery over an array of image URLs;
the SDK owns the cursor and wires the arrows for you.
<div rechat-gallery="gallery_image_urls">
<img rechat-gallery-image alt="">
<div rechat-gallery-controls>
<button type="button" rechat-gallery-prev>‹</button>
<button type="button" rechat-gallery-next>›</button>
</div>
</div>
| Attribute | On | Does |
|---|---|---|
rechat-gallery="path" |
container | dotted path to an array of image URLs |
rechat-gallery-image |
<img> |
shows the current photo; hidden when there are none |
rechat-gallery-controls |
container | hidden when there is 1 photo or fewer |
rechat-gallery-prev / -next |
<button> |
steps the photo; each hides at its end of the range |
rechat-gallery-loading |
any | shown while a stepped-to photo loads; give it style="display:none" |
Arrow clicks never bubble into rechat-details, so paging photos inside a clickable
card won't open the details modal. If the card also carries rechat-media-controls
classes, the controls element gets the matching --both / --leftOnly / --rightOnly
modifier as the cursor moves.
rechat-status-color="path" paints an element's background with the same colour the
built-in status badge uses, so a templated badge matches the default card. It reads
the --rechat-theme-status-* variables, falling back to the SDK palette.
<span class="rechat-listing-status-badge" rechat-status-color="status">{{ status }}</span>
| Component | Directive | Renders |
|---|---|---|
<rechat-map-listings-grid> |
rechat-each="listings" |
your card, per listing |
<rechat-listing-details> |
rechat-details (single listing) |
your details-modal body |
<rechat-property-search-form> |
rechat-form |
your search form |
<rechat-filter-*> |
inner <template> |
your filter dropdown (see below) |
In <rechat-listing-details> the single listing is under listing, e.g.
{{ listing.formatted.price.text }}.
<rechat-filter-*> accept a <template> whose controls bind back to the search with
rechat-model:
<rechat-filter-price>
<template>
<button rechat-toggle>{{ label }}</button>
<div data-if="isOpen">
<select rechat-model="minimumPrice">…</select>
<select rechat-model="maximumPrice">…</select>
<button rechat-reset>Clear</button>
</div>
</template>
</rechat-filter-price>
rechat-model="key" two-way binds an <input>/<select> to a filter value.rechat-toggle / rechat-open / rechat-close control the dropdown; rechat-reset
clears the filter.isOpen, label, badge, hasValue (beds/baths also
min / max). Beds additionally exposes isExact and plus — plus is "+"
normally and "" once Exact match is on, so {{ min }}{{ plus }} renders 2+
or 2 for the same segment without any conditional logic.<script>, inline event handlers (onclick), and
style="{{…}}" are not allowed, and javascript:/data: URLs are blocked.id as results change, so the DOM isn't rebuilt on every
update.