Skip to content

Commit 4a9c2eb

Browse files
committed
feat: dynamically create grid from imported CSV data
1 parent 5d61692 commit 4a9c2eb

File tree

7 files changed

+265
-0
lines changed

7 files changed

+265
-0
lines changed

src/app/app-routing.module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { GridTabsComponent } from './examples/grid-tabs.component';
4040
import { GridTradingComponent } from './examples/grid-trading.component';
4141
import { GridTreeDataHierarchicalComponent } from './examples/grid-tree-data-hierarchical.component';
4242
import { GridTreeDataParentChildComponent } from './examples/grid-tree-data-parent-child.component';
43+
import { Grid43Component } from './examples/grid43.component';
4344
import { SwtCommonGridTestComponent } from './examples/swt-common-grid-test.component';
4445

4546
import { NgModule } from '@angular/core';
@@ -58,6 +59,7 @@ const routes: Routes = [
5859
{ path: 'context', component: GridContextMenuComponent },
5960
{ path: 'custom-pagination', component: GridCustomPaginationComponent },
6061
{ path: 'custom-tooltip', component: GridCustomTooltipComponent },
62+
{ path: 'csv-grid', component: Grid43Component },
6163
{ path: 'drag-recycle', component: GridDragRecycleComponent },
6264
{ path: 'editor', component: GridEditorComponent },
6365
{ path: 'excel-formula', component: GridExcelFormulaComponent },

src/app/app.component.html

+5
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,11 @@
201201
42- Custom Pagination
202202
</a>
203203
</li>
204+
<li class="nav-item">
205+
<a class="nav-link" routerLinkActive="active" [routerLink]="['/csv-grid']">
206+
43- Create Grid from CSV
207+
</a>
208+
</li>
204209
</ul>
205210
</section>
206211

src/app/app.module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import { GridTabsComponent } from './examples/grid-tabs.component';
5656
import { GridTradingComponent } from './examples/grid-trading.component';
5757
import { GridTreeDataHierarchicalComponent } from './examples/grid-tree-data-hierarchical.component';
5858
import { GridTreeDataParentChildComponent } from './examples/grid-tree-data-parent-child.component';
59+
import { Grid43Component } from './examples/grid43.component';
5960
import { HomeComponent } from './examples/home.component';
6061
import { CustomPagerComponent } from './examples/grid-custom-pager.component';
6162
import { RowDetailPreloadComponent } from './examples/rowdetail-preload.component';
@@ -144,6 +145,7 @@ export function appInitializerFactory(translate: TranslateService, injector: Inj
144145
GridTradingComponent,
145146
GridTreeDataParentChildComponent,
146147
GridTreeDataHierarchicalComponent,
148+
Grid43Component,
147149
RowDetailPreloadComponent,
148150
RowDetailViewComponent,
149151
SwtCommonGridTestComponent,
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<div id="demo-container" class="container-fluid">
2+
<h2>
3+
Example 43: Dynamically Create Grid from CSV / Excel import
4+
<span class="float-end">
5+
<a
6+
style="font-size: 18px"
7+
target="_blank"
8+
href="https://github.com/ghiscoding/Angular-Slickgrid/blob/master/src/app/examples/grid43.component.ts">
9+
<span class="mdi mdi-link mdi-v-align-sub"></span> code
10+
</a>
11+
</span>
12+
<button
13+
class="ms-2 btn btn-outline-secondary btn-sm btn-icon"
14+
type="button"
15+
data-test="toggle-subtitle"
16+
(click)="toggleSubTitle()"
17+
>
18+
<span class="mdi mdi-information-outline" title="Toggle example sub-title details"></span>
19+
</button>
20+
</h2>
21+
22+
<div class="subtitle">
23+
Allow creating a grid dynamically by importing an external CSV or Excel file. This script demo will read the CSV file and will
24+
consider the first row as the column header and create the column definitions accordingly, while the next few rows will be
25+
considered the dataset. Note that this example is demoing a CSV file import but in your application you could easily implemnt
26+
an Excel file uploading.
27+
</div>
28+
29+
<div>A default CSV file can be download <a id="template-dl" [href]="templateUrl">here</a>.</div>
30+
31+
<div class="d-flex mt-5 align-items-end">
32+
<div class="file-upload">
33+
<label for="formFile" class="form-label">Choose a CSV file…</label>
34+
<input class="form-control" type="file" data-test="file-upload-input" [(ngModel)]="uploadFileRef" (input)="handleFileImport($event)" />
35+
</div>
36+
<span class="mx-3">or</span>
37+
<div>
38+
<button id="uploadBtn" data-test="static-data-btn" class="btn btn-outline-secondary" (click)="handleDefaultCsv()">
39+
Use default CSV data
40+
</button>
41+
<button class="btn btn-outline-secondary" (click)="destroyGrid()">Destroy Grid</button>
42+
</div>
43+
</div>
44+
45+
<hr />
46+
47+
<angular-slickgrid
48+
*ngIf="gridCreated"
49+
gridId="grid43"
50+
[columnDefinitions]="columnDefinitions"
51+
[gridOptions]="gridOptions"
52+
[dataset]="dataset"
53+
>
54+
</angular-slickgrid>
55+
</div>

src/app/examples/grid43.component.ts

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { ChangeDetectorRef, Component, ViewEncapsulation } from '@angular/core';
2+
import { ExcelExportService } from '@slickgrid-universal/excel-export';
3+
import { type Column, type GridOption, toCamelCase } from './../modules/angular-slickgrid';
4+
5+
const sampleDataRoot = 'assets/data';
6+
7+
@Component({
8+
styles: ['.file-upload { max-width: 300px; }'],
9+
encapsulation: ViewEncapsulation.None,
10+
templateUrl: './grid43.component.html'
11+
})
12+
export class Grid43Component {
13+
columnDefinitions: Column[] = [];
14+
gridOptions!: GridOption;
15+
dataset: any[] = [];
16+
gridCreated = false;
17+
showSubTitle = true;
18+
uploadFileRef = '';
19+
templateUrl = `${sampleDataRoot}/users.csv`;
20+
21+
constructor(private readonly cd: ChangeDetectorRef) { }
22+
23+
handleFileImport(event: any) {
24+
const file = event.target.files[0];
25+
if (file) {
26+
const reader = new FileReader();
27+
reader.onload = (e: any) => {
28+
const content = e.target.result;
29+
this.dynamicallyCreateGrid(content);
30+
};
31+
reader.readAsText(file);
32+
}
33+
}
34+
35+
handleDefaultCsv() {
36+
const staticDataCsv = `First Name,Last Name,Age,Type\nBob,Smith,33,Teacher\nJohn,Doe,20,Student\nJane,Doe,21,Student`;
37+
this.dynamicallyCreateGrid(staticDataCsv);
38+
this.uploadFileRef = '';
39+
}
40+
41+
destroyGrid() {
42+
this.gridCreated = false;
43+
}
44+
45+
dynamicallyCreateGrid(csvContent: string) {
46+
// dispose of any previous grid before creating a new one
47+
this.gridCreated = false;
48+
this.cd.detectChanges();
49+
50+
const dataRows = csvContent?.split('\n');
51+
const colDefs: Column[] = [];
52+
const outputData: any[] = [];
53+
54+
// create column definitions
55+
dataRows.forEach((dataRow, rowIndex) => {
56+
const cellValues = dataRow.split(',');
57+
const dataEntryObj: any = {};
58+
59+
if (rowIndex === 0) {
60+
// the 1st row is considered to be the header titles, we can create the column definitions from it
61+
for (const cellVal of cellValues) {
62+
const camelFieldName = toCamelCase(cellVal);
63+
colDefs.push({
64+
id: camelFieldName,
65+
name: cellVal,
66+
field: camelFieldName,
67+
filterable: true,
68+
sortable: true,
69+
});
70+
}
71+
} else {
72+
// at this point all column defs were created and we can loop through them and
73+
// we can now start adding data as an object and then simply push it to the dataset array
74+
cellValues.forEach((cellVal, colIndex) => {
75+
dataEntryObj[colDefs[colIndex].id] = cellVal;
76+
});
77+
78+
// a unique "id" must be provided, if not found then use the row index and push it to the dataset
79+
if ('id' in dataEntryObj) {
80+
outputData.push(dataEntryObj);
81+
} else {
82+
outputData.push({ ...dataEntryObj, id: rowIndex });
83+
}
84+
}
85+
});
86+
87+
this.gridOptions = {
88+
gridHeight: 300,
89+
gridWidth: 800,
90+
enableFiltering: true,
91+
enableExcelExport: true,
92+
externalResources: [new ExcelExportService()],
93+
headerRowHeight: 35,
94+
rowHeight: 33,
95+
};
96+
97+
this.dataset = outputData;
98+
this.columnDefinitions = colDefs;
99+
this.gridCreated = true;
100+
this.cd.detectChanges();
101+
}
102+
103+
toggleSubTitle() {
104+
this.showSubTitle = !this.showSubTitle;
105+
const action = this.showSubTitle ? 'remove' : 'add';
106+
document.querySelector('.subtitle')?.classList[action]('hidden');
107+
}
108+
}

src/assets/data/users.csv

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
First Name,Last Name,Age,User Type
2+
John,Doe,20,Student
3+
Bob,Smith,33,Assistant Teacher
4+
Jane,Doe,21,Student
5+
Robert,Ken,42,Teacher

test/cypress/e2e/example43.cy.ts

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
describe('Example 43 - Dynamically Create Grid from CSV / Excel import', () => {
2+
const defaultCsvTitles = ['First Name', 'Last Name', 'Age', 'Type'];
3+
const GRID_ROW_HEIGHT = 33;
4+
5+
it('should display Example title', () => {
6+
cy.visit(`${Cypress.config('baseUrl')}/csv-grid`);
7+
cy.get('h2').should('contain', 'Example 43: Dynamically Create Grid from CSV / Excel import');
8+
});
9+
10+
it('should load default CSV file and expect default column titles', () => {
11+
cy.get('[data-test="static-data-btn"]')
12+
.click();
13+
14+
cy.get('.slick-header-columns')
15+
.children()
16+
.each(($child, index) => expect($child.text()).to.eq(defaultCsvTitles[index]));
17+
});
18+
19+
it('should expect default data in the grid', () => {
20+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Bob');
21+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Smith');
22+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '33');
23+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Teacher');
24+
25+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'John');
26+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe');
27+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', '20');
28+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'Student');
29+
30+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Jane');
31+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe');
32+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).should('contain', '21');
33+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', 'Student');
34+
});
35+
36+
it('should sort by "Age" and expect it to be sorted in ascending order', () => {
37+
cy.get('.slick-header-columns .slick-header-column:nth(2)')
38+
.click();
39+
40+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'John');
41+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe');
42+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '20');
43+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Student');
44+
45+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Jane');
46+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe');
47+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', '21');
48+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'Student');
49+
50+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'Bob');
51+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Smith');
52+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).should('contain', '33');
53+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', 'Teacher');
54+
});
55+
56+
it('should click again the "Age" column and expect it to be sorted in descending order', () => {
57+
cy.get('.slick-header-columns .slick-header-column:nth(2)')
58+
.click();
59+
60+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Bob');
61+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Smith');
62+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '33');
63+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Teacher');
64+
65+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(0)`).should('contain', 'Jane');
66+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe');
67+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(2)`).should('contain', '21');
68+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 1}px;"] > .slick-cell:nth(3)`).should('contain', 'Student');
69+
70+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(0)`).should('contain', 'John');
71+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(1)`).should('contain', 'Doe');
72+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(2)`).should('contain', '20');
73+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 2}px;"] > .slick-cell:nth(3)`).should('contain', 'Student');
74+
});
75+
76+
it('should filter Smith as "Last Name" and expect only 1 row in the grid', () => {
77+
cy.get('.slick-headerrow .slick-headerrow-column:nth(1) input')
78+
.type('Smith');
79+
80+
cy.get('.slick-row')
81+
.should('have.length', 1);
82+
83+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(0)`).should('contain', 'Bob');
84+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(1)`).should('contain', 'Smith');
85+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(2)`).should('contain', '33');
86+
cy.get(`[style="top: ${GRID_ROW_HEIGHT * 0}px;"] > .slick-cell:nth(3)`).should('contain', 'Teacher');
87+
});
88+
});

0 commit comments

Comments
 (0)