<!DOCTYPE html>
<html>
  <head>
    <title>Angular Material Plunker</title>
    
    <!-- Load common libraries -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/typescript/2.1.6/typescript.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.4.1/core.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.7.2/zone.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/systemjs/0.19.47/system.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/web-animations/2.2.2/web-animations.min.js"></script>

  <link href="https://fonts.googleapis.com/icon?family=Material+Icons"
      rel="stylesheet">
      
    <!-- Configure SystemJS -->
    <script src="systemjs.config.js"></script>

    <script>
      System
        .import('main.ts')
        .catch(console.error.bind(console));
    </script>
    
    <!-- Load the Angular Material stylesheet -->
    <link href="https://unpkg.com/@angular/material/prebuilt-themes/indigo-pink.css" rel="stylesheet">
    <style>body { font-family: Roboto, Arial, sans-serif; margin: 0 }</style>

  </head>

  <body class="mat-app-background">
    <p>!!! Latest maintained version at <a href="https://github.com/merlosy/ngx-material-file-input">NGX Material File Upload</a></p>
    <p>!!! Install latest: <code>npm i ngx-material-file-input</code></p>
    <material-app>Loading the Angular Material App...</material-app>
  </body>

</html>

<!--
  Copyright 2017 Google LLC. All Rights Reserved.
  Use of this source code is governed by an MIT-style license that
  can be found in the LICENSE file at http://angular.io/license
-->
import {Component, OnInit} from '@angular/core';
import {HttpClient} from '@angular/common/http'
import {bootstrap} from '@angular/platform-browser-dynamic';
import { FormGroup, FormBuilder, FormControl, Validators } from '@angular/forms';
import { FileValidators } from './file-validators';
import {VERSION} from '@angular/material';

import 'rxjs/add/operator/map'

