Skip to main content English Español Claro Oscuro Sistema

Data Grid

<gstock-data-grid> | GstockDataGrid

Ejemplos

<gstock-data-grid></gstock-data-grid>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('gstock-data-grid');

    grid.columns = [
      { key: 'name', title: 'Nombre' },
      { key: 'email', title: 'Correo electrónico' },
      { key: 'role', title: 'Rol' },
      { key: 'status', title: 'Estado', align: 'center' },
    ];

    grid.data = [
      {
        id: 1,
        name: 'Juan Pérez',
        email: 'juan.perez@example.com',
        role: 'Administrador',
        status: 'Activo',
      },
      {
        id: 2,
        name: 'María García',
        email: 'maria.garcia@example.com',
        role: 'Usuario',
        status: 'Activo',
      },
      {
        id: 3,
        name: 'Carlos López',
        email: 'carlos.lopez@example.com',
        role: 'Editor',
        status: 'Inactivo',
      },
      {
        id: 4,
        name: 'Ana Rodríguez',
        email: 'ana.rodriguez@example.com',
        role: 'Usuario',
        status: 'Activo',
      },
      {
        id: 5,
        name: 'Luis Martín',
        email: 'luis.martin@example.com',
        role: 'Editor',
        status: 'Activo',
      },
    ];
  });
</script>

Tamaño

Utilice el atributo size para cambiar el tamaño del data grid.



<gstock-data-grid size="small"></gstock-data-grid>

<br>

<gstock-data-grid size="medium"></gstock-data-grid>

<br>

<gstock-data-grid size="large"></gstock-data-grid>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const gridSmall = document.querySelector('gstock-data-grid[size="small"]');
    const gridMedium = document.querySelector('gstock-data-grid[size="medium"]');
    const gridLarge = document.querySelector('gstock-data-grid[size="large"]');

    const columns = [
      { key: 'name', title: 'Nombre' },
      { key: 'role', title: 'Rol' },
      { key: 'status', title: 'Estado' },
    ];

    const data = [
      { id: 1, name: 'Juan Pérez', role: 'Admin', status: 'Activo' },
      { id: 2, name: 'María García', role: 'Usuario', status: 'Activo' },
      { id: 3, name: 'Carlos López', role: 'Editor', status: 'Inactivo' },
    ];

    gridSmall.columns = columns;
    gridSmall.data = data;
    gridMedium.columns = columns;
    gridMedium.data = data;
    gridLarge.columns = columns;
    gridLarge.data = data;
  });
</script>

Columnas

Utilice JavaScript para configurar columnas y datos programáticamente.

<gstock-data-grid></gstock-data-grid>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('gstock-data-grid');

    grid.columns = [
      {
        key: 'name',
        title: 'Full Name',
        sortable: true,
        width: '250px',
        align: 'left',
      },
      {
        key: 'email',
        title: 'Email Address',
        sortable: true,
        width: '250px',
        type: 'text',
      },
      {
        key: 'age',
        title: 'Age',
        sortable: true,
        width: '80px',
        align: 'center',
        type: 'number',
      },
      {
        key: 'active',
        title: 'Active',
        sortable: true,
        width: '150px',
        align: 'center',
        type: 'boolean',
        formatter: (value, row) => {
          return value ? '✅' : '❌';
        },
      },
    ];

    grid.data = [
      {
        id: 1,
        name: 'John Doe Smith',
        email: 'john.doe@company.com',
        age: 32,
        active: true,
      },
      {
        id: 2,
        name: 'Jane Marie Johnson',
        email: 'jane.johnson@company.com',
        age: 28,
        active: true,
      },
      {
        id: 3,
        name: 'Mike Brown Wilson',
        email: 'mike.brown@company.com',
        age: 35,
        active: false,
      },
      {
        id: 4,
        name: 'Sarah Davis Miller',
        email: 'sarah.davis@company.com',
        age: 30,
        active: true,
      },
      {
        id: 5,
        name: 'David Lee Taylor',
        email: 'david.lee@company.com',
        age: 26,
        active: true,
      },
    ];
  });
</script>

Rayado

Utilice el atributo striped para alternar los colores de fondo de las filas.

<gstock-data-grid striped></gstock-data-grid>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('gstock-data-grid');

    grid.columns = [
      { key: 'name', title: 'Nombre' },
      { key: 'email', title: 'Correo electrónico' },
      { key: 'role', title: 'Rol' },
      { key: 'status', title: 'Estado', align: 'center' },
    ];

    grid.data = [
      {
        id: 1,
        name: 'Juan Pérez',
        email: 'juan.perez@example.com',
        role: 'Administrador',
        status: 'Activo',
      },
      {
        id: 2,
        name: 'María García',
        email: 'maria.garcia@example.com',
        role: 'Usuario',
        status: 'Activo',
      },
      {
        id: 3,
        name: 'Carlos López',
        email: 'carlos.lopez@example.com',
        role: 'Editor',
        status: 'Inactivo',
      },
      {
        id: 4,
        name: 'Ana Rodríguez',
        email: 'ana.rodriguez@example.com',
        role: 'Usuario',
        status: 'Activo',
      },
      {
        id: 5,
        name: 'Luis Martín',
        email: 'luis.martin@example.com',
        role: 'Editor',
        status: 'Activo',
      },
    ];
  });
</script>

Con bordes

Utilice el atributo bordered para mostrar bordes en todas las celdas.

<gstock-data-grid bordered></gstock-data-grid>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('gstock-data-grid');

    grid.columns = [
      { key: 'name', title: 'Nombre del producto', sortable: true },
      { key: 'category', title: 'Categoría', sortable: true },
      { key: 'price', title: 'Precio', sortable: true, align: 'right' },
      { key: 'inStock', title: 'En stock', align: 'center' },
      { key: 'rating', title: 'Valoración', sortable: true, align: 'center' },
    ];

    grid.data = [
      {
        id: 1,
        name: 'Auriculares inalámbricos',
        category: 'Electrónica',
        price: 99.99,
        inStock: true,
        rating: 4.5,
      },
      {
        id: 2,
        name: 'Cafetera',
        category: 'Electrodomésticos',
        price: 129.99,
        inStock: false,
        rating: 4.2,
      },
      {
        id: 3,
        name: 'Esterilla de yoga',
        category: 'Fitness',
        price: 29.99,
        inStock: true,
        rating: 4.7,
      },
      {
        id: 4,
        name: 'Smartphone',
        category: 'Electrónica',
        price: 699.99,
        inStock: true,
        rating: 4.3,
      },
      {
        id: 5,
        name: 'Zapatillas de running',
        category: 'Deportes',
        price: 89.99,
        inStock: false,
        rating: 4.1,
      },
      {
        id: 6,
        name: 'Lámpara de escritorio',
        category: 'Hogar',
        price: 39.99,
        inStock: true,
        rating: 4.0,
      },
      { id: 7, name: 'Mochila', category: 'Viaje', price: 59.99, inStock: true, rating: 4.4 },
      {
        id: 8,
        name: 'Botella de agua',
        category: 'Deportes',
        price: 19.99,
        inStock: true,
        rating: 4.6,
      },
    ];
  });
</script>

Efecto hover

Utilice el atributo hoverable para agregar efectos de hover en las filas.

<gstock-data-grid hoverable></gstock-data-grid>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('gstock-data-grid');

    grid.columns = [
      { key: 'name', title: 'Nombre', sortable: true },
      { key: 'email', title: 'Correo electrónico', sortable: true },
      { key: 'role', title: 'Rol', sortable: true },
      { key: 'status', title: 'Estado', sortable: true, align: 'center' },
    ];

    grid.data = [
      {
        id: 1,
        name: 'Juan Pérez',
        email: 'juan.perez@example.com',
        role: 'Administrador',
        status: 'Activo',
      },
      {
        id: 2,
        name: 'María García',
        email: 'maria.garcia@example.com',
        role: 'Usuario',
        status: 'Activo',
      },
      {
        id: 3,
        name: 'Carlos López',
        email: 'carlos.lopez@example.com',
        role: 'Editor',
        status: 'Inactivo',
      },
      {
        id: 4,
        name: 'Ana Rodríguez',
        email: 'ana.rodriguez@example.com',
        role: 'Usuario',
        status: 'Activo',
      },
      {
        id: 5,
        name: 'Luis Martín',
        email: 'luis.martin@example.com',
        role: 'Editor',
        status: 'Activo',
      },
    ];
  });
</script>

Compacto

Utilice el atributo compact para mostrar el data grid con un diseño más compacto.

<gstock-data-grid size="small" compact></gstock-data-grid>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('gstock-data-grid');

    // Configure columns
    grid.columns = [
      { key: 'id', title: 'ID', sortable: true, width: '60px' },
      { key: 'code', title: 'Code', sortable: true, width: '100px' },
      { key: 'name', title: 'Item Name', sortable: true },
      {
        key: 'quantity',
        title: 'Qty',
        sortable: true,
        type: 'number',
        align: 'center',
        width: '80px',
      },
      { key: 'unit', title: 'Unit', sortable: true, width: '80px' },
      {
        key: 'price',
        title: 'Price',
        sortable: true,
        type: 'currency',
        align: 'center',
        width: '100px',
      },
      {
        key: 'total',
        title: 'Total',
        sortable: true,
        type: 'currency',
        align: 'center',
        width: '100px',
      },
      {
        key: 'status',
        title: 'Status',
        sortable: true,
        width: '150px',
      },
    ];

    const sampleData = [
      {
        id: 1,
        code: 'ITM001',
        name: 'Widget A',
        quantity: 25,
        unit: 'pcs',
        price: 12.5,
        total: 312.5,
        status: 'Active',
      },
      {
        id: 2,
        code: 'ITM002',
        name: 'Component B',
        quantity: 50,
        unit: 'pcs',
        price: 8.75,
        total: 437.5,
        status: 'Active',
      },
      {
        id: 3,
        code: 'ITM003',
        name: 'Part C',
        quantity: 100,
        unit: 'pcs',
        price: 3.2,
        total: 320.0,
        status: 'Low Stock',
      },
      {
        id: 4,
        code: 'ITM004',
        name: 'Assembly D',
        quantity: 15,
        unit: 'sets',
        price: 45.0,
        total: 675.0,
        status: 'Active',
      },
      {
        id: 5,
        code: 'ITM005',
        name: 'Tool E',
        quantity: 8,
        unit: 'pcs',
        price: 125.0,
        total: 1000.0,
        status: 'Active',
      },
      {
        id: 6,
        code: 'ITM006',
        name: 'Material F',
        quantity: 200,
        unit: 'kg',
        price: 2.25,
        total: 450.0,
        status: 'Active',
      },
      {
        id: 7,
        code: 'ITM007',
        name: 'Equipment G',
        quantity: 3,
        unit: 'units',
        price: 850.0,
        total: 2550.0,
        status: 'Reserved',
      },
      {
        id: 8,
        code: 'ITM008',
        name: 'Supply H',
        quantity: 75,
        unit: 'boxes',
        price: 15.6,
        total: 1170.0,
        status: 'Active',
      },
      {
        id: 9,
        code: 'ITM009',
        name: 'Kit I',
        quantity: 12,
        unit: 'sets',
        price: 78.9,
        total: 946.8,
        status: 'Discontinued',
      },
      {
        id: 10,
        code: 'ITM010',
        name: 'Module J',
        quantity: 30,
        unit: 'pcs',
        price: 28.4,
        total: 852.0,
        status: 'Active',
      },
    ];

    grid.data = sampleData;
  });
