card-list

Template-based list rendering with safe data binding. Fetch or inline JSON, bind to templates, and render lists with grid, stack, or reel layouts.

Overview

The <card-list> component renders a list of items from JSON data using an HTML <template>. Data binding uses data-field attributes — only safe property paths are allowed, no expressions or method calls.

Supply data inline via data-items or fetch from a URL via src. Choose a layout with data-layout: grid, stack, or reel.

<card-list data-layout="grid" data-items='[ {"id": 1, "name": "Headphones", "price": "$249", "inStock": true}, {"id": 2, "name": "Keyboard", "price": "$159", "inStock": true}, {"id": 3, "name": "Monitor", "price": "$449", "inStock": false} ]' data-key="id"> <template> <layout-card> <header><h3 data-field="name"></h3></header> <section> <p data-field="price"></p> </section> <footer> <button data-field-if="inStock">Add to Cart</button> <span data-field-unless="inStock">Out of Stock</span> </footer> </layout-card> </template> </card-list>

Attributes

Attribute Type Default Description
src URL URL to fetch JSON data from. Expects an array or an object with items or data property.
data-items JSON string Inline JSON array of items. Takes priority over src.
data-key string id Property name to use as the unique key for each item.
data-layout string block Layout mode: grid (auto-fit columns), stack (vertical), or reel (horizontal scroll).

Binding Attributes

These go on elements inside the <template> to bind item data.

Attribute Description
data-field="path" Set the element's text content from a property path (e.g. user.name).
data-field-attr="href: url, title: name" Set one or more HTML attributes from property paths. Comma-separated pairs of attribute: path.
data-field-html="path" Set innerHTML from a property path. Content is sanitized (scripts, iframes, and event handlers are stripped).
data-field-if="path" Keep element only if the property value is truthy. Removed from DOM otherwise.
data-field-unless="path" Keep element only if the property value is falsy. Removed from DOM otherwise.

Data Sources

Inline Data

Pass a JSON array directly in the data-items attribute. Good for small, static datasets.

Remote Data

Set src to fetch JSON from a URL. The component shows a loading state while fetching and an error state on failure.

<card-list src="/api/products.json" data-layout="grid" data-key="id"> <template> <layout-card> <header><h3 data-field="name"></h3></header> <section><p data-field="description"></p></section> </layout-card> </template> </card-list>

The response can be a plain array, or an object with an items or data property containing the array.

Nested Properties

Access nested data with dot notation. Array indices are also supported: items[0].title.

<card-list data-layout="stack" data-items='[ {"id": 1, "user": {"name": "Alice", "email": "alice@example.com"}}, {"id": 2, "user": {"name": "Bob", "email": "bob@example.com"}} ]' data-key="id"> <template> <layout-card data-variant="outlined"> <layout-cluster data-layout-justify="between" data-layout-align="center"> <strong data-field="user.name"></strong> <small data-field="user.email"></small> </layout-cluster> </layout-card> </template> </card-list>

Attribute Binding

Use data-field-attr to set HTML attributes from item properties. Comma-separated pairs map attributes to property paths.

<card-list data-layout="grid" data-items='[ {"id": 1, "title": "Getting Started", "url": "#start", "desc": "Learn the basics."}, {"id": 2, "title": "API Reference", "url": "#api", "desc": "Full documentation."} ]' data-key="id"> <template> <layout-card data-variant="outlined"> <h3><a data-field="title" data-field-attr="href: url, title: desc"></a></h3> <p data-field="desc"></p> </layout-card> </template> </card-list>

Conditional Rendering

Use data-field-if and data-field-unless to conditionally include or exclude elements based on item data. Elements are removed from the DOM, not hidden.

<card-list data-layout="grid" data-items='[ {"id": 1, "name": "Free", "price": "$0/mo", "featured": false}, {"id": 2, "name": "Pro", "price": "$29/mo", "featured": true} ]' data-key="id"> <template> <layout-card> <header> <h3 data-field="name"></h3> <layout-badge data-field-if="featured">Popular</layout-badge> </header> <section><p data-field="price"></p></section> <footer> <button class="primary" data-field-if="featured">Get Started</button> <button data-field-unless="featured">Learn More</button> </footer> </layout-card> </template> </card-list>

Rich HTML Content

Use data-field-html to render HTML content from a property. Content is sanitized — <script>, <iframe>, <object>, <embed>, <form>, event handlers, and javascript: URIs are all stripped.

<card-list data-items='[ {"id": 1, "title": "Bold", "content": "<strong>Rich</strong> HTML content"} ]' data-key="id"> <template> <article> <h3 data-field="title"></h3> <div data-field-html="content"></div> </article> </template> </card-list>

Layout Modes

Value Display Description
grid CSS Grid Auto-fit columns with minmax(280px, 1fr). Responsive by default.
stack Flex column Vertical stack with gap.
reel Flex row Horizontal scroll with snap points.

States

Attribute When Visual
data-loading During src fetch Reduced opacity with "Loading..." message.
data-error Fetch failure Error message banner with the HTTP status.

Events

Event Detail Description
card-list:rendered { count: number } Fired after items are rendered. Bubbles.
const list = document.querySelector('card-list'); list.addEventListener('card-list:rendered', (e) => { console.log(`Rendered ${e.detail.count} items`); });

JavaScript API

Method / Property Description
setItems(items) Replace the item list and re-render.
getItems() Returns a shallow copy of the current items array.
const list = document.querySelector('card-list'); // Set items programmatically list.setItems([ { id: 1, name: 'Item A' }, { id: 2, name: 'Item B' }, ]); // Read current items const items = list.getItems(); // returns a copy

Security

All data-field paths are validated against a strict regex. Only simple property access is allowed:

  • Allowed: name, user.email, items[0].title
  • Rejected: price.toFixed(2), `${price}`, price * 1.1, stock > 0 ? 'yes' : 'no'

Invalid paths log a console warning and resolve to empty content.

Progressive Enhancement

Without JavaScript, the <template> is hidden by default. Consider providing a <noscript> fallback or static content before the template for no-JS contexts.

Related