Skip to content

Commit 4f8ddd1

Browse files
author
Sébastien HEYD
committed
feat(datatables): add column sum calculation feature
- Add sum() method to Column class for enabling footer calculations - Implement getSumColumns() method in Datatable class to identify sum-enabled columns - Add automatic footerCallback generation with JavaScript for client-side sum calculations - Add conditional table footer in datatable Blade component when sum columns exist - Add comprehensive test coverage with SumTest and TestSumDatatable classes - Update column documentation with sum method usage and features - Support HTML content extraction and locale-formatted number display in footer
1 parent a523d70 commit 4f8ddd1

File tree

6 files changed

+199
-0
lines changed

6 files changed

+199
-0
lines changed

docs/docs/8.x/datatables/column.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ public function columns(): array
5050
| [class](#class) | Sets the column class |
5151
| [width](#width) | Sets the column width |
5252
| [hidden](#hidden) | Hide the column |
53+
| [sum](#sum) | Enable sum calculation in footer for numeric columns |
5354

5455
---
5556

@@ -319,3 +320,33 @@ Hide the column.
319320
```php
320321
->hidden()
321322
```
323+
324+
## sum
325+
326+
Enables automatic sum calculation for numeric columns. When applied to a column, the sum of all visible values will be displayed in the table footer.
327+
328+
```php
329+
Column::add('Price')
330+
->data('price')
331+
->sum(),
332+
333+
Column::add('Quantity')
334+
->data('quantity')
335+
->sum(),
336+
```
337+
338+
**Features**
339+
340+
- **Automatic Calculation**: Sums are calculated client-side using JavaScript for optimal performance
341+
- **Locale Formatting**: Numbers are formatted according to the user's locale (e.g., `1,234.56`)
342+
- **Dynamic Updates**: Sums update automatically when filtering or searching the table
343+
- **Current Page Only**: Calculations are performed on currently visible rows only
344+
- The column data should contain numeric values
345+
- Non-numeric values are treated as zero in calculations
346+
- Works with both integer and decimal numbers
347+
- The sum is calculated using `parseFloat()` to handle both integers and decimals
348+
- Empty or non-numeric cells contribute 0 to the sum
349+
- The footer is automatically generated when at least one column has sum enabled
350+
- Multiple columns can have sum calculations simultaneously
351+
- **HTML-formatted columns**: The sum calculation automatically extracts numeric values from HTML content (e.g., `<span class="badge">5</span>``5`)
352+
- Special handling for empty indicators: dots (`.`) are treated as zero values

src/Datatables/Column.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class Column
1515
protected $raw = null;
1616
protected $title = '';
1717
protected $tooltip = '';
18+
protected $sum = false;
1819

1920
/**
2021
* Instanciate a new column.
@@ -331,6 +332,18 @@ public function __get($name)
331332
return null;
332333
}
333334

335+
/**
336+
* Enable calculation of the sum for this column in the footer.
337+
*
338+
* @return Column
339+
*/
340+
public function sum(): Column
341+
{
342+
$this->sum = true;
343+
344+
return $this;
345+
}
346+
334347
/**
335348
* Magic method to know if magic property or attribute is set.
336349
*

src/Datatables/Datatable.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ abstract class Datatable
4141
'condensed' => false,
4242
'lengthMenu' => [[10, 25, 50, 100, -1], [10, 25, 50, 100, '']],
4343
'buttons' => ['filters'],
44+
'footerCallback' => null,
4445
];
4546

4647
/**
@@ -537,6 +538,23 @@ protected function getRequestSearchValue($name)
537538
return request()->input('columns')[$idx]['search']['value'];
538539
}
539540

541+
/**
542+
* Get columns that must calculate a sum.
543+
*
544+
* @return array
545+
*/
546+
public function getSumColumns(): array
547+
{
548+
$sumColumns = [];
549+
foreach ($this->getColumns() as $index => $column) {
550+
if ($column->sum) {
551+
$sumColumns[] = $index;
552+
}
553+
}
554+
555+
return $sumColumns;
556+
}
557+
540558
/**
541559
* Magic method to get property or attribute.
542560
*
@@ -559,6 +577,41 @@ public function __get($name)
559577
return json_encode($this->attributes[$name]);
560578
}
561579

580+
if ($name === 'footerCallback') {
581+
$sumColumns = $this->getSumColumns();
582+
if (! empty($sumColumns)) {
583+
$columnsJson = json_encode($sumColumns);
584+
585+
return "function (row, data, start, end, display) {
586+
var api = this.api();
587+
var sumColumns = $columnsJson;
588+
589+
sumColumns.forEach(function(colIndex) {
590+
var total = api
591+
.column(colIndex, {page: 'current'})
592+
.data()
593+
.reduce(function (a, b) {
594+
// Extract numeric value from HTML content if present
595+
var val = b;
596+
if (typeof b === 'string' && b.includes('<')) {
597+
// Extract text content from HTML tags
598+
var tempDiv = document.createElement('div');
599+
tempDiv.innerHTML = b;
600+
val = tempDiv.textContent || tempDiv.innerText || '';
601+
// Replace '.' with 0 for empty level indicators
602+
val = val === '.' ? '0' : val;
603+
}
604+
return a + (parseFloat(val) || 0);
605+
}, 0);
606+
607+
\$(api.column(colIndex).footer()).html(total.toLocaleString());
608+
});
609+
}";
610+
}
611+
612+
return $this->attributes[$name];
613+
}
614+
562615
if (isset($this->attributes[$name])) {
563616
return $this->attributes[$name];
564617
}

src/resources/views/components/datatable.blade.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@
4444
@endif
4545
</thead>
4646
<tbody></tbody>
47+
@if(!empty($datatable->getSumColumns()))
48+
<tfoot>
49+
<tr>
50+
@foreach($datatable->getColumns() as $index => $column)
51+
<th>{{ $column->sum ? '0' : '' }}</th>
52+
@endforeach
53+
</tr>
54+
</tfoot>
55+
@endif
4756
</table>
4857
</div>
4958
@include('boilerplate::load.async.datatables', ['buttons' => true])
@@ -89,6 +98,9 @@
8998
{!! $column->get() !!},
9099
@endforeach
91100
],
101+
@if($datatable->footerCallback)
102+
footerCallback: {!! $datatable->footerCallback !!},
103+
@endif
92104
initComplete: $.fn.dataTable.init,
93105
dom:
94106
"<'d-flex flex-wrap justify-content-between'<'dt_top_left mb-2 mr-2'l><'dt_top_right d-flex mb-2'f>>" +

tests/Datatables/SumTest.php

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace Sebastienheyd\Boilerplate\Tests\Datatables;
4+
5+
use Sebastienheyd\Boilerplate\Tests\TestCase;
6+
7+
class SumTest extends TestCase
8+
{
9+
public function testColumnSumMethod()
10+
{
11+
$datatable = new TestSumDatatable();
12+
$columns = $datatable->getColumns();
13+
14+
// Vérifier que les colonnes Price et Quantity ont la propriété sum activée
15+
$this->assertTrue($columns[2]->sum); // Price column
16+
$this->assertTrue($columns[3]->sum); // Quantity column
17+
$this->assertFalse($columns[0]->sum); // ID column
18+
$this->assertFalse($columns[1]->sum); // Name column
19+
}
20+
21+
public function testGetSumColumns()
22+
{
23+
$datatable = new TestSumDatatable();
24+
$sumColumns = $datatable->getSumColumns();
25+
26+
// Vérifier que les index des colonnes avec sum sont corrects
27+
$this->assertEquals([2, 3], $sumColumns);
28+
}
29+
30+
public function testFooterCallbackGeneration()
31+
{
32+
$datatable = new TestSumDatatable();
33+
$footerCallback = $datatable->footerCallback;
34+
35+
// Vérifier que le footerCallback est généré
36+
$this->assertNotNull($footerCallback);
37+
$this->assertStringContainsString('function (row, data, start, end, display)', $footerCallback);
38+
$this->assertStringContainsString('[2,3]', $footerCallback); // Les colonnes avec sum
39+
}
40+
41+
public function testFooterCallbackHandlesFormattedData()
42+
{
43+
$datatable = new TestSumDatatable();
44+
$footerCallback = $datatable->footerCallback;
45+
46+
// Vérifier que le JavaScript gère le contenu HTML
47+
$this->assertStringContainsString('tempDiv.innerHTML = b;', $footerCallback);
48+
$this->assertStringContainsString('tempDiv.textContent', $footerCallback);
49+
$this->assertStringContainsString("val === '.' ? '0' : val", $footerCallback);
50+
}
51+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
namespace Sebastienheyd\Boilerplate\Tests\Datatables;
4+
5+
use Sebastienheyd\Boilerplate\Datatables\Column;
6+
use Sebastienheyd\Boilerplate\Datatables\Datatable;
7+
8+
class TestSumDatatable extends Datatable
9+
{
10+
public $slug = 'test-sum';
11+
12+
public function datasource()
13+
{
14+
return collect([
15+
['id' => 1, 'name' => 'Item 1', 'price' => 100.50, 'quantity' => 5],
16+
['id' => 2, 'name' => 'Item 2', 'price' => 75.25, 'quantity' => 3],
17+
['id' => 3, 'name' => 'Item 3', 'price' => 200.00, 'quantity' => 2],
18+
]);
19+
}
20+
21+
public function columns(): array
22+
{
23+
return [
24+
Column::add('ID')
25+
->data('id'),
26+
27+
Column::add('Name')
28+
->data('name'),
29+
30+
Column::add('Price')
31+
->data('price')
32+
->sum(),
33+
34+
Column::add('Quantity')
35+
->data('quantity')
36+
->sum(),
37+
];
38+
}
39+
}

0 commit comments

Comments
 (0)