</script>

Seleccionable

Utilice el atributo selectable para habilitar la selección de una sola fila con casillas de verificación. Solo se puede seleccionar una fila a la vez.

Limpiar selección Acción sobre seleccionado
Seleccionados: 0 filas
<div class="selection-actions">
  <gstock-button id="clear-selection-btn" color="secondary" variant="outlined">
    Limpiar selección
  </gstock-button>
  <gstock-button id="perform-action-btn">Acción sobre seleccionado</gstock-button>
</div>

<div class="selection-info">
  <div id="selection-info">
    Seleccionados:
    <strong>0</strong>
    filas
  </div>
</div>

<gstock-data-grid selectable></gstock-data-grid>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('gstock-data-grid');
    const selectionInfo = document.querySelector('#selection-info');
    const clearSelectionBtn = document.querySelector('#clear-selection-btn');
    const performActionBtn = document.querySelector('#perform-action-btn');

    grid.columns = [
      { key: 'name', title: 'Nombre', sortable: true },
      { key: 'email', title: 'Correo electrónico', sortable: true },
      { key: 'role', title: 'Rol', sortable: true },
      { key: 'status', title: 'Estado', sortable: true, align: 'center' },
    ];

    grid.data = [
      {
        id: 1,
        name: 'Juan Pérez',
        email: 'juan.perez@example.com',
        role: 'Administrador',
        status: 'Activo',
      },
      {
        id: 2,
        name: 'María García',
        email: 'maria.garcia@example.com',
        role: 'Usuario',
        status: 'Activo',
      },
      {
        id: 3,
        name: 'Carlos López',
        email: 'carlos.lopez@example.com',
        role: 'Editor',
        status: 'Inactivo',
      },
      {
        id: 4,
        name: 'Ana Rodríguez',
        email: 'ana.rodriguez@example.com',
        role: 'Usuario',
        status: 'Activo',
      },
      {
        id: 5,
        name: 'Luis Martín',
        email: 'luis.martin@example.com',
        role: 'Editor',
        status: 'Activo',
      },
    ];

    grid.addEventListener('gstock-data-grid-selection-change-event', e => {
      updateSelectionInfo();
    });

    function updateSelectionInfo() {
      const selectedRows = grid.getSelectedRows();
      const count = selectedRows.length;
      selectionInfo.innerHTML = `Seleccionados: <strong>${count}</strong> ${count === 1 ? 'fila' : 'filas'}`;
    }

    function clearSelection() {
      grid.deselectAll();
    }

    function performAction() {
      const selectedRows = grid.getSelectedRows();
      if (selectedRows.length === 0) {
        alert('Por favor, selecciona una fila para realizar una acción.');
        return;
      }

      const selectedRow = selectedRows[0];
      alert(`Acción realizada sobre: ${selectedRow.name}`);
    }

    clearSelectionBtn.addEventListener('click', clearSelection);
    performActionBtn.addEventListener('click', performAction);

    updateSelectionInfo();
  });
</script>

<style>
  .selection-info {
    background: var(--gstock-color-neutral-100);
    padding: 1rem;
    border-radius: var(--gstock-border-radius-md);
    margin: 1rem 0;
    border-left: 4px solid var(--gstock-color-primary-500);
    font-size: 0.875rem;
  }

  .selection-actions {
    margin: 1rem 0;
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
  }
</style>

Multi-selección

Combine los atributos selectable y multi-select para permitir la selección de varias filas. Aparecerá un checkbox en el encabezado para seleccionar o deseleccionar todas las filas de la página visible. Con paginación del servidor, la selección se acumula entre páginas: al volver a una página anterior los checkboxes ya marcados siguen vivos, y selectAll() / deselectAll() afectan solo a la página actual mientras que clearSelection() borra el conjunto completo.

Seleccionar página Deseleccionar página Limpiar todo Acción sobre seleccionados
Seleccionados: 0 filas
<div class="selection-actions">
  <gstock-button id="select-all-btn" color="secondary" variant="outlined">
    Seleccionar página
  </gstock-button>
  <gstock-button id="deselect-all-btn" color="secondary" variant="outlined">
    Deseleccionar página
  </gstock-button>
  <gstock-button id="clear-selection-btn" color="secondary" variant="outlined">
    Limpiar todo
  </gstock-button>
  <gstock-button id="perform-action-btn" disabled>Acción sobre seleccionados</gstock-button>
</div>

<div class="selection-info">
  <div id="selection-info">
    Seleccionados:
    <strong>0</strong>
    filas
  </div>
</div>

<gstock-data-grid selectable multi-select paginated page-size="5" current-page="1" total-pages="3" total-items="15"></gstock-data-grid>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('gstock-data-grid');
    const selectionInfo = document.querySelector('#selection-info');
    const selectAllBtn = document.querySelector('#select-all-btn');
    const deselectAllBtn = document.querySelector('#deselect-all-btn');
    const clearSelectionBtn = document.querySelector('#clear-selection-btn');
    const performActionBtn = document.querySelector('#perform-action-btn');

    grid.columns = [
      { key: 'name', title: 'Nombre' },
      { key: 'email', title: 'Correo electrónico' },
      { key: 'role', title: 'Rol' },
      { key: 'status', title: 'Estado', align: 'center' },
    ];

    const roles = ['Administrador', 'Usuario', 'Editor'];
    const statuses = ['Activo', 'Activo', 'Activo', 'Inactivo'];
    const names = [
      'Juan Pérez',
      'María García',
      'Carlos López',
      'Ana Rodríguez',
      'Luis Martín',
      'Elena Ruiz',
      'Miguel Sánchez',
      'Laura Torres',
      'David Herrera',
      'Carmen Vega',
      'Pablo Iglesias',
      'Sofía Castro',
      'Andrés Núñez',
      'Lucía Romero',
      'Javier Soto',
    ];

    const accentMap = { á: 'a', é: 'e', í: 'i', ó: 'o', ú: 'u' };
    const toEmail = name =>
      name
        .toLowerCase()
        .replace(/[áéíóú]/g, char => accentMap[char])
        .replace(/\s+/g, '.');

    const allRows = names.map((name, i) => ({
      id: i + 1,
      name,
      email: `${toEmail(name)}@example.com`,
      role: roles[i % roles.length],
      status: statuses[i % statuses.length],
    }));

    function loadPage(page, pageSize) {
      const start = (page - 1) * pageSize;
      grid.data = allRows.slice(start, start + pageSize);
    }

    grid.addEventListener('gstock-page-change-event', event => {
      loadPage(event.detail.currentPage, grid.pageSize);
    });

    grid.addEventListener('gstock-page-size-change-event', event => {
      grid.totalPages = Math.ceil(allRows.length / event.detail.currentPageSize);
      loadPage(event.detail.currentPage, event.detail.currentPageSize);
    });

    // Selections persist across server-side pages: every selection-change event
    // returns the full accumulated set, not just the rows on the visible page.
    grid.addEventListener('gstock-data-grid-selection-change-event', event => {
      const selectedCount = event.detail.selectedData.length;
      selectionInfo.innerHTML = `Seleccionados: <strong>${selectedCount}</strong> filas`;
      performActionBtn.disabled = selectedCount === 0;
    });

    selectAllBtn.addEventListener('click', () => grid.selectAll());
    deselectAllBtn.addEventListener('click', () => grid.deselectAll());
    clearSelectionBtn.addEventListener('click', () => grid.clearSelection());

    performActionBtn.addEventListener('click', () => {
      const selected = grid.getSelectedRows();
      if (selected.length > 0) {
        alert(
          `Realizando acción sobre ${selected.length} filas seleccionadas:\n${selected.map(row => row.name).join(', ')}`,
        );
      } else {
        alert('No hay filas seleccionadas');
      }
    });

    loadPage(1, 5);
  });
</script>

<style>
  .selection-actions {
    margin-bottom: 1rem;
    display: flex;
    gap: 0.5rem;
    flex-wrap: wrap;
  }

  .selection-info {
    margin-bottom: 1rem;
    padding: 0.75rem;
    background-color: var(--gstock-color-neutral-50);
    border: 1px solid var(--gstock-color-neutral-200);
    border-radius: var(--gstock-border-radius-md);
  }

  #selection-info {
    margin: 0;
    font-size: var(--gstock-legacy-font-size-small);
  }
</style>

Ordenable

Utilice el atributo sortable para habilitar la funcionalidad de ordenamiento de columnas.

<gstock-data-grid sortable></gstock-data-grid>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('gstock-data-grid');

    grid.columns = [
      { key: 'name', title: 'Nombre', sortable: true },
      { key: 'email', title: 'Correo electrónico', sortable: true },
      { key: 'role', title: 'Rol', sortable: true },
      { key: 'status', title: 'Estado', sortable: true, align: 'center' },
      { key: 'lastLogin', title: 'Último acceso', sortable: true, type: 'date' },
    ];

    grid.data = [
      {
        id: 1,
        name: 'Juan Pérez',
        email: 'juan.perez@example.com',
        role: 'Administrador',
        status: 'Activo',
        lastLogin: '2024-01-15',
      },
      {
        id: 2,
        name: 'María García',
        email: 'maria.garcia@example.com',
        role: 'Usuario',
        status: 'Activo',
        lastLogin: '2024-01-14',
      },
      {
        id: 3,
        name: 'Carlos López',
        email: 'carlos.lopez@example.com',
        role: 'Editor',
        status: 'Inactivo',
        lastLogin: '2024-01-10',
      },
      {
        id: 4,
        name: 'Ana Rodríguez',
        email: 'ana.rodriguez@example.com',
        role: 'Usuario',
        status: 'Activo',
        lastLogin: '2024-01-16',
      },
      {
        id: 5,
        name: 'Luis Martín',
        email: 'luis.martin@example.com',
        role: 'Editor',
        status: 'Activo',
        lastLogin: '2024-01-12',
      },
    ];

    grid.addEventListener('gstock-data-grid-sort-change-event', event => {
      console.log('Sort changed:', event.detail);
    });
  });
</script>

Filtrable

Utilice el atributo filterable para habilitar el filtrado de columnas.