@Component({
  selector: 'material-app',
  templateUrl: 'app.component.html'
  styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {
  
  private version: any;
  formDoc: FormGroup;
  private ngVersion = VERSION;
  
  constructor(http: HttpClient, private _fb: FormBuilder) {
    // Display the currently used Material 2 version.
    http
      .get('https://api.github.com/repos/angular/material2-builds/commits/HEAD')
      .subscribe(d => this.version = d);
  }
  
  ngOnInit() {
    this.formDoc = this._fb.group({
      basicfile: []
      requiredfile: [{ value: undefined, disabled: false }, [Validators.required, FileValidators.maxContentSize(104857600)]],
      disabledfile: [{ value: undefined, disabled: true }],
      multiplefile: [{ value: undefined, disabled: false }],
    });
  }
  
  onSubmit() {
    console.log('SUBMITTED', this.formDoc);
  }
  
}

/*
  Copyright 2017 Google LLC. All Rights Reserved.
  Use of this source code is governed by an MIT-style license that
  can be found in the LICENSE file at http://angular.io/license
 */
<mat-toolbar color="primary">
  Angular Material - File Input
</mat-toolbar>

<p class="shout">This is now a lib : <a title="see on Github" href="https://github.com/merlosy/ngx-material-file-input">nx-material-file-input</a>
<br>Therefore, this plunker will not be maintained anymore.
</p>

<div style="padding: 7px">
  
  <form [formGroup]="formDoc" (ngSubmit)="onSubmit()" novalidate>
    
    <mat-form-field>
      <app-input-file formControlName="basicfile" placeholder="Basic Input" ></app-input-file>
      <mat-icon matSuffix>folder</mat-icon>
    </mat-form-field>
    
    <mat-form-field>
      <app-input-file formControlName="requiredfile" placeholder="Required input" valuePlaceholder="No file selected" required></app-input-file>
      <mat-icon matSuffix>folder</mat-icon>
      <mat-error *ngIf="formDoc.get('requiredfile').hasError('required')">
        Please select a file
      </mat-error>
      <mat-error *ngIf="formDoc.get('requiredfile').hasError('maxContentSize')">
        The total size must not exceed {{formDoc.get('requiredfile')?.getError('maxContentSize').maxSize | byteFormat}}
        ({{formDoc.get('requiredfile')?.getError('maxContentSize').actualSize | byteFormat}}).
      </mat-error>
    </mat-form-field>
    <pre>{{formDoc.get('requiredfile').errors | json}}</pre>
    
    <mat-form-field>
      <app-input-file formControlName="disabledfile" placeholder="Disabled Input" ></app-input-file>
      <mat-icon matSuffix>folder</mat-icon>
    </mat-form-field>
    
    <mat-form-field>
      <app-input-file formControlName="multiplefile" placeholder="Multiple inputs" multiple></app-input-file>
      <mat-icon matSuffix>folder</mat-icon>
    </mat-form-field>
    
    <button type="submit" [disabled]="formDoc.invalid" mat-raised-button>Submit</button>
    
    <hr>
    <p>Angular Version: {{ngVersion.full}}</p>
    <p>Material Version: {{version?.commit.author.date | json}}</p>
    
  </form>
  

</div>

<!--
  Copyright 2017 Google LLC. All Rights Reserved.
  Use of this source code is governed by an MIT-style license that
  can be found in the LICENSE file at http://angular.io/license
-->
/** Add Transpiler for Typescript */
System.config({
  transpiler: 'typescript',
  typescriptOptions: {
    emitDecoratorMetadata: true
  },
  packages: {
    '.': {
      defaultExtension: 'ts'
    },
    'vendor': {
      defaultExtension: 'js'
    }
  }
});

System.config({
  paths: {
    // 'npm:': 'node_modules/',
    'npm:': 'https://unpkg.com/',
  },
  map: {
    'main': 'main.js',
    
    // Angular specific mappings.
    '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
    '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
    '@angular/common/http': 'npm:@angular/common/bundles/common-http.umd.js',
    '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
    '@angular/animations': "npm:@angular/animations/bundles/animations.umd.js",
    '@angular/animations/browser': 'npm:@angular/animations/bundles/animations-browser.umd.js',
    '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
    '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
    '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
    '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
    '@angular/platform-browser/animations': 'npm:@angular/platform-browser/bundles/platform-browser-animations.umd.js',
    '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
    
    '@angular/material': 'npm:@angular/material/bundles/material.umd.js',
    '@angular/cdk': 'npm:@angular/cdk/bundles/cdk.umd.js',

    // CDK Secondary entry points
    '@angular/cdk/a11y': 'npm:@angular/cdk/bundles/cdk-a11y.umd.js',
    '@angular/cdk/accordion': 'npm:@angular/cdk/bundles/cdk-accordion.umd.js',
    '@angular/cdk/bidi': 'npm:@angular/cdk/bundles/cdk-bidi.umd.js',
    '@angular/cdk/coercion': 'npm:@angular/cdk/bundles/cdk-coercion.umd.js',
    '@angular/cdk/keycodes': 'npm:@angular/cdk/bundles/cdk-keycodes.umd.js',
    '@angular/cdk/observers': 'npm:@angular/cdk/bundles/cdk-observers.umd.js',
    '@angular/cdk/platform': 'npm:@angular/cdk/bundles/cdk-platform.umd.js',
    '@angular/cdk/portal': 'npm:@angular/cdk/bundles/cdk-portal.umd.js',
    '@angular/cdk/rxjs': 'npm:@angular/cdk/bundles/cdk-rxjs.umd.js',
    '@angular/cdk/table': 'npm:@angular/cdk/bundles/cdk-table.umd.js',
    '@angular/cdk/testing': 'npm:@angular/cdk/bundles/cdk-testing.umd.js',
    '@angular/cdk/overlay': 'npm:@angular/cdk/bundles/cdk-overlay.umd.js',
    '@angular/cdk/scrolling': 'npm:@angular/cdk/bundles/cdk-scrolling.umd.js',
    '@angular/cdk/collections': 'npm:@angular/cdk/bundles/cdk-collections.umd.js',
    '@angular/cdk/stepper': 'npm:@angular/cdk/bundles/cdk-stepper.umd.js',
    '@angular/cdk/layout': 'npm:@angular/cdk/bundles/cdk-layout.umd.js',

    // Third party libraries
    'tslib': 'npm:tslib@1.7.1',
    'rxjs': 'npm:rxjs@5.5.2',
  },
  packages: {
    // Thirdparty barrels.
    'rxjs': { main: 'index' },
    'rxjs/operators': {main: 'index'},
  }
});

/*
  Copyright 2017 Google LLC. All Rights Reserved.
  Use of this source code is governed by an MIT-style license that
  can be found in the LICENSE file at http://angular.io/license
 */
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
import {BrowserModule} from '@angular/platform-browser';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {HttpClientModule} from '@angular/common/http';
import {AppComponent} from './app.component';
import {CdkTableModule} from '@angular/cdk/table'
import {OverlayModule} from '@angular/cdk/overlay';
import { ReactiveFormsModule } from '@angular/forms';
import { InputFileComponent } from './input-file.component';
import { ByteFormatPipe } from './byte-format.pipe';
import {
  MatAutocompleteModule,
  MatButtonModule,
  MatButtonToggleModule,
  MatCardModule,
  MatCheckboxModule,
  MatChipsModule,
  MatDatepickerModule,
  MatDialogModule,
  MatExpansionModule,
  MatGridListModule,
  MatIconModule,
  MatInputModule,
  MatListModule,
  MatMenuModule,
  MatNativeDateModule,
  MatProgressBarModule,
  MatProgressSpinnerModule,
  MatRadioModule,
  MatRippleModule,
  MatSelectModule,
  MatSidenavModule,
  MatSliderModule,
  MatSlideToggleModule,
  MatSnackBarModule,
  MatTabsModule,
  MatToolbarModule,
  MatTooltipModule,
  MatSortModule,
  MatPaginatorModule
} from '@angular/material';

/**
 * NgModule that includes all Material modules that are required to serve 
 * the Plunker.
 */
@NgModule({
  exports: [
    // CDk
    CdkTableModule,
    OverlayModule,
    
    // Material
    MatAutocompleteModule,
    MatButtonModule,
    MatButtonToggleModule,
    MatCardModule,
    MatCheckboxModule,
    MatChipsModule,
    MatDatepickerModule,
    MatDialogModule,
    MatExpansionModule,
    MatGridListModule,
    MatIconModule,
    MatInputModule,
    MatListModule,
    MatMenuModule,
    MatProgressBarModule,
    MatProgressSpinnerModule,
    MatRadioModule,
    MatRippleModule,
    MatSelectModule,
    MatSidenavModule,
    MatSlideToggleModule,
    MatSliderModule,
    MatSnackBarModule,
    MatTabsModule,
    MatToolbarModule,
    MatTooltipModule,
    
    MatNativeDateModule,
    MatSortModule,
    MatPaginatorModule
  ]
})
export class PlunkerMaterialModule {}

@NgModule({

  imports: [
    BrowserModule,
    CommonModule,
    HttpClientModule,
    PlunkerMaterialModule,
    BrowserAnimationsModule,
    ReactiveFormsModule
  ],

  declarations: [
    AppComponent,
    InputFileComponent,
    ByteFormatPipe
  ],
  bootstrap: [AppComponent],
  providers: []
})
export class PlunkerAppModule {}

platformBrowserDynamic().bootstrapModule(PlunkerAppModule);

/*
  Copyright 2017 Google LLC. All Rights Reserved.
  Use of this source code is governed by an MIT-style license that
  can be found in the LICENSE file at http://angular.io/license
 */
import { Subject } from 'rxjs/Subject';
import { Component, OnInit, Input, ElementRef, OnDestroy, HostBinding, Renderer2, HostListener } from '@angular/core';
import { ControlValueAccessor, NgControl } from '@angular/forms';
import { MatFormFieldControl } from '@angular/material';
import { FocusMonitor } from '@angular/cdk/a11y';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { FileInput } from './file-input.model';

@Component({
  selector: 'app-input-file',
  templateUrl: './input-file.component.html',
  styleUrls: ['./input-file.component.css'],
  providers: [
    { provide: MatFormFieldControl, useExisting: InputFileComponent }
  ]
})
export class InputFileComponent implements MatFormFieldControl<FileInput>, ControlValueAccessor, OnInit, OnDestroy {

  static nextId = 0;

  stateChanges = new Subject<void>();
  focused = false;
  controlType = 'file-input';

  private _placeholder: string;
  private _required = false;

  @Input() valuePlaceholder: string;
  @Input() multiple: boolean;

  @HostBinding() id = `app-input-file-${InputFileComponent.nextId++}`;
  @HostBinding('attr.aria-describedby') describedBy = '';

  @Input() get value(): FileInput | null {
    return this.empty ? null : new FileInput(this._elementRef.nativeElement.value || []);
  }
  set value(fileInput: FileInput | null) {
    this.writeValue(fileInput.files);
    this.stateChanges.next();
  }

  @Input() get placeholder() {
    return this._placeholder;
  }
  set placeholder(plh) {
    this._placeholder = plh;
    this.stateChanges.next();
  }

  get empty() {
    return !this._elementRef.nativeElement.value || this._elementRef.nativeElement.value.length === 0;
  }

  @HostBinding('class.mat-form-field-should-float') get shouldPlaceholderFloat() {
    return this.focused || !this.empty || this.valuePlaceholder !== undefined;
  }

  @Input() get required() {
    return this._required;
  }
  set required(req: boolean) {
    this._required = coerceBooleanProperty(req);
    this.stateChanges.next();
  }

  @HostBinding('class.file-input-disabled') get isDisabled() {
    return this.disabled;
  }
  @Input() get disabled() {
    return this._elementRef.nativeElement.disabled;
  }
  set disabled(dis: boolean) {
    this.setDisabledState( coerceBooleanProperty(dis) )
    this.stateChanges.next();
  }
  
  @Input() get errorState() {
    return this.ngControl.errors !== null && this.ngControl.touched;
  }

  setDescribedByIds(ids: string[]) {
    this.describedBy = ids.join(' ');
  }

  onContainerClick(event: MouseEvent) {
    if ((event.target as Element).tagName.toLowerCase() !== 'input' && !this.disabled) {
      this._elementRef.nativeElement.querySelector('input').focus();
      this.focused = true;
      this.open();
    }
  }


  /**
   * @see https://angular.io/api/forms/ControlValueAccessor
   */
  constructor(public ngControl: NgControl,
    private fm: FocusMonitor, private _elementRef: ElementRef, private _renderer: Renderer2) {

    ngControl.valueAccessor = this;
    fm.monitor(_elementRef.nativeElement, _renderer, true).subscribe(origin => {
      this.focused = !!origin;
      this.stateChanges.next();
    });
  }

  private _onChange = (_: any) => { };
  private _onTouched = () => { };

  writeValue(obj: any): void {
    this._renderer.setProperty(this._elementRef.nativeElement, 'value', obj);
  }

  registerOnChange(fn: (_: any) => void): void {
    this._onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this._onTouched = fn;
  }

  @HostListener('change', ['$event']) change(event) {
    const fileList = event.target.files;
    const fileArray = [];
    if (fileList) {
      for (let i = 0; i < fileList.length; i++) {
        fileArray.push(fileList[i]);
      }
    }
    this.value = new FileInput(fileArray);
    this._onChange(this.value);
  }

  @HostListener('focusout') blur() {
    this.focused = false;
    this._onTouched();
  }

  setDisabledState?(isDisabled: boolean): void {
    this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled);
  }

  ngOnInit() {
    this.multiple = coerceBooleanProperty(this.multiple);
  }

  open() {
    if (!this.disabled) {
      this._elementRef.nativeElement.querySelector('input').click();
    }
  }

  get fileNames() {
    return this.value ? this.value.fileNames : this.valuePlaceholder;
  }

  ngOnDestroy() {
    this.stateChanges.complete();
    this.fm.stopMonitoring(this._elementRef.nativeElement);
  }

}
:host {
  display: inline-block;
}
:host:not(.file-input-disabled) {
    cursor: pointer;
}

input {
    width: 0px;
    height: 0px;
    opacity: 0;
    overflow: hidden;
    position: absolute;
    z-index: -1;
}

.filename {
    display: inline-block;
}
<input #input id="md-input-file" type="file" [attr.multiple]="multiple? '' : null">
<span class="filename">{{fileNames}}</span>
export class FileInput {
  private _fileNames;

  constructor(private _files: File[], private delimiter: string = ', ') {
    this._fileNames = this._files.map((f: File) => f.name).join(delimiter);
  }

  get files() {
    return this._files || [];
  }

  get fileNames(): string {
    return this._fileNames;
  }
}
import { FileInput } from './file-input.model';
import { FormControl, ValidatorFn, AbstractControl } from '@angular/forms';

export class FileValidators {

    /**
     * Function to control content of files
     *
     * @param control
     *
     * @returns
     */
    static maxContentSize(bytes: number): ValidatorFn {
        return (control: AbstractControl): { [key: string]: any } => {
            const size = control && control.value ? (control.value as FileInput).files.map(f => f.size).reduce((acc, i) => acc + i, 0) : 0;
            const condition = bytes >= size;
            return condition ? null : {
                maxContentSize: {
                    actualSize: size,
                    maxSize: bytes
                }
            };
        }
    }
}
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'byteFormat'
})
export class ByteFormatPipe implements PipeTransform {

