@deeby/dottmoon-media-dropzone
v1.0.0
Published
A standalone Angular library for file uploads with drag & drop, paste, and configurable validation for images, videos, and more
Maintainers
Readme
@deeby/dottmoon-media-dropzone
A standalone Angular 21+ library for file uploads with drag & drop, paste from clipboard, and click-to-browse functionality. Includes built-in validation, preview generation, image cropping, and full Reactive Forms support.
Features
- Drag & Drop — Visual drop zone with hover feedback
- Paste from Clipboard — Ctrl+V/Cmd+V to paste images directly
- Click to Browse — Traditional file picker support
- Image Crop & Rotate — Interactive canvas-based crop tool with edge + corner handles, rule-of-thirds grid, cursor feedback, and live dimension display
- Multi-file Support — Single or multiple file uploads
- Auto Previews — Image and video thumbnails
- File Validation — Type, size, dimensions, and duration checks
- Reactive Forms — Full
ControlValueAccessorimplementation - Theming — Full CSS custom property theming (light, dark, brand)
- i18n Ready — Fully customizable labels
- Tree-shakeable — ESM modules for optimal bundle size
- Angular 21 Best Practices — Signal-based inputs/outputs,
inject(),viewChild(), OnPush change detection
Installation
npm install @deeby/dottmoon-media-dropzoneRequirements: Angular 21+
Quick Start
Basic Usage
import { Component } from '@angular/core';
import { MediaDropzoneComponent, MediaFile } from '@deeby/dottmoon-media-dropzone';
@Component({
selector: 'app-upload',
imports: [MediaDropzoneComponent],
template: `
<lib-media-dropzone
[config]="uploadConfig"
(filesSelected)="onFilesSelected($event)"
/>
`
})
export class UploadComponent {
uploadConfig = {
accept: 'image/*',
maxFileSize: 5_000_000,
showPreview: true
};
onFilesSelected(files: MediaFile[]) {
console.log('Selected files:', files);
}
}Reactive Forms Integration
import { Component } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { MediaDropzoneComponent, MediaFile } from '@deeby/dottmoon-media-dropzone';
@Component({
selector: 'app-form',
imports: [ReactiveFormsModule, MediaDropzoneComponent],
template: `
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<lib-media-dropzone
formControlName="profileImage"
[config]="uploadConfig"
/>
<button type="submit" [disabled]="form.invalid">Submit</button>
</form>
`
})
export class FormComponent {
private fb = new FormBuilder();
form = this.fb.group({
username: ['', Validators.required],
profileImage: [null, Validators.required]
});
uploadConfig = {
accept: 'image/*',
maxFileSize: 10_000_000,
showPreview: true
};
onSubmit() {
if (this.form.valid) {
const mediaFile = this.form.value.profileImage as MediaFile;
console.log('Form data:', this.form.value);
}
}
}API Reference
Component Selector
<lib-media-dropzone />Inputs
| Input | Type | Description |
|-------|------|-------------|
| config | Partial<MediaDropzoneConfig> | Configuration object (all properties optional) |
Outputs
| Output | Type | Description |
|--------|------|-------------|
| filesSelected | MediaFile[] | Emits when files are selected (after validation) |
| fileRemoved | MediaFile | Emits when a file is removed |
| validationError | string[] | Emits validation error messages |
Configuration Options
MediaDropzoneConfig
interface MediaDropzoneConfig {
// File Type Configuration
accept?: string; // MIME types (e.g., 'image/*,video/*')
allowedExtensions?: string[]; // Specific extensions (e.g., ['.jpg', '.png'])
// Size Limits
maxFileSize?: number; // Max file size in bytes (default: 10MB)
maxTotalSize?: number; // Total size limit for all files combined
maxFiles?: number; // Max number of files (default: 1)
// Image Validation
maxImageWidth?: number; // Max width in pixels
maxImageHeight?: number; // Max height in pixels
// Crop & Rotation
enableCrop?: boolean; // Enable crop modal for images (default: false)
cropAspectRatio?: number; // Lock aspect ratio (e.g., 16/9, 1, 4/3)
cropOutputWidth?: number; // Final output width after crop
cropOutputHeight?: number; // Final output height after crop
enableRotation?: boolean; // Enable 90 deg rotation in crop modal (default: true)
// Video Validation
maxVideoDuration?: number; // Max duration in seconds
// Features
enableDragDrop?: boolean; // Enable drag & drop (default: true)
enablePaste?: boolean; // Enable paste from clipboard (default: true)
enableClick?: boolean; // Enable click to browse (default: true)
// Display Options
showPreview?: boolean; // Show file previews (default: true)
showFileSize?: boolean; // Show file size (default: true)
showProgressBar?: boolean; // Show progress bar (default: true)
// Output Format
outputFormat?: 'file' | 'base64' | 'both'; // Default: 'both'
// Internationalization
labels?: Partial<MediaDropzoneLabels>;
}Output Formats
'file'— Returns nativeFileobjects'base64'— Returns base64-encoded preview strings'both'(default) — ReturnsMediaFileobjects with both
MediaFile Interface
interface MediaFile {
id: string; // Unique identifier
file: File; // Original File object
preview?: string; // Base64 preview URL
type: 'image' | 'video' | 'other'; // Detected file type
isValid: boolean; // Validation result
errors: string[]; // Validation error messages
progress: number; // Upload progress (0-100)
}MediaDropzoneLabels (i18n)
interface MediaDropzoneLabels {
dropHere: string; // "Drop files here"
orClickToUpload: string; // "or click to upload"
chooseFiles: string; // "Choose Files"
removeFile: string; // "Remove file"
remove: string; // "Remove"
fileTooLarge: string; // "File is too large"
invalidFileType: string; // "Invalid file type"
maxFilesReached: string; // "Maximum files reached"
maxDimensionsExceeded: string; // "Image dimensions exceed limit"
maxDurationExceeded: string; // "Video duration exceeds limit"
cropTitle: string; // "Crop Image"
cropConfirm: string; // "Confirm"
cropCancel: string; // "Cancel"
rotateLeft: string; // "Rotate left"
rotateRight: string; // "Rotate right"
}Usage Examples
Single Image Upload
config = {
accept: 'image/*',
maxFileSize: 5_000_000,
maxFiles: 1,
showPreview: true
};Multiple Files with Size Limit
<lib-media-dropzone
[config]="{
accept: 'image/*,application/pdf',
maxFiles: 5,
maxFileSize: 10_000_000,
maxTotalSize: 50_000_000
}"
(filesSelected)="handleFiles($event)"
/>Video with Duration Limit
config = {
accept: 'video/*',
maxFileSize: 100_000_000,
maxVideoDuration: 120,
showPreview: true
};Image Crop with Aspect Ratio Lock
config = {
accept: 'image/*',
enableCrop: true,
cropAspectRatio: 16 / 9,
cropOutputWidth: 1920,
cropOutputHeight: 1080
};The crop modal features:
- Interactive canvas with draggable, resizable crop rectangle
- Corner handles (circular) and edge handles (N/S/E/W) for precise resizing
- Edge handles hidden automatically when aspect ratio is locked
- Rule-of-thirds grid overlay
- Dynamic cursor feedback (resize, move, crosshair)
- Live crop dimensions display in the toolbar
- 90 deg rotation with buttons
- Responsive dialog sizing that adapts to the image
Free-Form Image Crop
config = {
accept: 'image/*',
enableCrop: true,
enableRotation: true
};Square Crop (1:1)
config = {
accept: 'image/*',
enableCrop: true,
cropAspectRatio: 1,
cropOutputWidth: 500,
cropOutputHeight: 500
};Custom Labels (Internationalization)
config = {
accept: 'image/*',
labels: {
dropHere: 'Dateien hier ablegen',
orClickToUpload: 'oder klicken zum Hochladen',
chooseFiles: 'Dateien auswaehlen',
removeFile: 'Datei entfernen',
fileTooLarge: 'Datei ist zu gross',
maxFilesReached: 'Maximale Dateianzahl erreicht',
cropTitle: 'Bild zuschneiden',
cropConfirm: 'Bestaetigen',
cropCancel: 'Abbrechen'
}
};Advanced Usage
Handling Validation Errors
<lib-media-dropzone
[config]="config"
(filesSelected)="onFiles($event)"
(validationError)="onError($event)"
/>onError(errors: string[]) {
errors.forEach(error => this.toastService.error(error));
}
onFiles(files: MediaFile[]) {
const validFiles = files.filter(f => f.isValid);
this.uploadService.upload(validFiles);
}Using Directives Separately
Build custom UI with the underlying directives:
import {
DropZoneDirective,
PasteHandlerDirective
} from '@deeby/dottmoon-media-dropzone';
@Component({
template: `
<div
class="custom-dropzone"
libDropZone
libPasteHandler
(filesDropped)="onDrop($event)"
(filesPasted)="onPaste($event)"
tabindex="0">
Custom styled drop zone
</div>
`,
imports: [DropZoneDirective, PasteHandlerDirective]
})
export class CustomDropzoneComponent {
onDrop(files: File[]) { console.log('Dropped:', files); }
onPaste(files: File[]) { console.log('Pasted:', files); }
}Styling & Theming
The component supports full theming via CSS custom properties. All colors have sensible defaults — override only what you need.
Available CSS Variables
lib-media-dropzone {
/* Primary / accent */
--mdz-primary: #2196f3;
--mdz-primary-hover: #1976d2;
/* Backgrounds */
--mdz-bg: #fafafa;
--mdz-card-bg: #fff;
--mdz-hover-bg: #e3f2fd;
--mdz-drag-bg: #e8f5e9;
--mdz-preview-bg: #f5f5f5;
--mdz-error-bg: #fff5f5;
/* Borders */
--mdz-border-color: #bdbdbd;
--mdz-hover-border: var(--mdz-primary);
/* Text */
--mdz-text: #424242;
--mdz-text-secondary: #757575;
--mdz-icon-color: #757575;
--mdz-btn-text: white;
/* Feedback */
--mdz-error: #f44336;
--mdz-success: #4caf50;
--mdz-disabled: #bdbdbd;
/* Remove button */
--mdz-remove-bg: rgba(0, 0, 0, 0.5);
--mdz-remove-color: white;
--mdz-remove-hover-bg: rgba(0, 0, 0, 0.7);
/* Crop Modal */
--mdz-dialog-bg: #fff;
--mdz-toolbar-bg: #fafafa;
--mdz-crop-bg: #1a1a1a;
--mdz-btn-bg: #f0f0f0;
--mdz-btn-hover-bg: #e0e0e0;
--mdz-badge-bg: #e8e8e8;
}Example: Dark Mode
.dark-mode lib-media-dropzone {
--mdz-bg: #1e293b;
--mdz-card-bg: #1e293b;
--mdz-border-color: #334155;
--mdz-text: #f1f5f9;
--mdz-text-secondary: #94a3b8;
--mdz-error-bg: rgba(244, 67, 54, 0.1);
--mdz-dialog-bg: #1e293b;
--mdz-toolbar-bg: #1e293b;
--mdz-btn-bg: #334155;
--mdz-btn-hover-bg: #475569;
--mdz-crop-bg: #0f172a;
}Playground
A built-in playground app is included for testing all features interactively:
git clone https://github.com/deebyy/dottmoon-media-dropzone
cd dottmoon-media-dropzone
npm install
npm run start:playgroundOpen http://localhost:4200 to access 8 demo sections: basic upload, multi-file, crop mode, video, reactive forms, theming, validation, and drag & drop + paste.
Changelog
v1.0.0
Angular 21 Modernization
- Migrated to signal-based
input()/output()APIs throughout - Replaced
@ViewChildwithviewChild()signal queries - Replaced constructor DI with
inject() - Removed redundant
standalone: true(default in Angular 21) - Replaced deprecated
::ng-deepwith scoped selectors - Modernized directives to use
hostmetadata instead of@HostListener/@HostBinding - Removed
forwardRef(unnecessary in Angular 21) - Fixed
anytypes onControlValueAccessormethods
Crop Dialog UI Overhaul
- Responsive dialog sizing — adapts to image dimensions instead of fixed 900px width
- Reduced blank space — removed excessive min-height and padding
- Added edge resize handles (N/S/E/W) for free-crop mode
- Added dynamic cursor feedback (resize, move, crosshair)
- Added live crop dimensions display in the toolbar
- Circular corner handles with subtle shadows
- Thinner crop border with refined rule-of-thirds grid
- Compact, polished header/toolbar/footer spacing
Playground App
- Added interactive test application with 8 demo sections
- Run with
npm run start:playground
v0.10.1
- Image crop and rotate support
- File preview component
- Rewritten media dropzone component
License
MIT (c) Deebyy
Contributing
Contributions, issues, and feature requests are welcome! Check the issues page.
Resources
Made with Angular 21
