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.
<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.
<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.
<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.
<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.
<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.
- Expande la primera fila de la página 1 → Debe mostrar "Índice global: 0"
- Ve a la página 2
- Expande la primera fila de la página 2 → Debe mostrar "Índice global: 5" (NO 0)
- Ve a la página 3
- 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.
- Expande cualquier fila de la página 1
- Navega a la página 2 → Las expansiones se limpian automáticamente
- Expande una fila en la página 2
- 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.
<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.
<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.
<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.
<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>