├── .nvmrc
├── src
├── assets
│ └── .gitkeep
├── favicon.ico
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── lib
│ ├── public_api.ts
│ ├── ng-package.json
│ ├── csv.module.ts
│ ├── package.json
│ ├── csv.directive.ts
│ ├── csv.component.spec.ts
│ ├── util.ts
│ └── util.spec.ts
├── app
│ ├── download-svg
│ │ ├── download-svg.component.ts
│ │ └── download-svg.component.html
│ ├── app.module.ts
│ ├── app.component.spec.ts
│ ├── app.component.ts
│ └── app.component.html
├── main.ts
├── test.ts
├── index.html
├── styles.scss
└── polyfills.ts
├── vercel.json
├── .codecov.yml
├── .prettierrc
├── .editorconfig
├── tsconfig.app.json
├── tsconfig.spec.json
├── .browserslistrc
├── .gitignore
├── tsconfig.json
├── LICENSE
├── .circleci
└── config.yml
├── karma.conf.js
├── .eslintrc.json
├── package.json
├── README.md
└── angular.json
/.nvmrc:
--------------------------------------------------------------------------------
1 | 16
2 |
--------------------------------------------------------------------------------
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "github": {
3 | "silent": true
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/scttcper/ngx-csv/HEAD/src/favicon.ico
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true
3 | };
4 |
--------------------------------------------------------------------------------
/src/lib/public_api.ts:
--------------------------------------------------------------------------------
1 | export { CsvModule } from './csv.module';
2 | export { CsvDirective } from './csv.directive';
3 | export * from './util';
4 |
--------------------------------------------------------------------------------
/.codecov.yml:
--------------------------------------------------------------------------------
1 | coverage:
2 | range: "50..100"
3 | status:
4 | project: no
5 | patch: no
6 | comment:
7 | require_changes: yes
8 | behavior: once
9 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": true,
3 | "singleQuote": true,
4 | "trailingComma": "all",
5 | "bracketSpacing": true,
6 | "printWidth": 100,
7 | "arrowParens": "avoid"
8 | }
9 |
--------------------------------------------------------------------------------
/src/lib/ng-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "../../node_modules/ng-packagr/package.schema.json",
3 | "lib": {
4 | "entryFile": "public_api.ts"
5 | },
6 | "dest": "../../dist"
7 | }
8 |
--------------------------------------------------------------------------------
/src/app/download-svg/download-svg.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'download-svg',
5 | templateUrl: './download-svg.component.html',
6 | })
7 | export class DownloadSvgComponent {
8 | }
9 |
--------------------------------------------------------------------------------
/src/lib/csv.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 |
3 | import { CsvDirective } from './csv.directive';
4 |
5 | @NgModule({
6 | declarations: [CsvDirective],
7 | exports: [CsvDirective],
8 | })
9 | export class CsvModule {}
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.ts]
12 | quote_type = single
13 |
14 | [*.md]
15 | max_line_length = off
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/app",
6 | "types": []
7 | },
8 | "files": [
9 | "src/main.ts",
10 | "src/polyfills.ts"
11 | ],
12 | "include": [
13 | "src/**/*.d.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/spec",
6 | "types": [
7 | "jasmine"
8 | ]
9 | },
10 | "files": [
11 | "src/test.ts",
12 | "src/polyfills.ts"
13 | ],
14 | "include": [
15 | "src/**/*.spec.ts",
16 | "src/**/*.d.ts"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // The file contents for the current environment will overwrite these during build.
2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do
3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead.
4 | // The list of which env maps to which file can be found in `.angular-cli.json`.
5 |
6 | export const environment = {
7 | production: false
8 | };
9 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { enableProdMode } from '@angular/core';
2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
3 |
4 | import { AppModule } from './app/app.module';
5 | import { environment } from './environments/environment';
6 |
7 | if (environment.production) {
8 | enableProdMode();
9 | }
10 |
11 | platformBrowserDynamic()
12 | .bootstrapModule(AppModule)
13 | .catch(err => console.error(err));
14 |
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
2 | # For additional information regarding the format and rule options, please see:
3 | # https://github.com/browserslist/browserslist#queries
4 |
5 | # You can see what browsers were selected by your queries by running:
6 | # npx browserslist
7 |
8 | # Googlebot uses an older version of Chrome
9 | # For additional information see: https://developers.google.com/search/docs/guides/rendering
10 |
11 | > 5% in US
12 |
--------------------------------------------------------------------------------
/src/lib/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ctrl/ngx-csv",
3 | "description": "Easily generate a CSV download in the browser with Angular",
4 | "publishConfig": {
5 | "access": "public"
6 | },
7 | "version": "0.0.0-placeholder",
8 | "peerDependencies": {
9 | "@angular/core": ">=14.0.0-0"
10 | },
11 | "repository": "scttcper/ngx-csv",
12 | "homepage": "https://ngx-csv.vercel.app",
13 | "license": "MIT",
14 | "keywords": [
15 | "ngx",
16 | "angular",
17 | "angular-component",
18 | "csv"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { BrowserModule } from '@angular/platform-browser';
3 |
4 | import { GhButtonModule } from '@ctrl/ngx-github-buttons';
5 |
6 | import { CsvModule } from '../lib/csv.module';
7 | import { AppComponent } from './app.component';
8 | import { DownloadSvgComponent } from './download-svg/download-svg.component';
9 |
10 | @NgModule({
11 | declarations: [AppComponent, DownloadSvgComponent],
12 | imports: [BrowserModule, GhButtonModule, CsvModule],
13 | bootstrap: [AppComponent],
14 | })
15 | export class AppModule {}
16 |
--------------------------------------------------------------------------------
/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, waitForAsync } from '@angular/core/testing';
2 |
3 | import { GhButtonModule } from '@ctrl/ngx-github-buttons';
4 | import { AppComponent } from './app.component';
5 | import { AppModule } from './app.module';
6 |
7 | describe('AppComponent', () => {
8 | beforeEach(waitForAsync(() => {
9 | TestBed.configureTestingModule({
10 | imports: [AppModule, GhButtonModule],
11 | }).compileComponents();
12 | }));
13 |
14 | it('should create the app', waitForAsync(() => {
15 | const fixture = TestBed.createComponent(AppComponent);
16 | const app = fixture.debugElement.componentInstance;
17 | expect(app).toBeTruthy();
18 | }));
19 | });
20 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-root',
5 | templateUrl: './app.component.html',
6 | styles: [],
7 | })
8 | export class AppComponent {
9 | arrayOfObjects = [
10 | { firstname: 'Ahmed', lastname: 'Tomi', email: 'ah@smthing.co.com' },
11 | { firstname: 'Raed', lastname: 'Labes', email: 'rl@smthing.co.com' },
12 | { firstname: 'Yezzi', lastname: 'Min l3b', email: 'ymin@cocococo.com' },
13 | ];
14 | arrayOfArrays = [
15 | ['firstname', 'lastname', 'email'],
16 | ['Ahmed', 'Tomi', 'ah@smthing.co.com'],
17 | ['Raed', 'Labes', 'rl@smthing.co.com'],
18 | ['Yezzi', 'Min l3b', 'ymin@cocococo.com'],
19 | ];
20 | }
21 |
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files
2 | import 'zone.js/testing';
3 |
4 | import { getTestBed } from '@angular/core/testing';
5 | import {
6 | platformBrowserDynamicTesting,
7 | BrowserDynamicTestingModule,
8 | } from '@angular/platform-browser-dynamic/testing';
9 | declare const require: any;
10 |
11 | // First, initialize the Angular testing environment.
12 | getTestBed().initTestEnvironment(
13 | BrowserDynamicTestingModule,
14 | platformBrowserDynamicTesting(), {
15 | teardown: { destroyAfterEach: false }
16 | }
17 | );
18 | // Then we find all the tests.
19 | const context = require.context('./', true, /\.spec\.ts$/);
20 | // And load the modules.
21 | context.keys().map(context);
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /dist-server
6 | /tmp
7 | /out-tsc
8 |
9 | # dependencies
10 | /node_modules
11 |
12 | # IDEs and editors
13 | /.idea
14 | .project
15 | .classpath
16 | .c9/
17 | *.launch
18 | .settings/
19 | *.sublime-workspace
20 |
21 | # IDE - VSCode
22 | .vscode/*
23 | !.vscode/settings.json
24 | !.vscode/tasks.json
25 | !.vscode/launch.json
26 | !.vscode/extensions.json
27 |
28 | # misc
29 | /.angular/cache
30 | /.sass-cache
31 | /connect.lock
32 | /coverage
33 | /libpeerconnection.log
34 | npm-debug.log
35 | yarn-error.log
36 | testem.log
37 | /typings
38 |
39 | # e2e
40 | /e2e/*.js
41 | /e2e/*.map
42 |
43 | # System Files
44 | .DS_Store
45 | Thumbs.db
46 | yarn.lock
47 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "compileOnSave": false,
4 | "compilerOptions": {
5 | "baseUrl": "./",
6 | "outDir": "./dist/out-tsc",
7 | "forceConsistentCasingInFileNames": true,
8 | "strict": true,
9 | "noImplicitReturns": true,
10 | "noFallthroughCasesInSwitch": true,
11 | "sourceMap": true,
12 | "declaration": false,
13 | "downlevelIteration": true,
14 | "experimentalDecorators": true,
15 | "moduleResolution": "node",
16 | "importHelpers": true,
17 | "target": "es2020",
18 | "module": "es2020",
19 | "lib": [
20 | "es2018",
21 | "dom"
22 | ]
23 | },
24 | "angularCompilerOptions": {
25 | "strictInjectionParameters": true,
26 | "strictTemplates": true
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/download-svg/download-svg.component.html:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | ngx-csv
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) Scott Cooper
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2.1
2 | orbs:
3 | node: circleci/node@5
4 | browser-tools: circleci/browser-tools@1
5 | codecov: codecov/codecov@3
6 | jobs:
7 | test:
8 | docker:
9 | - image: cimg/node:current-browsers
10 | environment:
11 | CHROME_BIN: '/usr/bin/google-chrome'
12 | steps:
13 | - browser-tools/install-chrome
14 | - checkout
15 | - node/install-packages
16 | - run:
17 | name: test
18 | command: npm run test:ci
19 | - run:
20 | name: lint
21 | command: npm run lint
22 | - codecov/upload
23 | release:
24 | executor:
25 | name: node/default
26 | tag: 'current'
27 | steps:
28 | - checkout
29 | - run: npm ci
30 | - run: npm run build
31 | - run: cd dist && npx semantic-release
32 |
33 | workflows:
34 | version: 2
35 | test_and_release:
36 | # Run the test jobs first, then the release only when all the test jobs are successful
37 | jobs:
38 | - test
39 | - release:
40 | filters:
41 | branches:
42 | only:
43 | - master
44 | context:
45 | - npm
46 | requires:
47 | - test
48 |
--------------------------------------------------------------------------------
/src/lib/csv.directive.ts:
--------------------------------------------------------------------------------
1 | import { Directive, HostBinding, Input, OnChanges } from '@angular/core';
2 | import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
3 |
4 | import { buildURI, HeaderObj } from './util';
5 |
6 | @Directive({ selector: '[csvLink]' })
7 | export class CsvDirective implements OnChanges {
8 | /** the body of the csv */
9 | @Input() data: string | string[][] | { [key: string]: string }[] | any[] = [];
10 | /** Set the first line of the csv */
11 | @Input() headers?: string[] | HeaderObj[];
12 | /** Set the seperator between values */
13 | @Input() delimiter = ',';
14 | /** Set the filename of the csv. Default is `data.csv` */
15 | @Input()
16 | set filename(a: string) {
17 | this.download = a;
18 | }
19 | /** adds a Byte order mark to setup the csv as UTF-8 */
20 | @Input() uFEFF = true;
21 | @HostBinding() href?: SafeResourceUrl;
22 | /** filename */
23 | @HostBinding() download = 'data.csv';
24 | @Input() @HostBinding() target = '_blank';
25 |
26 | constructor(private sanitizer: DomSanitizer) {}
27 |
28 | ngOnChanges() {
29 | this.href = this.sanitizer.bypassSecurityTrustResourceUrl(
30 | buildURI(this.data, this.uFEFF, this.headers, this.delimiter),
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/1.0/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular-devkit/build-angular/plugins/karma'),
14 | ],
15 | client: {
16 | clearContext: false, // leave Jasmine Spec Runner output visible in browser
17 | },
18 | coverageIstanbulReporter: {
19 | dir: require('path').join(__dirname, './coverage/zzz'),
20 | reports: ['html', 'lcovonly', 'text-summary'],
21 | fixWebpackSourcePaths: true,
22 | },
23 | reporters: ['coverage-istanbul', 'progress', 'kjhtml'],
24 | port: 9876,
25 | colors: true,
26 | logLevel: config.LOG_INFO,
27 | autoWatch: true,
28 | browsers: ['Chrome'],
29 | customLaunchers: {
30 | ChromeCI: {
31 | base: `${process.env['TRAVIS'] ? 'ChromeHeadless' : 'Chrome'}`,
32 | flags: process.env['TRAVIS'] ? ['--no-sandbox'] : [],
33 | },
34 | },
35 | singleRun: false,
36 | restartOnFileChange: true,
37 | });
38 | };
39 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "ignorePatterns": ["projects/**/*"],
4 | "overrides": [
5 | {
6 | "files": ["*.ts"],
7 | "parserOptions": {
8 | "project": ["tsconfig.json"],
9 | "createDefaultProgram": true
10 | },
11 | "extends": [
12 | "plugin:@angular-eslint/ng-cli-compat",
13 | "plugin:@angular-eslint/ng-cli-compat--formatting-add-on",
14 | "plugin:@angular-eslint/template/process-inline-templates"
15 | ],
16 | "rules": {
17 | "id-blacklist": "off",
18 | "@typescript-eslint/member-ordering": "off",
19 | "@typescript-eslint/naming-convention": "off",
20 | "@angular-eslint/component-class-suffix": "off",
21 | "@angular-eslint/component-selector": [
22 | "off",
23 | {
24 | "type": "element",
25 | "prefix": "app",
26 | "style": "kebab-case"
27 | }
28 | ],
29 | "@angular-eslint/directive-class-suffix": "off",
30 | "@angular-eslint/directive-selector": [
31 | "off",
32 | {
33 | "type": "attribute",
34 | "prefix": "app",
35 | "style": "camelCase"
36 | }
37 | ]
38 | }
39 | },
40 | {
41 | "files": ["*.html"],
42 | "extends": ["plugin:@angular-eslint/template/recommended"],
43 | "rules": {}
44 | }
45 | ]
46 | }
47 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | @import "~bootstrap/scss/functions";
2 | @import "~bootstrap/scss/variables";
3 | @import "~bootstrap/scss/mixins";
4 | @import "~bootstrap/scss/root";
5 | @import "~bootstrap/scss/reboot";
6 | @import "~bootstrap/scss/type";
7 | // @import "~bootstrap/scss/images";
8 | @import "~bootstrap/scss/code";
9 | @import "~bootstrap/scss/grid";
10 | // @import "~bootstrap/scss/tables";
11 | // @import "~bootstrap/scss/forms";
12 | @import "~bootstrap/scss/buttons";
13 | @import "~bootstrap/scss/transitions";
14 | // @import "~bootstrap/scss/dropdown";
15 | // @import "~bootstrap/scss/button-group";
16 | // @import "~bootstrap/scss/input-group";
17 | // @import "~bootstrap/scss/custom-forms";
18 | // @import "~bootstrap/scss/nav";
19 | // @import "~bootstrap/scss/navbar";
20 | // @import "~bootstrap/scss/card";
21 | // @import "~bootstrap/scss/breadcrumb";
22 | // @import "~bootstrap/scss/pagination";
23 | // @import "~bootstrap/scss/badge";
24 | // @import "~bootstrap/scss/jumbotron";
25 | // @import "~bootstrap/scss/alert";
26 | // @import "~bootstrap/scss/progress";
27 | // @import "~bootstrap/scss/media";
28 | // @import "~bootstrap/scss/list-group";
29 | // @import "~bootstrap/scss/close";
30 | // @import "~bootstrap/scss/modal";
31 | // @import "~bootstrap/scss/tooltip";
32 | // @import "~bootstrap/scss/popover";
33 | // @import "~bootstrap/scss/carousel";
34 | @import "~bootstrap/scss/utilities";
35 | @import "~bootstrap/scss/print";
36 |
--------------------------------------------------------------------------------
/src/lib/csv.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { Component, NgModule } from '@angular/core';
2 | import { TestBed, waitForAsync } from '@angular/core/testing';
3 |
4 | import { CsvModule } from './csv.module';
5 |
6 | @Component({
7 | selector: 'test-component',
8 | template: `
9 |
10 | `,
11 | })
12 | export class TestComponent {
13 | data = [
14 | { firstname: 'Ahmed', lastname: 'Tomi', email: 'ah@smthing.co.com' },
15 | { firstname: 'Raed', lastname: 'Labes', email: 'rl@smthing.co.com' },
16 | { firstname: 'Yezzi', lastname: 'Min l3b', email: 'ymin@cocococo.com' },
17 | ];
18 | }
19 |
20 | @NgModule({
21 | imports: [CsvModule],
22 | declarations: [TestComponent],
23 | })
24 | export class NameModule {}
25 |
26 | describe('CsvDirective', () => {
27 | beforeEach(waitForAsync(() => {
28 | TestBed.configureTestingModule({
29 | imports: [NameModule],
30 | }).compileComponents();
31 | }));
32 | it('should create the app', waitForAsync(() => {
33 | const fixture = TestBed.createComponent(TestComponent);
34 | const app = fixture.debugElement.componentInstance;
35 | expect(app).toBeTruthy();
36 | }));
37 | it('should set href', waitForAsync(() => {
38 | const fixture = TestBed.createComponent(TestComponent);
39 | fixture.detectChanges();
40 | const compiled = fixture.debugElement.nativeElement;
41 | const link = compiled.querySelector('a') as HTMLElement;
42 | expect(link.getAttribute('href')).toContain('blob:');
43 | }));
44 | });
45 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ctrl/ngx-csv",
3 | "version": "0.0.0",
4 | "license": "MIT",
5 | "repository": "scttcper/ngx-csv",
6 | "scripts": {
7 | "ng": "ng",
8 | "start": "ng serve",
9 | "prebuild": "del-cli dist",
10 | "build": "ng-packagr -p src/lib/ng-package.json",
11 | "postbuild": "cpy README.md LICENSE dist",
12 | "test": "ng test --browsers=ChromeCI --watch=false",
13 | "test:ci": "ng test --watch=false --code-coverage --no-progress",
14 | "lint": "ng lint",
15 | "lint:fix": "ng lint --fix",
16 | "ghpages": "ng build --configuration production --no-progress"
17 | },
18 | "private": true,
19 | "dependencies": {},
20 | "devDependencies": {
21 | "@angular-devkit/build-angular": "14.0.2",
22 | "@angular-eslint/builder": "13.5.0",
23 | "@angular-eslint/eslint-plugin": "13.5.0",
24 | "@angular-eslint/eslint-plugin-template": "13.5.0",
25 | "@angular-eslint/template-parser": "13.5.0",
26 | "@angular/animations": "14.0.2",
27 | "@angular/cli": "14.0.2",
28 | "@angular/common": "14.0.2",
29 | "@angular/compiler-cli": "14.0.2",
30 | "@angular/compiler": "14.0.2",
31 | "@angular/core": "14.0.2",
32 | "@angular/forms": "14.0.2",
33 | "@angular/language-service": "14.0.2",
34 | "@angular/platform-browser-dynamic": "14.0.2",
35 | "@angular/platform-browser": "14.0.2",
36 | "@angular/router": "14.0.2",
37 | "@ctrl/ngx-github-buttons": "7.1.0",
38 | "@types/jasmine": "4.0.3",
39 | "@types/node": "18.0.0",
40 | "@typescript-eslint/eslint-plugin": "5.28.0",
41 | "@typescript-eslint/parser": "5.28.0",
42 | "bootstrap": "4.5.0",
43 | "core-js": "3.23.1",
44 | "cpy-cli": "4.1.0",
45 | "del-cli": "4.0.1",
46 | "eslint": "8.18.0",
47 | "eslint-plugin-import": "2.26.0",
48 | "eslint-plugin-jsdoc": "39.3.3",
49 | "eslint-plugin-prefer-arrow": "1.2.3",
50 | "jasmine-core": "4.2.0",
51 | "karma": "6.4.0",
52 | "karma-chrome-launcher": "3.1.1",
53 | "karma-cli": "2.0.0",
54 | "karma-coverage-istanbul-reporter": "3.0.3",
55 | "karma-jasmine": "5.1.0",
56 | "karma-jasmine-html-reporter": "2.0.0",
57 | "ng-packagr": "14.0.2",
58 | "rxjs": "7.5.5",
59 | "tslib": "2.4.0",
60 | "typescript": "4.7.4",
61 | "zone.js": "0.11.6"
62 | },
63 | "release": {
64 | "branches": ["master"]
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This file includes polyfills needed by Angular and is loaded before the app.
3 | * You can add your own extra polyfills to this file.
4 | *
5 | * This file is divided into 2 sections:
6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main
8 | * file.
9 | *
10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that
11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
13 | *
14 | * Learn more in https://angular.io/guide/browser-support
15 | */
16 |
17 | /***************************************************************************************************
18 | * BROWSER POLYFILLS
19 | */
20 |
21 | /**
22 | * By default, zone.js will patch all possible macroTask and DomEvents
23 | * user can disable parts of macroTask/DomEvents patch by setting following flags
24 | * because those flags need to be set before `zone.js` being loaded, and webpack
25 | * will put import in the top of bundle, so user need to create a separate file
26 | * in this directory (for example: zone-flags.ts), and put the following flags
27 | * into that file, and then add the following code before importing zone.js.
28 | * import './zone-flags.ts';
29 | *
30 | * The flags allowed in zone-flags.ts are listed here.
31 | *
32 | * The following flags will work for all browsers.
33 | *
34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
37 | *
38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js
39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge
40 | *
41 | * (window as any).__Zone_enable_cross_context_check = true;
42 | *
43 | */
44 |
45 | /***************************************************************************************************
46 | * Zone JS is required by default for Angular itself.
47 | */
48 | import 'zone.js'; // Included with Angular CLI.
49 |
50 |
51 | /***************************************************************************************************
52 | * APPLICATION IMPORTS
53 | */
54 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # @ctrl/ngx-csv
2 |
3 | [](https://www.npmjs.com/package/@ctrl/ngx-csv)
4 | [](https://circleci.com/gh/scttcper/ngx-csv)
5 | [](https://codecov.io/gh/scttcper/ngx-csv)
6 |
7 | > Easily generate a CSV download in the browser with Angular
8 |
9 | **Demo**: https://ngx-csv.vercel.app
10 |
11 | ### Install
12 |
13 | ```sh
14 | npm install @ctrl/ngx-csv
15 | ```
16 |
17 | ## Dependencies
18 |
19 | Latest version available for each version of Angular
20 |
21 | | ngx-csv | Angular |
22 | | ------- | ---------- |
23 | | 2.1.1 | 8.x |
24 | | 3.0.1 | 9.x |
25 | | 4.0.0 | 10.x |
26 | | 5.0.0 | 12.x, 13.x |
27 | | current | >= 14.x |
28 |
29 | ### Import
30 |
31 | ```ts
32 | import { CsvModule } from '@ctrl/ngx-csv';
33 | ```
34 |
35 | ### Use
36 |
37 | Add the csvLink directive to your `` tag
38 |
39 | ```html
40 | Download
41 | ```
42 |
43 | ### Input
44 |
45 | - **data**: The body of the csv
46 | - **headers**: Set the first line of the csv
47 | - **delimiter**: Set the seperator between values. Default `','`
48 | - **filename**: Set the filename of the csv. Default `data.csv`
49 | - **uFEFF**: Adds a Byte order mark to setup the csv as UTF-8. Default `true`
50 | - **target**: Element target. Default `\_blank
51 |
52 | ### Accepted Data Formats
53 |
54 | ##### Array of objects
55 |
56 | keys are used as the column headers
57 |
58 | ```ts
59 | const data = [
60 | { firstname: 'Ahmed', lastname: 'Tomi', email: 'ah@smthing.co.com' },
61 | { firstname: 'Raed', lastname: 'Labes', email: 'rl@smthing.co.com' },
62 | { firstname: 'Yezzi', lastname: 'Min l3b', email: 'ymin@cocococo.com' },
63 | ];
64 | ```
65 |
66 | ##### Array of strings
67 |
68 | first line used as headers if not supplied
69 |
70 | ```ts
71 | const data = [
72 | ['firstname', 'lastname', 'email'],
73 | ['Ahmed', 'Tomi', 'ah@smthing.co.com'],
74 | ['Raed', 'Labes', 'rl@smthing.co.com'],
75 | ['Yezzi', 'Min l3b', 'ymin@cocococo.com'],
76 | ];
77 | ```
78 |
79 | ##### String
80 |
81 | An already formatted csv from an outside source
82 |
83 | ```ts
84 | const data = `firstname,lastname
85 | Ahmed,Tomi
86 | Raed,Labes
87 | Yezzi,Min l3b
88 | `;
89 | ```
90 |
91 | ##### Array of objects with custom headers
92 |
93 | provided headers
94 |
95 | ```ts
96 | const headers = [
97 | { label: 'First Name', key: 'firstname' },
98 | { label: 'Last Name', key: 'lastname' },
99 | { label: 'Email', key: 'email' },
100 | ];
101 |
102 | const data = [
103 | { firstname: 'Ahmed', lastname: 'Tomi', email: 'ah@smthing.co.com' },
104 | { firstname: 'Raed', lastname: 'Labes', email: 'rl@smthing.co.com' },
105 | { firstname: 'Yezzi', lastname: 'Min l3b', email: 'ymin@cocococo.com' },
106 | ];
107 | ```
108 |
109 | ### See Also
110 |
111 | - [react-csv](https://github.com/abdennour/react-csv)
112 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 | ngx-csv
21 |
22 |
Easily generate a CSV download in the browser with Angular
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
Install
31 |
npm install @ctrl/ngx-csv
32 |
33 |
34 |
35 |
36 |
37 |
38 |
Import
39 |
import {{ '{' }} CsvModule } from '@ctrl/ngx-csv';
42 |
43 |
44 |
45 |
46 |
47 |
48 |
Use
49 |
<a csvLink [data]="data">Download</a>
52 |
53 |
54 |
55 |
56 |
57 |
58 |
Examples
59 |
60 |
61 |
Array of objects
62 |
[
63 | {{ '{' }} firstname: 'Ahmed', lastname: 'Tomi', email: 'ah@smthing.co.com' },
64 | {{ '{' }} firstname: 'Raed', lastname: 'Labes', email: 'rl@smthing.co.com' },
65 | {{ '{' }} firstname: 'Yezzi', lastname: 'Min l3b', email: 'ymin@cocococo.com' },
66 | ]
67 |
68 |
69 | Download
70 |
71 |
72 |
73 |
74 |
Array of array of strings
75 |
[
76 | ['firstname', 'lastname', 'email'] ,
77 | ['Ahmed', 'Tomi' , 'ah@smthing.co.com'] ,
78 | ['Raed', 'Labes' , 'rl@smthing.co.com'] ,
79 | ['Yezzi', 'Min l3b', 'ymin@cocococo.com'],
80 | ]
81 |
82 |
83 | Download
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/src/lib/util.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable prefer-arrow/prefer-arrow-functions */
2 | export interface HeaderObj {
3 | label: string;
4 | key: string;
5 | }
6 |
7 | export const isJsons = (array: any[]) =>
8 | Array.isArray(array) &&
9 | array.every(row => typeof row === 'object' && !(row instanceof Array));
10 |
11 | export const isArrays = (array: any[]) =>
12 | Array.isArray(array) && array.every(row => Array.isArray(row));
13 |
14 | export function jsonsHeaders(array: object[]) {
15 | return Array.from(
16 | new Set(
17 | array.map(item => Object.keys(item)).reduce((a, b) => [...a, ...b], []),
18 | ),
19 | );
20 | }
21 |
22 | export function jsons2arrays(
23 | jsons: { [key: string]: string }[],
24 | headers?: string[] | HeaderObj[],
25 | ) {
26 | headers = headers || jsonsHeaders(jsons);
27 |
28 | // allow headers to have custom labels, defaulting to having the header data key be the label
29 | let headerLabels: string[] = headers as string[];
30 | let headerKeys: string[] = headers as string[];
31 | if (isJsons(headers)) {
32 | headerLabels = (headers as HeaderObj[]).map(header => header.label);
33 | headerKeys = (headers as HeaderObj[]).map(header => header.key);
34 | }
35 |
36 | const data = jsons.map(object =>
37 | headerKeys.map(header => (header in object ? object[header] : '')),
38 | );
39 | return [headerLabels, ...data];
40 | }
41 |
42 | export const elementOrEmpty = (element: any) =>
43 | element || element === 0 ? element : '';
44 |
45 | export function joiner(data: any, delimiter = ',') {
46 | return data
47 | .map((row: any, index: number) =>
48 | row.map((element: any) => '"' + elementOrEmpty(element) + '"').join(delimiter),
49 | )
50 | .join(`\n`);
51 | }
52 |
53 | export function arrays2csv(
54 | data: string[][],
55 | headers?: string[] | HeaderObj[],
56 | delimiter?: string,
57 | ) {
58 | return joiner(headers ? [headers, ...data] : data, delimiter);
59 | }
60 |
61 | export function jsons2csv(
62 | data: { [key: string]: string }[],
63 | headers?: string[] | HeaderObj[],
64 | delimiter?: string,
65 | ) {
66 | return joiner(jsons2arrays(data, headers), delimiter);
67 | }
68 |
69 | export function string2csv(
70 | data: string,
71 | headers?: string[],
72 | delimiter?: string,
73 | ) {
74 | return headers ? `${headers.join(delimiter)}\n${data}` : data;
75 | }
76 |
77 | export function toCSV(
78 | data: string | string[][] | { [key: string]: string }[] | any[],
79 | headers?: string[] | HeaderObj[],
80 | delimiter?: string,
81 | ) {
82 | if (isJsons(data as any)) {
83 | return jsons2csv(data as { [key: string]: string }[], headers, delimiter);
84 | }
85 | if (isArrays(data as any)) {
86 | return arrays2csv(data as string[][], headers, delimiter);
87 | }
88 | if (typeof data === 'string') {
89 | return string2csv(data, headers as string[], delimiter);
90 | }
91 | throw new TypeError(
92 | `Data should be a "String", "Array of arrays" OR "Array of objects" `,
93 | );
94 | }
95 |
96 | export function blob(
97 | data: string | string[][] | { [key: string]: string }[] | any[],
98 | uFEFF = true,
99 | headers?: string[] | HeaderObj[],
100 | delimiter?: string,
101 | ) {
102 | const csv = toCSV(data, headers, delimiter);
103 | return new Blob([uFEFF ? '\uFEFF' : '', csv], { type: 'text/csv' });
104 | }
105 |
106 | export function buildURI(
107 | data: string | string[][] | { [key: string]: string }[] | any[],
108 | uFEFF = true,
109 | headers?: string[] | HeaderObj[],
110 | delimiter?: string,
111 | ) {
112 | return URL.createObjectURL(blob(data, uFEFF, headers, delimiter));
113 | }
114 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "ngx-csv": {
7 | "projectType": "application",
8 | "schematics": {
9 | "@schematics/angular:component": {
10 | "style": "scss"
11 | },
12 | "@schematics/angular:application": {
13 | "strict": true
14 | }
15 | },
16 | "root": "",
17 | "sourceRoot": "src",
18 | "prefix": "app",
19 | "architect": {
20 | "build": {
21 | "builder": "@angular-devkit/build-angular:browser",
22 | "options": {
23 | "outputPath": "dist",
24 | "index": "src/index.html",
25 | "main": "src/main.ts",
26 | "polyfills": "src/polyfills.ts",
27 | "tsConfig": "tsconfig.app.json",
28 | "assets": [
29 | "src/favicon.ico",
30 | "src/assets"
31 | ],
32 | "styles": [
33 | "src/styles.scss"
34 | ],
35 | "scripts": [],
36 | "vendorChunk": true,
37 | "extractLicenses": false,
38 | "buildOptimizer": false,
39 | "sourceMap": true,
40 | "optimization": false,
41 | "namedChunks": true
42 | },
43 | "configurations": {
44 | "production": {
45 | "fileReplacements": [
46 | {
47 | "replace": "src/environments/environment.ts",
48 | "with": "src/environments/environment.prod.ts"
49 | }
50 | ],
51 | "optimization": true,
52 | "outputHashing": "all",
53 | "sourceMap": false,
54 | "namedChunks": false,
55 | "extractLicenses": true,
56 | "vendorChunk": false,
57 | "buildOptimizer": true,
58 | "budgets": [
59 | {
60 | "type": "initial",
61 | "maximumWarning": "500kb",
62 | "maximumError": "1mb"
63 | },
64 | {
65 | "type": "anyComponentStyle",
66 | "maximumWarning": "2kb",
67 | "maximumError": "4kb"
68 | }
69 | ]
70 | }
71 | }
72 | },
73 | "serve": {
74 | "builder": "@angular-devkit/build-angular:dev-server",
75 | "options": {
76 | "browserTarget": "ngx-csv:build"
77 | },
78 | "configurations": {
79 | "production": {
80 | "browserTarget": "ngx-csv:build:production"
81 | }
82 | }
83 | },
84 | "extract-i18n": {
85 | "builder": "@angular-devkit/build-angular:extract-i18n",
86 | "options": {
87 | "browserTarget": "ngx-csv:build"
88 | }
89 | },
90 | "test": {
91 | "builder": "@angular-devkit/build-angular:karma",
92 | "options": {
93 | "main": "src/test.ts",
94 | "polyfills": "src/polyfills.ts",
95 | "tsConfig": "tsconfig.spec.json",
96 | "karmaConfig": "karma.conf.js",
97 | "assets": [
98 | "src/favicon.ico",
99 | "src/assets"
100 | ],
101 | "styles": [
102 | "src/styles.scss"
103 | ],
104 | "scripts": []
105 | }
106 | },
107 | "lint": {
108 | "builder": "@angular-eslint/builder:lint",
109 | "options": {
110 | "lintFilePatterns": [
111 | "src/**/*.ts",
112 | "src/**/*.html"
113 | ]
114 | }
115 | }
116 | }
117 | }
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/src/lib/util.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | arrays2csv,
3 | blob,
4 | buildURI,
5 | isArrays,
6 | isJsons,
7 | jsons2arrays,
8 | jsons2csv,
9 | jsonsHeaders,
10 | string2csv,
11 | toCSV,
12 | } from './util';
13 |
14 | describe(`core::isJsons`, () => {
15 | it(`returns true if all items of array are literal objects`, () => {
16 | const target = [{}, {}, {}, {}];
17 | expect(isJsons(target)).toBeTruthy();
18 | });
19 | it(`returns false if one of array items is not literal object`, () => {
20 | let target = ['', {}, {}, {}];
21 | expect(isJsons(target)).toBeFalsy();
22 | target = [{}, [], {}, {}];
23 | expect(isJsons(target)).toBeFalsy();
24 | });
25 | });
26 |
27 | describe(`core::isArrays`, () => {
28 | it(`retruns true if all array items are arrays too`, () => {
29 | const target = [[], [], [], []];
30 | expect(isArrays(target)).toBeTruthy();
31 | });
32 | it(`retruns false if one of array items is not array`, () => {
33 | let target = [{}, [], [], []];
34 | expect(isArrays(target)).toBeFalsy();
35 | target = [[], new Set([]), [], []];
36 | expect(isArrays(target)).toBeFalsy();
37 | target = [[], '[]', [], []];
38 | expect(isArrays(target)).toBeFalsy();
39 | });
40 | });
41 |
42 | describe(`core::jsonsHeaders`, () => {
43 | const fixtures = [
44 | {
45 | maths: 90,
46 | phy: 80,
47 | },
48 | {
49 | maths: 50,
50 | sport: 97,
51 | ch: 66,
52 | },
53 | {
54 | ch: 77,
55 | sport: 99,
56 | },
57 | ];
58 |
59 | it(`returns union of keys of all array items `, () => {
60 | const actual = jsonsHeaders(fixtures);
61 | expect(actual).toEqual([`maths`, `phy`, 'sport', 'ch']);
62 | });
63 | it(`does not duplicate keys on the array`, () => {
64 | const actual = jsonsHeaders(fixtures);
65 | const keysOfAllItems = fixtures
66 | .map(object => Object.keys(object))
67 | .reduce((a, b) => [...a, ...b], []);
68 | expect(actual.length).toBeLessThanOrEqual(keysOfAllItems.length);
69 | expect(actual.length).toEqual(new Set(keysOfAllItems).size);
70 | });
71 | });
72 | describe(`core::jsons2arrays`, () => {
73 | const fixtures: any[] = [
74 | {
75 | maths: '90',
76 | },
77 | {
78 | sport: '97',
79 | },
80 | {
81 | maths: '77',
82 | sport: 0,
83 | },
84 | ];
85 | it(`converts an Array of literal objects to Array of arrays`, () => {
86 | const actual = jsons2arrays(fixtures);
87 | const expected: any[] = [
88 | ['maths', 'sport'],
89 | ['90', ''],
90 | ['', '97'],
91 | ['77', 0],
92 | ];
93 | expect(actual).toEqual(expected);
94 | });
95 |
96 | it(`converts to Array of arrays following the order of headers`, () => {
97 | const actual = jsons2arrays(fixtures, ['sport', 'maths']);
98 | const expected: any[] = [
99 | ['maths', 'sport'].reverse(),
100 | ['90', ''].reverse(),
101 | ['', '97'].reverse(),
102 | ['77', 0].reverse(),
103 | ];
104 | expect(actual).toEqual(expected);
105 | });
106 | it(`accepts any size of headers list`, () => {
107 | const headers = ['maths', 'sport', 'phy', 'ch'];
108 | const actual = jsons2arrays(fixtures, headers);
109 | const expected: any[] = [headers, ['90', '', '', ''], ['', '97', '', ''], ['77', 0, '', '']];
110 | expect(actual).toEqual(expected);
111 | });
112 | });
113 |
114 | describe(`core::arrays2csv`, () => {
115 | const fixtures = [
116 | [`a`, `b`],
117 | [`c`, `d`],
118 | ];
119 | it(`converts Array of arrays to string in CSV format`, () => {
120 | const actual = arrays2csv(fixtures);
121 | expect(typeof actual).toBe('string');
122 | expect(actual.split(`\n`).join(`|`)).toEqual(`"a","b"|"c","d"`);
123 | });
124 |
125 | it(`renders CSV headers whenever it was given `, () => {
126 | const headers = [`X`, `Y`];
127 | const firstLineOfCSV = arrays2csv(fixtures, headers).split(`\n`)[0];
128 | expect(firstLineOfCSV).toEqual(`"X","Y"`);
129 | });
130 | });
131 |
132 | describe(`core::jsons2csv`, () => {
133 | const fixtures = [
134 | {
135 | X: '88',
136 | Y: '97',
137 | },
138 | {
139 | X: '77',
140 | Y: '99',
141 | },
142 | ];
143 |
144 | it(`converts Array of literal objects to string in CSV format including headers`, () => {
145 | const actual = jsons2csv(fixtures);
146 | const expected = `"X","Y"|"88","97"|"77","99"`;
147 |
148 | expect(typeof actual).toEqual('string');
149 | expect(actual.split(`\n`).join(`|`)).toEqual(expected);
150 | });
151 |
152 | it(`renders CSV string according to order of given headers`, () => {
153 | const otherfixtures = [
154 | { X: '12', Y: 'bb' },
155 | { Y: 'ee', X: '55' },
156 | ];
157 | const headers = ['Y', 'X', 'Z'];
158 | const actual = jsons2csv(otherfixtures, headers);
159 | expect(actual.startsWith(`"Y","X","Z"`)).toBeTruthy();
160 | expect(actual.endsWith(`"ee","55",""`)).toBeTruthy();
161 | });
162 |
163 | it(`converts Array of literal objects to string in CSV format including custom header labels`, () => {
164 | const customHeaders = [
165 | { label: 'Letter X', key: 'X' },
166 | { label: 'Letter Y', key: 'Y' },
167 | ];
168 |
169 | const actual = jsons2csv(fixtures, customHeaders);
170 |
171 | expect(typeof actual).toEqual('string');
172 | expect(actual.startsWith(`"Letter X","Letter Y"`)).toBeTruthy();
173 | expect(actual.endsWith(`"77","99"`)).toBeTruthy();
174 | });
175 | });
176 |
177 | describe(`core::string2csv`, () => {
178 | const fixtures = `33,44\n55,66`;
179 | it(`returns the same string if no header given`, () => {
180 | expect(string2csv(fixtures)).toEqual(fixtures);
181 | });
182 |
183 | it(`prepends headers at the top of input`, () => {
184 | const headers = [`X`, `Y`];
185 | expect(string2csv(fixtures, headers)).toEqual(`X,Y\n${fixtures}`);
186 | });
187 | });
188 |
189 | describe(`core::toCSV`, () => {
190 | const fixtures = { string: 'Xy', arrays: [[], []], jsons: [{}, {}] };
191 | // it(`requires one argument at least`, () => {
192 | // expect(() => toCSV()).toThrow();
193 | // });
194 |
195 | it(`accepts data as "Array" of jsons `, () => {
196 | expect(() => toCSV(fixtures.jsons)).toBeTruthy();
197 | });
198 |
199 | it(`accepts data as "Array" of arrays `, () => {
200 | expect(() => toCSV(fixtures.arrays)).toBeTruthy();
201 | });
202 |
203 | it(`accepts data as "string" `, () => {
204 | expect(() => toCSV(fixtures.string)).toBeTruthy();
205 | });
206 | });
207 |
208 | describe(`core::blob`, () => {
209 | const fixtures = {
210 | string: 'Xy',
211 | arrays: [
212 | ['a', 'b'],
213 | ['c', 'd'],
214 | ],
215 | jsons: [{}, {}],
216 | };
217 |
218 | it(`creates blob instance`, () => {
219 | expect(blob(fixtures.arrays)).toBeTruthy();
220 | expect(blob(fixtures.string)).toBeTruthy();
221 | });
222 | });
223 |
224 | describe(`core::buildURI`, () => {
225 | const fixtures = {
226 | string: 'Xy',
227 | arrays: [
228 | ['a', 'b'],
229 | ['c', 'd'],
230 | ],
231 | jsons: [{}, {}],
232 | };
233 |
234 | it(`generates URI to download data in CSV format`, () => {
235 | const prefixCsvURI = `blob:`;
236 | expect(buildURI(fixtures.jsons, false).startsWith(prefixCsvURI)).toBeTruthy();
237 | expect(buildURI(fixtures.arrays).startsWith(prefixCsvURI)).toBeTruthy();
238 | expect(buildURI(fixtures.string).startsWith(prefixCsvURI)).toBeTruthy();
239 | });
240 | });
241 |
--------------------------------------------------------------------------------