All Departments Engineering Marketing Sales HR Finance
Apply Filters Clear All
Showing all 12 records
<div class="filter-controls">
  <div class="filter-group">
    <gstock-input type="text" id="name-filter" label="Name Filter" placeholder="Search by name..."></gstock-input>
  </div>

  <div class="filter-group">
    <gstock-select id="department-filter" label="Department">
      <gstock-option value>All Departments</gstock-option>
      <gstock-option value="Engineering">Engineering</gstock-option>
      <gstock-option value="Marketing">Marketing</gstock-option>
      <gstock-option value="Sales">Sales</gstock-option>
      <gstock-option value="HR">HR</gstock-option>
      <gstock-option value="Finance">Finance</gstock-option>
    </gstock-select>
  </div>

  <div class="filter-group">
    <gstock-input type="number" id="salary-min" placeholder="0" min="0" step="1000" label="Min Salary"></gstock-input>
  </div>

  <div class="filter-group">
    <gstock-input type="number" id="salary-max" placeholder="200000" min="0" step="1000" label="Max Salary"></gstock-input>
  </div>

  <div class="filter-actions">
    <gstock-button id="apply-filters-btn" class="primary">Apply Filters</gstock-button>
    <gstock-button id="clear-filters-btn">Clear All</gstock-button>
  </div>
</div>

<div class="filter-status" id="filter-status">Showing all 12 records</div>

<gstock-data-grid filterable></gstock-data-grid>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('gstock-data-grid');
    const filterStatus = document.querySelector('#filter-status');
    const applyFiltersBtn = document.querySelector('#apply-filters-btn');
    const clearFiltersBtn = document.querySelector('#clear-filters-btn');

    grid.columns = [
      { key: 'id', title: 'ID', sortable: true, width: '60px' },
      { key: 'name', title: 'Employee Name', sortable: true, filterable: true },
      { key: 'department', title: 'Department', sortable: true, filterable: true },
      { key: 'position', title: 'Position', sortable: true },
      { key: 'salary', title: 'Salary', sortable: true, align: 'right' },
      { key: 'startDate', title: 'Start Date', sortable: true, type: 'date' },
      { key: 'email', title: 'Email', sortable: true, filterable: true },
    ];

    const originalData = [
      {
        id: 1,
        name: 'John Smith',
        department: 'Engineering',
        position: 'Software Engineer',
        salary: 85000,
        startDate: '2023-01-15',
        email: 'john.smith@company.com',
      },
      {
        id: 2,
        name: 'Sarah Johnson',
        department: 'Marketing',
        position: 'Marketing Manager',
        salary: 75000,
        startDate: '2022-11-20',
        email: 'sarah.johnson@company.com',
      },
      {
        id: 3,
        name: 'Mike Davis',
        department: 'Sales',
        position: 'Sales Representative',
        salary: 55000,
        startDate: '2023-03-10',
        email: 'mike.davis@company.com',
      },
      {
        id: 4,
        name: 'Emily Chen',
        department: 'Engineering',
        position: 'Senior Developer',
        salary: 95000,
        startDate: '2021-09-05',
        email: 'emily.chen@company.com',
      },
      {
        id: 5,
        name: 'David Wilson',
        department: 'HR',
        position: 'HR Specialist',
        salary: 60000,
        startDate: '2022-07-12',
        email: 'david.wilson@company.com',
      },
      {
        id: 6,
        name: 'Lisa Anderson',
        department: 'Finance',
        position: 'Financial Analyst',
        salary: 70000,
        startDate: '2023-02-28',
        email: 'lisa.anderson@company.com',
      },
      {
        id: 7,
        name: 'Chris Taylor',
        department: 'Engineering',
        position: 'DevOps Engineer',
        salary: 88000,
        startDate: '2022-12-01',
        email: 'chris.taylor@company.com',
      },
      {
        id: 8,
        name: 'Amanda Brown',
        department: 'Marketing',
        position: 'Content Specialist',
        salary: 52000,
        startDate: '2023-04-18',
        email: 'amanda.brown@company.com',
      },
      {
        id: 9,
        name: 'Robert Lee',
        department: 'Sales',
        position: 'Sales Manager',
        salary: 82000,
        startDate: '2021-08-30',
        email: 'robert.lee@company.com',
      },
      {
        id: 10,
        name: 'Jennifer White',
        department: 'Finance',
        position: 'Senior Accountant',
        salary: 78000,
        startDate: '2022-05-15',
        email: 'jennifer.white@company.com',
      },
      {
        id: 11,
        name: 'Kevin Martinez',
        department: 'Engineering',
        position: 'Tech Lead',
        salary: 105000,
        startDate: '2020-11-12',
        email: 'kevin.martinez@company.com',
      },
      {
        id: 12,
        name: 'Michelle Garcia',
        department: 'HR',
        position: 'HR Manager',
        salary: 85000,
        startDate: '2021-06-08',
        email: 'michelle.garcia@company.com',
      },
    ];

    grid.data = originalData;

    grid.addEventListener('gstock-data-grid-filter-change-event', e => {
      updateFilterStatus();
    });

    function applyFilters() {
      const nameFilter = document.querySelector('#name-filter').value.toLowerCase();
      const departmentFilter = document.querySelector('#department-filter').value;
      const salaryMin = parseInt(document.querySelector('#salary-min').value) || 0;
      const salaryMax = parseInt(document.querySelector('#salary-max').value) || Infinity;

      let filteredData = originalData.filter(item => {
        const nameMatch = !nameFilter || item.name.toLowerCase().includes(nameFilter);
        const departmentMatch = !departmentFilter || item.department === departmentFilter;
        const salaryMatch = item.salary >= salaryMin && item.salary <= salaryMax;

        return nameMatch && departmentMatch && salaryMatch;
      });

      grid.data = filteredData;
      updateFilterStatus();
    }

    function clearFilters() {
      document.querySelector('#name-filter').value = '';
      document.querySelector('#department-filter').value = '';
      document.querySelector('#salary-min').value = '';
      document.querySelector('#salary-max').value = '';

      grid.data = originalData;
      grid.clearFilters();
      updateFilterStatus();
    }

    function updateFilterStatus() {
      const currentData = grid.data || [];
      const total = originalData.length;
      const showing = currentData.length;

      if (showing === total) {
        filterStatus.textContent = `Showing all ${total} records`;
      } else {
        filterStatus.textContent = `Showing ${showing} of ${total} records (filtered)`;
      }
    }

    applyFiltersBtn.addEventListener('click', applyFilters);
    clearFiltersBtn.addEventListener('click', clearFilters);

    document.querySelector('#name-filter').addEventListener('input', applyFilters);
    document.querySelector('#department-filter').addEventListener('change', applyFilters);
    document.querySelector('#salary-min').addEventListener('input', applyFilters);
    document.querySelector('#salary-max').addEventListener('input', applyFilters);

    updateFilterStatus();
  });
</script>

<style>
  .filter-controls {
    background: #f8f9fa;
    padding: 1rem;
    border-radius: 4px;
    margin: 1rem 0;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 1rem;
  }

  .filter-group {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
  }

  .filter-group label {
    font-weight: 600;
    font-size: 14px;
    color: #333;
  }

  .filter-group input,
  .filter-group select {
    padding: 0.5rem;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 14px;
  }

  .filter-actions {
    display: flex;
    gap: 0.5rem;
    align-items: end;
  }

  .filter-actions button {
    padding: 0.5rem 1rem;
    border: 1px solid #ddd;
    background: white;
    border-radius: 4px;
    cursor: pointer;
    font-size: 14px;
    height: fit-content;
  }

  .filter-actions button:hover {
    background: #f8f9fa;
  }

  .filter-actions button.primary {
    background: #007bff;
    color: white;
    border-color: #007bff;
  }

  .filter-actions button.primary:hover {
    background: #0056b3;
  }

  .filter-status {
    margin: 1rem 0;
    padding: 0.5rem;
    background: #e3f2fd;
    border-radius: 4px;
    font-size: 14px;
  }
</style>

Paginación

Utilice el atributo paginated para habilitar paginación virtual para datasets.

Estado de la paginación:
Página actual: 1 | Tamaño de página: 3 | Total de elementos: 10
<gstock-data-grid paginated page-size="3" show-page-size-selector></gstock-data-grid>

<div class="pagination-status">
  <strong>Estado de la paginación:</strong>
  <div id="pagination-info">Página actual: 1 | Tamaño de página: 3 | Total de elementos: 10</div>
</div>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('gstock-data-grid');
    const paginationInfo = document.querySelector('#pagination-info');

    grid.columns = [
      { key: 'name', title: 'Nombre', sortable: true },
      { key: 'email', title: 'Correo electrónico', sortable: true },
      { key: 'role', title: 'Rol', sortable: true },
      { key: 'status', title: 'Estado', sortable: true, align: 'center' },
    ];

    grid.data = [
      {
        id: 1,
        name: 'Juan Pérez',
        email: 'juan.perez@example.com',
        role: 'Administrador',
        status: 'Activo',
      },
      {
        id: 2,
        name: 'María García',
        email: 'maria.garcia@example.com',
        role: 'Usuario',
        status: 'Activo',
      },
      {
        id: 3,
        name: 'Carlos López',
        email: 'carlos.lopez@example.com',
        role: 'Editor',
        status: 'Inactivo',
      },
      {
        id: 4,
        name: 'Ana Rodríguez',
        email: 'ana.rodriguez@example.com',
        role: 'Usuario',
        status: 'Activo',
      },
      {
        id: 5,
        name: 'Luis Martín',
        email: 'luis.martin@example.com',
        role: 'Editor',
        status: 'Activo',
      },
      {
        id: 6,
        name: 'Elena Ruiz',
        email: 'elena.ruiz@example.com',
        role: 'Usuario',
        status: 'Activo',
      },
      {
        id: 7,
        name: 'Miguel Sánchez',
        email: 'miguel.sanchez@example.com',
        role: 'Administrador',
        status: 'Activo',
      },
      {
        id: 8,
        name: 'Laura Torres',
        email: 'laura.torres@example.com',
        role: 'Editor',
        status: 'Inactivo',
      },
      {
        id: 9,
        name: 'David Herrera',
        email: 'david.herrera@example.com',
        role: 'Usuario',
        status: 'Activo',
      },
      {
        id: 10,
        name: 'Carmen Vega',
        email: 'carmen.vega@example.com',
        role: 'Editor',
        status: 'Activo',
      },
    ];

    const updateInfo = () => {
      const info = `Página actual: ${grid.currentPage} | Tamaño de página: ${grid.pageSize} | Total de elementos: ${grid.data.length}`;
      paginationInfo.textContent = info;
    };

    grid.addEventListener('gstock-page-change-event', event => {
      console.log('📄 Page changed:', event.detail);
      updateInfo();
    });

    grid.addEventListener('gstock-page-size-change-event', event => {
      console.log('📏 Page size changed:', event.detail);
      updateInfo();
    });

    grid.updateComplete.then(() => {
      updateInfo();
    });
  });
</script>