  private readonly unit = 'Byte';

  transform(value: any, args?: any): any {
    if (!!value) {
      value = this.formatBytes(+value, +args);
    }
    return value;
  }

  private formatBytes(bytes: number, decimals?: number) {
    if (bytes === 0) { return '0 ' + this.unit };
    const B = this.unit.charAt(0);
    const k = 1024,
      dm = decimals || 2,
      sizes = [this.unit, 'K' + B, 'M' + B, 'G' + B, 'T' + B, 'P' + B, 'E' + B, 'Z' + B, 'Y' + B],
      i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
  }

}
mat-form-field {
  width: 100%;
}

form {
  padding: 15px;
}

.shout {
  font-size: 2rem;
}
import { ReactiveFormsModule, FormsModule, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
import { MatInputModule, MatButtonModule, MatIconModule, MatFormFieldModule } from '@angular/material';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { InputFileComponent } from './input-file.component';

describe('InputFileComponent', () => {
  let component: InputFileComponent;
  let fixture: ComponentFixture<InputFileComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        InputFileComponent
      ],
      imports: [
        ReactiveFormsModule,
        FormsModule,
        // Material modules
        MatFormFieldModule,
        MatInputModule,
        MatButtonModule,
        MatIconModule
      ],
      providers: [
        {provide: NgControl, useValue: NG_VALUE_ACCESSOR}
      ]
    })
      .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(InputFileComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should be created', () => {
    expect(component).toBeTruthy();
  });
});
import { FormControl } from '@angular/forms';
import { FileInput } from './file-input.model';
import { FileValidators } from './file-validators';

