Skip to content

Angular

Basic Example

typescript
// avatar-upload.component.ts
import { Component, signal } from '@angular/core'
import { HqCropper, type HqCropperInstance } from 'hq-cropper'

@Component({
    selector: 'app-avatar-upload',
    standalone: true,
    template: `
        <div class="avatar-upload">
            @if (avatar()) {
                <img [src]="avatar()" alt="Avatar" />
            }
            <button (click)="openCropper()">Upload Avatar</button>
        </div>
    `,
})
export class AvatarUploadComponent {
    avatar = signal('')
    private cropper: HqCropperInstance

    constructor() {
        this.cropper = HqCropper((base64) => {
            this.avatar.set(base64)
        })
    }

    openCropper() {
        this.cropper.open()
    }
}

With Configuration

typescript
// profile-picture.component.ts
import { Component, signal } from '@angular/core'
import {
    HqCropper,
    type HqCropperInstance,
    type ApplicationState,
} from 'hq-cropper'

@Component({
    selector: 'app-profile-picture',
    standalone: true,
    template: `
        <div class="profile-picture">
            @if (error()) {
                <div class="error">{{ error() }}</div>
            }

            @if (image()) {
                <div class="preview">
                    <img [src]="image()" alt="Profile" />
                    <p>{{ fileName() }}</p>
                </div>
            } @else {
                <div class="placeholder">No image selected</div>
            }

            <button (click)="openCropper()">
                {{ image() ? 'Change Image' : 'Upload Image' }}
            </button>
        </div>
    `,
})
export class ProfilePictureComponent {
    image = signal('')
    fileName = signal('')
    error = signal<string | null>(null)

    private cropper: HqCropperInstance

    constructor() {
        this.cropper = HqCropper(
            (base64, blob, state: ApplicationState) => {
                this.image.set(base64)
                this.fileName.set(state.fileName)
                this.error.set(null)
            },
            {
                outputSize: 256,
                type: 'jpeg',
                compression: 0.8,
                maxFileSize: 5 * 1024 * 1024,
            },
            undefined,
            (message) => {
                this.error.set(message)
            }
        )
    }

    openCropper() {
        this.cropper.open()
    }
}

Service-based Approach

typescript
// cropper.service.ts
import { Injectable, signal } from '@angular/core'
import {
    HqCropper,
    type HqCropperInstance,
    type ConfigurationOptions,
    type ClassNames,
    type ApplicationState,
} from 'hq-cropper'

@Injectable({
    providedIn: 'root',
})
export class CropperService {
    private cropper: HqCropperInstance | null = null

    readonly image = signal<string | null>(null)
    readonly blob = signal<Blob | null>(null)
    readonly state = signal<ApplicationState | null>(null)
    readonly error = signal<string | null>(null)

    initialize(
        config?: Partial<ConfigurationOptions>,
        css?: Partial<ClassNames>
    ) {
        this.cropper = HqCropper(
            (base64, blobData, stateData) => {
                this.image.set(base64)
                this.blob.set(blobData)
                this.state.set(stateData)
                this.error.set(null)
            },
            config,
            css,
            (message) => {
                this.error.set(message)
            }
        )
    }

    open() {
        this.cropper?.open()
    }

    clear() {
        this.image.set(null)
        this.blob.set(null)
        this.state.set(null)
    }
}
typescript
// app.component.ts
import { Component, inject, OnInit } from '@angular/core'
import { CropperService } from './cropper.service'

@Component({
    selector: 'app-root',
    standalone: true,
    template: `
        <div>
            @if (cropperService.image()) {
                <img [src]="cropperService.image()" alt="" />
            }
            <button (click)="cropperService.open()">Select Image</button>
            @if (cropperService.image()) {
                <button (click)="cropperService.clear()">Clear</button>
            }
        </div>
    `,
})
export class AppComponent implements OnInit {
    cropperService = inject(CropperService)

    ngOnInit() {
        this.cropperService.initialize({
            outputSize: 512,
            type: 'png',
        })
    }
}

With Reactive Forms

typescript
// registration-form.component.ts
import { Component, signal } from '@angular/core'
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'
import { HqCropper, type HqCropperInstance } from 'hq-cropper'

@Component({
    selector: 'app-registration-form',
    standalone: true,
    imports: [ReactiveFormsModule],
    template: `
        <form [formGroup]="form" (ngSubmit)="onSubmit()">
            <div class="avatar-section">
                @if (preview()) {
                    <img [src]="preview()" alt="Avatar preview" />
                }
                <button type="button" (click)="openCropper()">
                    Upload Avatar
                </button>
            </div>

            <input formControlName="name" placeholder="Name" />
            <input formControlName="email" placeholder="Email" type="email" />

            <button type="submit" [disabled]="form.invalid">Register</button>
        </form>
    `,
})
export class RegistrationFormComponent {
    preview = signal('')
    private avatarBlob: Blob | null = null
    private cropper: HqCropperInstance

    form = inject(FormBuilder).group({
        name: ['', Validators.required],
        email: ['', [Validators.required, Validators.email]],
    })

    constructor() {
        this.cropper = HqCropper(
            (base64, blob) => {
                this.preview.set(base64)
                this.avatarBlob = blob
            },
            { outputSize: 256 }
        )
    }

    openCropper() {
        this.cropper.open()
    }

