Chandra
view release on metacpan or search on metacpan
lib/Chandra/Table.pm view on Meta::CPAN
my $wrap = Chandra::Element->new({ tag => 'div', class => 'chandra-table-wrap' });
# ââ Filter row (never re-rendered by _update_body) ââââ
my $has_filters = grep { $_->{filterable} } @$columns;
if ($has_filters) {
my $filter_div = Chandra::Element->new({ tag => 'div', class => 'chandra-table-filters' });
for my $col (@$columns) {
next unless $col->{filterable};
my $key = $col->{key};
my $current = $filters->{$key} // '';
if ($col->{filter_options}) {
my $select = Chandra::Element->new({
tag => 'select', class => 'chandra-table-filter',
'data-action' => "filter:$key",
});
$select->add_child(Chandra::Element->new({
tag => 'option', data => "All $col->{label}",
})->attribute('value', ''));
for my $opt (@{$col->{filter_options}}) {
my $option = Chandra::Element->new({ tag => 'option', data => $opt });
$option->attribute('value', $opt);
$option->attribute('selected', 'selected') if $current eq $opt;
$select->add_child($option);
}
$filter_div->add_child($select);
} else {
my $input = Chandra::Element->new({
tag => 'input', class => 'chandra-table-filter',
'data-action' => "filter:$key",
});
$input->attribute('type', 'text');
$input->attribute('placeholder', "Filter $col->{label}...");
$input->attribute('value', $current);
$filter_div->add_child($input);
}
}
$wrap->add_child($filter_div);
}
# ââ Table with thead + tbody ââââââââââââââââââââââââââ
my $table = Chandra::Element->new({ tag => 'table', class => 'chandra-table' });
$table->add_child($self->_render_thead);
$table->add_child($self->_render_tbody);
$wrap->add_child($table);
# ââ Pagination ââââââââââââââââââââââââââââââââââââââââ
$wrap->add_child($self->_render_pagination);
return $wrap->render;
}
# ââ CSS ââââââââââââââââââââââââââââââââââââââââââââââââââââ
sub css {
return <<'CSS';
.chandra-table-wrap { font-family: system-ui, -apple-system, sans-serif; }
.chandra-table { width: 100%; border-collapse: collapse; }
.chandra-table th, .chandra-table td { padding: 8px 12px; text-align: left; border-bottom: 1px solid #e0e0e0; }
.chandra-table th { background: #f5f5f5; font-weight: 600; user-select: none; }
.chandra-table-sortable { cursor: pointer; }
.chandra-table-sortable:hover { background: #e8e8e8; }
.chandra-table-stripe { background: #fafafa; }
.chandra-table-selected { background: #e3f2fd !important; }
.chandra-table-select { width: 40px; text-align: center; }
.chandra-table-empty, .chandra-table-loading { text-align: center; padding: 24px; color: #999; }
.chandra-table-filters { padding: 8px 0; display: flex; gap: 8px; flex-wrap: wrap; }
.chandra-table-filter { padding: 4px 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 13px; }
.chandra-table-pagination { display: flex; align-items: center; justify-content: center; gap: 4px; padding: 12px 0; }
.chandra-table-page-btn { padding: 4px 10px; border: 1px solid #ddd; border-radius: 4px; background: #fff; cursor: pointer; font-size: 13px; }
.chandra-table-page-btn:hover { background: #f0f0f0; }
.chandra-table-page-active { background: #2196F3 !important; color: #fff; border-color: #2196F3; }
.chandra-table-info { margin-right: 12px; font-size: 13px; color: #666; }
CSS
}
1;
__END__
=head1 NAME
Chandra::Table - Sortable, filterable, paginated data grid component
=head1 SYNOPSIS
use Chandra::Table;
my $table = Chandra::Table->new(
columns => [
{ key => 'name', label => 'Name', sortable => 1 },
{ key => 'email', label => 'Email', sortable => 1 },
{ key => 'role', label => 'Role', filterable => 1,
filter_options => [qw(admin user guest)] },
],
data => \@users,
page_size => 10,
selectable => 'multi',
on_row_click => sub { my ($row) = @_; print $row->{name} },
);
$app->css(Chandra::Table->css);
$table->mount($app, '#content');
=head1 DESCRIPTION
C<Chandra::Table> is a L<Chandra::Component> subclass providing a
data grid. Renders using L<Chandra::Element> for proper event wiring.
=head1 SEE ALSO
L<Chandra::Component>, L<Chandra::Element>, L<Chandra::App>
=cut
( run in 0.862 second using v1.01-cache-2.11-cpan-140bd7fdf52 )