<style>
  .pagination-status {
    margin-top: 1rem;
    padding: 1rem;
    background: var(--gstock-color-neutral-100);
    border-radius: var(--gstock-border-radius-md);
  }
</style>

Combine los atributos paginated junto a los atributos total-pages y total-items para utilizar paginación del servidor y cargar datos bajo demanda.

Paginación del servidor:
Cargando datos desde el servidor...
💡 Los datos se cargan desde el servidor cuando cambias de página o el tamaño de página.
<gstock-data-grid paginated page-size="5" current-page="1" total-pages="10" total-items="50" show-page-size-selector></gstock-data-grid>

<div class="info-box">
  <strong>Paginación del servidor:</strong>
  <div id="server-info">Cargando datos desde el servidor...</div>
  <div class="info-tip">
    💡 Los datos se cargan desde el servidor cuando cambias de página o el tamaño de página.
  </div>
</div>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('gstock-data-grid');
    const serverInfo = document.querySelector('#server-info');

    grid.columns = [
      { key: 'id', title: 'ID', sortable: true, width: '80px' },
      { key: 'name', title: 'Nombre', sortable: true },
      { key: 'email', title: 'Correo electrónico', sortable: true },
      { key: 'department', title: 'Departamento', sortable: true },
      { key: 'joinDate', title: 'Fecha de ingreso', sortable: true, align: 'center' },
    ];

    function loadPageData(page, pageSize, isInitialLoad = false) {
      if (isInitialLoad) {
        grid.loading = true;
      }

      serverInfo.textContent = `Cargando página ${page} con ${pageSize} elementos por página...`;

      setTimeout(() => {
        const pageData = [];
        const startId = (page - 1) * pageSize + 1;
        const itemsToLoad = Math.min(pageSize, 50 - (page - 1) * pageSize);

        const departments = ['Ingeniería', 'Marketing', 'Ventas', 'RRHH', 'Finanzas'];
        const names = [
          'Juan Pérez',
          'María García',
          'Carlos López',
          'Ana Rodríguez',
          'Luis Martín',
          'Elena Ruiz',
          'Miguel Sánchez',
          'Laura Torres',
          'David Herrera',
          'Carmen Vega',
        ];

        for (let i = 0; i < itemsToLoad; i++) {
          const id = startId + i;
          const nameIndex = (id - 1) % names.length;
          pageData.push({
            id: id,
            name: `${names[nameIndex]} ${id}`,
            email: `empleado${id}@empresa.com`,
            department: departments[id % departments.length],
            joinDate: `2023-${String(Math.floor(Math.random() * 12) + 1).padStart(2, '0')}-${String(Math.floor(Math.random() * 28) + 1).padStart(2, '0')}`,
          });
        }

        grid.data = pageData;
        grid.loading = false;

        serverInfo.textContent = `✅ Página ${page} cargada | Mostrando ${itemsToLoad} de 50 elementos totales | Tamaño de página: ${pageSize}`;
      }, 800);
    }

    grid.addEventListener('gstock-page-change-event', event => {
      console.log('Server-side page change:', event.detail);
      loadPageData(event.detail.currentPage, grid.pageSize, false);
    });

    grid.addEventListener('gstock-page-size-change-event', event => {
      console.log('Server-side page size change:', event.detail);

      const newTotalPages = Math.ceil(event.detail.totalItems / event.detail.currentPageSize);
      grid.totalPages = newTotalPages;

      loadPageData(event.detail.currentPage, event.detail.currentPageSize, false);
    });

    loadPageData(1, 5, true);
  });
</script>

<style>
  .info-box {
    margin-top: 1rem;
    padding: 1rem;
    background: var(--gstock-color-neutral-100);
    border-radius: var(--gstock-border-radius-md);
  }

  .info-tip {
    margin-top: 0.5rem;
    font-size: 0.875rem;
    color: var(--gstock-color-neutral-600);
  }
</style>

Redimensionable

Utilice el atributo resizable para habilitar la función de redimensionamiento de columnas.

<gstock-data-grid resizable bordered hoverable></gstock-data-grid>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('gstock-data-grid');

    grid.columns = [
      {
        key: 'id',
        title: 'ID',
        width: '60px',
        resizable: true,
        sortable: true,
      },
      {
        key: 'name',
        title: 'Nombre del producto',
        resizable: true,
        sortable: true,
      },
      {
        key: 'category',
        title: 'Categoría',
        width: '200px',
        resizable: true,
        sortable: true,
      },
      {
        key: 'price',
        title: 'Precio',
        width: '150px',
        align: 'right',
        resizable: true,
        sortable: true,
        formatter: value => `${parseFloat(value).toFixed(2)}`,
      },
      {
        key: 'stock',
        title: 'Stock',
        width: '150px',
        align: 'center',
        resizable: true,
        sortable: true,
      },
      {
        key: 'status',
        title: 'Estado',
        width: '200px',
        align: 'center',
        resizable: false, // This column cannot be resized
        formatter: value => {
          const color =
            value === 'Disponible' ? 'success' : value === 'Stock bajo' ? 'warning' : 'danger';
          return `<gstock-badge color="${color}">${value}</gstock-badge>`;
        },
      },
    ];

    // Sample data
    grid.data = [
      {
        id: 1,
        name: 'Laptop Dell XPS 13',
        category: 'Electrónica',
        price: 999.99,
        stock: 15,
        status: 'Disponible',
      },
      {
        id: 2,
        name: 'Ratón inalámbrico Logitech',
        category: 'Accesorios',
        price: 29.99,
        stock: 3,
        status: 'Stock bajo',
      },
      {
        id: 3,
        name: 'Teclado mecánico',
        category: 'Accesorios',
        price: 79.99,
        stock: 0,
        status: 'Agotado',
      },
      {
        id: 4,
        name: 'Monitor 4K Samsung',
        category: 'Electrónica',
        price: 349.99,
        stock: 8,
        status: 'Disponible',
      },
      {
        id: 5,
        name: 'Hub USB-C',
        category: 'Accesorios',
        price: 49.99,
        stock: 12,
        status: 'Disponible',
      },
      {
        id: 6,
        name: 'Webcam HD',
        category: 'Electrónica',
        price: 89.99,
        stock: 2,
        status: 'Stock bajo',
      },
    ];

    // Listen for column resize events
    grid.addEventListener('gstock-data-grid-column-resize-event', event => {
      console.log('Column resized:', {
        column: event.detail.column,
        newWidth: event.detail.width,
        allColumnWidths: event.detail.columnWidths,
      });
    });
  });
</script>

Filas expandibles

Utilice el atributo expandable junto con la propiedad expandedContentRenderer para habilitar la expansión de filas. Esto permite mostrar información adicional o nested content cuando el usuario expande una fila.

<gstock-data-grid id="expandable-grid" expandable></gstock-data-grid>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('#expandable-grid');

    grid.columns = [
      {
        key: 'expand',
        title: '',
        width: '50px',
        formatter: (value, row, index, dataGrid) => {
          const isExpanded = dataGrid?.isRowExpanded(row.id);

          const button = document.createElement('gstock-icon-button');
          button.setAttribute('name', isExpanded ? 'chevron-down' : 'chevron-right');
          button.setAttribute('size', 'small');
          button.setAttribute('data-expand-button', '');

          return button;
        },
      },
      { key: 'name', title: 'Nombre' },
      { key: 'email', title: 'Correo electrónico' },
      { key: 'role', title: 'Rol' },
    ];

    grid.data = [
      {
        id: 1,
        name: 'Juan Pérez',
        email: 'juan.perez@example.com',
        role: 'Administrador',
        phone: '+34 600 123 456',
        department: 'Tecnología',
      },
      {
        id: 2,
        name: 'María García',
        email: 'maria.garcia@example.com',
        role: 'Usuario',
        phone: '+34 600 234 567',
        department: 'Marketing',
      },
      {
        id: 3,
        name: 'Carlos López',
        email: 'carlos.lopez@example.com',
        role: 'Editor',
        phone: '+34 600 345 678',
        department: 'Contenido',
      },
    ];

    grid.expandedContentRenderer = (row, index, columns) => {
      return `
        <div class="expanded-content">
          <div class="expanded-row">
            <div class="expanded-item">
              <strong>Teléfono:</strong>
              <span>${row.phone}</span>
            </div>
            <div class="expanded-item">
              <strong>Departamento:</strong>
              <span>${row.department}</span>
            </div>
          </div>
        </div>
      `;
    };
  });
</script>

<style>
  .expanded-content {
    padding: var(--gstock-space-padding-block-xl);
  }

  .expanded-row {
    display: flex;
    gap: var(--gstock-space-gap-2xl);
    flex-wrap: wrap;
  }

  .expanded-item {
    display: flex;
    flex-direction: column;
    gap: var(--gstock-space-gap-sm);
  }

  .expanded-item strong {
    font-size: var(--gstock-typography-font-size-xs);
    color: var(--gstock-color-text-subtle);
    text-transform: uppercase;
  }

  .expanded-item span {
    font-size: var(--gstock-typography-font-size-sm);
    color: var(--gstock-color-text);
  }
</style>

Cuando se combina con paginación client-side, las filas expandidas se mantienen al navegar entre páginas. El índice que recibe expandedContentRenderer es el índice global del dataset completo, no el índice local de la página actual.

🧪 Prueba del bug de índices:
  1. Expande la primera fila de la página 1 → Debe mostrar "Índice global: 0"
  2. Ve a la página 2
  3. Expande la primera fila de la página 2 → Debe mostrar "Índice global: 5" (NO 0)
  4. Ve a la página 3
  5. Expande la primera fila de la página 3 → Debe mostrar "Índice global: 10" (NO 0)
<gstock-data-grid id="expandable-paginated-grid" expandable paginated page-size="5"></gstock-data-grid>

<div class="test-info">
  <strong>🧪 Prueba del bug de índices:</strong>
  <ol>
    <li>Expande la primera fila de la página 1 → Debe mostrar "Índice global: 0"</li>
    <li>Ve a la página 2</li>
    <li>Expande la primera fila de la página 2 → Debe mostrar "Índice global: 5" (NO 0)</li>
    <li>Ve a la página 3</li>
    <li>Expande la primera fila de la página 3 → Debe mostrar "Índice global: 10" (NO 0)</li>
  </ol>