    async onSubmit() {
        if (this.form.invalid) return

        const formData = new FormData()
        formData.append('name', this.form.value.name!)
        formData.append('email', this.form.value.email!)

        if (this.avatarBlob) {
            formData.append('avatar', this.avatarBlob, 'avatar.jpg')
        }

        await fetch('/api/register', {
            method: 'POST',
            body: formData,
        })
    }
}

Directive Approach

typescript
// cropper.directive.ts
import {
    Directive,
    ElementRef,
    EventEmitter,
    Input,
    OnInit,
    Output,
} from '@angular/core'
import {
    HqCropper,
    type HqCropperInstance,
    type ConfigurationOptions,
    type ApplicationState,
} from 'hq-cropper'

@Directive({
    selector: '[appCropper]',
    standalone: true,
})
export class CropperDirective implements OnInit {
    @Input() cropperConfig?: Partial<ConfigurationOptions>
    @Output() imageCropped = new EventEmitter<{
        base64: string
        blob: Blob | null
        state: ApplicationState
    }>()
    @Output() cropperError = new EventEmitter<string>()

    private cropper!: HqCropperInstance

    constructor(private el: ElementRef<HTMLElement>) {}

    ngOnInit() {
        this.cropper = HqCropper(
            (base64, blob, state) => {
                this.imageCropped.emit({ base64, blob, state })
            },
            this.cropperConfig,
            undefined,
            (error) => {
                this.cropperError.emit(error)
            }
        )

        this.el.nativeElement.addEventListener('click', () => {
            this.cropper.open()
        })
    }
}
typescript
// Usage in component
import { Component, signal } from '@angular/core'
import { CropperDirective } from './cropper.directive'

@Component({
    selector: 'app-example',
    standalone: true,
    imports: [CropperDirective],
    template: `
        @if (avatar()) {
            <img [src]="avatar()" alt="Avatar" />
        }
        <button
            appCropper
            [cropperConfig]="{ outputSize: 256 }"
            (imageCropped)="onImageCropped($event)"
            (cropperError)="onError($event)"
        >
            Upload
        </button>
    `,
})
export class ExampleComponent {
    avatar = signal('')

    onImageCropped(event: { base64: string }) {
        this.avatar.set(event.base64)
    }

    onError(message: string) {
        console.error(message)
    }
}

With Angular Material

typescript
// material-avatar.component.ts
import { Component, signal } from '@angular/core'
import { MatButtonModule } from '@angular/material/button'
import { MatIconModule } from '@angular/material/icon'
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar'
import { HqCropper, type HqCropperInstance } from 'hq-cropper'

@Component({
    selector: 'app-material-avatar',
    standalone: true,
    imports: [MatButtonModule, MatIconModule, MatSnackBarModule],
    template: `
        <div class="avatar-container">
            @if (avatar()) {
                <img [src]="avatar()" alt="Avatar" class="avatar-image" />
            } @else {
                <div class="avatar-placeholder">
                    <mat-icon>person</mat-icon>
                </div>
            }

            <button mat-fab color="primary" (click)="openCropper()">
                <mat-icon>photo_camera</mat-icon>
            </button>
        </div>
    `,
    styles: [
        `
            .avatar-container {
                position: relative;
                width: 150px;
            }
            .avatar-image,
            .avatar-placeholder {
                width: 150px;
                height: 150px;
                border-radius: 50%;
            }
            .avatar-placeholder {
                background: #e0e0e0;
                display: flex;
                align-items: center;
                justify-content: center;
            }
            .avatar-placeholder mat-icon {
                font-size: 64px;
                width: 64px;
                height: 64px;
            }
            button {
                position: absolute;
                bottom: 0;
                right: 0;
            }
        `,
    ],
})
export class MaterialAvatarComponent {
    avatar = signal('')
    private cropper: HqCropperInstance

    private snackBar = inject(MatSnackBar)

    constructor() {
        this.cropper = HqCropper(
            (base64) => {
                this.avatar.set(base64)
                this.snackBar.open('Avatar updated!', 'Close', {
                    duration: 3000,
                })
            },
            {
                outputSize: 256,
                type: 'jpeg',
                compression: 0.9,
            },
            undefined,
            (error) => {
                this.snackBar.open(error, 'Close', { duration: 5000 })
            }
        )
    }

    openCropper() {
        this.cropper.open()
    }
}

NgModule (Non-standalone)

typescript
// cropper.module.ts
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { AvatarUploadComponent } from './avatar-upload.component'

@NgModule({
    declarations: [AvatarUploadComponent],
    imports: [CommonModule],
    exports: [AvatarUploadComponent],
})
export class CropperModule {}
typescript
// avatar-upload.component.ts (non-standalone)
import { Component } from '@angular/core'
import { HqCropper, type HqCropperInstance } from 'hq-cropper'

@Component({
    selector: 'app-avatar-upload',
    template: `
        <div>
            <img *ngIf="avatar" [src]="avatar" alt="Avatar" />
            <button (click)="openCropper()">Upload</button>
        </div>
    `,
})
export class AvatarUploadComponent {
    avatar = ''
    private cropper: HqCropperInstance

    constructor() {
        this.cropper = HqCropper((base64) => {
            this.avatar = base64
        })
    }

    openCropper() {
        this.cropper.open()
    }
}

Released under the MIT License.