├── .editorconfig ├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ ├── create-tag.yml │ ├── deploy-npm.yml │ ├── main-flow.yml │ └── validate.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── angular.json ├── apps ├── .gitkeep ├── demo-e2e │ ├── .eslintrc.json │ ├── cypress.json │ ├── project.json │ ├── src │ │ ├── fixtures │ │ │ └── example.json │ │ ├── integration │ │ │ └── app.spec.ts │ │ └── support │ │ │ ├── app.po.ts │ │ │ ├── commands.ts │ │ │ └── index.ts │ └── tsconfig.json ├── demo-packaged │ ├── project.json │ └── tsconfig.packaged.json └── demo │ ├── .browserslistrc │ ├── .eslintrc.json │ ├── karma.conf.js │ ├── project.json │ ├── src │ ├── .nojekyll │ ├── app │ │ ├── app.component.css │ │ ├── app.component.html │ │ ├── app.component.spec.ts │ │ ├── app.component.ts │ │ ├── app.module.ts │ │ ├── appearance │ │ │ ├── appearance.component.css │ │ │ ├── appearance.component.html │ │ │ ├── appearance.component.spec.ts │ │ │ └── appearance.component.ts │ │ ├── code-sample │ │ │ ├── code-sample.component.html │ │ │ ├── code-sample.component.scss │ │ │ ├── code-sample.component.spec.ts │ │ │ └── code-sample.component.ts │ │ ├── menu │ │ │ ├── menu.component.css │ │ │ ├── menu.component.html │ │ │ ├── menu.component.spec.ts │ │ │ └── menu.component.ts │ │ ├── usage │ │ │ ├── usage.component.css │ │ │ ├── usage.component.html │ │ │ ├── usage.component.spec.ts │ │ │ └── usage.component.ts │ │ └── utils │ │ │ └── example-error-state-matcher.ts │ ├── assets │ │ └── .gitkeep │ ├── environments │ │ ├── environment.prod.ts │ │ └── environment.ts │ ├── favicon.ico │ ├── index.html │ ├── main.ts │ ├── polyfills.ts │ ├── styles.scss │ └── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.packaged.json │ └── tsconfig.spec.json ├── decorate-angular-cli.js ├── karma.conf.js ├── libs ├── .gitkeep └── material-file-input │ ├── .eslintrc.json │ ├── karma.conf.js │ ├── ng-package.json │ ├── ng-package.prod.json │ ├── package.json │ ├── project.json │ ├── src │ ├── index.ts │ ├── lib │ │ ├── file-input │ │ │ ├── file-input-mixin.ts │ │ │ ├── file-input.component.css │ │ │ ├── file-input.component.html │ │ │ ├── file-input.component.spec.ts │ │ │ └── file-input.component.ts │ │ ├── material-file-input.module.spec.ts │ │ ├── material-file-input.module.ts │ │ ├── model │ │ │ ├── file-input-config.model.ts │ │ │ ├── file-input.model.spec.ts │ │ │ └── file-input.model.ts │ │ ├── pipe │ │ │ ├── byte-format.pipe.spec.ts │ │ │ └── byte-format.pipe.ts │ │ └── validator │ │ │ ├── file-validator.spec.ts │ │ │ └── file-validator.ts │ └── test.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ ├── tsconfig.lib.prod.json │ └── tsconfig.spec.json ├── nx.json ├── package-lock.json ├── package.json ├── tools ├── generators │ └── .gitkeep └── tsconfig.tools.json ├── tsconfig.base.json └── tsconfig.packaged.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://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 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nrwl/nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nrwl/nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ], 22 | "no-empty-function": "warn" 23 | } 24 | }, 25 | { 26 | "files": ["*.ts", "*.tsx"], 27 | "extends": ["plugin:@nrwl/nx/typescript"], 28 | "rules": {} 29 | }, 30 | { 31 | "files": ["*.js", "*.jsx"], 32 | "extends": ["plugin:@nrwl/nx/javascript"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [merlosy] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/create-tag.yml: -------------------------------------------------------------------------------- 1 | name: Create new tag 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | increment: 7 | description: 'Increment for version' 8 | required: true 9 | default: 'patch' 10 | type: choice 11 | options: 12 | - prepatch 13 | - patch 14 | - preminor 15 | - minor 16 | - premajor 17 | - major 18 | 19 | jobs: 20 | check: 21 | uses: ./.github/workflows/validate.yml 22 | create-tag: 23 | runs-on: ubuntu-latest 24 | # if Running on the default branch 25 | if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) 26 | needs: check 27 | environment: deploy # env needed to access PAT 28 | permissions: 29 | contents: write 30 | steps: 31 | - uses: actions/checkout@v3 32 | with: 33 | ref: ${{ github.head_ref }} 34 | token: ${{ secrets.PAT }} 35 | fetch-depth: 0 36 | - uses: actions/setup-node@v3 37 | with: 38 | node-version: 16 39 | # Compute new version for monorepo (not tagged) 40 | - name: Set version for monorepo root 41 | run: npm --no-git-tag-version version "${{ github.event.inputs.increment }}" 42 | - name: Set version for library 43 | run: | 44 | cd libs/material-file-input/ 45 | npm --no-git-tag-version version "${{ github.event.inputs.increment }}" 46 | - name: Get newly set monorepo version 47 | id: repo-version 48 | uses: martinbeentjes/npm-get-version-action@main 49 | - run: echo "${{ steps.repo-version.outputs.current-version }}" 50 | # Apply changes > Git 51 | - name: Setup git config 52 | run: | 53 | git config user.name "GitHub Actions Bot" 54 | git config user.email "<>" 55 | - name: Commit changes with tag 56 | run: | 57 | git add package.json package-lock.json libs/material-file-input/package.json 58 | git commit -m "Release version ${{ steps.repo-version.outputs.current-version }}" 59 | git push 60 | git tag -a "v${{ steps.repo-version.outputs.current-version }}" -m "Tag version ${{ steps.repo-version.outputs.current-version }}" 61 | git push --tags 62 | - name: Build library for publish 63 | run: | 64 | npm ci --no-audit 65 | npm run build:lib 66 | - name: Publish to npm 67 | id: publish 68 | uses: JS-DevTools/npm-publish@v1 69 | with: 70 | token: ${{ secrets.NPM_TOKEN }} 71 | package: ./dist/material-file-input/package.json 72 | # dry-run: true 73 | - if: steps.publish.outputs.type != 'none' 74 | run: | 75 | echo "Version changed: ${{ steps.publish.outputs.old-version }} => ${{ steps.publish.outputs.version }}" 76 | 77 | create-release: 78 | needs: create-tag 79 | runs-on: ubuntu-latest 80 | # if Running on the default branch 81 | if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) 82 | permissions: 83 | contents: write 84 | steps: 85 | - uses: actions/checkout@v3 86 | - name: Get the new tag version 87 | id: tag_version 88 | run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} 89 | - name: Create draft release 90 | uses: ncipollo/release-action@v1 91 | with: 92 | tag: ${{ steps.tag_version.outputs.VERSION }} 93 | token: ${{ secrets.GITHUB_TOKEN }} 94 | draft: true 95 | -------------------------------------------------------------------------------- /.github/workflows/deploy-npm.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to NPM 2 | 3 | on: 4 | # push: 5 | # tags: 6 | # - '*' 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | environment: deploy 12 | # if Running on the default branch 13 | if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: 16 19 | - run: npm ci --no-audit 20 | - run: npm run build:lib 21 | - name: Publish to npm 22 | id: publish 23 | uses: JS-DevTools/npm-publish@v1 24 | with: 25 | token: ${{ secrets.NPM_TOKEN }} 26 | # dry-run: true 27 | - if: steps.publish.outputs.type != 'none' 28 | run: | 29 | echo "Version changed: ${{ steps.publish.outputs.old-version }} => ${{ steps.publish.outputs.version }}" 30 | 31 | create-release: 32 | runs-on: ubuntu-latest 33 | # if Running on the default branch 34 | if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) 35 | permissions: 36 | contents: write 37 | steps: 38 | - uses: actions/checkout@v3 39 | - name: Get the new tag version 40 | id: tag_version 41 | run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} 42 | - name: Create draft release 43 | uses: ncipollo/release-action@v1 44 | with: 45 | tag: ${{ steps.tag_version.outputs.VERSION }} 46 | token: ${{ secrets.GITHUB_TOKEN }} 47 | draft: true 48 | -------------------------------------------------------------------------------- /.github/workflows/main-flow.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy demo app 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | check: 9 | uses: ./.github/workflows/validate.yml 10 | deploy-ghpages: 11 | needs: check 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | - run: npm ci --no-audit 19 | - run: npm run build:lib 20 | - run: npm run build:demo 21 | - name: Deploy to GitHub Pages 22 | uses: crazy-max/ghaction-github-pages@v3 23 | with: 24 | target_branch: gh-pages 25 | build_dir: dist/apps/demo-packaged 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 28 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | push: 7 | branches: 8 | - "*" 9 | - "!master" # Done through workflow call 10 | workflow_call: 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node: [14, 16, 18] 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Use Node.js ${{ matrix.node }} 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: ${{ matrix.node }} 24 | - run: npm ci --no-audit 25 | - run: npm run lint 26 | 27 | unit-test: 28 | runs-on: ubuntu-latest 29 | strategy: 30 | matrix: 31 | node: [14, 16, 18] 32 | steps: 33 | - uses: actions/checkout@v3 34 | - name: Use Node.js ${{ matrix.node }} 35 | uses: actions/setup-node@v3 36 | with: 37 | node-version: ${{ matrix.node }} 38 | - run: npm ci --no-audit 39 | - run: npm run test:once -- --code-coverage 40 | - uses: actions/upload-artifact@v3 41 | with: 42 | name: material-file-input-${{ matrix.node }}-coverage 43 | path: coverage/libs/material-file-input 44 | retention-days: 7 45 | 46 | build-lib: 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v3 50 | - uses: actions/setup-node@v3 51 | with: 52 | node-version: 16 53 | - run: npm ci --no-audit 54 | - run: npm run build:lib 55 | - uses: actions/upload-artifact@v3 56 | with: 57 | name: material-file-input-build 58 | path: dist/material-file-input 59 | if-no-files-found: error 60 | retention-days: 7 61 | 62 | build-demo: 63 | needs: build-lib 64 | runs-on: ubuntu-latest 65 | steps: 66 | - uses: actions/checkout@v3 67 | - uses: actions/setup-node@v3 68 | with: 69 | node-version: 16 70 | - run: npm ci --no-audit 71 | - uses: actions/download-artifact@v3 72 | with: 73 | name: material-file-input-build 74 | path: dist/material-file-input 75 | - run: npm run build:demo 76 | - name: Upload packaged demo 77 | uses: actions/upload-artifact@v3 78 | with: 79 | name: demo-build 80 | path: dist/apps/demo-packaged 81 | if-no-files-found: error 82 | retention-days: 7 83 | 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.angular/cache 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | yarn-error.log 35 | testem.log 36 | /typings 37 | 38 | # e2e 39 | /e2e/*.js 40 | /e2e/*.map 41 | 42 | # System Files 43 | .DS_Store 44 | Thumbs.db 45 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 140 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["nrwl.angular-console", "esbenp.prettier-vscode", "angular.ng-template", "redhat.vscode-yaml"] 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Jeremy Legros 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/ngx-material-file-input.svg)](https://badge.fury.io/js/ngx-material-file-input) 2 | [![Build Status](https://app.travis-ci.com/merlosy/ngx-material-file-input.svg?branch=master)](https://app.travis-ci.com/merlosy/ngx-material-file-input) 3 | [![npm](https://img.shields.io/npm/dt/ngx-material-file-input.svg)](https://www.npmjs.com/package/ngx-material-file-input) 4 | [![](http://img.badgesize.io/https://unpkg.com/ngx-material-file-input@latest/bundles/ngx-material-file-input.umd.min.js?label=full%20size%20as%20min.js&compression=gzip&style=square&color=02adff)](https://www.npmjs.com/package/ngx-material-file-input) 5 | [![Coverage Status](https://coveralls.io/repos/github/merlosy/ngx-material-file-input/badge.svg)](https://coveralls.io/github/merlosy/ngx-material-file-input) 6 | [![Known Vulnerabilities](https://snyk.io/test/github/merlosy/ngx-material-file-input/badge.svg)](https://snyk.io/test/github/merlosy/ngx-material-file-input) 7 | 8 | # material-file-input 9 | 10 | This project provides : 11 | 12 | * `ngx-mat-file-input` component, to use inside Angular Material `mat-form-field` 13 | * a `FileValidator` with `maxContentSize`, to limit the file size 14 | * a `ByteFormatPipe` to format the file size in a human-readable format 15 | 16 | For more code samples, have a look at the [DEMO SITE](https://merlosy.github.io/ngx-material-file-input) 17 | 18 | ## Install 19 | 20 | ``` 21 | npm i ngx-material-file-input 22 | ``` 23 | 24 | ## API reference 25 | 26 | ### MaterialFileInputModule 27 | 28 | ```ts 29 | import { MaterialFileInputModule } from 'ngx-material-file-input'; 30 | 31 | @NgModule({ 32 | imports: [ 33 | // the module for this lib 34 | MaterialFileInputModule 35 | ] 36 | }) 37 | ``` 38 | 39 | #### NGX_MAT_FILE_INPUT_CONFIG token (optional): 40 | 41 | Change the unit of the ByteFormat pipe 42 | 43 | ```ts 44 | export const config: FileInputConfig = { 45 | sizeUnit: 'Octet' 46 | }; 47 | 48 | // add with module injection 49 | providers: [{ provide: NGX_MAT_FILE_INPUT_CONFIG, useValue: config }]; 50 | ``` 51 | 52 | ### FileInputComponent 53 | 54 | selector: `` 55 | 56 | implements: [MatFormFieldControl](https://material.angular.io/components/form-field/api#MatFormFieldControl) from Angular Material 57 | 58 | **Additionnal properties** 59 | 60 | | Name | Description | 61 | | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | 62 | | _@Input()_ valuePlaceholder: `string` | Placeholder for file names, empty by default | 63 | | _@Input()_ multiple: `boolean` | Allows multiple file inputs, `false` by default | 64 | | _@Input()_ autofilled: `boolean` | Whether the input is currently in an autofilled state. If property is not present on the control it is assumed to be false. | 65 | | _@Input()_ accept: `string` | Any value that `accept` attribute can get. [more about "accept"](https://www.w3schools.com/tags/att_input_accept.asp) | 66 | | value: `FileInput` | Form control value | 67 | | empty: `boolean` | Whether the input is empty (no files) or not | 68 | | clear(): `(event?) => void` | Removes all files from the input | 69 | 70 | ### ByteFormatPipe 71 | 72 | **Example** 73 | 74 | ```html 75 | {{ 104857600 | byteFormat }} 76 | ``` 77 | 78 | _Output:_ 100 MB 79 | 80 | ### FileValidator 81 | 82 | | Name | Description | Error structure | 83 | | ---------------------------------------------- | ----------------------------------------------- | ----------------------------------------- | 84 | | maxContentSize(value: `number`): `ValidatorFn` | Limit the total file(s) size to the given value | `{ actualSize: number, maxSize: number }` | 85 | 86 | # About me 87 | 88 | [@jereyleg](https://twitter.com/jereyleg) 89 | 90 | ☆ to show support :) 91 | 92 | # Roadmap 93 | 94 | * drop event to add files 95 | * _ideas?_ 96 | 97 | # Kudos to 98 | 99 | * https://github.com/dherges/ng-packagr 100 | * Jason Aden - Packaging Angular Libraries https://www.youtube.com/watch?v=QfvwQEJVOig 101 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "newProjectRoot": "projects", 4 | "projects": { 5 | "demo": "apps/demo", 6 | "demo-e2e": "apps/demo-e2e", 7 | "demo-packaged": "apps/demo-packaged", 8 | "material-file-input": "libs/material-file-input" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/demo-e2e/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["plugin:cypress/recommended", "../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["src/plugins/index.js"], 11 | "rules": { 12 | "@typescript-eslint/no-var-requires": "off", 13 | "no-undef": "off" 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /apps/demo-e2e/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileServerFolder": ".", 3 | "fixturesFolder": "./src/fixtures", 4 | "integrationFolder": "./src/integration", 5 | "modifyObstructiveCode": false, 6 | "supportFile": "./src/support/index.ts", 7 | "pluginsFile": false, 8 | "video": true, 9 | "videosFolder": "../../dist/cypress/apps/test-e2e/videos", 10 | "screenshotsFolder": "../../dist/cypress/apps/test-e2e/screenshots", 11 | "chromeWebSecurity": false 12 | } 13 | -------------------------------------------------------------------------------- /apps/demo-e2e/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceRoot": "apps/demo-e2e/src", 3 | "projectType": "application", 4 | "targets": { 5 | "e2e": { 6 | "executor": "@nrwl/cypress:cypress", 7 | "options": { 8 | "cypressConfig": "apps/demo-e2e/cypress.json", 9 | "devServerTarget": "demo:serve:development" 10 | }, 11 | "configurations": { 12 | "production": { 13 | "devServerTarget": "demo:serve:production" 14 | } 15 | } 16 | }, 17 | "lint": { 18 | "executor": "@nrwl/linter:eslint", 19 | "outputs": ["{options.outputFile}"], 20 | "options": { 21 | "lintFilePatterns": ["apps/demo-e2e/**/*.{js,ts}"] 22 | } 23 | } 24 | }, 25 | "tags": [], 26 | "implicitDependencies": ["demo"] 27 | } 28 | -------------------------------------------------------------------------------- /apps/demo-e2e/src/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io" 4 | } 5 | -------------------------------------------------------------------------------- /apps/demo-e2e/src/integration/app.spec.ts: -------------------------------------------------------------------------------- 1 | import { getGreeting } from '../support/app.po'; 2 | 3 | describe('test', () => { 4 | beforeEach(() => cy.visit('/')); 5 | 6 | it('should display welcome message', () => { 7 | // Custom command example, see `../support/commands.ts` file 8 | cy.login('my-email@something.com', 'myPassword'); 9 | 10 | // Function helper example, see `../support/app.po.ts` file 11 | getGreeting().contains('Welcome to test!'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /apps/demo-e2e/src/support/app.po.ts: -------------------------------------------------------------------------------- 1 | export const getGreeting = () => cy.get('h1'); 2 | -------------------------------------------------------------------------------- /apps/demo-e2e/src/support/commands.ts: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | 11 | // eslint-disable-next-line @typescript-eslint/no-namespace 12 | declare namespace Cypress { 13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 14 | interface Chainable { 15 | login(email: string, password: string): void; 16 | } 17 | } 18 | // 19 | // -- This is a parent command -- 20 | Cypress.Commands.add('login', (email, password) => { 21 | console.log('Custom command example: Login', email, password); 22 | }); 23 | // 24 | // -- This is a child command -- 25 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 26 | // 27 | // 28 | // -- This is a dual command -- 29 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 30 | // 31 | // 32 | // -- This will overwrite an existing command -- 33 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 34 | -------------------------------------------------------------------------------- /apps/demo-e2e/src/support/index.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | -------------------------------------------------------------------------------- /apps/demo-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "sourceMap": false, 5 | "outDir": "../../dist/out-tsc", 6 | "allowJs": true, 7 | "types": ["cypress", "node"], 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true 12 | }, 13 | "include": ["src/**/*.ts", "src/**/*.js"], 14 | "angularCompilerOptions": { 15 | "strictInjectionParameters": true, 16 | "strictInputAccessModifiers": true, 17 | "strictTemplates": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /apps/demo-packaged/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceRoot": "apps/demo/src", 3 | "projectType": "application", 4 | "prefix": "app", 5 | "generators": {}, 6 | "targets": { 7 | "build": { 8 | "executor": "@angular-devkit/build-angular:browser", 9 | "options": { 10 | "outputPath": "dist/apps/demo-packaged", 11 | "index": "apps/demo/src/index.html", 12 | "main": "apps/demo/src/main.ts", 13 | "tsConfig": "apps/demo/tsconfig.packaged.json", 14 | "polyfills": "apps/demo/src/polyfills.ts", 15 | "assets": ["apps/demo/src/assets", "apps/demo/src/favicon.ico"], 16 | "styles": ["apps/demo/src/styles.scss"], 17 | "scripts": [], 18 | "vendorChunk": true, 19 | "extractLicenses": false, 20 | "buildOptimizer": false, 21 | "sourceMap": true, 22 | "optimization": false, 23 | "namedChunks": true 24 | }, 25 | "configurations": { 26 | "production": { 27 | "budgets": [ 28 | { 29 | "type": "anyComponentStyle", 30 | "maximumWarning": "6kb" 31 | } 32 | ], 33 | "optimization": true, 34 | "outputHashing": "all", 35 | "sourceMap": false, 36 | "namedChunks": false, 37 | "extractLicenses": true, 38 | "vendorChunk": false, 39 | "buildOptimizer": true, 40 | "fileReplacements": [ 41 | { 42 | "replace": "apps/demo/src/environments/environment.ts", 43 | "with": "apps/demo/src/environments/environment.prod.ts" 44 | } 45 | ] 46 | } 47 | }, 48 | "defaultConfiguration": "", 49 | "outputs": ["{options.outputPath}"] 50 | }, 51 | "serve": { 52 | "executor": "@angular-devkit/build-angular:dev-server", 53 | "options": { 54 | "browserTarget": "demo-packaged:build" 55 | }, 56 | "configurations": { 57 | "production": { 58 | "browserTarget": "demo-packaged:build:production" 59 | } 60 | } 61 | }, 62 | "extract-i18n": { 63 | "executor": "@angular-devkit/build-angular:extract-i18n", 64 | "options": { 65 | "browserTarget": "demo-packaged:build" 66 | } 67 | }, 68 | "test": { 69 | "executor": "@angular-devkit/build-angular:karma", 70 | "options": { 71 | "main": "apps/demo/src/../../../test.js", 72 | "polyfills": "apps/demo/src/polyfills.ts", 73 | "tsConfig": "apps/demo/tsconfig.spec.json", 74 | "karmaConfig": "apps/demo/karma.conf.js", 75 | "scripts": [], 76 | "styles": ["apps/demo/src/styles.scss"], 77 | "assets": ["apps/demo/src/assets", "apps/demo/src/favicon.ico"] 78 | }, 79 | "outputs": ["coverage/apps/demo-packaged"] 80 | } 81 | }, 82 | "tags": [] 83 | } 84 | -------------------------------------------------------------------------------- /apps/demo-packaged/tsconfig.packaged.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.packaged.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/apps/demo-packaged" 5 | }, 6 | "include": [ 7 | "../demo/**/*.ts" 8 | ], 9 | "exclude": [ 10 | "../demo/src/test.ts", 11 | "../demo/**/*.spec.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /apps/demo/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | # IE 9-11 -------------------------------------------------------------------------------- /apps/demo/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "app", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "app", 25 | "style": "kebab-case" 26 | } 27 | ] 28 | } 29 | }, 30 | { 31 | "files": ["*.html"], 32 | "extends": ["plugin:@nrwl/nx/angular-template"], 33 | "rules": {} 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /apps/demo/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'), 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/apps/demo'), 20 | subdir: '.', 21 | reports: [ 22 | { type: 'html' }, 23 | { type: 'text-summary' } 24 | ] 25 | }, 26 | reporters: ['progress', 'kjhtml'], 27 | port: 9876, 28 | colors: true, 29 | logLevel: config.LOG_INFO, 30 | autoWatch: true, 31 | browsers: ['Chrome'], 32 | singleRun: false, 33 | restartOnFileChange: true 34 | }); 35 | }; 36 | -------------------------------------------------------------------------------- /apps/demo/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceRoot": "apps/demo/src", 3 | "projectType": "application", 4 | "prefix": "app", 5 | "generators": {}, 6 | "targets": { 7 | "build": { 8 | "executor": "@angular-devkit/build-angular:browser", 9 | "outputs": ["{options.outputPath}"], 10 | "options": { 11 | "outputPath": "dist/apps/demo", 12 | "index": "apps/demo/src/index.html", 13 | "main": "apps/demo/src/main.ts", 14 | "polyfills": "apps/demo/src/polyfills.ts", 15 | "tsConfig": "apps/demo/tsconfig.app.json", 16 | "inlineStyleLanguage": "scss", 17 | "assets": ["apps/demo/src/favicon.ico", "apps/demo/src/assets"], 18 | "styles": ["apps/demo/src/styles.scss"], 19 | "scripts": [] 20 | }, 21 | "configurations": { 22 | "production": { 23 | "budgets": [ 24 | { 25 | "type": "initial", 26 | "maximumWarning": "500kb", 27 | "maximumError": "1mb" 28 | }, 29 | { 30 | "type": "anyComponentStyle", 31 | "maximumWarning": "2kb", 32 | "maximumError": "4kb" 33 | } 34 | ], 35 | "fileReplacements": [ 36 | { 37 | "replace": "apps/demo/src/environments/environment.ts", 38 | "with": "apps/demo/src/environments/environment.prod.ts" 39 | } 40 | ], 41 | "outputHashing": "all" 42 | }, 43 | "development": { 44 | "buildOptimizer": false, 45 | "optimization": false, 46 | "vendorChunk": true, 47 | "extractLicenses": false, 48 | "sourceMap": true, 49 | "namedChunks": true 50 | } 51 | }, 52 | "defaultConfiguration": "production" 53 | }, 54 | "serve": { 55 | "executor": "@angular-devkit/build-angular:dev-server", 56 | "configurations": { 57 | "production": { 58 | "browserTarget": "demo:build:production" 59 | }, 60 | "development": { 61 | "browserTarget": "demo:build:development" 62 | } 63 | }, 64 | "defaultConfiguration": "development" 65 | }, 66 | "extract-i18n": { 67 | "executor": "@angular-devkit/build-angular:extract-i18n", 68 | "options": { 69 | "browserTarget": "demo:build" 70 | } 71 | }, 72 | "lint": { 73 | "executor": "@nrwl/linter:eslint", 74 | "options": { 75 | "lintFilePatterns": ["apps/demo/src/**/*.ts", "apps/demo/src/**/*.html"] 76 | } 77 | }, 78 | "test": { 79 | "executor": "@angular-devkit/build-angular:karma", 80 | "options": { 81 | "main": "apps/demo/src/test.ts", 82 | "polyfills": "apps/demo/src/polyfills.ts", 83 | "tsConfig": "apps/demo/tsconfig.spec.json", 84 | "karmaConfig": "apps/demo/karma.conf.js", 85 | "styles": ["apps/demo/src/styles.scss"], 86 | "scripts": [], 87 | "assets": ["apps/demo/src/favicon.ico", "apps/demo/src/assets"] 88 | }, 89 | "outputs": ["coverage/apps/demo"] 90 | } 91 | }, 92 | "tags": [] 93 | } 94 | -------------------------------------------------------------------------------- /apps/demo/src/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merlosy/ngx-material-file-input/1b40f64e3e33439e945bde49962cb32f64d10119/apps/demo/src/.nojekyll -------------------------------------------------------------------------------- /apps/demo/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | .toolbar-title { 2 | margin-left: 16px; 3 | } 4 | 5 | .sidenav-content { 6 | height: calc(100vh - 64px); 7 | overflow: auto; 8 | } 9 | 10 | .main { 11 | margin: 0 auto; 12 | padding: 15px; 13 | max-width: 900px; 14 | } 15 | -------------------------------------------------------------------------------- /apps/demo/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | Angular Material - File Input 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 |
17 | 18 |
19 |
20 | 21 |
22 | -------------------------------------------------------------------------------- /apps/demo/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { MatToolbarModule } from '@angular/material/toolbar'; 3 | import { MatInputModule } from '@angular/material/input'; 4 | import { MatIconModule } from '@angular/material/icon'; 5 | import { MatFormFieldModule } from '@angular/material/form-field'; 6 | import { MatButtonModule } from '@angular/material/button'; 7 | import { ReactiveFormsModule } from '@angular/forms'; 8 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 9 | 10 | import { MaterialFileInputModule } from 'ngx-material-file-input'; 11 | import { AppComponent } from './app.component'; 12 | import { CodeSampleComponent } from './code-sample/code-sample.component'; 13 | 14 | describe('AppComponent', () => { 15 | let component: AppComponent; 16 | let fixture: ComponentFixture; 17 | 18 | beforeEach( 19 | waitForAsync(() => { 20 | TestBed.configureTestingModule({ 21 | declarations: [AppComponent, CodeSampleComponent], 22 | imports: [ 23 | ReactiveFormsModule, 24 | NoopAnimationsModule, 25 | // Material modules 26 | MatButtonModule, 27 | MatFormFieldModule, 28 | MatIconModule, 29 | MatInputModule, 30 | MatToolbarModule, 31 | // Lib Module 32 | MaterialFileInputModule 33 | ] 34 | }).compileComponents(); 35 | }) 36 | ); 37 | 38 | beforeEach(() => { 39 | fixture = TestBed.createComponent(AppComponent); 40 | component = fixture.componentInstance; 41 | fixture.detectChanges(); 42 | }); 43 | 44 | it('should create', () => { 45 | expect(component).toBeTruthy(); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /apps/demo/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { BreakpointObserver, BreakpointState } from '@angular/cdk/layout'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | templateUrl: './app.component.html', 7 | styleUrls: ['./app.component.css'] 8 | }) 9 | export class AppComponent implements OnInit { 10 | 11 | menuOpened = true; 12 | 13 | menuDisplayMode: 'over' | 'push' | 'side'; 14 | 15 | constructor(public breakpointObserver: BreakpointObserver){} 16 | 17 | ngOnInit(): void { 18 | // Improve experience for mobile demo 19 | this.breakpointObserver 20 | .observe('(max-width: 992px)') 21 | .subscribe((state: BreakpointState) => { 22 | if (state.matches) { 23 | this.menuDisplayMode = 'over'; 24 | } else { 25 | this.menuDisplayMode = 'side'; 26 | } 27 | }); 28 | } 29 | 30 | onNavigate() { 31 | if (this.menuDisplayMode === 'over') { 32 | this.menuOpened = false; 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /apps/demo/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { BrowserModule } from '@angular/platform-browser'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { MatToolbarModule } from '@angular/material/toolbar'; 5 | import { MatInputModule } from '@angular/material/input'; 6 | import { MatIconModule } from '@angular/material/icon'; 7 | import { MatFormFieldModule } from '@angular/material/form-field'; 8 | import { MatButtonModule } from '@angular/material/button'; 9 | import { ReactiveFormsModule } from '@angular/forms'; 10 | 11 | import { AppComponent } from './app.component'; 12 | import { MaterialFileInputModule } from 'ngx-material-file-input'; 13 | import { CodeSampleComponent } from './code-sample/code-sample.component'; 14 | import { MenuComponent } from './menu/menu.component'; 15 | import { UsageComponent } from './usage/usage.component'; 16 | import { MatSidenavModule } from '@angular/material/sidenav'; 17 | import { MatListModule } from '@angular/material/list'; 18 | import { AppearanceComponent } from './appearance/appearance.component'; 19 | 20 | @NgModule({ 21 | imports: [ 22 | BrowserModule, 23 | BrowserAnimationsModule, 24 | ReactiveFormsModule, 25 | // Material modules 26 | MatButtonModule, 27 | MatFormFieldModule, 28 | MatIconModule, 29 | MatInputModule, 30 | MatListModule, 31 | MatSidenavModule, 32 | MatToolbarModule, 33 | // Lib Module 34 | MaterialFileInputModule 35 | ], 36 | declarations: [ 37 | AppComponent, 38 | AppearanceComponent, 39 | CodeSampleComponent, 40 | MenuComponent, 41 | UsageComponent 42 | ], 43 | bootstrap: [AppComponent] 44 | }) 45 | export class AppModule {} 46 | -------------------------------------------------------------------------------- /apps/demo/src/app/appearance/appearance.component.css: -------------------------------------------------------------------------------- 1 | mat-form-field { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /apps/demo/src/app/appearance/appearance.component.html: -------------------------------------------------------------------------------- 1 |

Form field appearance variants

2 |

3 | File input appearance is fully compatible with Angular Material form field appearance. 6 |

7 |

8 | There are significant differences between the legacy variant and the 3 newer ones (see link above). Especially, 9 | "standard, fill, and outline appearances do not promote placeholders to labels." This means you'll need to add the 10 | "mat-label" element if you want to show some text. 11 |

12 | 13 |
14 | 15 |

Legacy (default)

16 | 17 | 18 | 19 | 20 | 21 | folder 22 | 23 | 24 |

Legacy (default) with mat-label

25 | 26 | 27 | 28 | 29 | Basic legacy input 30 | 31 | folder 32 | 33 | 34 |

Standard

35 | 36 | 37 | 38 | 39 | Basic standard input 40 | 41 | folder 42 | 43 | 44 |

Fill

45 | 46 | 47 | 48 | 49 | Basic fill input 50 | 51 | folder 52 | 53 | 54 |

Outline

55 | 56 | 57 | 58 | 59 | Basic outline input 60 | 61 | folder 62 | 63 | 64 |
65 | -------------------------------------------------------------------------------- /apps/demo/src/app/appearance/appearance.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { ReactiveFormsModule } from '@angular/forms'; 3 | import { MaterialFileInputModule } from 'ngx-material-file-input'; 4 | import { AppearanceComponent } from './appearance.component'; 5 | 6 | describe('AppearanceComponent', () => { 7 | let component: AppearanceComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach( 11 | waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [AppearanceComponent], 14 | imports: [ReactiveFormsModule, MaterialFileInputModule] 15 | }).compileComponents(); 16 | }) 17 | ); 18 | 19 | beforeEach(() => { 20 | fixture = TestBed.createComponent(AppearanceComponent); 21 | component = fixture.componentInstance; 22 | fixture.detectChanges(); 23 | }); 24 | 25 | it('should create', () => { 26 | expect(component).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /apps/demo/src/app/appearance/appearance.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormBuilder, FormGroup } from '@angular/forms'; 3 | 4 | @Component({ 5 | selector: 'app-appearance', 6 | templateUrl: './appearance.component.html', 7 | styleUrls: ['./appearance.component.css'] 8 | }) 9 | export class AppearanceComponent implements OnInit { 10 | formDoc: FormGroup; 11 | 12 | constructor(private _fb: FormBuilder) {} 13 | 14 | ngOnInit() { 15 | this.formDoc = this._fb.group({ 16 | legacyNoLabel: [], 17 | legacy: [], 18 | standard: [], 19 | fill: [], 20 | outline: [] 21 | }); 22 | } 23 | 24 | get legacyNoLabel() { 25 | return ` 26 | 27 | folder 28 | `; 29 | } 30 | 31 | get legacy() { 32 | return ` 33 | Basic legacy input 34 | 35 | folder 36 | `; 37 | } 38 | 39 | get standard() { 40 | return ` 41 | Basic standard input 42 | 43 | folder 44 | `; 45 | } 46 | 47 | get fill() { 48 | return ` 49 | Basic fill input 50 | 51 | folder 52 | `; 53 | } 54 | 55 | get outline() { 56 | return ` 57 | Basic outline input 58 | 59 | folder 60 | `; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /apps/demo/src/app/code-sample/code-sample.component.html: -------------------------------------------------------------------------------- 1 |
2 |   
3 |     {{innerCode}}
4 |   
5 | 
6 | -------------------------------------------------------------------------------- /apps/demo/src/app/code-sample/code-sample.component.scss: -------------------------------------------------------------------------------- 1 | pre { 2 | white-space: pre-wrap; 3 | background-color: #333; 4 | overflow: auto; 5 | } 6 | -------------------------------------------------------------------------------- /apps/demo/src/app/code-sample/code-sample.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { CodeSampleComponent } from './code-sample.component'; 4 | 5 | describe('CodeSampleComponent', () => { 6 | let component: CodeSampleComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ CodeSampleComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CodeSampleComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /apps/demo/src/app/code-sample/code-sample.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-code-sample', 5 | templateUrl: './code-sample.component.html', 6 | styleUrls: ['./code-sample.component.scss'], 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class CodeSampleComponent { 10 | innerCode: string; 11 | 12 | @Input() 13 | set code(value: string) { 14 | this.innerCode = value; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /apps/demo/src/app/menu/menu.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merlosy/ngx-material-file-input/1b40f64e3e33439e945bde49962cb32f64d10119/apps/demo/src/app/menu/menu.component.css -------------------------------------------------------------------------------- /apps/demo/src/app/menu/menu.component.html: -------------------------------------------------------------------------------- 1 | 2 | Input examples 3 | 4 | Simple input 5 | With clear button 6 | With form validation 7 | With disabled state 8 | With multiple files 9 | With file type constraint (accept) 10 | With custom ErrorStateMatcher 11 | 12 | 13 | 14 | Other examples 15 | 16 | ByteFormat pipe 17 | Appearance variants 18 | 19 | -------------------------------------------------------------------------------- /apps/demo/src/app/menu/menu.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | 3 | import { MenuComponent } from './menu.component'; 4 | 5 | describe('MenuComponent', () => { 6 | let component: MenuComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(waitForAsync(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ MenuComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(MenuComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /apps/demo/src/app/menu/menu.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Output, EventEmitter } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-menu', 5 | templateUrl: './menu.component.html', 6 | styleUrls: ['./menu.component.css'] 7 | }) 8 | export class MenuComponent { 9 | 10 | @Output() navigate = new EventEmitter(); 11 | 12 | scrollTo(){ 13 | this.navigate.emit(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apps/demo/src/app/usage/usage.component.css: -------------------------------------------------------------------------------- 1 | mat-form-field { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /apps/demo/src/app/usage/usage.component.html: -------------------------------------------------------------------------------- 1 |

Code samples

2 | 3 |
4 | 5 |

Simple input

6 | 7 | 8 | 9 | 10 | 11 | folder 12 | 13 | 14 |

Input with clear button

15 | 16 |

17 | This is a workaround for an issue with Firefox that doesn't triggers change event when the user cancels the upload. 18 | With other browsers, it results in removing files from the input. 19 |

20 | 21 | 22 | 23 |

Add a file to reveal the clear button.

24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 |

Input with validation: required and maxSize

34 | 35 |

The maximum accepted file size in bytes (binary calculation: 2 ** x).

36 | 37 | 38 | 39 | 40 | 41 | 43 | folder 44 | Please select a file 45 | 46 | The total size must not exceed {{ formDoc.get('requiredfile')?.getError('maxContentSize').maxSize | byteFormat }} 47 | ({{ formDoc.get('requiredfile')?.getError('maxContentSize').actualSize | byteFormat }}). 48 | 49 | 50 |
{{ formDoc.get('requiredfile')?.errors | json }}
51 | 52 |

Disabled input

53 | 54 | 55 | 56 | 57 | 58 | folder 59 | 60 | 61 |

Multiple input

62 | 63 | 64 | 65 | 66 | 67 | folder 68 | 69 | 70 |

Input with file type constraint (accept)

71 | 72 |

The accept property matches the native accept attribute. 73 |

74 | 75 | 76 | 77 | 78 | 79 | folder 80 | 81 | 82 |

Input with custom ErrorStateMatcher

83 | 84 |

An ErrorStateMatcher defines when a control displays the error message. A custom ErrorStateMatcher can for example 85 | be used to display validations on untouched controls. 86 | ErrorStateMatchers can either be defined globally or for single controls (seen in the following example). 87 |

88 |

Learn more about custom ErrorStateMatcher 91 |

92 | 93 | 94 | 95 | 96 | 97 | 99 | 100 | 101 |

ByteFormat pipe

102 | 103 | 104 | 105 |

A file size of {{ maxSize }} gives a human readable size of {{ maxSize | byteFormat }}

106 |
107 | -------------------------------------------------------------------------------- /apps/demo/src/app/usage/usage.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; 2 | import { ReactiveFormsModule } from '@angular/forms'; 3 | import { MaterialFileInputModule } from 'ngx-material-file-input'; 4 | import { UsageComponent } from './usage.component'; 5 | 6 | describe('UsageComponent', () => { 7 | let component: UsageComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach( 11 | waitForAsync(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [UsageComponent], 14 | imports: [ReactiveFormsModule, MaterialFileInputModule] 15 | }).compileComponents(); 16 | }) 17 | ); 18 | 19 | beforeEach(() => { 20 | fixture = TestBed.createComponent(UsageComponent); 21 | component = fixture.componentInstance; 22 | fixture.detectChanges(); 23 | }); 24 | 25 | it('should create', () => { 26 | expect(component).toBeTruthy(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /apps/demo/src/app/usage/usage.component.ts: -------------------------------------------------------------------------------- 1 | import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { FileValidator } from 'ngx-material-file-input'; 4 | import { ExampleErrorStateMatcher } from '../utils/example-error-state-matcher'; 5 | 6 | @Component({ 7 | selector: 'app-usage', 8 | templateUrl: './usage.component.html', 9 | styleUrls: ['./usage.component.css'], 10 | changeDetection: ChangeDetectionStrategy.OnPush 11 | }) 12 | export class UsageComponent implements OnInit { 13 | errorStateMatcher = new ExampleErrorStateMatcher(); 14 | formDoc: FormGroup; 15 | 16 | // 100 MB 17 | readonly maxSize = 104857600; 18 | 19 | constructor(private _fb: FormBuilder) {} 20 | 21 | ngOnInit() { 22 | this.formDoc = this._fb.group({ 23 | basicfile: [], 24 | removablefile: [], 25 | acceptfile: [], 26 | requiredfile: [{ value: undefined, disabled: false }, [Validators.required, FileValidator.maxContentSize(this.maxSize)]], 27 | disabledfile: [{ value: undefined, disabled: true }], 28 | multiplefile: [{ value: undefined, disabled: false }], 29 | errorStateFile: [] 30 | }); 31 | } 32 | 33 | onSubmit(form: FormGroup) { 34 | // Send file 35 | } 36 | 37 | get simple() { 38 | return ` 39 | 40 | folder 41 | `; 42 | } 43 | 44 | get advancedTs() { 45 | return `import { FileValidator } from 'ngx-material-file-input'; 46 | 47 | [...] 48 | 49 | /** 50 | * In this example, it's 100 MB (=100 * 2 ** 20). 51 | */ 52 | readonly maxSize = 104857600; 53 | 54 | constructor(private _fb: FormBuilder) {} 55 | 56 | ngOnInit() { 57 | this.formDoc = this._fb.group({ 58 | requiredfile: [ 59 | undefined, 60 | [Validators.required, FileValidator.maxContentSize(this.maxSize)] 61 | ] 62 | }); 63 | }`; 64 | } 65 | 66 | get advanced() { 67 | return ` 68 | 69 | folder 70 | 71 | Please select a file 72 | 73 | 74 | The total size must not exceed {{formDoc.get('requiredfile')?.getError('maxContentSize').maxSize | byteFormat}} ({{formDoc.get('requiredfile')?.getError('maxContentSize').actualSize 75 | | byteFormat}}). 76 | 77 | `; 78 | } 79 | 80 | get disabledTs() { 81 | return `constructor(private _fb: FormBuilder) {} 82 | 83 | ngOnInit() { 84 | this.formDoc = this._fb.group({ 85 | disabledfile: [{ value: undefined, disabled: true }] 86 | }); 87 | }`; 88 | } 89 | 90 | get accept() { 91 | return ` 92 | 93 | folder 94 | `; 95 | } 96 | 97 | get multiple() { 98 | return ` 99 | 100 | folder 101 | `; 102 | } 103 | 104 | get removable() { 105 | return ` 106 | 107 | 110 | `; 111 | } 112 | 113 | get bytePipe() { 114 | return `

A file size of {{ maxSize }} gives a human readable size of {{ maxSize | byteFormat }}

`; 115 | } 116 | 117 | get errorStateTs() { 118 | return `class ExampleErrorStateMatcher implements ErrorStateMatcher { 119 | public isErrorState(control: FormControl, _: NgForm | FormGroupDirective): boolean { 120 | return 121 | (control && control.value && control.value._fileNames && control.value._fileNames.endsWith('pdf')); 122 | } 123 | }`; 124 | } 125 | get errorState() { 126 | return ` 127 | 129 | 130 | `; 131 | } 132 | 133 | } 134 | -------------------------------------------------------------------------------- /apps/demo/src/app/utils/example-error-state-matcher.ts: -------------------------------------------------------------------------------- 1 | import { ErrorStateMatcher } from '@angular/material/core'; 2 | import { FormControl, NgForm, FormGroupDirective } from '@angular/forms'; 3 | 4 | /** 5 | * Shows error state on the file-input if a pdf-file is selected. 6 | */ 7 | export class ExampleErrorStateMatcher implements ErrorStateMatcher { 8 | public isErrorState(control: FormControl, _: NgForm | FormGroupDirective): boolean { 9 | return (control && control.value && control.value._fileNames && control.value._fileNames.endsWith('pdf')); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/demo/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merlosy/ngx-material-file-input/1b40f64e3e33439e945bde49962cb32f64d10119/apps/demo/src/assets/.gitkeep -------------------------------------------------------------------------------- /apps/demo/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /apps/demo/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 | -------------------------------------------------------------------------------- /apps/demo/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merlosy/ngx-material-file-input/1b40f64e3e33439e945bde49962cb32f64d10119/apps/demo/src/favicon.ico -------------------------------------------------------------------------------- /apps/demo/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Angular Material File Input 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | Loading app... 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /apps/demo/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.log(err)); 14 | -------------------------------------------------------------------------------- /apps/demo/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 recent versions of Safari, Chrome (including 12 | * Opera), Edge on the desktop, and iOS 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'; 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 | * APPLICATION IMPORTS 52 | */ 53 | -------------------------------------------------------------------------------- /apps/demo/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import '@angular/material/prebuilt-themes/indigo-pink.css'; 3 | -------------------------------------------------------------------------------- /apps/demo/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /apps/demo/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": [] 6 | }, 7 | "files": ["src/main.ts", "src/polyfills.ts"], 8 | "include": ["**/*.d.ts"], 9 | "exclude": ["**/*.test.ts", "**/*.spec.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.app.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "forceConsistentCasingInFileNames": true, 15 | "strict": true, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": true 18 | }, 19 | "angularCompilerOptions": { 20 | "strictInjectionParameters": true, 21 | "strictInputAccessModifiers": true, 22 | "strictTemplates": true 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /apps/demo/tsconfig.packaged.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.packaged.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/apps/demo-packaged" 5 | }, 6 | "files": [ 7 | "src/main.ts", 8 | "src/polyfills.ts" 9 | ], 10 | "include": [ 11 | "**/*.d.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /apps/demo/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["jasmine", "node"] 6 | }, 7 | "files": ["src/test.ts", "src/polyfills.ts"], 8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /decorate-angular-cli.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file decorates the Angular CLI with the Nx CLI to enable features such as computation caching 3 | * and faster execution of tasks. 4 | * 5 | * It does this by: 6 | * 7 | * - Patching the Angular CLI to warn you in case you accidentally use the undecorated ng command. 8 | * - Symlinking the ng to nx command, so all commands run through the Nx CLI 9 | * - Updating the package.json postinstall script to give you control over this script 10 | * 11 | * The Nx CLI decorates the Angular CLI, so the Nx CLI is fully compatible with it. 12 | * Every command you run should work the same when using the Nx CLI, except faster. 13 | * 14 | * Because of symlinking you can still type `ng build/test/lint` in the terminal. The ng command, in this case, 15 | * will point to nx, which will perform optimizations before invoking ng. So the Angular CLI is always invoked. 16 | * The Nx CLI simply does some optimizations before invoking the Angular CLI. 17 | * 18 | * To opt out of this patch: 19 | * - Replace occurrences of nx with ng in your package.json 20 | * - Remove the script from your postinstall script in your package.json 21 | * - Delete and reinstall your node_modules 22 | */ 23 | 24 | const fs = require('fs'); 25 | const os = require('os'); 26 | const cp = require('child_process'); 27 | const isWindows = os.platform() === 'win32'; 28 | let output; 29 | try { 30 | output = require('@nrwl/workspace').output; 31 | } catch (e) { 32 | console.warn('Angular CLI could not be decorated to enable computation caching. Please ensure @nrwl/workspace is installed.'); 33 | process.exit(0); 34 | } 35 | 36 | /** 37 | * Symlink of ng to nx, so you can keep using `ng build/test/lint` and still 38 | * invoke the Nx CLI and get the benefits of computation caching. 39 | */ 40 | function symlinkNgCLItoNxCLI() { 41 | try { 42 | const ngPath = './node_modules/.bin/ng'; 43 | const nxPath = './node_modules/.bin/nx'; 44 | if (isWindows) { 45 | /** 46 | * This is the most reliable way to create symlink-like behavior on Windows. 47 | * Such that it works in all shells and works with npx. 48 | */ 49 | ['', '.cmd', '.ps1'].forEach(ext => { 50 | if (fs.existsSync(nxPath + ext)) fs.writeFileSync(ngPath + ext, fs.readFileSync(nxPath + ext)); 51 | }); 52 | } else { 53 | // If unix-based, symlink 54 | cp.execSync(`ln -sf ./nx ${ngPath}`); 55 | } 56 | } 57 | catch(e) { 58 | output.error({ title: 'Unable to create a symlink from the Angular CLI to the Nx CLI:' + e.message }); 59 | throw e; 60 | } 61 | } 62 | 63 | try { 64 | symlinkNgCLItoNxCLI(); 65 | require('@nrwl/cli/lib/decorate-cli').decorateCli(); 66 | output.log({ title: 'Angular CLI has been decorated to enable computation caching.' }); 67 | } catch(e) { 68 | output.error({ title: 'Decoration of the Angular CLI did not complete successfully' }); 69 | } 70 | -------------------------------------------------------------------------------- /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 | // const { makeSureNoAppIsSelected } = require('@nrwl/schematics/src/utils/cli-config-utils'); 5 | // Nx only supports running unit tests for all apps and libs. 6 | // makeSureNoAppIsSelected(); 7 | 8 | module.exports = function(config) { 9 | config.set({ 10 | basePath: '', 11 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 12 | plugins: [ 13 | require('karma-jasmine'), 14 | require('karma-chrome-launcher'), 15 | require('karma-jasmine-html-reporter'), 16 | require('karma-coverage'), 17 | require('@angular-devkit/build-angular/plugins/karma') 18 | ], 19 | client: { 20 | clearContext: false // leave Jasmine Spec Runner output visible in browser 21 | }, 22 | coverageReporter: { 23 | dir: require('path').join(__dirname, 'coverage'), 24 | reports: [ 25 | { type: 'html' }, 26 | { type: 'text-summary' } 27 | ] 28 | }, 29 | angularCli: { 30 | environment: 'dev' 31 | }, 32 | reporters: ['progress', 'kjhtml'], 33 | port: 9876, 34 | colors: true, 35 | logLevel: config.LOG_INFO, 36 | autoWatch: true, 37 | browsers: ['Chrome'], 38 | singleRun: false 39 | }); 40 | }; 41 | -------------------------------------------------------------------------------- /libs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merlosy/ngx-material-file-input/1b40f64e3e33439e945bde49962cb32f64d10119/libs/.gitkeep -------------------------------------------------------------------------------- /libs/material-file-input/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts"], 7 | "extends": [ 8 | "plugin:@nrwl/nx/angular", 9 | "plugin:@angular-eslint/template/process-inline-templates" 10 | ], 11 | "rules": { 12 | "@angular-eslint/directive-selector": [ 13 | "error", 14 | { 15 | "type": "attribute", 16 | "prefix": "ngx", 17 | "style": "camelCase" 18 | } 19 | ], 20 | "@angular-eslint/component-selector": [ 21 | "error", 22 | { 23 | "type": "element", 24 | "prefix": "ngx", 25 | "style": "kebab-case" 26 | } 27 | ], 28 | "no-empty-function": "warn", 29 | "@typescript-eslint/no-empty-function": "warn" 30 | } 31 | }, 32 | { 33 | "files": ["*.html"], 34 | "extends": ["plugin:@nrwl/nx/angular-template"], 35 | "rules": {} 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /libs/material-file-input/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'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageReporter: { 19 | dir: require('path').join(__dirname, '../../coverage/libs/material-file-input'), 20 | subdir: '.', 21 | reporters: [ 22 | { type: 'html' }, 23 | { type: 'lcovonly' }, 24 | { type: 'text-summary' } 25 | ] 26 | }, 27 | reporters: ['progress', 'kjhtml'], 28 | port: 9876, 29 | colors: true, 30 | logLevel: config.LOG_INFO, 31 | autoWatch: true, 32 | browsers: ['Chrome', 'ChromeHeadless'], 33 | singleRun: false, 34 | restartOnFileChange: true 35 | }); 36 | }; 37 | -------------------------------------------------------------------------------- /libs/material-file-input/ng-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/material-file-input", 4 | "deleteDestPath": false, 5 | "lib": { 6 | "entryFile": "src/index.ts" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /libs/material-file-input/ng-package.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", 3 | "dest": "../../dist/material-file-input", 4 | "lib": { 5 | "entryFile": "src/index.ts" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/material-file-input/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngx-material-file-input", 3 | "version": "4.0.1", 4 | "license": "MIT", 5 | "author": { 6 | "name": "Jeremy Legros" 7 | }, 8 | "description": "File input management for Angular Material", 9 | "homepage": "https://merlosy.github.io/ngx-material-file-input/", 10 | "keywords": [ 11 | "angular", 12 | "material", 13 | "file", 14 | "input", 15 | "mat-form-field" 16 | ], 17 | "bugs": { 18 | "url": "https://github.com/merlosy/ngx-material-file-input/issues" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/merlosy/ngx-material-file-input" 23 | }, 24 | "dependencies": {}, 25 | "peerDependencies": { 26 | "@angular/cdk": "^14.0.0", 27 | "@angular/common": "^14.0.0", 28 | "@angular/core": "^14.0.0", 29 | "@angular/material": "^14.0.0" 30 | }, 31 | "ngPackage": { 32 | "lib": { 33 | "entryFile": "src/index.ts" 34 | }, 35 | "dest": "../../dist/material-file-input" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /libs/material-file-input/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceRoot": "libs/material-file-input/src", 3 | "projectType": "library", 4 | "targets": { 5 | "build": { 6 | "executor": "@angular-devkit/build-angular:ng-packagr", 7 | "options": { 8 | "tsConfig": "libs/material-file-input/tsconfig.lib.json", 9 | "project": "libs/material-file-input/ng-package.json" 10 | }, 11 | "configurations": { 12 | "production": { 13 | "project": "libs/material-file-input/ng-package.prod.json", 14 | "tsConfig": "libs/material-file-input/tsconfig.lib.prod.json" 15 | } 16 | } 17 | }, 18 | "test": { 19 | "executor": "@angular-devkit/build-angular:karma", 20 | "options": { 21 | "main": "libs/material-file-input/src/test.ts", 22 | "tsConfig": "libs/material-file-input/tsconfig.spec.json", 23 | "karmaConfig": "libs/material-file-input/karma.conf.js" 24 | }, 25 | "outputs": ["coverage/libs/material-file-input"] 26 | }, 27 | "lint": { 28 | "executor": "@nrwl/linter:eslint", 29 | "options": { 30 | "lintFilePatterns": ["libs/material-file-input/src/**/*.ts", "libs/material-file-input/src/**/*.html"] 31 | } 32 | } 33 | }, 34 | "tags": [] 35 | } 36 | -------------------------------------------------------------------------------- /libs/material-file-input/src/index.ts: -------------------------------------------------------------------------------- 1 | // Module 2 | export { MaterialFileInputModule } from './lib/material-file-input.module'; 3 | 4 | // Model & Constant 5 | export { NGX_MAT_FILE_INPUT_CONFIG } from './lib/model/file-input-config.model'; 6 | export { FileInput } from './lib/model/file-input.model'; 7 | export { FileInputConfig } from './lib/model/file-input-config.model'; 8 | 9 | // Components 10 | export { FileInputComponent } from './lib/file-input/file-input.component'; 11 | 12 | // Filters 13 | export { ByteFormatPipe } from './lib/pipe/byte-format.pipe'; 14 | 15 | // Utilities 16 | export { FileValidator } from './lib/validator/file-validator'; 17 | -------------------------------------------------------------------------------- /libs/material-file-input/src/lib/file-input/file-input-mixin.ts: -------------------------------------------------------------------------------- 1 | import { FormGroupDirective, NgControl, NgForm } from '@angular/forms'; 2 | import { ErrorStateMatcher, mixinErrorState } from '@angular/material/core'; 3 | import { Subject } from "rxjs"; 4 | 5 | // Boilerplate for applying mixins to FileInput 6 | /** @docs-private */ 7 | export class FileInputBase { 8 | constructor( 9 | public _defaultErrorStateMatcher: ErrorStateMatcher, 10 | public _parentForm: NgForm, 11 | public _parentFormGroup: FormGroupDirective, 12 | public ngControl: NgControl, 13 | public stateChanges: Subject 14 | ) {} 15 | } 16 | 17 | /** 18 | * Allows to use a custom ErrorStateMatcher with the file-input component 19 | */ 20 | export const FileInputMixinBase = mixinErrorState(FileInputBase); 21 | -------------------------------------------------------------------------------- /libs/material-file-input/src/lib/file-input/file-input.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: inline-block; 3 | width: 100%; 4 | } 5 | :host:not(.file-input-disabled) { 6 | cursor: pointer; 7 | } 8 | 9 | input { 10 | width: 0px; 11 | height: 0px; 12 | opacity: 0; 13 | overflow: hidden; 14 | position: absolute; 15 | z-index: -1; 16 | } 17 | 18 | .filename { 19 | display: inline-block; 20 | text-overflow: ellipsis; 21 | overflow: hidden; 22 | width: 100%; 23 | } 24 | -------------------------------------------------------------------------------- /libs/material-file-input/src/lib/file-input/file-input.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{ fileNames }} 3 | -------------------------------------------------------------------------------- /libs/material-file-input/src/lib/file-input/file-input.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed, tick, fakeAsync, waitForAsync } from '@angular/core/testing'; 2 | import { FormsModule, NG_VALUE_ACCESSOR, NgControl, ReactiveFormsModule, FormControl, FormGroupDirective, NgForm } from '@angular/forms'; 3 | import { ErrorStateMatcher } from '@angular/material/core'; 4 | import { MatButtonModule } from '@angular/material/button'; 5 | import { MatFormFieldModule } from '@angular/material/form-field'; 6 | import { MatIconModule } from '@angular/material/icon'; 7 | import { MatInputModule } from '@angular/material/input'; 8 | 9 | import { FileInput } from '../model/file-input.model'; 10 | import { FileInputComponent } from './file-input.component'; 11 | 12 | // function createComponent(component: Type, 13 | // providers: Provider[] = [], 14 | // imports: any[] = []): ComponentFixture { 15 | // TestBed.configureTestingModule({ 16 | // imports: [ 17 | // ReactiveFormsModule, 18 | // NoopAnimationsModule, 19 | // // Material modules 20 | // MatButtonModule, 21 | // MatFormFieldModule, 22 | // MatIconModule, 23 | // MatInputModule, 24 | // MatToolbarModule, 25 | // // Lib Module 26 | // MaterialFileInputModule, 27 | // ...imports 28 | // ], 29 | // declarations: [AppComponent], 30 | // providers, 31 | // }).compileComponents(); 32 | 33 | // return TestBed.createComponent(component); 34 | // } 35 | 36 | /** 37 | * Shows error state on a control if it is touched and has any error. 38 | * Used as global ErrorStateMatcher for all tests. 39 | */ 40 | class FileInputSpecErrorStateMatcher implements ErrorStateMatcher { 41 | public isErrorState(control: FormControl | null, _: FormGroupDirective | NgForm | null): boolean { 42 | return !!(control && control.errors !== null && control.touched); 43 | } 44 | } 45 | 46 | /** 47 | * Shows error state on a control with exactly two validation errors. 48 | * Used to change the ErrorStateMatcher of a single component. 49 | */ 50 | class OverrideErrorStateMatcher implements ErrorStateMatcher { 51 | public isErrorState(control: FormControl | null, _: FormGroupDirective | NgForm | null): boolean { 52 | return !!(control && control.errors && control.errors.length === 2); 53 | } 54 | } 55 | 56 | describe('FileInputComponent', () => { 57 | let component: FileInputComponent; 58 | let fixture: ComponentFixture; 59 | 60 | beforeEach( 61 | waitForAsync(() => { 62 | TestBed.configureTestingModule({ 63 | declarations: [FileInputComponent], 64 | imports: [ 65 | ReactiveFormsModule, 66 | FormsModule, 67 | // Material modules 68 | MatFormFieldModule, 69 | MatInputModule, 70 | MatButtonModule, 71 | MatIconModule 72 | ], 73 | providers: [{ provide: NgControl, useValue: NG_VALUE_ACCESSOR }, { provide: ErrorStateMatcher, useClass: FileInputSpecErrorStateMatcher }] 74 | }).compileComponents(); 75 | }) 76 | ); 77 | 78 | beforeEach(() => { 79 | fixture = TestBed.createComponent(FileInputComponent); 80 | component = fixture.componentInstance; 81 | fixture.detectChanges(); 82 | }); 83 | 84 | it('should be created', () => { 85 | expect(component).toBeTruthy(); 86 | }); 87 | 88 | it('should have no files by default', () => { 89 | expect(component.value).toBeNull(); 90 | }); 91 | 92 | it('should add file from Input', () => { 93 | const file = new File(['test'], 'test.pdf', { type: 'application/pdf' }); 94 | component.value = new FileInput([file]); 95 | expect(component.value.files.length).toBe(1); 96 | }); 97 | 98 | it('should set/get placeholder', () => { 99 | const plh = 'Upload file'; 100 | component.placeholder = plh; 101 | expect(component.placeholder).toBe(plh); 102 | }); 103 | 104 | it('should set/get valuePlaceholder', () => { 105 | const plh = 'Filenames here'; 106 | component.valuePlaceholder = plh; 107 | expect(component.valuePlaceholder).toBe(plh); 108 | }); 109 | 110 | it('should replace valuePlaceholder with fileNames when adding a file', () => { 111 | component.valuePlaceholder = 'Initial text'; 112 | const file = new File(['test'], 'test.pdf', { type: 'application/pdf' }); 113 | component.value = new FileInput([file]); 114 | expect(component.fileNames).toBe(file.name); 115 | }); 116 | 117 | it('should set/get disabled state', () => { 118 | component.disabled = true; 119 | expect(component.disabled).toBeTruthy(); 120 | }); 121 | 122 | it('should have `accept` attribute', () => { 123 | const accept = '.pdf'; 124 | component.accept = accept; 125 | expect(component.accept).toBe(accept); 126 | }); 127 | 128 | xit('should refuse invalid format, based on `accept` attribute', () => { 129 | const accept = '.png'; 130 | component.accept = accept; 131 | const file = new File(['test'], 'test.pdf', { type: 'application/pdf' }); 132 | component.value = new FileInput([file]); 133 | expect(component.fileNames).toBe(''); 134 | }); 135 | 136 | it('should propagate onContainerClick()', () => { 137 | spyOn(component, 'open').and.stub(); 138 | component.onContainerClick({ 139 | target: { 140 | tagName: 'not-input' 141 | } as Partial 142 | } as MouseEvent); 143 | expect(component.open).toHaveBeenCalled(); 144 | }); 145 | 146 | it('should not propagate onContainerClick(), when disabled', () => { 147 | spyOn(component, 'open').and.stub(); 148 | component.disabled = true; 149 | component.onContainerClick({ 150 | target: { 151 | tagName: 'not-input' 152 | } as Partial 153 | } as MouseEvent); 154 | expect(component.open).not.toHaveBeenCalled(); 155 | }); 156 | 157 | it('should remove file from Input', fakeAsync(() => { 158 | const file = new File(['test'], 'test.pdf', { type: 'application/pdf' }); 159 | component.value = new FileInput([file]); 160 | fixture.nativeElement.querySelector('input').dispatchEvent(new Event('input')); 161 | tick(); 162 | fixture.detectChanges(); 163 | expect(component.value.files.length).toBe(1); 164 | // expect(fixture.nativeElement.querySelector('input').files.length).toBe(1); // is 0, this should be incremented 165 | component.clear(); 166 | tick(); 167 | fixture.detectChanges(); 168 | expect(component.empty).toBeTruthy(); 169 | expect(component.value).toBeNull(); 170 | // expect(fixture.nativeElement.querySelector('input').value).toBe(''); 171 | })); 172 | 173 | xit('should propagate click', () => { 174 | spyOn(component, 'open').and.stub(); 175 | fixture.debugElement.nativeElement.click(); 176 | expect(component.open).toHaveBeenCalled(); 177 | }); 178 | 179 | it('should recognize all errorstate changes', () => { 180 | spyOn(component.stateChanges, 'next'); 181 | component.ngControl = { control: { errors: null, touched: false } }; 182 | expect(component.errorState).toBeFalsy(); 183 | expect(component.stateChanges.next).not.toHaveBeenCalled(); 184 | 185 | fixture.detectChanges(); 186 | expect(component.errorState).toBeFalsy(); 187 | expect(component.stateChanges.next).not.toHaveBeenCalled(); 188 | component.ngControl = { control: { errors: ['some error'], touched: true } }; 189 | 190 | expect(component.stateChanges.next).not.toHaveBeenCalled(); 191 | 192 | fixture.detectChanges(); 193 | expect(component.errorState).toBeTruthy(); 194 | expect(component.stateChanges.next).toHaveBeenCalledTimes(1); 195 | }); 196 | 197 | it('should use input ErrorStateMatcher over provided', () => { 198 | component.ngControl = { control: { errors: ['some error'], touched: true } }; 199 | 200 | fixture.detectChanges(); 201 | expect(component.errorState).toBeTruthy(); 202 | 203 | component.errorStateMatcher = new OverrideErrorStateMatcher(); 204 | expect(component.errorState).toBeTruthy(); 205 | 206 | fixture.detectChanges(); 207 | expect(component.errorState).toBeFalsy(); 208 | component.ngControl = { control: { errors: ['some error', 'another error'] } }; 209 | expect(component.errorState).toBeFalsy(); 210 | 211 | fixture.detectChanges(); 212 | expect(component.errorState).toBeTruthy(); 213 | }); 214 | }); 215 | -------------------------------------------------------------------------------- /libs/material-file-input/src/lib/file-input/file-input.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, ElementRef, OnDestroy, HostBinding, Renderer2, HostListener, Optional, Self, DoCheck } from '@angular/core'; 2 | import { ControlValueAccessor, NgControl, NgForm, FormGroupDirective } from '@angular/forms'; 3 | 4 | 5 | import { FileInput } from '../model/file-input.model'; 6 | import { FileInputMixinBase } from './file-input-mixin'; 7 | import { MatFormFieldControl } from "@angular/material/form-field"; 8 | import { ErrorStateMatcher } from "@angular/material/core"; 9 | import { coerceBooleanProperty } from "@angular/cdk/coercion"; 10 | import { FocusMonitor } from "@angular/cdk/a11y"; 11 | import { Observable, Subject } from "rxjs"; 12 | 13 | @Component({ 14 | selector: 'ngx-mat-file-input', 15 | templateUrl: './file-input.component.html', 16 | styleUrls: ['./file-input.component.css'], 17 | providers: [{ provide: MatFormFieldControl, useExisting: FileInputComponent }] 18 | }) 19 | export class FileInputComponent extends FileInputMixinBase implements MatFormFieldControl, ControlValueAccessor, OnInit, OnDestroy, DoCheck { 20 | static nextId = 0; 21 | 22 | focused = false; 23 | controlType = 'file-input'; 24 | 25 | @Input() autofilled = false; 26 | 27 | private _placeholder: string; 28 | private _required = false; 29 | private _multiple: boolean; 30 | 31 | @Input() valuePlaceholder: string; 32 | @Input() accept: string | null = null; 33 | @Input() errorStateMatcher: ErrorStateMatcher; 34 | 35 | @HostBinding() id = `ngx-mat-file-input-${FileInputComponent.nextId++}`; 36 | @HostBinding('attr.aria-describedby') describedBy = ''; 37 | 38 | setDescribedByIds(ids: string[]) { 39 | this.describedBy = ids.join(' '); 40 | } 41 | 42 | @Input() 43 | get value(): FileInput | null { 44 | return this.empty ? null : new FileInput(this._elementRef.nativeElement.value || []); 45 | } 46 | set value(fileInput: FileInput | null) { 47 | if (fileInput) { 48 | this.writeValue(fileInput); 49 | this.stateChanges.next(); 50 | } 51 | } 52 | 53 | @Input() 54 | get multiple(): boolean { 55 | return this._multiple; 56 | } 57 | set multiple(value: boolean | string) { 58 | this._multiple = coerceBooleanProperty(value); 59 | this.stateChanges.next(); 60 | } 61 | 62 | @Input() 63 | get placeholder() { 64 | return this._placeholder; 65 | } 66 | set placeholder(plh) { 67 | this._placeholder = plh; 68 | this.stateChanges.next(); 69 | } 70 | 71 | /** 72 | * Whether the current input has files 73 | */ 74 | get empty() { 75 | return !this._elementRef.nativeElement.value || this._elementRef.nativeElement.value.length === 0; 76 | } 77 | 78 | @HostBinding('class.mat-form-field-should-float') 79 | get shouldLabelFloat() { 80 | return this.focused || !this.empty || this.valuePlaceholder !== undefined; 81 | } 82 | 83 | @Input() 84 | get required(): boolean { 85 | return this._required; 86 | } 87 | set required(req: boolean | string) { 88 | this._required = coerceBooleanProperty(req); 89 | this.stateChanges.next(); 90 | } 91 | 92 | @HostBinding('class.file-input-disabled') 93 | get isDisabled() { 94 | return this.disabled; 95 | } 96 | @Input() 97 | get disabled(): boolean { 98 | return this._elementRef.nativeElement.disabled; 99 | } 100 | set disabled(dis: boolean | string) { 101 | this.setDisabledState(coerceBooleanProperty(dis)); 102 | this.stateChanges.next(); 103 | } 104 | 105 | onContainerClick(event: MouseEvent) { 106 | if ((event.target as Element).tagName.toLowerCase() !== 'input' && !this.disabled) { 107 | this._elementRef.nativeElement.querySelector('input').focus(); 108 | this.focused = true; 109 | this.open(); 110 | } 111 | } 112 | 113 | /** 114 | * @see https://angular.io/api/forms/ControlValueAccessor 115 | */ 116 | constructor( 117 | private fm: FocusMonitor, 118 | private _elementRef: ElementRef, 119 | private _renderer: Renderer2, 120 | public _defaultErrorStateMatcher: ErrorStateMatcher, 121 | @Optional() 122 | @Self() 123 | public ngControl: NgControl, 124 | @Optional() public _parentForm: NgForm, 125 | @Optional() public _parentFormGroup: FormGroupDirective, 126 | ) { 127 | super(_defaultErrorStateMatcher, _parentForm, _parentFormGroup, ngControl, new Subject()) 128 | 129 | if (this.ngControl != null) { 130 | this.ngControl.valueAccessor = this; 131 | } 132 | fm.monitor(_elementRef.nativeElement, true).subscribe(origin => { 133 | this.focused = !!origin; 134 | this.stateChanges.next(); 135 | }); 136 | } 137 | 138 | private _onChange = (_: any) => {}; 139 | private _onTouched = () => {}; 140 | 141 | get fileNames() { 142 | return this.value ? this.value.fileNames : this.valuePlaceholder; 143 | } 144 | 145 | writeValue(obj: FileInput | null): void { 146 | this._renderer.setProperty(this._elementRef.nativeElement, 'value', obj instanceof FileInput ? obj.files : null); 147 | } 148 | 149 | registerOnChange(fn: (_: any) => void): void { 150 | this._onChange = fn; 151 | } 152 | 153 | registerOnTouched(fn: any): void { 154 | this._onTouched = fn; 155 | } 156 | 157 | /** 158 | * Remove all files from the file input component 159 | * @param [event] optional event that may have triggered the clear action 160 | */ 161 | clear(event?: Event) { 162 | if (event) { 163 | event.preventDefault(); 164 | event.stopPropagation(); 165 | } 166 | this.value = new FileInput([]); 167 | this._elementRef.nativeElement.querySelector('input').value = null; 168 | this._onChange(this.value); 169 | } 170 | 171 | @HostListener('change', ['$event']) 172 | change(event: Event) { 173 | const fileList: FileList | null = (event.target).files; 174 | const fileArray: File[] = []; 175 | if (fileList) { 176 | for (let i = 0; i < fileList.length; i++) { 177 | fileArray.push(fileList[i]); 178 | } 179 | } 180 | this.value = new FileInput(fileArray); 181 | this._onChange(this.value); 182 | } 183 | 184 | @HostListener('focusout') 185 | blur() { 186 | this.focused = false; 187 | this._onTouched(); 188 | } 189 | 190 | setDisabledState(isDisabled: boolean): void { 191 | this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled); 192 | } 193 | 194 | ngOnInit() { 195 | this.multiple = coerceBooleanProperty(this.multiple); 196 | } 197 | 198 | open() { 199 | if (!this.disabled) { 200 | this._elementRef.nativeElement.querySelector('input').click(); 201 | } 202 | } 203 | 204 | ngOnDestroy() { 205 | this.stateChanges.complete(); 206 | this.fm.stopMonitoring(this._elementRef.nativeElement); 207 | } 208 | 209 | ngDoCheck(): void { 210 | if (this.ngControl) { 211 | // We need to re-evaluate this on every change detection cycle, because there are some 212 | // error triggers that we can't subscribe to (e.g. parent form submissions). This means 213 | // that whatever logic is in here has to be super lean or we risk destroying the performance. 214 | this.updateErrorState(); 215 | } 216 | } 217 | 218 | } 219 | -------------------------------------------------------------------------------- /libs/material-file-input/src/lib/material-file-input.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { MaterialFileInputModule } from './material-file-input.module'; 2 | 3 | describe('MaterialFileInputModule', () => { 4 | it('should work', () => { 5 | expect(new MaterialFileInputModule()).toBeDefined(); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /libs/material-file-input/src/lib/material-file-input.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { FocusMonitor } from '@angular/cdk/a11y'; 3 | import { FileInputComponent } from './file-input/file-input.component'; 4 | import { ByteFormatPipe } from './pipe/byte-format.pipe'; 5 | 6 | @NgModule({ 7 | declarations: [FileInputComponent, ByteFormatPipe], 8 | providers: [FocusMonitor], 9 | exports: [FileInputComponent, ByteFormatPipe] 10 | }) 11 | export class MaterialFileInputModule {} 12 | -------------------------------------------------------------------------------- /libs/material-file-input/src/lib/model/file-input-config.model.ts: -------------------------------------------------------------------------------- 1 | import { InjectionToken } from '@angular/core'; 2 | 3 | /** 4 | * Optional token to provide custom configuration to the module 5 | */ 6 | export const NGX_MAT_FILE_INPUT_CONFIG = new InjectionToken( 7 | 'ngx-mat-file-input.config' 8 | ); 9 | 10 | /** 11 | * Provide additional configuration to dynamically customize the module injection 12 | */ 13 | export interface FileInputConfig { 14 | /** 15 | * Unit used with the ByteFormatPipe, default value is *Byte*. 16 | * The first letter is used for the short notation. 17 | */ 18 | sizeUnit: string; 19 | } 20 | -------------------------------------------------------------------------------- /libs/material-file-input/src/lib/model/file-input.model.spec.ts: -------------------------------------------------------------------------------- 1 | import { FileInput } from './file-input.model'; 2 | 3 | describe('FileInput', () => { 4 | let model: FileInput; 5 | 6 | it('should have empty fileName (empty array)', () => { 7 | model = new FileInput([]); 8 | expect(model.fileNames).toBe(''); 9 | }); 10 | 11 | it('should have empty fileName (null)', () => { 12 | model = new FileInput(null); 13 | expect(model.fileNames).toBe(''); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /libs/material-file-input/src/lib/model/file-input.model.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The files to be uploaded 3 | */ 4 | export class FileInput { 5 | private _fileNames: string; 6 | 7 | constructor(private _files: File[] | null, private delimiter: string = ', ') { 8 | this._fileNames = (this._files || []).map((f: File) => f.name).join(delimiter); 9 | } 10 | 11 | get files() { 12 | return this._files || []; 13 | } 14 | 15 | get fileNames(): string { 16 | return this._fileNames; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /libs/material-file-input/src/lib/pipe/byte-format.pipe.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FileInputConfig, 3 | NGX_MAT_FILE_INPUT_CONFIG 4 | } from './../model/file-input-config.model'; 5 | import { ByteFormatPipe } from './byte-format.pipe'; 6 | import { TestBed } from '@angular/core/testing'; 7 | 8 | describe('ByteFormatPipe', () => { 9 | let pipe: ByteFormatPipe; 10 | 11 | beforeEach(() => { 12 | TestBed.configureTestingModule({ 13 | providers: [ByteFormatPipe] 14 | }); 15 | pipe = TestBed.get(ByteFormatPipe); 16 | }); 17 | 18 | it('should format a given value', () => { 19 | const text = pipe.transform(104857600); 20 | expect(text).toBe('100 MB'); 21 | }); 22 | 23 | it('should not format invalid value', () => { 24 | const text = pipe.transform(undefined); 25 | expect(text).toBeUndefined(); 26 | }); 27 | 28 | it('should format 0 value', () => { 29 | const text = pipe.transform(0); 30 | expect(text).toBe('0 Byte'); 31 | }); 32 | }); 33 | 34 | describe('ByteFormatPipe with injection token', () => { 35 | let pipe: ByteFormatPipe; 36 | const config: FileInputConfig = { 37 | sizeUnit: 'Octet' 38 | }; 39 | 40 | beforeEach(() => { 41 | TestBed.configureTestingModule({ 42 | providers: [ 43 | ByteFormatPipe, 44 | { provide: NGX_MAT_FILE_INPUT_CONFIG, useValue: config } 45 | ] 46 | }); 47 | pipe = TestBed.get(ByteFormatPipe); 48 | }); 49 | 50 | it('should format a given value', () => { 51 | const text = pipe.transform(104857600); 52 | expect(text).toBe('100 MO'); 53 | }); 54 | 55 | it('should format 0 value', () => { 56 | const text = pipe.transform(0); 57 | expect(text).toBe('0 Octet'); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /libs/material-file-input/src/lib/pipe/byte-format.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform, Optional, Inject } from '@angular/core'; 2 | import { FileInputConfig, NGX_MAT_FILE_INPUT_CONFIG } from '../model/file-input-config.model'; 3 | 4 | @Pipe({ 5 | name: 'byteFormat' 6 | }) 7 | export class ByteFormatPipe implements PipeTransform { 8 | private unit: string; 9 | 10 | constructor( 11 | @Optional() 12 | @Inject(NGX_MAT_FILE_INPUT_CONFIG) 13 | private config: FileInputConfig 14 | ) { 15 | this.unit = config ? config.sizeUnit : 'Byte'; 16 | } 17 | 18 | transform(value: any, args?: any): any { 19 | if (parseInt(value, 10) >= 0) { 20 | value = this.formatBytes(+value, +args); 21 | } 22 | return value; 23 | } 24 | 25 | private formatBytes(bytes: number, decimals?: number) { 26 | if (bytes === 0) { 27 | return '0 ' + this.unit; 28 | } 29 | const B = this.unit.charAt(0); 30 | const k = 1024; 31 | const dm = decimals || 2; 32 | const sizes = [this.unit, 'K' + B, 'M' + B, 'G' + B, 'T' + B, 'P' + B, 'E' + B, 'Z' + B, 'Y' + B]; 33 | const i = Math.floor(Math.log(bytes) / Math.log(k)); 34 | return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /libs/material-file-input/src/lib/validator/file-validator.spec.ts: -------------------------------------------------------------------------------- 1 | import { FormControl, ValidationErrors } from '@angular/forms'; 2 | import { FileInput } from '../model/file-input.model'; 3 | import { FileValidator } from './file-validator'; 4 | 5 | describe('FileValidator', () => { 6 | describe('maxContentSize', () => { 7 | it('should validate', () => { 8 | const data = new FileInput([new File(['test'], 'test.txt')]); 9 | const control = new FormControl(data, [FileValidator.maxContentSize(5)]); 10 | expect(control.value).toBe(data); 11 | expect(control.valid).toBeTruthy(); 12 | }); 13 | 14 | it('should validate with size equal', () => { 15 | const data = new FileInput([new File(['test'], 'test.txt')]); 16 | const control = new FormControl(data, [FileValidator.maxContentSize(4)]); 17 | expect(control.value).toBe(data); 18 | expect(control.valid).toBeTruthy(); 19 | }); 20 | 21 | it('should not validate', () => { 22 | const data = new FileInput([new File(['test'], 'test.txt')]); 23 | const control = new FormControl(data, [FileValidator.maxContentSize(3)]); 24 | expect(control.value).toBe(data); 25 | expect(control.valid).toBeFalsy(); 26 | }); 27 | 28 | it('should not validate, with "maxContentSize" error', () => { 29 | const data = new FileInput([new File(['test'], 'test.txt')]); 30 | const control = new FormControl(data, [FileValidator.maxContentSize(3)]); 31 | const errors: ValidationErrors | null = (control.errors as ValidationErrors); 32 | const maxSizeError: { [key: string]: any } | null = (errors.maxContentSize as { [key: string]: any }) 33 | expect(maxSizeError).toEqual({ 34 | actualSize: 4, 35 | maxSize: 3 36 | }); 37 | expect(control.hasError('maxContentSize')).toBeTruthy(); 38 | }); 39 | 40 | it('should validate with no files', () => { 41 | const control = new FormControl(undefined, [FileValidator.maxContentSize(3)]); 42 | expect(control.value).toBe(null); 43 | expect(control.valid).toBeTruthy(); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /libs/material-file-input/src/lib/validator/file-validator.ts: -------------------------------------------------------------------------------- 1 | import { ValidatorFn, AbstractControl } from '@angular/forms'; 2 | import { FileInput } from '../model/file-input.model'; 3 | 4 | export class FileValidator { 5 | /** 6 | * Function to control content of files 7 | * 8 | * @param bytes max number of bytes allowed 9 | * 10 | * @returns 11 | */ 12 | static maxContentSize(bytes: number): ValidatorFn { 13 | return (control: AbstractControl): { [key: string]: any } | null => { 14 | const size = control && control.value ? (control.value as FileInput).files.map(f => f.size).reduce((acc, i) => acc + i, 0) : 0; 15 | const condition = bytes >= size; 16 | return condition 17 | ? null 18 | : { 19 | maxContentSize: { 20 | actualSize: size, 21 | maxSize: bytes 22 | } 23 | }; 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /libs/material-file-input/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone'; 4 | import 'zone.js/dist/zone-testing'; 5 | import { getTestBed } from '@angular/core/testing'; 6 | import { 7 | BrowserDynamicTestingModule, 8 | platformBrowserDynamicTesting 9 | } from '@angular/platform-browser-dynamic/testing'; 10 | 11 | declare const require: { 12 | context(path: string, deep?: boolean, filter?: RegExp): { 13 | keys(): string[]; 14 | (id: string): T; 15 | }; 16 | }; 17 | 18 | // First, initialize the Angular testing environment. 19 | getTestBed().initTestEnvironment( 20 | BrowserDynamicTestingModule, 21 | platformBrowserDynamicTesting() 22 | ); 23 | // Then we find all the tests. 24 | const context = require.context('./', true, /\.spec\.ts$/); 25 | // And load the modules. 26 | context.keys().map(context); 27 | -------------------------------------------------------------------------------- /libs/material-file-input/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/material-file-input/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "declarationMap": true, 6 | "target": "es2015", 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "declaration": true, 10 | "sourceMap": true, 11 | "inlineSources": true, 12 | "importHelpers": true, 13 | "types": [], 14 | "lib": ["dom", "es2017"] 15 | }, 16 | "angularCompilerOptions": { 17 | "skipTemplateCodegen": true, 18 | "strictMetadataEmit": true, 19 | "fullTemplateTypeCheck": true, 20 | "strictInjectionParameters": true, 21 | "flatModuleId": "AUTOGENERATED", 22 | "flatModuleOutFile": "AUTOGENERATED" 23 | }, 24 | "exclude": ["src/test.ts", "**/*.spec.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /libs/material-file-input/tsconfig.lib.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.lib.json", 3 | "compilerOptions": { 4 | "declarationMap": false 5 | }, 6 | "angularCompilerOptions": { 7 | "compilationMode": "partial" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /libs/material-file-input/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "types": ["jasmine", "node"] 6 | }, 7 | "files": ["src/test.ts"], 8 | "include": ["**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "material-file-input", 3 | "affected": { 4 | "defaultBase": "master" 5 | }, 6 | "cli": { 7 | "defaultCollection": "@nrwl/angular", 8 | "packageManager": "npm" 9 | }, 10 | "implicitDependencies": { 11 | "angular.json": "*", 12 | "package.json": { 13 | "dependencies": "*", 14 | "devDependencies": "*" 15 | }, 16 | "tsconfig.base.json": "*", 17 | ".eslintrc.json": "*" 18 | }, 19 | "tasksRunnerOptions": { 20 | "default": { 21 | "runner": "@nrwl/workspace/tasks-runners/default", 22 | "options": { 23 | "cacheableOperations": ["build", "lint", "test", "e2e"] 24 | } 25 | } 26 | }, 27 | "targetDependencies": { 28 | "build": [ 29 | { 30 | "target": "build", 31 | "projects": "dependencies" 32 | } 33 | ] 34 | }, 35 | "defaultProject": "demo" 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "material-file-input", 3 | "version": "4.0.1", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "nx", 7 | "start": "./node_modules/.bin/ng serve", 8 | "clean": "rm -rf dist", 9 | "build": "ng build", 10 | "build:lib": "ng-packagr -p libs/material-file-input/ng-package.prod.json && cp README.md dist/material-file-input/", 11 | "test": "nx test", 12 | "test:once": "nx test material-file-input --watch=false --browsers ChromeHeadless", 13 | "lint": "./node_modules/.bin/nx workspace-lint && nx lint && nx lint material-file-input", 14 | "e2e": "ng e2e", 15 | "build:demo": "ng build --project=demo-packaged --configuration production --base-href /ngx-material-file-input/", 16 | "postbuild:demo": "cp apps/demo/src/.nojekyll dist/apps/demo-packaged", 17 | "commit:demo": "git add -f dist/apps/demo-packaged && git commit -m \"update demo\"", 18 | "push:demo": "git subtree push --prefix dist/apps/demo-packaged origin gh-pages", 19 | "erase:demo": "git push origin --delete gh-pages", 20 | "deploy:demo": "npm run commit:demo && npm run push:demo", 21 | "publish:lib": "npm publish ./dist/material-file-input", 22 | "affected:apps": "./node_modules/.bin/nx affected:apps", 23 | "affected:build": "./node_modules/.bin/nx affected:build", 24 | "affected:e2e": "./node_modules/.bin/nx affected:e2e", 25 | "affected:test": "./node_modules/.bin/nx affected:test", 26 | "affected:lint": "./node_modules/.bin/nx affected:lint", 27 | "affected:dep-graph": "./node_modules/.bin/nx affected:dep-graph", 28 | "format": "./node_modules/.bin/nx format:write", 29 | "format:write": "./node_modules/.bin/nx format:write", 30 | "format:check": "./node_modules/.bin/nx format:check", 31 | "update": "nx migrate latest", 32 | "update:check": "./node_modules/.bin/nx update:check", 33 | "workspace-schematic": "./node_modules/.bin/nx workspace-schematic", 34 | "dep-graph": "./node_modules/.bin/nx dep-graph", 35 | "help": "./node_modules/.bin/nx help", 36 | "postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points && node ./decorate-angular-cli.js" 37 | }, 38 | "private": false, 39 | "dependencies": { 40 | "@angular/animations": "14.2.12", 41 | "@angular/cdk": "14.2.7", 42 | "@angular/common": "14.2.12", 43 | "@angular/compiler": "14.2.12", 44 | "@angular/core": "14.2.12", 45 | "@angular/forms": "14.2.12", 46 | "@angular/material": "14.2.7", 47 | "@angular/platform-browser": "14.2.12", 48 | "@angular/platform-browser-dynamic": "14.2.12", 49 | "@angular/router": "14.2.12", 50 | "@ngrx/effects": "14.0.2", 51 | "@ngrx/router-store": "14.0.2", 52 | "@ngrx/store": "14.0.2", 53 | "@ngrx/store-devtools": "14.0.2", 54 | "@nrwl/angular": "14.8.4", 55 | "rxjs": "~6.5.0", 56 | "tslib": "^2.3.0", 57 | "zone.js": "~0.11.4" 58 | }, 59 | "devDependencies": { 60 | "@angular-devkit/build-angular": "14.2.10", 61 | "@angular-eslint/eslint-plugin": "14.0.4", 62 | "@angular-eslint/eslint-plugin-template": "14.0.4", 63 | "@angular-eslint/template-parser": "14.0.4", 64 | "@angular/cli": "~14.2.0", 65 | "@angular/compiler-cli": "14.2.12", 66 | "@angular/language-service": "14.2.12", 67 | "@ngrx/schematics": "14.0.2", 68 | "@nrwl/cli": "14.8.4", 69 | "@nrwl/eslint-plugin-nx": "14.8.4", 70 | "@nrwl/linter": "14.8.4", 71 | "@nrwl/tao": "14.8.4", 72 | "@nrwl/workspace": "14.8.4", 73 | "@types/jasmine": "~3.6.0", 74 | "@types/jasminewd2": "^2.0.6", 75 | "@types/node": "^16.11.9", 76 | "@typescript-eslint/eslint-plugin": "5.48.1", 77 | "@typescript-eslint/parser": "5.48.1", 78 | "codelyzer": "^6.0.0", 79 | "cypress": "^9.7.0", 80 | "eslint": "8.20.0", 81 | "eslint-config-prettier": "^8.1.0", 82 | "eslint-plugin-cypress": "^2.10.3", 83 | "eslint-plugin-ngrx": "^2.1.0", 84 | "jasmine-core": "4.2.0", 85 | "jasmine-marbles": "^0.8.0", 86 | "jasmine-spec-reporter": "~5.0.0", 87 | "karma": "6.4.1", 88 | "karma-chrome-launcher": "~3.1.0", 89 | "karma-coverage": "^2.0.0", 90 | "karma-jasmine": "5.1.0", 91 | "karma-jasmine-html-reporter": "2.0.0", 92 | "ng-packagr": "14.2.2", 93 | "nx": "14.8.6", 94 | "postcss": "^8.4.14", 95 | "postcss-import": "^14.0.2", 96 | "postcss-preset-env": "^7.7.0", 97 | "postcss-url": "^10.1.1", 98 | "prettier": "^2.7.1", 99 | "ts-node": "10.9.1", 100 | "typescript": "4.8.4" 101 | }, 102 | "engines": { 103 | "node": ">=16.13.0" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tools/generators/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/merlosy/ngx-material-file-input/1b40f64e3e33439e945bde49962cb32f64d10119/tools/generators/.gitkeep -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"], 9 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "importHelpers": true, 6 | "module": "esnext", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "moduleResolution": "node", 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "target": "es2015", 13 | "lib": ["es2017", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "strictPropertyInitialization": false, 17 | "baseUrl": ".", 18 | "paths": { 19 | "ngx-material-file-input": ["libs/material-file-input/src/index.ts"] 20 | } 21 | }, 22 | "angularCompilerOptions": { 23 | "fullTemplateTypeCheck": true, 24 | "preserveWhitespaces": false, 25 | "strictNullChecks": true 26 | }, 27 | "exclude": ["node_modules", "tmp"] 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.packaged.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "paths": { 5 | "ngx-material-file-input": ["dist/material-file-input"] 6 | } 7 | } 8 | } 9 | --------------------------------------------------------------------------------