</div>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('#expandable-paginated-grid');

    grid.columns = [
      {
        key: 'expand',
        title: '',
        width: '50px',
        formatter: (value, row, index, dataGrid) => {
          const isExpanded = dataGrid?.isRowExpanded(row.id);

          const button = document.createElement('gstock-icon-button');
          button.setAttribute('name', isExpanded ? 'chevron-down' : 'chevron-right');
          button.setAttribute('size', 'small');
          button.setAttribute('data-expand-button', '');

          return button;
        },
      },
      { key: 'id', title: 'ID', width: '80px' },
      { key: 'name', title: 'Nombre' },
      { key: 'email', title: 'Correo electrónico' },
      { key: 'role', title: 'Rol' },
    ];

    grid.data = Array.from({ length: 15 }, (_, i) => ({
      id: i + 1,
      name: `Usuario ${i + 1}`,
      email: `usuario${i + 1}@example.com`,
      role: ['Administrador', 'Usuario', 'Editor'][i % 3],
      phone: `+34 600 ${String(i).padStart(3, '0')} ${String(i * 11).padStart(3, '0')}`,
      department: ['Tecnología', 'Marketing', 'Contenido', 'Ventas', 'RRHH'][i % 5],
    }));

    grid.expandedContentRenderer = (row, index, columns) => {
      return `
        <div class="expanded-content">
          <div class="test-result">
            <gstock-icon name="info"></gstock-icon>
            <strong>Índice global recibido:</strong>
            <span class="index-badge">${index}</span>
          </div>
          <div class="expected-result">
            <strong>Índice esperado según ID:</strong>
            <span>${row.id - 1}</span>
          </div>
          ${
            index !== row.id - 1
              ? `
            <div class="error-message">
              <gstock-icon name="exclamation-triangle"></gstock-icon>
              <strong>❌ ERROR: El índice NO coincide!</strong>
            </div>
          `
              : `
            <div class="success-message">
              <gstock-icon name="check-circle"></gstock-icon>
              <strong>✅ CORRECTO: El índice coincide</strong>
            </div>
          `
          }
          <div class="expanded-row">
            <div class="expanded-item">
              <strong>Teléfono:</strong>
              <span>${row.phone}</span>
            </div>
            <div class="expanded-item">
              <strong>Departamento:</strong>
              <span>${row.department}</span>
            </div>
          </div>
        </div>
      `;
    };
  });
</script>

<style>
  .test-info {
    margin-bottom: var(--gstock-space-margin-bottom-lg);
    padding: var(--gstock-space-padding-block-lg);
    background: var(--gstock-color-background-info-subtle);
    border-radius: var(--gstock-border-radius-md);
    border-left: 4px solid var(--gstock-color-border-info);
  }

  .test-info strong {
    display: block;
    margin-bottom: var(--gstock-space-margin-bottom-sm);
    color: var(--gstock-color-text-info);
  }

  .test-info ol {
    margin: 0;
    padding-left: var(--gstock-space-padding-inline-xl);
  }

  .test-info li {
    margin-bottom: var(--gstock-space-margin-bottom-xs);
    color: var(--gstock-color-text-primary);
  }

  .expanded-content {
    padding: var(--gstock-space-padding-block-xl);
    display: flex;
    flex-direction: column;
    gap: var(--gstock-space-gap-lg);
  }

  .test-result {
    display: flex;
    align-items: center;
    gap: var(--gstock-space-gap-md);
    padding: var(--gstock-space-padding-block-md);
    background: var(--gstock-color-background-neutral-subtle);
    border-radius: var(--gstock-border-radius-sm);
  }

  .test-result gstock-icon {
    font-size: var(--gstock-typography-font-size-xl);
    color: var(--gstock-color-text-info);
  }

  .index-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 3rem;
    padding: var(--gstock-space-padding-block-xs) var(--gstock-space-padding-inline-sm);
    background: var(--gstock-color-background-primary);
    color: var(--gstock-color-text-on-primary);
    border-radius: var(--gstock-border-radius-full);
    font-weight: var(--gstock-typography-font-weight-bold);
    font-size: var(--gstock-typography-font-size-lg);
  }

  .expected-result {
    display: flex;
    align-items: center;
    gap: var(--gstock-space-gap-md);
    padding: var(--gstock-space-padding-block-sm);
    color: var(--gstock-color-text-subtle);
    font-size: var(--gstock-typography-font-size-sm);
  }

  .error-message {
    display: flex;
    align-items: center;
    gap: var(--gstock-space-gap-md);
    padding: var(--gstock-space-padding-block-md);
    background: var(--gstock-color-background-danger-subtle);
    color: var(--gstock-color-text-danger);
    border-radius: var(--gstock-border-radius-sm);
    border: 1px solid var(--gstock-color-border-danger);
  }

  .error-message gstock-icon {
    font-size: var(--gstock-typography-font-size-xl);
  }

  .success-message {
    display: flex;
    align-items: center;
    gap: var(--gstock-space-gap-md);
    padding: var(--gstock-space-padding-block-md);
    background: var(--gstock-color-background-success-subtle);
    color: var(--gstock-color-text-success);
    border-radius: var(--gstock-border-radius-sm);
    border: 1px solid var(--gstock-color-border-success);
  }

  .success-message gstock-icon {
    font-size: var(--gstock-typography-font-size-xl);
  }

  .expanded-row {
    display: flex;
    gap: var(--gstock-space-gap-2xl);
    flex-wrap: wrap;
    padding-top: var(--gstock-space-padding-block-md);
    border-top: 1px solid var(--gstock-color-border-subtle);
  }

  .expanded-item {
    display: flex;
    flex-direction: column;
    gap: var(--gstock-space-gap-sm);
  }

  .expanded-item strong {
    font-size: var(--gstock-typography-font-size-xs);
    color: var(--gstock-color-text-subtle);
    text-transform: uppercase;
  }

  .expanded-item span {
    font-size: var(--gstock-typography-font-size-sm);
    color: var(--gstock-color-text-primary);
  }
</style>

En paginación server-side, las filas expandidas se limpian automáticamente al cambiar de página, ya que cada página contiene datos nuevos cargados desde el servidor. Esto evita mostrar contenido desincronizado.

🧪 Prueba de Paginación Server-Side:
  1. Expande cualquier fila de la página 1
  2. Navega a la página 2 → Las expansiones se limpian automáticamente
  3. Expande una fila en la página 2
  4. Vuelve a la página 1 → Las expansiones también se limpiaron

En paginación server-side, cada cambio de página trae datos nuevos del backend, por lo que las expansiones se limpian para evitar mostrar contenido desincronizado.

<gstock-data-grid id="expandable-server-grid" expandable paginated page-size="5" show-page-size-selector></gstock-data-grid>

<div class="test-info">
  <strong>🧪 Prueba de Paginación Server-Side:</strong>
  <ol>
    <li>Expande cualquier fila de la página 1</li>
    <li>
      Navega a la página 2 →
      <strong>Las expansiones se limpian automáticamente</strong>
    </li>
    <li>Expande una fila en la página 2</li>
    <li>
      Vuelve a la página 1 →
      <strong>Las expansiones también se limpiaron</strong>
    </li>
  </ol>
  <p>
    <em>
      En paginación server-side, cada cambio de página trae datos nuevos del backend, por lo que las
      expansiones se limpian para evitar mostrar contenido desincronizado.
    </em>
  </p>
</div>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('#expandable-server-grid');

    const serverData = {
      page1: Array.from({ length: 5 }, (_, i) => ({
        id: i + 1,
        name: `Usuario Página 1 - ${i + 1}`,
        email: `usuario-p1-${i + 1}@example.com`,
        role: ['Administrador', 'Usuario', 'Editor'][i % 3],
        phone: `+34 600 10${i} ${String(i * 11).padStart(3, '0')}`,
        department: ['Tecnología', 'Marketing', 'Contenido'][i % 3],
        page: 1,
      })),
      page2: Array.from({ length: 5 }, (_, i) => ({
        id: i + 6,
        name: `Usuario Página 2 - ${i + 1}`,
        email: `usuario-p2-${i + 1}@example.com`,
        role: ['Usuario', 'Editor', 'Administrador'][i % 3],
        phone: `+34 600 20${i} ${String(i * 13).padStart(3, '0')}`,
        department: ['Ventas', 'RRHH', 'Diseño'][i % 3],
        page: 2,
      })),
      page3: Array.from({ length: 5 }, (_, i) => ({
        id: i + 11,
        name: `Usuario Página 3 - ${i + 1}`,
        email: `usuario-p3-${i + 1}@example.com`,
        role: ['Editor', 'Administrador', 'Usuario'][i % 3],
        phone: `+34 600 30${i} ${String(i * 17).padStart(3, '0')}`,
        department: ['Producto', 'Legal', 'Finanzas'][i % 3],
        page: 3,
      })),
    };

    const totalItems = 15;
    let currentPage = 1;

    grid.columns = [
      {
        key: 'expand',
        title: '',
        width: '50px',
        formatter: (value, row, index, dataGrid) => {
          const isExpanded = dataGrid?.isRowExpanded(row.id);

          const button = document.createElement('gstock-icon-button');
          button.setAttribute('name', isExpanded ? 'chevron-down' : 'chevron-right');
          button.setAttribute('size', 'small');
          button.setAttribute('data-expand-button', '');

          return button;
        },
      },
      { key: 'id', title: 'ID', width: '80px' },
      { key: 'name', title: 'Nombre' },
      { key: 'email', title: 'Correo electrónico' },
      { key: 'role', title: 'Rol' },
    ];

    grid.totalItems = totalItems;
    grid.totalPages = 3;

    const loadPageData = page => {
      currentPage = page;

      grid.paginationLoading = true;

      setTimeout(() => {
        const pageKey = `page${page}`;
        grid.data = serverData[pageKey] || [];
        grid.paginationLoading = false;
      }, 300);
    };

    loadPageData(1);

    grid.expandedContentRenderer = (row, index, columns) => {
      return `
        <div class="expanded-content">
          <div class="page-indicator">
            <gstock-icon name="server"></gstock-icon>
            <strong>Datos cargados desde el servidor - Página ${row.page}</strong>
          </div>
          <div class="expanded-row">
            <div class="expanded-item">
              <strong>ID:</strong>
              <span>${row.id}</span>
            </div>
            <div class="expanded-item">
              <strong>Teléfono:</strong>
              <span>${row.phone}</span>
            </div>
            <div class="expanded-item">
              <strong>Departamento:</strong>
              <span>${row.department}</span>
            </div>
          </div>
          <div class="info-box">
            <gstock-icon name="info"></gstock-icon>
            <p>Este contenido fue cargado del servidor para la página ${row.page}.
            Si navegas a otra página y vuelves, esta expansión se habrá limpiado porque
            los datos se recargan del servidor.</p>
          </div>
        </div>
      `;
    };

    grid.addEventListener('gstock-page-change-event', event => {
      const newPage = event.detail.currentPage;
      loadPageData(newPage);
    });
  });
</script>

