File

src/lib/upload-button/upload-button.component.ts

Implements

ControlValueAccessor MatFormFieldControl OnDestroy

Metadata

Index

Properties
Methods
Inputs
Outputs
HostListeners
Accessors

Constructor

constructor()

Inputs

disabled
Type : boolean
Default value : false
placeholder
Type : string
required
Type : boolean
Default value : false
allowDownload
Default value : false

Outputs

uploaded

HostListeners

focusin
Arguments : '$event'
focusin(event: FocusEvent)
focusout
Arguments : '$event'
focusout(event: FocusEvent)

Methods

clear
clear()
Returns : void
download
download()
Returns : void
Public onContainerClick
onContainerClick(event: MouseEvent)
Parameters :
Name Type Optional
event MouseEvent No
Returns : void
onFileInputChange
onFileInputChange($event: any)
Parameters :
Name Type Optional
$event any No
Returns : void
onFocusIn
onFocusIn(event: FocusEvent)
Decorators :
@HostListener('focusin', ['$event'])
Parameters :
Name Type Optional
event FocusEvent No
Returns : void
onFocusOut
onFocusOut(event: FocusEvent)
Decorators :
@HostListener('focusout', ['$event'])
Parameters :
Name Type Optional
event FocusEvent No
Returns : void
Public openOverlay
openOverlay()
Returns : void
Public registerOnChange
registerOnChange(fn: any)
Parameters :
Name Type Optional
fn any No
Returns : void
Public registerOnTouched
registerOnTouched(fn: any)
Parameters :
Name Type Optional
fn any No
Returns : void
Public setDescribedByIds
setDescribedByIds(ids: string[])
Parameters :
Name Type Optional
ids string[] No
Returns : void
Public setDisabledState
setDisabledState(isDisabled: boolean)
Parameters :
Name Type Optional
isDisabled boolean No
Returns : void
Public writeValue
writeValue(file: File)
Parameters :
Name Type Optional
file File No
Returns : void

Properties

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

Accessors

placeholder
getplaceholder()
setplaceholder(plh)
Parameters :
Name Optional
plh No
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>&nbsp;
      @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;
}
Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""