<!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();
});
});
});