<style>
  .test-info {
    margin-bottom: var(--gstock-space-margin-bottom-lg);
    padding: var(--gstock-space-padding-block-lg);
    background: var(--gstock-color-background-warning-subtle);
    border-radius: var(--gstock-border-radius-md);
    border-left: 4px solid var(--gstock-color-border-warning);
  }

  .test-info strong {
    display: block;
    margin-bottom: var(--gstock-space-margin-bottom-sm);
    color: var(--gstock-color-text-warning);
  }

  .test-info ol {
    margin: 0 0 var(--gstock-space-margin-bottom-md) 0;
    padding-left: var(--gstock-space-padding-inline-xl);
  }

  .test-info li {
    margin-bottom: var(--gstock-space-margin-bottom-xs);
    color: var(--gstock-color-text-primary);
  }

  .test-info p {
    margin: 0;
    font-size: var(--gstock-typography-font-size-sm);
    color: var(--gstock-color-text-subtle);
    font-style: italic;
  }

  .expanded-content {
    padding: var(--gstock-space-padding-block-xl);
    display: flex;
    flex-direction: column;
    gap: var(--gstock-space-gap-lg);
  }

  .page-indicator {
    display: flex;
    align-items: center;
    gap: var(--gstock-space-gap-md);
    padding: var(--gstock-space-padding-block-md);
    background: var(--gstock-color-background-info-subtle);
    border-radius: var(--gstock-border-radius-sm);
    color: var(--gstock-color-text-info);
  }

  .page-indicator gstock-icon {
    font-size: var(--gstock-typography-font-size-xl);
  }

  .expanded-row {
    display: flex;
    gap: var(--gstock-space-gap-2xl);
    flex-wrap: wrap;
    padding: var(--gstock-space-padding-block-md) 0;
    border-top: 1px solid var(--gstock-color-border-subtle);
    border-bottom: 1px solid var(--gstock-color-border-subtle);
  }

  .expanded-item {
    display: flex;
    flex-direction: column;
    gap: var(--gstock-space-gap-sm);
  }

  .expanded-item strong {
    font-size: var(--gstock-typography-font-size-xs);
    color: var(--gstock-color-text-subtle);
    text-transform: uppercase;
  }

  .expanded-item span {
    font-size: var(--gstock-typography-font-size-sm);
    color: var(--gstock-color-text-primary);
  }

  .info-box {
    display: flex;
    align-items: flex-start;
    gap: var(--gstock-space-gap-md);
    padding: var(--gstock-space-padding-block-md);
    background: var(--gstock-color-background-neutral-subtle);
    border-radius: var(--gstock-border-radius-sm);
    border-left: 3px solid var(--gstock-color-border-brand);
  }

  .info-box gstock-icon {
    font-size: var(--gstock-typography-font-size-lg);
    color: var(--gstock-color-text-brand);
    flex-shrink: 0;
    margin-top: 2px;
  }

  .info-box p {
    margin: 0;
    font-size: var(--gstock-typography-font-size-sm);
    color: var(--gstock-color-text-primary);
    line-height: 1.5;
  }
</style>

El contenido expandido puede coincidir con las columnas del grid, creando una apariencia de fila adicional con información complementaria que mantiene la alineación visual.

<gstock-data-grid id="matching-columns-grid" expandable striped></gstock-data-grid>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('#matching-columns-grid');

    grid.columns = [
      {
        key: 'expand',
        title: '',
        width: '50px',
        formatter: (value, row, index, dataGrid) => {
          const isExpanded = dataGrid?.isRowExpanded(row.id);

          const button = document.createElement('gstock-icon-button');
          button.setAttribute('name', isExpanded ? 'chevron-down' : 'chevron-right');
          button.setAttribute('size', 'small');
          button.setAttribute('data-expand-button', '');

          return button;
        },
      },
      { key: 'name', title: 'Nombre', width: '200px' },
      { key: 'email', title: 'Email', width: '250px' },
      { key: 'role', title: 'Rol', width: '150px' },
      { key: 'department', title: 'Departamento', width: '150px' },
    ];

    grid.data = [
      {
        id: 1,
        name: 'Juan Pérez',
        email: 'juan.perez@example.com',
        role: 'Desarrollador Senior',
        department: 'Tecnología',
        manager: 'Laura Martínez',
        startDate: '15/03/2022',
        location: 'Madrid',
        extension: '2345',
      },
      {
        id: 2,
        name: 'María García',
        email: 'maria.garcia@example.com',
        role: 'Diseñadora UX',
        department: 'Diseño',
        manager: 'Roberto Sánchez',
        startDate: '22/07/2021',
        location: 'Barcelona',
        extension: '3456',
      },
      {
        id: 3,
        name: 'Carlos López',
        email: 'carlos.lopez@example.com',
        role: 'Product Manager',
        department: 'Producto',
        manager: 'Patricia Jiménez',
        startDate: '10/11/2023',
        location: 'Valencia',
        extension: '4567',
      },
    ];

    grid.expandedContentRenderer = (row, index, columns) => {
      return `
        <div class="expanded-row-as-grid">
          <div class="grid-cell" style="width: 50px;"></div>
          <div class="grid-cell" style="width: 200px;">
            <span class="cell-label">Manager:</span>
            <span class="cell-value">${row.manager}</span>
          </div>
          <div class="grid-cell" style="width: 250px;">
            <span class="cell-label">Fecha de inicio:</span>
            <span class="cell-value">${row.startDate}</span>
          </div>
          <div class="grid-cell" style="width: 150px;">
            <span class="cell-label">Ubicación:</span>
            <span class="cell-value">${row.location}</span>
          </div>
          <div class="grid-cell" style="width: 150px;">
            <span class="cell-label">Extensión:</span>
            <span class="cell-value">${row.extension}</span>
          </div>
        </div>
      `;
    };
  });
</script>

<style>
  .expanded-row-as-grid {
    display: flex;
    align-items: center;
    min-height: 48px;
    padding: 0 var(--gstock-space-padding-inline-xl);
    border-bottom: var(--gstock-border-width) solid var(--gstock-color-background-neutral);
  }

  .grid-cell {
    display: flex;
    flex-direction: column;
    gap: var(--gstock-space-gap-2xs);
    padding: var(--gstock-space-padding-block-sm) var(--gstock-space-padding-inline-md);
    flex-shrink: 0;
  }

  .cell-label {
    font-size: var(--gstock-typography-font-size-xs);
    font-weight: var(--gstock-typography-font-weight-medium);
    color: var(--gstock-color-text-subtle);
  }

  .cell-value {
    font-size: var(--gstock-typography-font-size-sm);
    color: var(--gstock-color-text);
  }
</style>

Puede mostrar contenido más detallado y estructurado en las filas expandidas, incluyendo múltiples secciones y acciones.

<gstock-data-grid id="detailed-grid" expandable></gstock-data-grid>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('#detailed-grid');

    grid.columns = [
      {
        key: 'expand',
        title: '',
        width: '50px',
        formatter: (value, row, index, dataGrid) => {
          const isExpanded = dataGrid?.isRowExpanded(row.id);

          const button = document.createElement('gstock-icon-button');
          button.setAttribute('name', isExpanded ? 'chevron-down' : 'chevron-right');
          button.setAttribute('size', 'small');
          button.setAttribute('data-expand-button', '');

          return button;
        },
      },
      { key: 'name', title: 'Nombre' },
      { key: 'email', title: 'Email' },
      { key: 'department', title: 'Departamento' },
    ];

    grid.data = [
      {
        id: 1,
        name: 'Juan Pérez',
        email: 'juan.perez@example.com',
        role: 'Administrador',
        address: 'Calle Mayor 123, Madrid',
        phone: '+34 600 123 456',
        department: 'Tecnología',
        joinDate: '15/03/2022',
      },
      {
        id: 2,
        name: 'María García',
        email: 'maria.garcia@example.com',
        role: 'Usuario',
        address: 'Av. Diagonal 456, Barcelona',
        phone: '+34 600 234 567',
        department: 'Marketing',
        joinDate: '22/07/2021',
      },
      {
        id: 3,
        name: 'Carlos López',
        email: 'carlos.lopez@example.com',
        role: 'Editor',
        address: 'Gran Vía 789, Valencia',
        phone: '+34 600 345 678',
        department: 'Contenido',
        joinDate: '10/11/2023',
      },
    ];

    grid.expandedContentRenderer = (row, index, columns) => {
      return `
        <div class="expanded-content-detailed">
          <div class="expanded-section">
            <h4>Información de Contacto</h4>
            <div class="expanded-grid">
              <div class="expanded-field">
                <strong>Teléfono:</strong>
                <span>${row.phone}</span>
              </div>
              <div class="expanded-field">
                <strong>Dirección:</strong>
                <span>${row.address}</span>
              </div>
            </div>
          </div>

          <div class="expanded-section">
            <h4>Información Laboral</h4>
            <div class="expanded-grid">
              <div class="expanded-field">
                <strong>Departamento:</strong>
                <span>${row.department}</span>
              </div>
              <div class="expanded-field">
                <strong>Fecha de ingreso:</strong>
                <span>${row.joinDate}</span>
              </div>
            </div>
          </div>

          <div class="expanded-actions">
            <gstock-button prefix="edit" size="small">Editar</gstock-button>
            <gstock-button prefix="mail" size="small" variant="text">Enviar correo</gstock-button>
          </div>
        </div>
      `;
    };
  });
</script>

<style>
  .expanded-content-detailed {
    padding: var(--gstock-space-padding-block-xl);
    display: flex;
    flex-direction: column;
    gap: var(--gstock-space-gap-2xl);
  }

  .expanded-section h4 {
    margin: 0 0 var(--gstock-space-margin-block-md) 0;
    font-size: var(--gstock-typography-font-size-sm);
    font-weight: var(--gstock-typography-font-weight-semibold);
    color: var(--gstock-color-text-brand);
  }

  .expanded-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    gap: var(--gstock-space-gap-lg);
  }

  .expanded-field {
    display: flex;
    flex-direction: column;
    gap: var(--gstock-space-gap-xs);
  }

  .expanded-field strong {
    font-size: var(--gstock-typography-font-size-xs);
    color: var(--gstock-color-text-subtle);
    text-transform: uppercase;
  }

  .expanded-field span {
    font-size: var(--gstock-typography-font-size-sm);
    color: var(--gstock-color-text);
  }

  .expanded-actions {
    display: flex;
    gap: var(--gstock-space-gap-md);
    padding-top: var(--gstock-space-padding-block-md);
    border-top: var(--gstock-border-width) solid var(--gstock-color-background-neutral);
  }
</style>

Las filas expandibles también pueden contener otro data grid anidado, ideal para mostrar relaciones maestro-detalle.