describe('FileValidators', () => {

    describe('maxContentSize', () => {

        it('should validate', () => {
            const data = new FileInput([
                new File(['test'], 'test.txt')
            ]);
            const control = new FormControl(data, [FileValidators.maxContentSize(5)]);
            expect(control.value).toBe(data);
            expect(control.valid).toBeTruthy();
        });

        it('should validate with size equal', () => {
            const data = new FileInput([
                new File(['test'], 'test.txt')
            ]);
            const control = new FormControl(data, [FileValidators.maxContentSize(4)]);
            expect(control.value).toBe(data);
            expect(control.valid).toBeTruthy();
        });

        it('should not validate', () => {
            const data = new FileInput([
                new File(['test'], 'test.txt')
            ]);
            const control = new FormControl(data, [FileValidators.maxContentSize(3)]);
            expect(control.value).toBe(data);
            expect(control.valid).toBeFalsy();
        });

        it('should not validate, with "maxContentSize" error', () => {
            const data = new FileInput([
                new File(['test'], 'test.txt')
            ]);
            const control = new FormControl(data, [FileValidators.maxContentSize(3)]);
            expect(control.errors.maxContentSize).toEqual({
                actualSize: 4, maxSize: 3
            });
            expect(control.hasError('maxContentSize')).toBeTruthy();
        });

    });

});