├── .browserslistrc ├── .circleci └── config.yml ├── .codecov.yml ├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .nvmrc ├── .prettierrc ├── LICENSE ├── README.md ├── angular.json ├── karma.conf.js ├── package-lock.json ├── package.json ├── src ├── app │ ├── app.component.html │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ └── download-svg │ │ ├── download-svg.component.html │ │ └── download-svg.component.ts ├── assets │ └── .gitkeep ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── lib │ ├── csv.component.spec.ts │ ├── csv.directive.ts │ ├── csv.module.ts │ ├── ng-package.json │ ├── package.json │ ├── public_api.ts │ ├── util.spec.ts │ └── util.ts ├── main.ts ├── polyfills.ts ├── styles.scss └── test.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json └── vercel.json /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "bracketSpacing": true, 6 | "printWidth": 100, 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @ctrl/ngx-csv 2 | 3 | [![npm](https://badgen.net/npm/v/@ctrl/ngx-csv)](https://www.npmjs.com/package/@ctrl/ngx-csv) 4 | [![CircleCI](https://badgen.net/circleci/github/scttcper/ngx-csv)](https://circleci.com/gh/scttcper/ngx-csv) 5 | [![coverage](https://badgen.net/codecov/c/github/scttcper/ngx-csv)](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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/download-svg/download-svg.component.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /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/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scttcper/ngx-csv/34d9dcb5340ee8b54db9535388ed2988fceb5a17/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /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/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scttcper/ngx-csv/34d9dcb5340ee8b54db9535388ed2988fceb5a17/src/favicon.ico -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/lib/public_api.ts: -------------------------------------------------------------------------------- 1 | export { CsvModule } from './csv.module'; 2 | export { CsvDirective } from './csv.directive'; 3 | export * from './util'; 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | } 5 | } 6 | --------------------------------------------------------------------------------