<gstock-data-grid id="nested-grid" expandable></gstock-data-grid>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('#nested-grid');

    grid.columns = [
      {
        key: 'expand',
        title: '',
        width: '50px',
        formatter: (value, row, index, dataGrid) => {
          const isExpanded = dataGrid?.isRowExpanded(row.id);

          const button = document.createElement('gstock-icon-button');
          button.setAttribute('name', isExpanded ? 'chevron-down' : 'chevron-right');
          button.setAttribute('size', 'small');
          button.setAttribute('data-expand-button', '');

          return button;
        },
      },
      { key: 'department', title: 'Departamento' },
      { key: 'manager', title: 'Manager' },
      { key: 'teamSize', title: 'Tamaño del equipo', align: 'center' },
    ];

    grid.data = [
      {
        id: 1,
        department: 'Tecnología',
        manager: 'Laura Martínez',
        employees: 5,
        team: [
          { name: 'Juan Pérez', position: 'Frontend Developer', email: 'juan@example.com' },
          { name: 'Ana García', position: 'Backend Developer', email: 'ana@example.com' },
          { name: 'Carlos López', position: 'DevOps Engineer', email: 'carlos@example.com' },
          { name: 'Marta Silva', position: 'QA Engineer', email: 'marta@example.com' },
          { name: 'Pedro Ruiz', position: 'Tech Lead', email: 'pedro@example.com' },
        ],
      },
      {
        id: 2,
        department: 'Marketing',
        manager: 'Roberto Sánchez',
        employees: 3,
        team: [
          { name: 'María González', position: 'Content Manager', email: 'maria@example.com' },
          { name: 'Luis Fernández', position: 'Social Media', email: 'luis@example.com' },
          { name: 'Sara Navarro', position: 'SEO Specialist', email: 'sara@example.com' },
        ],
      },
      {
        id: 3,
        department: 'Ventas',
        manager: 'Patricia Jiménez',
        employees: 4,
        team: [
          { name: 'Jorge Moreno', position: 'Sales Executive', email: 'jorge@example.com' },
          { name: 'Elena Castro', position: 'Account Manager', email: 'elena@example.com' },
          { name: 'David Torres', position: 'Sales Rep', email: 'david@example.com' },
          { name: 'Lucía Romero', position: 'Business Dev', email: 'lucia@example.com' },
        ],
      },
    ];

    grid.expandedContentRenderer = (row, index, columns) => {
      const nestedGrid = document.createElement('gstock-data-grid');
      nestedGrid.classList.add('nested-grid');
      nestedGrid.setAttribute('no-header', '');
      nestedGrid.columns = [
        { key: 'name', title: 'Nombre' },
        { key: 'position', title: 'Puesto' },
        { key: 'email', title: 'Email' },
      ];
      nestedGrid.data = row.team;

      return nestedGrid;
    };
  });
</script>

<style>
  .nested-grid {
    margin: var(--gstock-space-margin-block-md);
    padding-left: 50px;
  }
</style>

Sin cabecera

Utilice el atributo no-header para ocultar la fila de cabecera con los títulos de las columnas. Esto es útil para grids anidados o cuando se usa el grid para mostrar datos simples sin necesidad de títulos.

<gstock-data-grid id="no-header-grid" no-header bordered></gstock-data-grid>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('#no-header-grid');

    grid.columns = [
      { key: 'name', title: 'Nombre', width: '200px' },
      { key: 'value', title: 'Valor', width: '150px' },
      { key: 'status', title: 'Estado', width: '120px' },
    ];

    grid.data = [
      { id: 1, name: 'Total Ventas', value: '€125,450', status: '✓ Completado' },
      { id: 2, name: 'Usuarios Activos', value: '1,234', status: '✓ Activo' },
      { id: 3, name: 'Tasa Conversión', value: '3.2%', status: '⚠ Bajo' },
      { id: 4, name: 'Satisfacción Cliente', value: '4.5/5', status: '✓ Excelente' },
    ];
  });
</script>

<style>
  #no-header-grid {
    max-width: 600px;
  }
</style>

Estado de carga

Utilice el atributo loading para mostrar un indicador de carga. Mientras el estado de carga está activo, se muestra un mensaje indicando el estado.

Load Data Slow Load (3s ) Simulate Error Clear Data
Ready to load data
<div class="demo-controls">
  <gstock-button id="load-data-btn">Load Data</gstock-button>
  <gstock-button id="slow-load-btn" variant="outlined">Slow Load (3s )</gstock-button>
  <gstock-button id="simulate-error-btn" variant="outlined">Simulate Error</gstock-button>
  <gstock-button id="clear-data-btn" variant="outlined">Clear Data</gstock-button>
</div>

<div class="status-indicator" id="status-indicator">Ready to load data</div>

<gstock-data-grid loading></gstock-data-grid>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('gstock-data-grid');
    const statusIndicator = document.querySelector('#status-indicator');
    const loadDataBtn = document.querySelector('#load-data-btn');
    const slowLoadBtn = document.querySelector('#slow-load-btn');
    const simulateErrorBtn = document.querySelector('#simulate-error-btn');
    const clearDataBtn = document.querySelector('#clear-data-btn');

    grid.columns = [
      { key: 'id', title: 'ID', sortable: true, width: '80px' },
      { key: 'title', title: 'Title', sortable: true },
      { key: 'author', title: 'Author', sortable: true },
      { key: 'category', title: 'Category', sortable: true },
      { key: 'publishDate', title: 'Date', sortable: true, type: 'date' },
    ];

    const sampleData = [
      {
        id: 1,
        title: 'Introduction to Web Components',
        author: 'John Pérez',
        category: 'Technology',
        publishDate: '2024-01-15',
      },
      {
        id: 2,
        title: 'CSS Best Practices',
        author: 'Mary García',
        category: 'Design',
        publishDate: '2024-01-14',
      },
      {
        id: 3,
        title: 'Modern JavaScript',
        author: 'Charles López',
        category: 'Programming',
        publishDate: '2024-01-13',
      },
      {
        id: 4,
        title: 'UX/UI Design Principles',
        author: 'Ana Rodríguez',
        category: 'Design',
        publishDate: '2024-01-12',
      },
      {
        id: 5,
        title: 'Performance Optimization',
        author: 'Luis Martín',
        category: 'Technology',
        publishDate: '2024-01-11',
      },
    ];

    function updateStatus(message, type = '') {
      statusIndicator.textContent = message;
      statusIndicator.className = `status-indicator ${type}`;
    }

    function loadData() {
      grid.loading = true;
      updateStatus('Loading data...', 'loading');

      setTimeout(() => {
        grid.data = sampleData;
        grid.loading = false;
        updateStatus('Data loaded successfully', 'loaded');
      }, 1000);
    }

    function simulateSlowLoad() {
      grid.loading = true;
      updateStatus('Loading data (slow)...', 'loading');

      setTimeout(() => {
        grid.data = sampleData;
        grid.loading = false;
        updateStatus('Data loaded after 3 seconds', 'loaded');
      }, 3000);
    }

    function simulateError() {
      grid.loading = true;
      updateStatus('Trying to load data...', 'loading');

      setTimeout(() => {
        grid.loading = false;
        grid.data = [];
        updateStatus('Error loading data', 'error');
      }, 2000);
    }

    function clearData() {
      grid.data = [];
      grid.loading = false;
      updateStatus('Data cleared', '');
    }

    loadDataBtn.addEventListener('click', loadData);
    slowLoadBtn.addEventListener('click', simulateSlowLoad);
    simulateErrorBtn.addEventListener('click', simulateError);
    clearDataBtn.addEventListener('click', clearData);

    updateStatus('Ready to load data', '');
  });
</script>

<style>
  .demo-controls {
    display: flex;
    gap: var(--gstock-legacy-spacing-x-small);
    margin-bottom: var(--gstock-legacy-spacing-medium);
    flex-wrap: wrap;
  }

  .status-indicator {
    margin: var(--gstock-legacy-spacing-medium) 0;
    padding: var(--gstock-legacy-spacing-small);
    border-radius: var(--gstock-border-radius-md);
    font-weight: var(--gstock-legacy-font-weight-semibold);
    border-width: 1px;
    border-style: solid;
    background-color: var(--gstock-color-neutral-100);
    color: var(--gstock-color-neutral-700);
    border-color: var(--gstock-color-neutral-300);
  }

  .status-indicator.loading {
    background-color: var(--gstock-color-warning-100);
    color: var(--gstock-color-warning-700);
    border-color: var(--gstock-color-warning-300);
  }

  .status-indicator.loaded {
    background-color: var(--gstock-color-success-100);
    color: var(--gstock-color-success-700);
    border-color: var(--gstock-color-success-300);
  }

  .status-indicator.error {
    background-color: var(--gstock-color-danger-100);
    color: var(--gstock-color-danger-700);
    border-color: var(--gstock-color-danger-300);
  }
</style>

Utilice el atributo loading-message para personalizar el mensaje del estado de carga.

<gstock-data-grid loading-message="Custom loading message..." loading></gstock-data-grid>

Estado vacío

El data grid muestra un mensaje indicando el estado cuando no hay datos disponibles.

Add Data Clear Data
<div class="demo-controls">
  <gstock-button id="add-data-btn">Add Data</gstock-button>
  <gstock-button id="clear-data-btn" variant="outlined">Clear Data</gstock-button>
</div>

<gstock-data-grid></gstock-data-grid>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('gstock-data-grid');
    const addDataBtn = document.querySelector('#add-data-btn');
    const clearDataBtn = document.querySelector('#clear-data-btn');

    grid.columns = [
      { key: 'name', title: 'Name', sortable: true },
      { key: 'email', title: 'Email', sortable: true },
      { key: 'role', title: 'Role', sortable: true },
      { key: 'status', title: 'Status', sortable: true, align: 'center' },
    ];

    grid.data = [];

    const sampleData = [
      {
        id: 1,
        name: 'John Pérez',
        email: 'john@example.com',
        role: 'Administrator',
        status: 'Active',
      },
      {
        id: 2,
        name: 'Mary García',
        email: 'mary@example.com',
        role: 'User',
        status: 'Active',
      },
      {
        id: 3,
        name: 'Charles López',
        email: 'charles@example.com',
        role: 'Editor',
        status: 'Inactive',
      },
    ];

    function addData() {
      grid.data = [...sampleData];
      console.log('Data added:', grid.data.length, 'records');
    }

    function clearData() {
      grid.data = [];
      console.log('Data cleared, current records:', grid.data.length);
      if (grid.requestUpdate) {
        grid.requestUpdate();
      }
    }

    addDataBtn.addEventListener('click', addData);
    clearDataBtn.addEventListener('click', clearData);
  });
</script>

Utilice el atributo empty-message para personalizar el mensaje del estado vacío.

<gstock-data-grid empty-message="No hay datos para mostrar. Personaliza este mensaje."></gstock-data-grid>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('gstock-data-grid');

    grid.columns = [
      { key: 'name', title: 'Nombre', sortable: true },
      { key: 'email', title: 'Correo electrónico', sortable: true },
      { key: 'role', title: 'Rol', sortable: true },
      { key: 'status', title: 'Estado', sortable: true, align: 'center' },
    ];

    grid.data = [];
  });
</script>

Edición en línea

Utilice la propiedad formatter de cada columna para renderizar componentes interactivos como gstock-input, gstock-select o gstock-autocomplete dentro de las celdas. Devuelva un HTMLElement desde el formatter y escuche gstock-change-event para sincronizar los cambios con los datos de la fila.

No hay cambios pendientes
<gstock-data-grid id="editable-grid"></gstock-data-grid>

