src/lib/upload-button/upload-button.component.ts
ControlValueAccessor
MatFormFieldControl
OnDestroy
| changeDetection | ChangeDetectionStrategy.OnPush |
| host | { |
| providers |
{
provide: MatFormFieldControl, useExisting: UploadButtonComponent,
}
|
| selector | rxap-upload-button |
| imports |
MatButtonModule
CdkOverlayOrigin
MethodDirective
MatIconModule
MatProgressSpinnerModule
MatTooltipModule
CdkConnectedOverlay
AsyncPipe
ReadAsDataURLPipe
MatSnackBarModule
|
| styleUrls | ./upload-button.component.scss |
| templateUrl | ./upload-button.component.html |
Properties |
|
Methods |
|
Inputs |
Outputs |
HostListeners |
Accessors |
constructor()
|
| disabled | |
Type : boolean
|
|
Default value : false
|
|
| placeholder | |
Type : string
|
|
| required | |
Type : boolean
|
|
Default value : false
|
|
| allowDownload | |
Default value : false
|
|
| uploaded | |
| focusin |
Arguments : '$event'
|
focusin(event: FocusEvent)
|
| focusout |
Arguments : '$event'
|
focusout(event: FocusEvent)
|
| clear |
clear()
|
|
Returns :
void
|
| download |
download()
|
|
Returns :
void
|
| Public onContainerClick | ||||||
onContainerClick(event: MouseEvent)
|
||||||
|
Parameters :
Returns :
void
|
| onFileInputChange | ||||||
onFileInputChange($event: any)
|
||||||
|
Parameters :
Returns :
void
|
| onFocusIn | ||||||
onFocusIn(event: FocusEvent)
|
||||||
Decorators :
@HostListener('focusin', ['$event'])
|
||||||
|
Parameters :
Returns :
void
|
| onFocusOut | ||||||
onFocusOut(event: FocusEvent)
|
||||||
Decorators :
@HostListener('focusout', ['$event'])
|
||||||
|
Parameters :
Returns :
void
|
| Public openOverlay |
openOverlay()
|
|
Returns :
void
|
| Public registerOnChange | ||||||
registerOnChange(fn: any)
|
||||||
|
Parameters :
Returns :
void
|
| Public registerOnTouched | ||||||
registerOnTouched(fn: any)
|
||||||
|
Parameters :
Returns :
void
|
| Public setDescribedByIds | ||||||
setDescribedByIds(ids: string[])
|
||||||
|
Parameters :
Returns :
void
|
| Public setDisabledState | ||||||
setDisabledState(isDisabled: boolean)
|
||||||
|
Parameters :
Returns :
void
|
| Public writeValue | ||||||
writeValue(file: File)
|
||||||
|
Parameters :
Returns :
void
|
| Public accept |
Default value : input<string | string[]>('**/**')
|
| Readonly acceptAttribute |
Default value : computed(() => Array.isArray(this.accept) ? this.accept.join(',') : this.accept)
|
| Public allowDownload |
Default value : false
|
| Optional autofilled |
Type : boolean | undefined
|
| Optional controlType |
Type : string | undefined
|
Default value : 'rxap-upload-button'
|
| errorState |
Default value : false
|
| focused |
Default value : false
|
| Public isImageUploaded |
Default value : signal(false)
|
| Static nextId |
Type : number
|
Default value : 0
|
| Public Readonly ngControl |
Default value : inject(NgControl, {
optional: true,
self: true,
})
|
| Public positions |
Type : ConnectedPosition[]
|
Default value : [
{
originY: 'bottom',
originX: 'start',
overlayX: 'start',
overlayY: 'top',
offsetY: 12,
},
]
|
| shouldLabelFloat |
Default value : true
|
| Public Readonly snackBar |
Default value : inject(MatSnackBar)
|
| Public stateChanges |
Default value : new Subject<void>()
|
| touched |
Default value : false
|
| Public uploaded |
| Optional userAriaDescribedBy |
Type : string | undefined
|
| Public value |
Type : File | null
|
Default value : null
|
| placeholder | ||||
getplaceholder()
|
||||
setplaceholder(plh)
|
||||
|
Parameters :
Returns :
void
|
| empty |
getempty()
|
| id |
getid()
|
| isImage |
getisImage()
|
import {
CdkConnectedOverlay,
CdkOverlayOrigin,
ConnectedPosition,
} from '@angular/cdk/overlay';
import {
AsyncPipe,
DOCUMENT,
NgIf,
} from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
computed,
ElementRef,
EventEmitter,
HostListener,
inject,
input,
Input,
OnDestroy,
output,
Output,
signal,
} from '@angular/core';
import {
ControlValueAccessor,
NgControl,
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldControl } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import {
MatSnackBar,
MatSnackBarModule,
} from '@angular/material/snack-bar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MethodDirective } from '@rxap/directives';
import { Subject } from 'rxjs';
import { ReadAsDataURLPipe } from './read-as-data-url.pipe';
@Component({
selector: 'rxap-upload-button',
templateUrl: './upload-button.component.html',
styleUrls: ['./upload-button.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'rxap-upload-button' },
providers: [
{
provide: MatFormFieldControl,
useExisting: UploadButtonComponent,
},
],
imports: [
MatButtonModule,
CdkOverlayOrigin,
MethodDirective,
MatIconModule,
MatProgressSpinnerModule,
MatTooltipModule,
CdkConnectedOverlay,
AsyncPipe,
ReadAsDataURLPipe,
MatSnackBarModule,
]
})
export class UploadButtonComponent implements ControlValueAccessor, MatFormFieldControl<File>, OnDestroy {
static nextId = 0;
public accept = input<string | string[]>('**/**');
readonly acceptAttribute = computed(() => Array.isArray(this.accept) ? this.accept.join(',') : this.accept);
public allowDownload = input(false);
public uploaded = output<File>();
public isImageUploaded = signal(false);
public positions: ConnectedPosition[] = [
{
originY: 'bottom',
originX: 'start',
overlayX: 'start',
overlayY: 'top',
offsetY: 12,
},
];
touched = false;
// region MatFormFieldControl
@Input()
public disabled = false;
autofilled?: boolean | undefined;
controlType?: string | undefined = 'rxap-upload-button';
errorState = false;
focused = false;
@Input()
public required = false;
shouldLabelFloat = true;
public stateChanges = new Subject<void>();
userAriaDescribedBy?: string | undefined;
public value: File | null = null;
public readonly ngControl = inject(NgControl, {
optional: true,
self: true,
});
private _placeholder!: string;
get placeholder() {
return this._placeholder;
}
@Input()
set placeholder(plh) {
this._placeholder = plh;
this.stateChanges.next();
}
public get empty(): boolean {
return this.value === null;
}
public get id(): string {
return `rxap-upload-button-${ UploadButtonComponent.nextId++ }`;
}
// endregion
private onChange?: (file: File) => any;
private onTouched?: () => any;
public readonly snackBar = inject(MatSnackBar);
private readonly cdr = inject(ChangeDetectorRef);
private readonly _elementRef = inject(ElementRef);
private readonly document = inject(DOCUMENT);
constructor() {
if (this.ngControl != null) {
this.ngControl.valueAccessor = this;
}
}
get isImage(): boolean {
return !!this.value?.type.match(/^image\//);
}
// region MatFormFieldControl
// eslint-disable-next-line @typescript-eslint/no-empty-function
public onContainerClick(event: MouseEvent): void {
}
public setDescribedByIds(ids: string[]): void {
this._elementRef
.nativeElement
.querySelector('button')
?.setAttribute('aria-describedby', ids.join(' '));
}
public ngOnDestroy() {
this.stateChanges.complete();
}
public registerOnChange(fn: any): void {
this.onChange = fn;
}
public registerOnTouched(fn: any): void {
this.onTouched = fn;
}
public setDisabledState(isDisabled: boolean) {
this.disabled = isDisabled;
}
// endregion
public writeValue(file: File): void {
this.value = file;
this.cdr.detectChanges();
}
public openOverlay() {
if (this.value?.type.match(/^image\//)) {
this.isImageUploaded.set(true);
}
}
@HostListener('focusin', [ '$event' ])
onFocusIn(event: FocusEvent) {
if (!this.focused) {
this.focused = true;
this.stateChanges.next();
}
}
@HostListener('focusout', [ '$event' ])
onFocusOut(event: FocusEvent) {
if (!this._elementRef.nativeElement.contains(event.relatedTarget as Element)) {
this.touched = true;
this.focused = false;
if (this.onTouched) {
this.onTouched();
}
this.stateChanges.next();
}
}
download() {
if (this.value) {
const elm = this.document.createElement('a');
this.document.body.appendChild(elm);
elm.href = URL.createObjectURL(this.value);
elm.setAttribute('download', this.value.name);
elm.click();
this.document.body.removeChild(elm);
}
}
onFileInputChange($event: any) {
const files: { [key: string]: File } = $event?.target?.files as any ?? {};
const file = Object.values(files)[0];
if (!file) {
this.snackBar.open('No file selected', 'close', { duration: 3000 });
} else {
this.uploaded.emit(file);
this.value = file;
if (this.onChange) {
this.onChange(file);
}
this.stateChanges.next();
}
}
clear() {
this.value = null;
}
}
<div class="flex flex-row gap-2 items-center">
<button
#trigger="cdkOverlayOrigin"
[disabled]="disabled"
(click)="fileInput.click()"
cdkOverlayOrigin
class="grow-0"
mat-stroked-button
type="button">
<span class="flex flex-row justify-center items-center gap-3">
<mat-icon class="grow-0" svgIcon="upload"></mat-icon>
@if (value) {
<span class="truncate grow-0">{{ value.name }}</span>
} @else {
@if (placeholder) {
<span>{{ placeholder }}</span>
}
<ng-content></ng-content>
}
</span>
</button>
@if (isImage) {
<button (click)="openOverlay()"
class="grow-0"
i18n-matTooltip
mat-icon-button
matTooltip="Open image preview"
type="button">
<mat-icon>preview</mat-icon>
</button>
}
@if (allowDownload() && value) {
<button (click)="download()" class="grow-0" i18n-matTooltip mat-icon-button
matTooltip="Download file" type="button">
<mat-icon svgIcon="download"></mat-icon>
</button>
}
</div>
<ng-template
(backdropClick)="isImageUploaded.set(false)"
[cdkConnectedOverlayHasBackdrop]="true"
[cdkConnectedOverlayOpen]="isImageUploaded()"
[cdkConnectedOverlayOrigin]="trigger"
[cdkConnectedOverlayPositions]="positions"
cdkConnectedOverlay
>
<div class="bg-white dark:bg-black drop-shadow-xl overlay-container p-2 rounded overflow-auto">
<img [src]="value | readAsDataURL | async" class="w-full">
</div>
</ng-template>
<input #fileInput (change)="onFileInputChange($event)" [accept]="acceptAttribute()" [id]="id" class="hidden"
type="file">
./upload-button.component.scss
.overlay-container {
min-width: 256px;
max-width: 50vw;
max-height: 50vh;
}