file-input-accessor
Version:
Angular directive that provides file input functionality in reactive & template driven Angular forms.
273 lines (267 loc) • 11.7 kB
JavaScript
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