<div class="editable-info" id="editable-info">No hay cambios pendientes</div>

<script type="module">
  
  const grid = document.querySelector('#editable-grid');
  const info = document.querySelector('#editable-info');

  const categories = [
    { value: 'beverages', label: 'Bebidas' },
    { value: 'bakery', label: 'Panadería' },
    { value: 'dairy', label: 'Lácteos' },
    { value: 'produce', label: 'Frutas y verduras' },
    { value: 'meat', label: 'Carnicería' },
  ];

  const suppliers = [
    { value: 'distribuciones-lopez', label: 'Distribuciones López' },
    { value: 'frutas-del-sur', label: 'Frutas del Sur S.L.' },
    { value: 'lacteos-la-vega', label: 'Lácteos La Vega' },
    { value: 'hornos-artesanos', label: 'Hornos Artesanos' },
    { value: 'carnes-premium', label: 'Carnes Premium' },
    { value: 'bebidas-ibericas', label: 'Bebidas Ibéricas' },
    { value: 'importaciones-norte', label: 'Importaciones Norte' },
  ];

  const originalData = [
    {
      id: 1,
      product: 'Café molido 250g',
      category: 'beverages',
      supplier: 'bebidas-ibericas',
      stock: 42,
    },
    { id: 2, product: 'Pan rústico', category: 'bakery', supplier: 'hornos-artesanos', stock: 18 },
    {
      id: 3,
      product: 'Yogur natural pack 4',
      category: 'dairy',
      supplier: 'lacteos-la-vega',
      stock: 60,
    },
    {
      id: 4,
      product: 'Manzanas Golden 1kg',
      category: 'produce',
      supplier: 'frutas-del-sur',
      stock: 75,
    },
    {
      id: 5,
      product: 'Solomillo de ternera',
      category: 'meat',
      supplier: 'carnes-premium',
      stock: 12,
    },
  ];

  const data = originalData.map(item => ({ ...item }));
  const pendingChanges = new Map();
  const cellCache = new Map();

  function trackChange(rowId, field, value, originalValue) {
    const key = `${rowId}:${field}`;
    if (value === originalValue) {
      pendingChanges.delete(key);
    } else {
      pendingChanges.set(key, { rowId, field, value });
    }
    info.textContent = pendingChanges.size
      ? `${pendingChanges.size} cambio${pendingChanges.size === 1 ? '' : 's'} pendiente${pendingChanges.size === 1 ? '' : 's'}`
      : 'No hay cambios pendientes';
  }

  function findOriginal(rowId) {
    return originalData.find(item => item.id === rowId);
  }

  function getCachedCell(row, field, factory) {
    const key = `${row.id}:${field}`;
    let element = cellCache.get(key);
    if (!element) {
      element = factory();
      cellCache.set(key, element);
    }
    return element;
  }

  grid.columns = [
    { key: 'id', title: 'ID', width: '60px', align: 'center' },
    {
      key: 'product',
      title: 'Producto',
      formatter: (value, row) =>
        getCachedCell(row, 'product', () => {
          const input = document.createElement('gstock-input');
          input.value = value ?? '';
          input.size = 'small';
          input.placeholder = 'Nombre del producto';
          input.addEventListener('gstock-change-event', e => {
            row.product = e.target.value;
            trackChange(row.id, 'product', e.target.value, findOriginal(row.id).product);
          });
          return input;
        }),
    },
    {
      key: 'category',
      title: 'Categoría',
      formatter: (value, row) =>
        getCachedCell(row, 'category', () => {
          const select = document.createElement('gstock-select');
          select.size = 'small';
          select.hoist = true;
          select.placeholder = 'Selecciona categoría';
          categories.forEach(cat => {
            const option = document.createElement('gstock-option');
            option.value = cat.value;
            option.textContent = cat.label;
            select.appendChild(option);
          });
          select.value = value ?? '';
          select.addEventListener('gstock-change-event', e => {
            row.category = e.target.value;
            trackChange(row.id, 'category', e.target.value, findOriginal(row.id).category);
          });
          return select;
        }),
    },
    {
      key: 'supplier',
      title: 'Proveedor',
      formatter: (value, row) =>
        getCachedCell(row, 'supplier', () => {
          const autocomplete = document.createElement('gstock-autocomplete');
          autocomplete.size = 'small';
          autocomplete.hoist = true;
          autocomplete.placeholder = 'Buscar proveedor';
          suppliers.forEach(supplier => {
            const option = document.createElement('gstock-option');
            option.value = supplier.value;
            option.textContent = supplier.label;
            autocomplete.appendChild(option);
          });
          autocomplete.value = value ?? '';
          autocomplete.addEventListener('gstock-change-event', e => {
            row.supplier = e.target.value;
            trackChange(row.id, 'supplier', e.target.value, findOriginal(row.id).supplier);
          });
          return autocomplete;
        }),
    },
    {
      key: 'stock',
      title: 'Stock',
      align: 'right',
      width: '120px',
      formatter: (value, row) =>
        getCachedCell(row, 'stock', () => {
          const input = document.createElement('gstock-input');
          input.type = 'number';
          input.value = String(value ?? 0);
          input.size = 'small';
          input.min = '0';
          input.step = '1';
          input.addEventListener('gstock-change-event', e => {
            const next = parseInt(e.target.value, 10) || 0;
            row.stock = next;
            trackChange(row.id, 'stock', next, findOriginal(row.id).stock);
          });
          return input;
        }),
    },
  ];

  grid.data = data;
</script>

<style>
  #editable-grid::part(cell),
  #editable-grid::part(cell-content) {
    overflow: visible;
  }

  .editable-info {
    margin-top: 1rem;
    padding: 0.5rem 0.75rem;
    background: var(--gstock-color-background-neutral-subtle);
    border-radius: var(--gstock-border-radius-md);
    font-size: 0.875rem;
    color: var(--gstock-color-text-subtle);
  }
</style>

Avanzado

Combine múltiples características para una experiencia completa del data grid.

Gestión de Usuarios

Exportar Agregar Usuario
<gstock-data-grid striped hoverable selectable multi-select sortable filterable paginated page-size="5">
  <div slot="toolbar">
    <div class="toolbar-content">
      <h3 class="toolbar-title">Gestión de Usuarios</h3>
      <div class="toolbar-actions">
        <gstock-button prefix="download" variant="plain" id="export-btn">Exportar</gstock-button>
        <gstock-button prefix="plus" id="add-btn">Agregar Usuario</gstock-button>
      </div>
    </div>
  </div>
</gstock-data-grid>

<div class="footer-info">
  <span>Última actualización: 2 de junio de 2025</span>
  <span id="selection-info">0 elementos seleccionados</span>
</div>

<script type="module">
  customElements.whenDefined('gstock-data-grid').then(() => {
    
    const grid = document.querySelector('gstock-data-grid');
    const selectionInfo = document.querySelector('#selection-info');
    const exportBtn = document.querySelector('#export-btn');
    const addBtn = document.querySelector('#add-btn');

    grid.columns = [
      { key: 'name', title: 'Nombre', sortable: true, filterable: true },
      { key: 'email', title: 'Correo electrónico', sortable: true, filterable: true },
      {
        key: 'role',
        title: 'Rol',
        sortable: true,
        filterable: true,
        formatter: (value, row) => {
          const color =
            value === 'Administrador' ? 'primary' : value === 'Editor' ? 'warning' : 'neutral';
          return `<gstock-badge color="${color}">${value}</gstock-badge>`;
        },
      },
      {
        key: 'status',
        title: 'Estado',
        align: 'center',
        sortable: true,
        filterable: true,
        formatter: (value, row) => {
          const color = value === 'Activo' ? 'success' : 'danger';
          const icon = value === 'Activo' ? 'check-circle' : 'x-circle';
          return `<gstock-icon name="${icon}" class="status-icon" style="color: var(--gstock-color-semantic-${color}-500);"></gstock-icon>`;
        },
      },
      { key: 'lastLogin', title: 'Último acceso', sortable: true, type: 'date' },
      {
        key: 'actions',
        title: 'Acciones',
        align: 'center',
        formatter: (value, row) => {
          return `
          <div class="actions-container">
            <gstock-icon-button icon="edit" variant="plain" size="small" title="Editar"></gstock-icon-button>
            <gstock-icon-button icon="trash" variant="plain" size="small" color="danger" title="Eliminar"></gstock-icon-button>
          </div>
        `;
        },
      },
    ];

    const roles = ['Administrador', 'Usuario', 'Editor'];
    const statuses = ['Activo', 'Inactivo'];
    const names = [
      'John Pérez',
      'Mary García',
      'Charles López',
      'Ana Rodríguez',
      'Luis Martín',
      'Elena Ruiz',
      'Miguel Sánchez',
      'Laura Torres',
      'David Herrera',
      'Carmen Vega',
      'Pedro Morales',
      'Sofia Castillo',
      'Alejandro Ramos',
      'Isabel Jiménez',
      'Roberto Silva',
    ];

    grid.data = names.map((name, index) => ({
      id: index + 1,
      name,
      email: `${name.toLowerCase().replace(' ', '.')}@example.com`,
      role: roles[index % roles.length],
      status: statuses[index % statuses.length],
      lastLogin: new Date(2024, 0, 15 - (index % 10)).toISOString().split('T')[0],
    }));

    grid.addEventListener('gstock-data-grid-selection-change-event', event => {
      const count = event.detail.selectedData.length;
      selectionInfo.textContent = `${count} elemento${count !== 1 ? 's' : ''} seleccionado${count !== 1 ? 's' : ''}`;
    });

    exportBtn.addEventListener('click', () => {
      const selectedData = grid.getSelectedRows();
      console.log('Exportando datos:', selectedData.length ? selectedData : grid.data);
      alert(
        `Exportando ${selectedData.length ? selectedData.length : grid.data.length} registros...`,
      );
    });

    addBtn.addEventListener('click', () => {
      alert('Abrir formulario para agregar nuevo usuario...');
    });

    grid.addEventListener('gstock-data-grid-row-click-event', event => {
      console.log('Fila clickeada:', event.detail.row);
    });

    grid.addEventListener('gstock-data-grid-sort-change-event', event => {
      console.log('Ordenamiento cambiado:', event.detail);
    });
  });
</script>

<style>
  .toolbar-content {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }

  .toolbar-title {
    margin: 0;
  }

  .toolbar-actions {
    display: flex;
    gap: 0.5rem;
  }

  .footer-info {
    display: flex;
    justify-content: space-between;
    align-items: center;
    font-size: 0.875rem;
    color: var(--gstock-legacy-color-grayscale-600);
    margin-top: 1rem;
    padding: 1rem;
    background: var(--gstock-color-neutral-100);
    border-radius: var(--gstock-border-radius-md);
  }

  .status-icon {
    display: inline-block;
  }

  .actions-container {
    display: flex;
    gap: 0.25rem;
    justify-content: center;
  }
</style>