logo dile-components dile-components

dile-crud-list

The dile-crud-list component is designed to list existing elements in a REST API resource. It is a sophisticated component with multiple customization options to adapt to various needs and web service configurations.

The component allows you to create lists of resource elements, with or without pagination, provide various controls on the items, such as edit and delete buttons, as well as apply filters, different sorting options, and render using either traditional list items or a full DataGrid component.

Installation

npm i @dile/crud

Usage

Import the dile-crud-list component.

import '@dile/crud/components/list/crud-list.js';

Use the component.

<dile-crud-list
  .config=${this.config}
></dile-crud-list>

Properties

Methods

Events

CSS Custom Properties

You can customize it using CSS Custom Properties.

Custom property Description Default
--dile-crud-list-summary-background-color Background color of the summary section var(--dile-primary-light-color, #999)
--dile-crud-list-summary-color Text color of the summary section var(--dile-on-primary-light-color, #fff)
--dile-crud-list-summary-font-size Font size of the summary section 0.8rem
--dile-crud-list-summary-border-bottom Border bottom of the summary section 1px solid #ccc
--dile-crud-list-summary-padding Padding of the summary section 0.4rem 1rem
--dile-crud-list-pagination-margin-top Top margin of the pagination section 1rem
--dile-crud-filters-list-margin Margin of the filters list container 0 0.5rem
--dile-crud-filters-list-gap Gap between filter items 0.5rem
--dile-crud-list-filters-list-margin Margin of the filters list inside dile-crud-list 0
--dile-crud-list-empty-padding Padding of the empty list message 3rem 1rem
--dile-crud-list-empty-text-align Text alignment of the empty list message center
--dile-crud-list-elements-container-template-columns CSS grid template columns for the elements container 1fr
--dile-crud-list-elements-container-gap Gap between elements in the container 0
--dile-icon-color Icon color in pagination navigation buttons var(--dile-on-secondary-color, #fff)
--dile-icon-rounded-background-color Background color of pagination navigation buttons var(--dile-secondary-color, #2962FF)
--dile-on-secondary-dark-color Icon color on hover for pagination navigation buttons #1942DF
--dile-secondary-dark-color Background color on hover for pagination navigation buttons #fff
--dile-crud-pagination-nav-button-disabled-icon-color Icon color when pagination button is disabled #f5f5f5
--dile-crud-pagination-nav-button-disabled-background-color Background color when pagination button is disabled #ddd
--dile-crud-select-all-checkbox-color Checkbox icon color in select all button #303030
--dile-crud-select-all-checkbox-size Checkbox icon size in select all 1.5rem
--dile-crud-select-all-overlay-checkbox-color Checkbox icon color in select all overlay menu #303030
--dile-crud-data-grid-margin-top Data grid component margin top 0.25rem

To customize the appearance of the listings, check other CRUD system components such as dile-list-item or dile-chip.

Configuration

This component requires a configuration object with numerous properties, methods, and templates to customize its behavior, functionality, and appearance.

Complete information on how to configure it easily can be found on the general CRUD guides page. Also, you can find implementation examples below.

To simplify the use of CRUD components, the configuration object required is the same for all the various CRUD components, such as lists, fully functional CRUD pages, or detail pages for a record.

Rendering Modes

dile-crud-list decides how to render each element with the following logic:

  1. Is a grid configured? A grid is considered configured when either config.templates.grid (a function) or config.grid.columns (a declarative array) is set. If neither is set, the list always renders each element via config.templates.item, exactly as described above under Usage and Configuration.
  2. Is config.templates.item also explicitly configured? If a grid is configured but no explicit item template is, the grid always renders — there's nothing to switch between. Within the grid itself, config.templates.grid wins over config.grid.columns when both happen to be set.
  3. Both configured? When a grid and an explicit config.templates.item are both present, the viewMode property decides which one renders: 'item' (the default) shows the item template, 'grid' shows the grid (again preferring config.templates.grid over config.grid.columns). dile-crud drives viewMode automatically with a toggle button — see List/Grid View Switch — but it's a plain reactive property, so you can also set it directly if you're using dile-crud-list on its own.

For most tabular listings, you don't need to write any DataGrid component yourself: configure Option A below. Only reach for Option B when the declarative columns aren't expressive enough for what you need.

Option A (Recommended): Declarative Data Grid via config.grid

Setting config.grid.columns renders the built-in <dile-crud-data-grid> component for you. It automatically builds an Actions column (using DileCrudItemActions.hasActions(config) and <dile-crud-item-actions> internally — see dile-crud-item-actions) and forwards grid-level options to the underlying @dile/ui <dile-data-grid> primitive. No custom component is required.

Column definition reference

Each entry in config.grid.columns accepts:

Property Type Description
field String Property name read from each row when no render is given.
header String Column header text. Falls back to field if omitted.
width String (CSS value) Sets a fixed column width, e.g. '110px'.
align 'left' | 'center' | 'right' Cell/header text alignment. Default 'left'.
sortable Boolean Enables the clickable sort header and sort icon for this column.
render(item, rowIndex, column) Function Custom cell content; overrides the field lookup.
hideOnCard Boolean Hides this column in the responsive card layout.
hideCardLabel Boolean Hides the auto-generated field label in card layout.
sticky true | 'left' | 'right' Pins the column.

Only mark a column sortable if the backend can actually order by it. dile-crud-data-grid always sorts externally (see Sort Mode below) — clicking the header just asks the backend to sort by field, with no client-side fallback. If the backend can't honor it (e.g. field is resolved from a joined/derived table), the request still goes out, and whatever order the backend falls back to will look like an unrelated re-shuffle of the list rather than a real sort. If you also expose sort.options via dile-crud-sort-form, keep both lists in sync: a sortable grid column whose field isn't in sort.options is silently ignored by the sort-form sync described below, rather than erroring — see Shared Behavior.

Grid-level options reference

All of the following live under config.grid:

Property Type Default Description
columns Array — Required to activate this rendering mode.
stickyFirstColumn Boolean true Pins the first column while scrolling horizontally.
striped Boolean true Alternates row background colors.
responsiveMode 'auto' | 'cards' | 'scroll' 'auto' Controls how the grid degrades on small containers.
emptyMessage String 'No data available' Message shown when there are no rows.
selectable Boolean !config.customization.hideCheckboxSelection Overrides the global checkbox-selection setting for this grid specifically.
hideActionsColumn Boolean false Suppresses the automatic Actions column entirely.
actionsColumnHeader String config.labels.actionsHeader or 'Actions' Header text for the auto-injected Actions column.
actionsColumnWidth String '110px' Width of the auto-injected Actions column.
actionsColumnAlign 'left' | 'center' | 'right' 'right' Alignment of the auto-injected Actions column.
actionsColumnSticky true | 'left' | 'right' — Pins the auto-injected Actions column, same semantics as a column's sticky property above.

Example

This is the configuration used by the Countries DataGrid demo:

grid: {
  columns: [
    {
      field: 'id',
      header: 'ID',
      width: '70px',
      align: 'center',
      sortable: true,
    },
    {
      field: 'name',
      header: 'Name',
      sortable: true,
    },
    {
      field: 'continent',
      header: 'Continent',
      sortable: true,
      render: (country) => html`
        <span style="display: inline-block; padding: 0.2rem 0.5rem; border-radius: 12px; background-color: var(--dile-primary-light-color, #e0f2fe); color: var(--dile-primary-dark-color, #0369a1); font-size: 0.8rem; font-weight: 600;">
          ${country.continent || 'N/A'}
        </span>
      `,
    },
  ],
  stickyFirstColumn: true,
  striped: true,
},

No templates.grid function and no custom component are needed — <dile-crud-data-grid> and its automatic Actions column handle the rest.

Option B (Escape Hatch): Fully Custom Component via config.templates.grid

Use this only when config.grid.columns isn't expressive enough — for example, if you need full control over the grid wrapper, multiple grids on the same page, non-<dile-data-grid> markup, or client-side sorting (see Sort Mode below). Instead of rendering individual items via templates.item, you render the entire listing yourself by configuring templates.grid(elements, actionIds, config, sort):

templates: {
  grid: (elements, actionIds, config, sort) => html`
    <customers-data-grid
      .items=${elements}
      .selectedIds=${actionIds}
      .config=${config}
      .sort=${sort}
    ></customers-data-grid>
  `,
}

When templates.grid is defined:

  1. DileCrudList delegates the rendering of all rows and selection check states to your DataGrid component.
  2. The config object is passed as the third parameter to the template, allowing your DataGrid (and its internal <dile-crud-item-actions .item=${item} .config=${this.config}>) to automatically respect global and per-item edit/delete/restore permissions without manual configuration.
  3. Clicking a column header emits dile-data-grid-sort, which is automatically captured by DileCrudList to trigger backend sorting via setSort({ sortField, sortDirection }).
  4. Checkbox changes emit item-checkbox-changed, synchronizing actionIds across both the list and batch actions toolbar.
  5. The sort object ({ sortField, sortDirection }, or null before any sort is applied) is passed as the fourth parameter, reflecting the sort currently confirmed by the backend. You must forward it back into <dile-data-grid> as .sortField/.sortDirection (see the example below) when using sort-mode="external". DileCrudList re-renders its content behind a loading indicator on every sort request, which unmounts and remounts your grid component — without feeding sort back in, the grid loses track of the current column/direction on every round-trip, the sort icon disappears, and clicking the same header again restarts at ascending instead of toggling to descending.

Unlike config.grid.columns, this path does not auto-inject an Actions column — you are responsible for adding one yourself using DileCrudItemActions.hasActions(config) and <dile-crud-item-actions>, exactly as shown in the example below and as documented in dile-crud-item-actions.

Implementing a CRUD DataGrid Component

Here is a complete, real-world example of a custom DataGrid component tailored for a CRUD resource. Notice how:

import { LitElement, html, css } from 'lit';
import '@dile/ui/components/data-grid/data-grid.js';
import { DileCrudItemActions } from '@dile/crud/components/list/src/DileCrudItemActions.js';
import '@dile/crud/components/list/crud-item-actions.js';

export class CustomersDataGrid extends LitElement {
  static styles = [
    css`
      :host {
        display: block;
      }
      dile-data-grid::part(country-badge) {
        display: inline-block;
        padding: 0.2rem 0.5rem;
        border-radius: 12px;
        background-color: var(--dile-primary-light-color, #e0f2fe);
        color: var(--dile-primary-dark-color, #0369a1);
        font-size: 0.8rem;
        font-weight: 600;
      }
    `
  ];

  static get properties() {
    return {
      items: { type: Array },
      selectedIds: { type: Array },
      config: { type: Object },
      sort: { type: Object },
    };
  }

  constructor() {
    super();
    this.items = [];
    this.selectedIds = [];
    this.config = null;
    this.sort = null;
  }

  get columns() {
    const cols = [
      {
        field: 'id',
        header: 'ID',
        width: '70px',
        align: 'center',
        sortable: true,
      },
      {
        field: 'name',
        header: 'Customer Name',
        sortable: true,
      },
      {
        field: 'country',
        header: 'Country',
        sortable: true,
        render: (customer) => html`
          <span part="country-badge">${customer.country || 'N/A'}</span>
        `,
      },
    ];

    // Conditionally include Actions column only if actions are enabled
    if (DileCrudItemActions.hasActions(this.config)) {
      cols.push({
        header: 'Actions',
        align: 'right',
        width: '110px',
        hideCardLabel: true,
        render: (customer) => html`
          <dile-crud-item-actions
            .item=${customer}
            .config=${this.config}
          ></dile-crud-item-actions>
        `,
      });
    }

    return cols;
  }

  render() {
    return html`
      <dile-data-grid
        .items=${this.items}
        .columns=${this.columns}
        .selectedIds=${this.selectedIds}
        .sortField=${this.sort?.sortField || ''}
        .sortDirection=${this.sort?.sortDirection || ''}
        ?selectable=${!this.config?.customization?.hideCheckboxSelection}
        sticky-first-column
        striped
        sort-mode="external"
      ></dile-data-grid>
    `;
  }
}

customElements.define('customers-data-grid', CustomersDataGrid);

Shared Behavior Between Both Grid Modes

Both Option A and Option B render inside the same .grid-container wrapper inside dile-crud-list, which listens for item-checkbox-changed (syncing actionIds/selection) and dile-data-grid-sort (calling setSort({ sortField, sortDirection })). Selection sync and server-driven sort behave identically regardless of which option you use.

Every call to setSort() — whether triggered by a grid header click or by any other sort control — also makes dile-crud-list dispatch a bubbling crud-list-sort-changed event (detail: { sortField, sortDirection }). Inside dile-crud, this keeps the dile-crud-sort-form control (the sort dropdown in the toolbar) in sync: sorting from the grid updates which option the form shows as selected, and vice versa. If the incoming sortField isn't one of config.sort.options — for example a grid column that's sortable but has no equivalent entry in sort.options — dile-crud leaves the form's current selection untouched rather than pointing it at an option it has no control for.

Sort Mode: One Real Capability Difference

config.grid.columns always uses sort-mode="external" on the underlying <dile-data-grid>, meaning every column sort click triggers a new server request via setSort. A hand-written templates.grid component (Option B) can instead set sort-mode="client" to sort a small/unpaginated dataset entirely client-side, without hitting the server — something the declarative path cannot currently do.

Unpaginated List Example

Create configuration object for a resource

Before looking at implementation examples of lists, it’s important to understand how to build configuration objects, as they are absolutely necessary for centrally providing the parameters needed to customize functionality.

You can learn how to create configuration objects on the page that explains the resource configuration object.

<script type="module">
import { html } from 'lit';
import { CrudConfigBuilder } from '@dile/crud/lib/CrudConfigBuilder';
import '@dile/ui/components/pages/pages';

// For the correct functioning of this declaration in the demo system, we have defined the variable with the configuration object globally. Normally, it would be created in a module and exported.
window.countryConfig = new CrudConfigBuilder('https://timer.escuelait.com/api/countries', {
  templates: {
    item: (country) => html`<demo-country-item .country=${country}></demo-country-item>`,
    insertForm: () => html`<demo-country-form id="insertform"></demo-country-form>`,
    updateForm: () => html`<demo-country-form id="updateform"></demo-country-form>`,
    help: () => html`<p>This is the help provided to the countries resource.</p>`,
    detail: (country) => html`<demo-country-detail .country="${country}"></demo-country-detail>`,
    relations: (country) => html`<demo-country-relations .country=${country}></demo-country-relations>`,
    formSingleActions: (actionName, country) => html`
        <dile-pages attrForSelected="action" selected="${actionName}">
            <demo-set-europe-as-continent-action action="SetEurope" .country=${country}></demo-set-europe-as-continent-action>
            <demo-set-asia-as-continent-action action="SetAsia" .country=${country}></demo-set-asia-as-continent-action>
        </dile-pages>
    `,
  },
  customization: {
    disablePagination: true,
    disableHelp: false,
    disableKeywordSearch: false,
    disableSort: false,
    disableFilter: false,
  },
  actions: {
    single: [
      {
        name: "SetEurope",
        label: "Set Europe as continent"
      },
      {
        name: "SetAsia",
        label: "Set Asia as continent"
      },
    ]
  },
  labels: {
    helpTitle: 'Country help',
  },
  sort: {
    options: [
      {
        name: 'name',
        label: 'Name',
        direction: 'desc'
      },
    ],
    initialSortField: 'name',
  },
  availableFilters: [
      {
        name: 'continent',
        label: 'Continent',
        active: false,
        value: false,
        type: 'select',
        options: [
            {
                value: 'Europe',
                label: 'Europe'
            },
            {
                value: 'Africa',
                label: 'Africa'
            },
            {
              value: 'Asia',
              label: 'Asia'
          },
        ]
      },
    ],
});
</script>

Create a item Component

The list components require a template to implement the list items. This template typically uses a component that receives the item object as an attribute to render it.

Here you can find more information on how to create components for list items, as well as examples and a guide to generate their scaffolding using the Dile Components CLI.

In this first example, you can find the implementation of a list item.

<script type="module">
import { LitElement, html, css } from 'lit';

export class DemoCountryItem extends LitElement {
  static styles = [
    css`
      :host {
        display: block;
        color: var(--dile-on-background-color, #303030);
      }
      span {
        font-style: italic;
        font-size: 0.9rem;
      }
    `
  ];

  static get properties() {
    return {
      country: { type: Object }
    };
  }

  render() {
    return html`
      ${this.country.name} - 
      <span @click=${this.dispatchContinent}>${this.country.continent}</span>
    `;
  }

  dispatchContinent() {
    this.dispatchEvent(new CustomEvent('continent-event', { 
      bubbles: true,
      composed: true,
      detail: this.country
    }));
  }
}
customElements.define('demo-country-item', DemoCountryItem);
</script>

Unpaginated list component

This is a simple list component without pagination. When the edit and delete icons are clicked, messages will be displayed in the console.

Additionally, clicking the name of the continent in each country's item will also log a message to the console. This continent event demonstrates that item components can have their own behaviors and events, which can be handled by the components implementing the lists.

<script type="module">
  import { LitElement, html, css } from 'lit';
  import '@dile/crud/components/list/crud-list.js'
  
  class DemoCountryList extends LitElement {
    static styles = [
      css`
        :host {
          display: block;
        }
      `
    ];

    static get properties() {
      return {
        config: { type: Object },
      };
    }

    constructor() {
      super();
      // The countryConfig variable has been defined globally to simplify this demo. Ideally, it should be imported from a module.
      this.config = window.countryConfig.getConfig();
    }

    render() {
      return html`
        <dile-crud-list
          @continent-event=${this.showOnConsole}
          @crud-item-edit=${this.showOnConsole}
          @crud-item-delete=${this.showOnConsole}
          .config="${this.config}"
        ></dile-crud-list>
      `;
    }

    showOnConsole(e) {
      console.log(e.detail);
    }

  }
  customElements.define('demo-country-list', DemoCountryList);
</script>
<demo-country-list></demo-country-list>

Paginated List Example

Create configuration object for a resource

Configuration objects creation are explained on the page that explains the resource configuration object.

<script type="module">
import { html } from 'lit';
import { CrudConfigBuilder } from '@dile/crud/lib/CrudConfigBuilder';
import { ResponseApiAdapter } from '@dile/crud/lib/ResponseApiAdapter';

class BoardGameResponseApiAdapter extends ResponseApiAdapter {
  getElementList() {
    return this.response.data.result.data;
  }
}
// For the correct functioning of this declaration in the demo system, we have defined the variable with the configuration object globally. Normally, it would be created in a module and exported.
window.boardGameConfig = new CrudConfigBuilder('https://timer.escuelait.com/api/board-games', {
  customization: {
    hideCountSummary: false,
    hideCheckboxSelection: false,
    disablePagination: false,
    disableHelp: true,
    disableKeywordSearch: false,
    disableSort: false,
    disableFilter: false,
  },
  sort: {
    options: [
      {
        name: 'name',
        label: 'Name',
        direction: 'asc'
      },
      {
        name: 'year',
        label: 'Year',
        direction: 'desc'
      },
    ],
    initialSortField: 'year',
  },
  availableFilters: [
    {
      name: 'essential',
      label: 'Is essential',
      active: false,
      value: false,
      type: 'boolean',
    },
  ],
  responseAdapter: new BoardGameResponseApiAdapter(),
  actions: {
    list: [
      {
        label: 'Delete board games',
        name: 'DeleteAction'
      },
      {
        label: 'Change Essential',
        name: 'DemoChangeEssentialAction'
      },
      {
        label: 'Change Name',
        name: 'DemoChangeNameAction'
      },
    ],
  },
  templates: {
    item: (boardGame) => html`<demo-board-game-item .boardGame=${boardGame}></demo-board-game-item>`,
    insertForm: (belongsTo, relationId) => html`<demo-board-game-form id="insertform" belongsTo="${belongsTo}" relationId="${relationId}"></demo-board-game-form>`,
    updateForm: () => html`<demo-board-game-form id="updateform"></demo-board-game-form>`,
    formActions: (actionName, actionIds) => html`
        <dile-pages attrForSelected="action" selected="${actionName}">
            <dile-crud-delete-action action="DeleteAction"></dile-crud-delete-action>
            <demo-change-essential-action action="DemoChangeEssentialAction" .actionIds=${actionIds}></demo-change-essential-action>
            <demo-change-name-action action="DemoChangeNameAction" .actionIds=${actionIds}></demo-change-name-action>
        </dile-pages>
    `,
  },
});
</script>

Create a item Component

This components acts as a template for the list items.

<script type="module">
import { LitElement, html, css } from 'lit';

export class DemoBoardGameItem extends LitElement {
  static styles = [
    css`
      :host {
        display: block;
        color: var(--dile-on-background-color, #303030);
      }
    `
  ];

  static get properties() {
    return {
      boardGame: { type: Object }
    };
  }

  render() {
    return html`
      ${this.boardGame.name} - (${this.boardGame.year})
    `;
  }
}
customElements.define('demo-board-game-item', DemoBoardGameItem);
</script>

Paginated list component

The use of paginated lists primarily differs in the definition of a different configuration object, which has additional requirements.

<script type="module">
import { LitElement, html, css } from 'lit';

export class DemoBoardGameList extends LitElement {
  static styles = [
    css`
      :host {
        display: block;
      }
    `
  ];

  static get properties() {
    return {
      config: { type: Object },
    };
  }

  constructor() {
    super();
    this.config = window.boardGameConfig.getConfig();
  }

  render() {
    return html`
      <dile-crud-list
        .config="${this.config}"
        pageSize="15"
      ></dile-crud-list>
    `;
  }

}
customElements.define('demo-board-game-list', DemoBoardGameList);
</script>
<demo-board-game-list></demo-board-game-list>