Export the transactions table to CSV
Angular · Angular · advanced · greenfield
Adds the CSV export service used by the transactions toolbar. Escaping covers the full RFC 4180 set — commas, quotes, and both line-break characters — with quote doubling; rows join with CRLF and the payload gets a BOM so Excel decodes UTF-8. The download path builds an object URL, clicks an attached anchor, and revokes the URL on a deferred tick per the documented pattern.
Requirements
- Implement `CsvExportService` with `buildCsv(header, rows)` and `download(filename, csv)` for the transactions table export.
- All cell values arrive as already-formatted strings (the table component owns number/date formatting); `buildCsv` only assembles and escapes.
- Quoting per RFC 4180: a field containing a comma, a double quote, a carriage return, or a line feed is wrapped in double quotes, with embedded double quotes doubled. Other fields are emitted bare.
- Records are separated by CRLF (`\r\n`), and the file ends with a trailing CRLF.
- The file must open with correct accents in Excel: prefix a UTF-8 BOM and serve the blob as `text/csv;charset=utf-8`.
- `download` triggers a browser download of the given filename and must not leak the object URL.
Files touched
- src/app/transactions/csv-export.service.ts
--- src/app/transactions/csv-export.service.ts
+import { Injectable } from '@angular/core';
+
+@Injectable({ providedIn: 'root' })
+export class CsvExportService {
+ buildCsv(header: string[], rows: string[][]): string {
+ const lines = [header, ...rows].map((row) =>
+ row.map((field) => this.escapeField(field)).join(','),
+ );
+ return '\ufeff' + lines.join('\r\n') + '\r\n';
+ }
+
+ download(filename: string, csv: string) {
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
+ const url = URL.createObjectURL(blob);
+ const link = document.createElement('a');
+ link.href = url;
+ link.download = filename;
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+ setTimeout(() => URL.revokeObjectURL(url), 0);
+ }
+
+ private escapeField(field: string): string {
+ if (/[",\r\n]/.test(field)) {
+ return '"' + field.replaceAll('"', '""') + '"';
+ }
+ return field;
+ }