UNPKG

file-input-accessor

Version:

Angular directive that provides file input functionality in reactive & template driven Angular forms.

273 lines (267 loc) 11.7 kB
import * as i0 from '@angular/core'; import { forwardRef, HostListener, Input, Directive, NgModule } from '@angular/core'; import { NG_VALUE_ACCESSOR, NG_ASYNC_VALIDATORS, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { of, forkJoin, fromEventPattern, ReplaySubject } from 'rxjs'; import { take, map, shareReplay, first } from 'rxjs/operators'; class FileInputAccessor { set allowedExt(value) { if (typeof value === 'string') { value = value + '$'; } if (value instanceof Array) { value = value.join('|') + '$'; } this._allowedExt = value; } get allowedExt() { return this._allowedExt; } constructor(_renderer, _elementRef) { this._renderer = _renderer; this._elementRef = _elementRef; this.onChange = (_) => { }; this.onTouched = () => { }; this.validator = this.generateAsyncValidator(); } writeValue(value) { this._renderer.setProperty(this._elementRef.nativeElement, 'value', null); } registerOnChange(fn) { this.onChange = this.onChangeGenerator(fn); } registerOnTouched(fn) { } setDisabledState(isDisabled) { this._renderer.setProperty(this._elementRef.nativeElement, 'disabled', isDisabled); } validate(c) { return this.validator(c); } /** * Generator method that I used to move the code for the AsyncValidator down here so it didn't * get in my way, way up there ^. */ generateAsyncValidator() { return (c) => { if (!c.value || !c.value.length || c.disabled) return of({}); const errors = {}; const loaders = []; for (const f of c.value) { if (this.size && this.size < f.size) { f.errors.fileSize = true; errors.fileSize = true; } if (f.isImg && (this.maxWidth || this.maxHeight || this.minWidth || this.minHeight)) { loaders.push(f.imgLoadReplay .pipe(take(1), map((e) => { const minWidthError = this.minWidth && f.imgWidth < this.minWidth; const minHeightError = this.minHeight && f.imgHeight < this.minHeight; const maxWidthError = this.maxWidth && f.imgWidth > this.maxWidth; const maxHeightError = this.maxHeight && f.imgHeight > this.maxHeight; if (minWidthError) { f.errors.minWidth = true; errors.minWidth = true; } if (minHeightError) { f.errors.minHeight = true; errors.minHeight = true; } if (maxWidthError) { f.errors.maxWidth = true; errors.maxWidth = true; } if (maxHeightError) { f.errors.maxHeight = true; errors.maxHeight = true; } /** will be @deprecated **/ if (minWidthError || maxWidthError) { f.errors.imageWidth = true; errors.imageWidth = true; } /** will be @deprecated **/ if (minHeightError || maxHeightError) { f.errors.imageHeight = true; errors.imageHeight = true; } return e; }))); } if (!this.allowedExt && !this.allowedTypes) continue; const extP = this.generateRegExp(this.allowedExt); const typeP = this.generateRegExp(this.allowedTypes); if (extP && !extP.test(f.name)) { f.errors.fileExt = true; errors.fileExt = true; } if (typeP && !typeP.test(f.type)) { f.errors.fileType = true; errors.fileType = true; } } if (loaders.length) { return forkJoin(...loaders).pipe(map(() => errors)); } return of(errors); }; } /** * Generator method that returns an onChange handler */ onChangeGenerator(fn) { return (files) => { const fileArr = []; for (const f of files) { if (this.withMeta && FileReader) { const fr = new FileReader(); this.generateFileMeta(f, fr); } f.errors = {}; fileArr.push(f); } fn(fileArr); }; } generateRegExp(pattern) { if (!pattern) return null; if (pattern instanceof RegExp) { return new RegExp(pattern); } else if (typeof pattern === 'string') { return new RegExp(pattern, 'ig'); } else if (pattern instanceof Array) { return new RegExp(`(${pattern.join('|')})`, 'ig'); } return null; } /** * The ICustomFile has a ReplaySubject property for text / image files that will emit * once the file has been loaded. Might get removed later since I haven't found a use for it yet. */ generateFileMeta(f, fr) { if (f.type.match(/text.*/)) { f.textLoadReplay = this.setText(f, fr); } else if (f.type.match(/image.*/)) { f.imgLoadReplay = this.setImage(f, fr); } } setImage(f, fr) { f.isImg = true; const img = new Image(); const imgLoadObs = fromEventPattern(((handler) => img.addEventListener('load', handler)), ((handler) => img.removeEventListener('load', handler))).pipe(take(1), shareReplay()); const frLoadObs = fromEventPattern(((handler) => fr.addEventListener('load', handler)), ((handler) => fr.removeEventListener('load', handler))).pipe(take(1), shareReplay()); const onloadReplay = new ReplaySubject(1); forkJoin([imgLoadObs, frLoadObs]).pipe(first()).subscribe(onloadReplay); imgLoadObs.pipe(first()).subscribe(() => { f.imgHeight = img.height; f.imgWidth = img.width; }); frLoadObs.pipe(first()).subscribe(() => { f.imgSrc = fr.result + ''; img.src = fr.result + ''; }); fr.readAsDataURL(f); return onloadReplay; } setText(f, fr) { const frLoadObs = fromEventPattern(((handler) => fr.addEventListener('load', handler)), ((handler) => fr.removeEventListener('load', handler))).pipe(take(1), shareReplay()); const onloadReplay = new ReplaySubject(1); frLoadObs.subscribe(onloadReplay); frLoadObs.pipe(first()).subscribe(() => { f.textContent = fr.result + ''; }); fr.readAsText(f); return onloadReplay; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: FileInputAccessor, deps: [{ token: i0.Renderer2 }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.0.3", type: FileInputAccessor, isStandalone: false, selector: "input[type=file][formControl],input[type=file][formControlName],input[type=file][ngModel]", inputs: { allowedTypes: "allowedTypes", size: "size", withMeta: "withMeta", maxHeight: "maxHeight", maxWidth: "maxWidth", minHeight: "minHeight", minWidth: "minWidth", allowedExt: "allowedExt" }, host: { listeners: { "change": "onChange($event.target.files)", "blur": "onTouched()" } }, providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FileInputAccessor), multi: true }, { provide: NG_ASYNC_VALIDATORS, useExisting: forwardRef(() => FileInputAccessor), multi: true } ], ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: FileInputAccessor, decorators: [{ type: Directive, args: [{ selector: 'input[type=file][formControl],input[type=file][formControlName],input[type=file][ngModel]', providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => FileInputAccessor), multi: true }, { provide: NG_ASYNC_VALIDATORS, useExisting: forwardRef(() => FileInputAccessor), multi: true } ], standalone: false }] }], ctorParameters: () => [{ type: i0.Renderer2 }, { type: i0.ElementRef }], propDecorators: { allowedTypes: [{ type: Input }], size: [{ type: Input }], withMeta: [{ type: Input }], maxHeight: [{ type: Input }], maxWidth: [{ type: Input }], minHeight: [{ type: Input }], minWidth: [{ type: Input }], allowedExt: [{ type: Input }], onChange: [{ type: HostListener, args: ['change', ['$event.target.files']] }], onTouched: [{ type: HostListener, args: ['blur'] }] } }); class FileInputAccessorModule { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: FileInputAccessorModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "20.0.3", ngImport: i0, type: FileInputAccessorModule, declarations: [FileInputAccessor], imports: [FormsModule, ReactiveFormsModule], exports: [FileInputAccessor, FormsModule, ReactiveFormsModule] }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: FileInputAccessorModule, imports: [FormsModule, ReactiveFormsModule, FormsModule, ReactiveFormsModule] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: FileInputAccessorModule, decorators: [{ type: NgModule, args: [{ declarations: [FileInputAccessor], imports: [ FormsModule, ReactiveFormsModule ], exports: [ FileInputAccessor, FormsModule, ReactiveFormsModule ] }] }] }); /* * Public API Surface of file-input-accessor */ /** * Generated bundle index. Do not edit. */ export { FileInputAccessor, FileInputAccessorModule }; //# sourceMappingURL=file-input-accessor.mjs.map