ngx-kit 6.1.0 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/angle-selector/README.md +34 -34
- package/box-shadow/README.md +33 -33
- package/color-picker/README.md +49 -49
- package/data-table/README.md +113 -113
- package/date-picker/README.md +69 -69
- package/dialog/README.md +92 -92
- package/drawer-menu/README.md +110 -62
- package/dropzone/README.md +422 -422
- package/fesm2022/ngx-kit-angle-selector.mjs +2 -2
- package/fesm2022/ngx-kit-angle-selector.mjs.map +1 -1
- package/fesm2022/ngx-kit-box-shadow.mjs +2 -2
- package/fesm2022/ngx-kit-box-shadow.mjs.map +1 -1
- package/fesm2022/ngx-kit-color-picker.mjs +4 -4
- package/fesm2022/ngx-kit-color-picker.mjs.map +1 -1
- package/fesm2022/ngx-kit-data-table.mjs +2 -2
- package/fesm2022/ngx-kit-data-table.mjs.map +1 -1
- package/fesm2022/ngx-kit-date-picker.mjs +1324 -200
- package/fesm2022/ngx-kit-date-picker.mjs.map +1 -1
- package/fesm2022/ngx-kit-dialog.mjs +2 -2
- package/fesm2022/ngx-kit-dialog.mjs.map +1 -1
- package/fesm2022/ngx-kit-drawer-menu.mjs +302 -140
- package/fesm2022/ngx-kit-drawer-menu.mjs.map +1 -1
- package/fesm2022/ngx-kit-dropzone.mjs +2 -2
- package/fesm2022/ngx-kit-dropzone.mjs.map +1 -1
- package/fesm2022/ngx-kit-form-field.mjs.map +1 -1
- package/fesm2022/ngx-kit-gallery.mjs +2 -2
- package/fesm2022/ngx-kit-gallery.mjs.map +1 -1
- package/fesm2022/ngx-kit-gradient-picker.mjs.map +1 -1
- package/fesm2022/ngx-kit-image-editor.mjs +75 -11
- package/fesm2022/ngx-kit-image-editor.mjs.map +1 -1
- package/fesm2022/ngx-kit-menu.mjs +8 -8
- package/fesm2022/ngx-kit-menu.mjs.map +1 -1
- package/fesm2022/ngx-kit-message.mjs +2 -2
- package/fesm2022/ngx-kit-message.mjs.map +1 -1
- package/fesm2022/ngx-kit-notify.mjs.map +1 -1
- package/fesm2022/ngx-kit-shared.mjs +2 -2
- package/fesm2022/ngx-kit-shared.mjs.map +1 -1
- package/fesm2022/ngx-kit-time-picker.mjs +2 -2
- package/fesm2022/ngx-kit-time-picker.mjs.map +1 -1
- package/gallery/README.md +71 -71
- package/gradient-picker/README.md +41 -41
- package/menu/README.md +85 -85
- package/message/README.md +70 -70
- package/notify/README.md +58 -58
- package/package.json +1 -1
- package/shared/README.md +117 -117
- package/time-picker/README.md +95 -95
- package/types/ngx-kit-date-picker.d.ts +303 -36
- package/types/ngx-kit-drawer-menu.d.ts +97 -86
- package/types/ngx-kit-dropzone.d.ts +5 -5
- package/types/ngx-kit-image-editor.d.ts +29 -4
package/dropzone/README.md
CHANGED
|
@@ -1,422 +1,422 @@
|
|
|
1
|
-
# ngx-kit/dropzone
|
|
2
|
-
|
|
3
|
-
A standalone Angular dropzone with file validation, previews, recursive folder support, keyboard accessibility, and a production-ready upload pipeline.
|
|
4
|
-
|
|
5
|
-
The upload layer is intentionally decoupled from the UI. The component only knows about `NgxDropzoneUploadService`, so applications can use the default Angular `HttpClient` implementation or replace it with Axios, `fetch`, tus, S3 multipart, or any custom transport.
|
|
6
|
-
|
|
7
|
-
## Features
|
|
8
|
-
|
|
9
|
-
- Drag & drop and file-picker selection
|
|
10
|
-
- MIME, wildcard MIME and extension validation
|
|
11
|
-
- Maximum file size and maximum file count
|
|
12
|
-
- Recursive folder traversal when the browser exposes `webkitGetAsEntry`
|
|
13
|
-
- Image previews with `URL.revokeObjectURL` cleanup
|
|
14
|
-
- Keyboard accessible dropzone
|
|
15
|
-
- Signal-based Angular component with `OnPush`
|
|
16
|
-
- Automatic or manual uploads
|
|
17
|
-
- Configurable multipart field alias
|
|
18
|
-
- Configurable upload URL
|
|
19
|
-
- Configurable parallel upload limit
|
|
20
|
-
- Per-file upload progress
|
|
21
|
-
- Per-file success/error state
|
|
22
|
-
- Retry failed uploads
|
|
23
|
-
- Cancel an active upload by removing the file
|
|
24
|
-
- Optional multipart fields, headers and credentials
|
|
25
|
-
- Pluggable upload adapter through an `InjectionToken`
|
|
26
|
-
|
|
27
|
-
## Basic selection
|
|
28
|
-
|
|
29
|
-
```html
|
|
30
|
-
<ngx-dropzone
|
|
31
|
-
[accept]="'image/*,.pdf'"
|
|
32
|
-
[multiple]="true"
|
|
33
|
-
[maxFileSize]="5 * 1024 * 1024"
|
|
34
|
-
[maxFiles]="10"
|
|
35
|
-
(filesAdded)="onFilesAdded($event)"
|
|
36
|
-
(filesRejected)="onFilesRejected($event)">
|
|
37
|
-
Drop files here or click to browse
|
|
38
|
-
</ngx-dropzone>
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Automatic upload
|
|
42
|
-
|
|
43
|
-
When `autoUpload` is enabled, every newly accepted file is queued immediately.
|
|
44
|
-
|
|
45
|
-
```html
|
|
46
|
-
<ngx-dropzone
|
|
47
|
-
[accept]="'image/*,.pdf'"
|
|
48
|
-
[uploadUrl]="'/api/files/upload'"
|
|
49
|
-
[uploadAlias]="'file'"
|
|
50
|
-
[autoUpload]="true"
|
|
51
|
-
[maxParallel]="3">
|
|
52
|
-
Drop files here or click to browse
|
|
53
|
-
</ngx-dropzone>
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
For example, the browser sends a multipart request equivalent to:
|
|
57
|
-
|
|
58
|
-
```text
|
|
59
|
-
POST /api/files/upload
|
|
60
|
-
Content-Type: multipart/form-data
|
|
61
|
-
|
|
62
|
-
file=<selected file>
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
`maxParallel="3"` means at most three upload requests are active at the same time. Additional files remain in an internal FIFO queue.
|
|
66
|
-
|
|
67
|
-
## Manual upload: one "Upload all" button
|
|
68
|
-
|
|
69
|
-
This is the default manual mode.
|
|
70
|
-
|
|
71
|
-
```html
|
|
72
|
-
<ngx-dropzone
|
|
73
|
-
[uploadUrl]="'/api/files/upload'"
|
|
74
|
-
[uploadAlias]="'files'"
|
|
75
|
-
[autoUpload]="false"
|
|
76
|
-
[uploadMode]="'all'"
|
|
77
|
-
[maxParallel]="4"></ngx-dropzone>
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
The component renders an **Upload all** button. Clicking it queues every pending file. `maxParallel` still controls the number of concurrent requests.
|
|
81
|
-
|
|
82
|
-
## Manual upload: one button per file
|
|
83
|
-
|
|
84
|
-
```html
|
|
85
|
-
<ngx-dropzone
|
|
86
|
-
[uploadUrl]="'/api/files/upload'"
|
|
87
|
-
[uploadAlias]="'file'"
|
|
88
|
-
[autoUpload]="false"
|
|
89
|
-
[uploadMode]="'single'"
|
|
90
|
-
[maxParallel]="2"></ngx-dropzone>
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
Each pending row gets its own **Upload** button. The user can decide which files enter the upload queue.
|
|
94
|
-
|
|
95
|
-
## Extra multipart fields
|
|
96
|
-
|
|
97
|
-
```html
|
|
98
|
-
<ngx-dropzone
|
|
99
|
-
[uploadUrl]="'/api/files/upload'"
|
|
100
|
-
[uploadAlias]="'file'"
|
|
101
|
-
[uploadData]="{
|
|
102
|
-
tenantId: 'abc',
|
|
103
|
-
folderId: 123,
|
|
104
|
-
isPublic: false
|
|
105
|
-
}"></ngx-dropzone>
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Every request contains the selected file plus those multipart fields.
|
|
109
|
-
|
|
110
|
-
## Headers and credentials
|
|
111
|
-
|
|
112
|
-
```html
|
|
113
|
-
<ngx-dropzone
|
|
114
|
-
[uploadUrl]="'https://api.example.com/files'"
|
|
115
|
-
[uploadHeaders]="{
|
|
116
|
-
'X-Tenant-Id': 'abc',
|
|
117
|
-
'X-Upload-Source': 'ngx-dropzone'
|
|
118
|
-
}"
|
|
119
|
-
[uploadWithCredentials]="true"></ngx-dropzone>
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Authentication headers that are already added by an Angular `HttpInterceptor` do not need to be supplied here.
|
|
123
|
-
|
|
124
|
-
## Upload lifecycle
|
|
125
|
-
|
|
126
|
-
Each file has one of these states:
|
|
127
|
-
|
|
128
|
-
```text
|
|
129
|
-
pending -> uploading -> uploaded
|
|
130
|
-
\
|
|
131
|
-
-> error -> retry -> uploading
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
Removing a file while it is uploading unsubscribes from the upload observable. With the default `HttpClient` adapter this cancels the underlying HTTP request.
|
|
135
|
-
|
|
136
|
-
Every row displays:
|
|
137
|
-
|
|
138
|
-
- current status
|
|
139
|
-
- upload percentage
|
|
140
|
-
- progress bar
|
|
141
|
-
- upload/retry action when appropriate
|
|
142
|
-
- remove action
|
|
143
|
-
|
|
144
|
-
## Outputs
|
|
145
|
-
|
|
146
|
-
| Output | Payload | Description |
|
|
147
|
-
| ---------------- | -------------------------------- | ----------------------------- |
|
|
148
|
-
| `filesAdded` | `File[]` | Newly accepted files |
|
|
149
|
-
| `filesRejected` | `NgxDropzoneRejectedFile[]` | Files rejected by validation |
|
|
150
|
-
| `filesChange` | `File[]` | Current selected files |
|
|
151
|
-
| `uploadStarted` | `NgxDropzoneFile` | A file started uploading |
|
|
152
|
-
| `uploadProgress` | `NgxDropzoneUploadProgressEvent` | Progress update |
|
|
153
|
-
| `uploadSuccess` | `NgxDropzoneFile` | Upload completed successfully |
|
|
154
|
-
| `uploadError` | `{ item, error }` | Upload failed |
|
|
155
|
-
|
|
156
|
-
Example:
|
|
157
|
-
|
|
158
|
-
```html
|
|
159
|
-
<ngx-dropzone
|
|
160
|
-
[uploadUrl]="'/api/files'"
|
|
161
|
-
[autoUpload]="true"
|
|
162
|
-
[maxParallel]="3"
|
|
163
|
-
(uploadProgress)="onUploadProgress($event)"
|
|
164
|
-
(uploadSuccess)="onUploadSuccess($event)"
|
|
165
|
-
(uploadError)="onUploadError($event)"></ngx-dropzone>
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
## Upload contracts
|
|
169
|
-
|
|
170
|
-
The upload boundary is defined by:
|
|
171
|
-
|
|
172
|
-
```ts
|
|
173
|
-
NgxDropzoneUploadService;
|
|
174
|
-
NgxDropzoneUploadRequest;
|
|
175
|
-
NgxDropzoneUploadEvent;
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
The service returns an `Observable`. It emits progress events and finally a response event.
|
|
179
|
-
|
|
180
|
-
This is important: **the component does not depend on `HttpClient`**.
|
|
181
|
-
|
|
182
|
-
## Default Angular HttpClient adapter
|
|
183
|
-
|
|
184
|
-
The library ships with `NgxDropzoneHttpUploadService`.
|
|
185
|
-
|
|
186
|
-
It uses:
|
|
187
|
-
|
|
188
|
-
- `HttpClient.post`
|
|
189
|
-
- `FormData`
|
|
190
|
-
- `reportProgress: true`
|
|
191
|
-
- `observe: 'events'`
|
|
192
|
-
|
|
193
|
-
The default adapter is connected through:
|
|
194
|
-
|
|
195
|
-
```ts
|
|
196
|
-
NGX_DROPZONE_UPLOAD_SERVICE;
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
The token has a default factory, so normal applications do not need to configure anything.
|
|
200
|
-
|
|
201
|
-
Make sure your application provides Angular's HTTP client:
|
|
202
|
-
|
|
203
|
-
```ts
|
|
204
|
-
import { provideHttpClient } from '@angular/common/http';
|
|
205
|
-
|
|
206
|
-
export const appConfig = {
|
|
207
|
-
providers: [provideHttpClient()],
|
|
208
|
-
};
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
## Replacing HttpClient with Axios
|
|
212
|
-
|
|
213
|
-
You can completely replace the transport without changing the dropzone component.
|
|
214
|
-
|
|
215
|
-
```ts
|
|
216
|
-
import { Injectable } from '@angular/core';
|
|
217
|
-
import { Observable } from 'rxjs';
|
|
218
|
-
import axios from 'axios';
|
|
219
|
-
|
|
220
|
-
import {
|
|
221
|
-
NgxDropzoneUploadEvent,
|
|
222
|
-
NgxDropzoneUploadRequest,
|
|
223
|
-
NgxDropzoneUploadService,
|
|
224
|
-
} from 'ngx-kit';
|
|
225
|
-
|
|
226
|
-
@Injectable()
|
|
227
|
-
export class AxiosDropzoneUploadService implements NgxDropzoneUploadService {
|
|
228
|
-
upload<TResponse = unknown>(
|
|
229
|
-
request: NgxDropzoneUploadRequest,
|
|
230
|
-
): Observable<NgxDropzoneUploadEvent<TResponse>> {
|
|
231
|
-
return new Observable((subscriber) => {
|
|
232
|
-
const controller = new AbortController();
|
|
233
|
-
|
|
234
|
-
const form = new FormData();
|
|
235
|
-
form.append(request.alias, request.file, request.file.name);
|
|
236
|
-
|
|
237
|
-
for (const [key, value] of Object.entries(request.data ?? {})) {
|
|
238
|
-
form.append(key, value instanceof Blob ? value : String(value));
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
axios
|
|
242
|
-
.post<TResponse>(request.url, form, {
|
|
243
|
-
headers: request.headers,
|
|
244
|
-
withCredentials: request.withCredentials,
|
|
245
|
-
signal: controller.signal,
|
|
246
|
-
onUploadProgress: (event) => {
|
|
247
|
-
const total = event.total;
|
|
248
|
-
subscriber.next({
|
|
249
|
-
type: 'progress',
|
|
250
|
-
loaded: event.loaded,
|
|
251
|
-
total,
|
|
252
|
-
percent: total ? Math.min(100, Math.round((event.loaded / total) * 100)) : 0,
|
|
253
|
-
});
|
|
254
|
-
},
|
|
255
|
-
})
|
|
256
|
-
.then((response) => {
|
|
257
|
-
subscriber.next({
|
|
258
|
-
type: 'response',
|
|
259
|
-
response: response.data,
|
|
260
|
-
status: response.status,
|
|
261
|
-
});
|
|
262
|
-
subscriber.complete();
|
|
263
|
-
})
|
|
264
|
-
.catch((error) => subscriber.error(error));
|
|
265
|
-
|
|
266
|
-
return () => controller.abort();
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
```
|
|
271
|
-
|
|
272
|
-
Then override the token:
|
|
273
|
-
|
|
274
|
-
```ts
|
|
275
|
-
import { NGX_DROPZONE_UPLOAD_SERVICE } from 'ngx-kit';
|
|
276
|
-
|
|
277
|
-
export const appConfig = {
|
|
278
|
-
providers: [
|
|
279
|
-
{
|
|
280
|
-
provide: NGX_DROPZONE_UPLOAD_SERVICE,
|
|
281
|
-
useClass: AxiosDropzoneUploadService,
|
|
282
|
-
},
|
|
283
|
-
],
|
|
284
|
-
};
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
This keeps the dropzone UI independent from the HTTP library.
|
|
288
|
-
|
|
289
|
-
## Custom adapter at component level
|
|
290
|
-
|
|
291
|
-
You can also replace the adapter for a specific dropzone:
|
|
292
|
-
|
|
293
|
-
```ts
|
|
294
|
-
@Component({
|
|
295
|
-
// ...
|
|
296
|
-
providers: [
|
|
297
|
-
{
|
|
298
|
-
provide: NGX_DROPZONE_UPLOAD_SERVICE,
|
|
299
|
-
useClass: AxiosDropzoneUploadService,
|
|
300
|
-
},
|
|
301
|
-
],
|
|
302
|
-
})
|
|
303
|
-
export class MyUploaderComponent {}
|
|
304
|
-
```
|
|
305
|
-
|
|
306
|
-
This is useful when one part of an application uses a different upload backend.
|
|
307
|
-
|
|
308
|
-
## Configuration API
|
|
309
|
-
|
|
310
|
-
| Input | Type | Default | Description |
|
|
311
|
-
| ----------------------- | ------------------------------------ | --------- | ---------------------------------- |
|
|
312
|
-
| `accept` | `string` | `''` | MIME/extension filters |
|
|
313
|
-
| `multiple` | `boolean` | `true` | Allow multiple files |
|
|
314
|
-
| `maxFileSize` | `number` | unlimited | Maximum size per file in bytes |
|
|
315
|
-
| `maxFiles` | `number` | unlimited | Maximum number of files |
|
|
316
|
-
| `disabled` | `boolean` | `false` | Disable interaction |
|
|
317
|
-
| `showFileList` | `boolean` | `true` | Show selected files |
|
|
318
|
-
| `uploadUrl` | `string` | `''` | Upload endpoint |
|
|
319
|
-
| `uploadAlias` | `string` | `'file'` | Multipart file field name |
|
|
320
|
-
| `autoUpload` | `boolean` | `false` | Automatically queue accepted files |
|
|
321
|
-
| `maxParallel` | `number` | `3` | Maximum simultaneous uploads |
|
|
322
|
-
| `uploadMode` | `'all' \| 'single'` | `'all'` | Manual upload UI behavior |
|
|
323
|
-
| `uploadData` | `Record<string, ...>` | `{}` | Extra multipart fields |
|
|
324
|
-
| `uploadHeaders` | `Record<string, string \| string[]>` | `{}` | Extra request headers |
|
|
325
|
-
| `uploadWithCredentials` | `boolean` | `false` | Send credentials |
|
|
326
|
-
|
|
327
|
-
## Tests
|
|
328
|
-
|
|
329
|
-
The upload implementation is covered at two levels.
|
|
330
|
-
|
|
331
|
-
### HTTP adapter tests
|
|
332
|
-
|
|
333
|
-
`ngx-dropzone-upload.spec.ts` verifies:
|
|
334
|
-
|
|
335
|
-
- multipart file alias
|
|
336
|
-
- extra form data
|
|
337
|
-
- POST request
|
|
338
|
-
- upload progress calculation
|
|
339
|
-
- final response event
|
|
340
|
-
|
|
341
|
-
### Component upload tests
|
|
342
|
-
|
|
343
|
-
`dropzone-upload.spec.ts` verifies:
|
|
344
|
-
|
|
345
|
-
- automatic upload
|
|
346
|
-
- `maxParallel` concurrency
|
|
347
|
-
- queued uploads starting after a slot is released
|
|
348
|
-
- per-file manual upload
|
|
349
|
-
- progress propagation
|
|
350
|
-
- failed upload state
|
|
351
|
-
- retry UI
|
|
352
|
-
|
|
353
|
-
The existing `file-matching.spec.ts` continues to cover file type/extension matching and byte formatting.
|
|
354
|
-
|
|
355
|
-
## Design decisions
|
|
356
|
-
|
|
357
|
-
### Why an `InjectionToken`?
|
|
358
|
-
|
|
359
|
-
The dropzone should not be coupled to a transport library.
|
|
360
|
-
|
|
361
|
-
Bad:
|
|
362
|
-
|
|
363
|
-
```ts
|
|
364
|
-
class Dropzone {
|
|
365
|
-
constructor(private http: HttpClient) {}
|
|
366
|
-
}
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
Better:
|
|
370
|
-
|
|
371
|
-
```ts
|
|
372
|
-
class Dropzone {
|
|
373
|
-
constructor(
|
|
374
|
-
@Inject(NGX_DROPZONE_UPLOAD_SERVICE)
|
|
375
|
-
private uploader: NgxDropzoneUploadService,
|
|
376
|
-
) {}
|
|
377
|
-
}
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
Now the UI depends on a small capability (`upload`) instead of Angular `HttpClient`.
|
|
381
|
-
|
|
382
|
-
### Why an Observable?
|
|
383
|
-
|
|
384
|
-
An upload is a stream of events rather than a single value:
|
|
385
|
-
|
|
386
|
-
```text
|
|
387
|
-
progress -> progress -> progress -> response
|
|
388
|
-
```
|
|
389
|
-
|
|
390
|
-
It also gives custom adapters a clean cancellation mechanism: unsubscribing from the observable cancels the transport.
|
|
391
|
-
|
|
392
|
-
### Why `maxParallel` is handled by the component
|
|
393
|
-
|
|
394
|
-
The upload adapter is responsible for **one file**.
|
|
395
|
-
|
|
396
|
-
The dropzone is responsible for **orchestration**:
|
|
397
|
-
|
|
398
|
-
```text
|
|
399
|
-
Files
|
|
400
|
-
|
|
|
401
|
-
+-- pending queue
|
|
402
|
-
|
|
|
403
|
-
+-- upload #1
|
|
404
|
-
+-- upload #2
|
|
405
|
-
+-- upload #3
|
|
406
|
-
|
|
|
407
|
-
+-- pending queue...
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
This keeps the adapter simple and lets every transport share the same concurrency behavior.
|
|
411
|
-
|
|
412
|
-
## Existing behavior retained
|
|
413
|
-
|
|
414
|
-
The upload feature is layered on top of the existing selection implementation. File acceptance, maximum size/count validation, recursive directory traversal, keyboard interaction, previews, and preview URL cleanup remain part of the component.
|
|
415
|
-
|
|
416
|
-
## Accessibility
|
|
417
|
-
|
|
418
|
-
The dropzone remains keyboard accessible through Enter/Space. Upload progress uses the ARIA `progressbar` role and exposes the current percentage.
|
|
419
|
-
|
|
420
|
-
## Dark mode and RTL
|
|
421
|
-
|
|
422
|
-
The existing CSS uses logical properties and `light-dark()`, so RTL and dark/light themes continue to work automatically.
|
|
1
|
+
# ngx-kit/dropzone
|
|
2
|
+
|
|
3
|
+
A standalone Angular dropzone with file validation, previews, recursive folder support, keyboard accessibility, and a production-ready upload pipeline.
|
|
4
|
+
|
|
5
|
+
The upload layer is intentionally decoupled from the UI. The component only knows about `NgxDropzoneUploadService`, so applications can use the default Angular `HttpClient` implementation or replace it with Axios, `fetch`, tus, S3 multipart, or any custom transport.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Drag & drop and file-picker selection
|
|
10
|
+
- MIME, wildcard MIME and extension validation
|
|
11
|
+
- Maximum file size and maximum file count
|
|
12
|
+
- Recursive folder traversal when the browser exposes `webkitGetAsEntry`
|
|
13
|
+
- Image previews with `URL.revokeObjectURL` cleanup
|
|
14
|
+
- Keyboard accessible dropzone
|
|
15
|
+
- Signal-based Angular component with `OnPush`
|
|
16
|
+
- Automatic or manual uploads
|
|
17
|
+
- Configurable multipart field alias
|
|
18
|
+
- Configurable upload URL
|
|
19
|
+
- Configurable parallel upload limit
|
|
20
|
+
- Per-file upload progress
|
|
21
|
+
- Per-file success/error state
|
|
22
|
+
- Retry failed uploads
|
|
23
|
+
- Cancel an active upload by removing the file
|
|
24
|
+
- Optional multipart fields, headers and credentials
|
|
25
|
+
- Pluggable upload adapter through an `InjectionToken`
|
|
26
|
+
|
|
27
|
+
## Basic selection
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<ngx-dropzone
|
|
31
|
+
[accept]="'image/*,.pdf'"
|
|
32
|
+
[multiple]="true"
|
|
33
|
+
[maxFileSize]="5 * 1024 * 1024"
|
|
34
|
+
[maxFiles]="10"
|
|
35
|
+
(filesAdded)="onFilesAdded($event)"
|
|
36
|
+
(filesRejected)="onFilesRejected($event)">
|
|
37
|
+
Drop files here or click to browse
|
|
38
|
+
</ngx-dropzone>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Automatic upload
|
|
42
|
+
|
|
43
|
+
When `autoUpload` is enabled, every newly accepted file is queued immediately.
|
|
44
|
+
|
|
45
|
+
```html
|
|
46
|
+
<ngx-dropzone
|
|
47
|
+
[accept]="'image/*,.pdf'"
|
|
48
|
+
[uploadUrl]="'/api/files/upload'"
|
|
49
|
+
[uploadAlias]="'file'"
|
|
50
|
+
[autoUpload]="true"
|
|
51
|
+
[maxParallel]="3">
|
|
52
|
+
Drop files here or click to browse
|
|
53
|
+
</ngx-dropzone>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For example, the browser sends a multipart request equivalent to:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
POST /api/files/upload
|
|
60
|
+
Content-Type: multipart/form-data
|
|
61
|
+
|
|
62
|
+
file=<selected file>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`maxParallel="3"` means at most three upload requests are active at the same time. Additional files remain in an internal FIFO queue.
|
|
66
|
+
|
|
67
|
+
## Manual upload: one "Upload all" button
|
|
68
|
+
|
|
69
|
+
This is the default manual mode.
|
|
70
|
+
|
|
71
|
+
```html
|
|
72
|
+
<ngx-dropzone
|
|
73
|
+
[uploadUrl]="'/api/files/upload'"
|
|
74
|
+
[uploadAlias]="'files'"
|
|
75
|
+
[autoUpload]="false"
|
|
76
|
+
[uploadMode]="'all'"
|
|
77
|
+
[maxParallel]="4"></ngx-dropzone>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The component renders an **Upload all** button. Clicking it queues every pending file. `maxParallel` still controls the number of concurrent requests.
|
|
81
|
+
|
|
82
|
+
## Manual upload: one button per file
|
|
83
|
+
|
|
84
|
+
```html
|
|
85
|
+
<ngx-dropzone
|
|
86
|
+
[uploadUrl]="'/api/files/upload'"
|
|
87
|
+
[uploadAlias]="'file'"
|
|
88
|
+
[autoUpload]="false"
|
|
89
|
+
[uploadMode]="'single'"
|
|
90
|
+
[maxParallel]="2"></ngx-dropzone>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Each pending row gets its own **Upload** button. The user can decide which files enter the upload queue.
|
|
94
|
+
|
|
95
|
+
## Extra multipart fields
|
|
96
|
+
|
|
97
|
+
```html
|
|
98
|
+
<ngx-dropzone
|
|
99
|
+
[uploadUrl]="'/api/files/upload'"
|
|
100
|
+
[uploadAlias]="'file'"
|
|
101
|
+
[uploadData]="{
|
|
102
|
+
tenantId: 'abc',
|
|
103
|
+
folderId: 123,
|
|
104
|
+
isPublic: false
|
|
105
|
+
}"></ngx-dropzone>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Every request contains the selected file plus those multipart fields.
|
|
109
|
+
|
|
110
|
+
## Headers and credentials
|
|
111
|
+
|
|
112
|
+
```html
|
|
113
|
+
<ngx-dropzone
|
|
114
|
+
[uploadUrl]="'https://api.example.com/files'"
|
|
115
|
+
[uploadHeaders]="{
|
|
116
|
+
'X-Tenant-Id': 'abc',
|
|
117
|
+
'X-Upload-Source': 'ngx-dropzone'
|
|
118
|
+
}"
|
|
119
|
+
[uploadWithCredentials]="true"></ngx-dropzone>
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Authentication headers that are already added by an Angular `HttpInterceptor` do not need to be supplied here.
|
|
123
|
+
|
|
124
|
+
## Upload lifecycle
|
|
125
|
+
|
|
126
|
+
Each file has one of these states:
|
|
127
|
+
|
|
128
|
+
```text
|
|
129
|
+
pending -> uploading -> uploaded
|
|
130
|
+
\
|
|
131
|
+
-> error -> retry -> uploading
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Removing a file while it is uploading unsubscribes from the upload observable. With the default `HttpClient` adapter this cancels the underlying HTTP request.
|
|
135
|
+
|
|
136
|
+
Every row displays:
|
|
137
|
+
|
|
138
|
+
- current status
|
|
139
|
+
- upload percentage
|
|
140
|
+
- progress bar
|
|
141
|
+
- upload/retry action when appropriate
|
|
142
|
+
- remove action
|
|
143
|
+
|
|
144
|
+
## Outputs
|
|
145
|
+
|
|
146
|
+
| Output | Payload | Description |
|
|
147
|
+
| ---------------- | -------------------------------- | ----------------------------- |
|
|
148
|
+
| `filesAdded` | `File[]` | Newly accepted files |
|
|
149
|
+
| `filesRejected` | `NgxDropzoneRejectedFile[]` | Files rejected by validation |
|
|
150
|
+
| `filesChange` | `File[]` | Current selected files |
|
|
151
|
+
| `uploadStarted` | `NgxDropzoneFile` | A file started uploading |
|
|
152
|
+
| `uploadProgress` | `NgxDropzoneUploadProgressEvent` | Progress update |
|
|
153
|
+
| `uploadSuccess` | `NgxDropzoneFile` | Upload completed successfully |
|
|
154
|
+
| `uploadError` | `{ item, error }` | Upload failed |
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
|
|
158
|
+
```html
|
|
159
|
+
<ngx-dropzone
|
|
160
|
+
[uploadUrl]="'/api/files'"
|
|
161
|
+
[autoUpload]="true"
|
|
162
|
+
[maxParallel]="3"
|
|
163
|
+
(uploadProgress)="onUploadProgress($event)"
|
|
164
|
+
(uploadSuccess)="onUploadSuccess($event)"
|
|
165
|
+
(uploadError)="onUploadError($event)"></ngx-dropzone>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Upload contracts
|
|
169
|
+
|
|
170
|
+
The upload boundary is defined by:
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
NgxDropzoneUploadService;
|
|
174
|
+
NgxDropzoneUploadRequest;
|
|
175
|
+
NgxDropzoneUploadEvent;
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The service returns an `Observable`. It emits progress events and finally a response event.
|
|
179
|
+
|
|
180
|
+
This is important: **the component does not depend on `HttpClient`**.
|
|
181
|
+
|
|
182
|
+
## Default Angular HttpClient adapter
|
|
183
|
+
|
|
184
|
+
The library ships with `NgxDropzoneHttpUploadService`.
|
|
185
|
+
|
|
186
|
+
It uses:
|
|
187
|
+
|
|
188
|
+
- `HttpClient.post`
|
|
189
|
+
- `FormData`
|
|
190
|
+
- `reportProgress: true`
|
|
191
|
+
- `observe: 'events'`
|
|
192
|
+
|
|
193
|
+
The default adapter is connected through:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
NGX_DROPZONE_UPLOAD_SERVICE;
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The token has a default factory, so normal applications do not need to configure anything.
|
|
200
|
+
|
|
201
|
+
Make sure your application provides Angular's HTTP client:
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
import { provideHttpClient } from '@angular/common/http';
|
|
205
|
+
|
|
206
|
+
export const appConfig = {
|
|
207
|
+
providers: [provideHttpClient()],
|
|
208
|
+
};
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Replacing HttpClient with Axios
|
|
212
|
+
|
|
213
|
+
You can completely replace the transport without changing the dropzone component.
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { Injectable } from '@angular/core';
|
|
217
|
+
import { Observable } from 'rxjs';
|
|
218
|
+
import axios from 'axios';
|
|
219
|
+
|
|
220
|
+
import {
|
|
221
|
+
NgxDropzoneUploadEvent,
|
|
222
|
+
NgxDropzoneUploadRequest,
|
|
223
|
+
NgxDropzoneUploadService,
|
|
224
|
+
} from 'ngx-kit';
|
|
225
|
+
|
|
226
|
+
@Injectable()
|
|
227
|
+
export class AxiosDropzoneUploadService implements NgxDropzoneUploadService {
|
|
228
|
+
upload<TResponse = unknown>(
|
|
229
|
+
request: NgxDropzoneUploadRequest,
|
|
230
|
+
): Observable<NgxDropzoneUploadEvent<TResponse>> {
|
|
231
|
+
return new Observable((subscriber) => {
|
|
232
|
+
const controller = new AbortController();
|
|
233
|
+
|
|
234
|
+
const form = new FormData();
|
|
235
|
+
form.append(request.alias, request.file, request.file.name);
|
|
236
|
+
|
|
237
|
+
for (const [key, value] of Object.entries(request.data ?? {})) {
|
|
238
|
+
form.append(key, value instanceof Blob ? value : String(value));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
axios
|
|
242
|
+
.post<TResponse>(request.url, form, {
|
|
243
|
+
headers: request.headers,
|
|
244
|
+
withCredentials: request.withCredentials,
|
|
245
|
+
signal: controller.signal,
|
|
246
|
+
onUploadProgress: (event) => {
|
|
247
|
+
const total = event.total;
|
|
248
|
+
subscriber.next({
|
|
249
|
+
type: 'progress',
|
|
250
|
+
loaded: event.loaded,
|
|
251
|
+
total,
|
|
252
|
+
percent: total ? Math.min(100, Math.round((event.loaded / total) * 100)) : 0,
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
})
|
|
256
|
+
.then((response) => {
|
|
257
|
+
subscriber.next({
|
|
258
|
+
type: 'response',
|
|
259
|
+
response: response.data,
|
|
260
|
+
status: response.status,
|
|
261
|
+
});
|
|
262
|
+
subscriber.complete();
|
|
263
|
+
})
|
|
264
|
+
.catch((error) => subscriber.error(error));
|
|
265
|
+
|
|
266
|
+
return () => controller.abort();
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Then override the token:
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
import { NGX_DROPZONE_UPLOAD_SERVICE } from 'ngx-kit';
|
|
276
|
+
|
|
277
|
+
export const appConfig = {
|
|
278
|
+
providers: [
|
|
279
|
+
{
|
|
280
|
+
provide: NGX_DROPZONE_UPLOAD_SERVICE,
|
|
281
|
+
useClass: AxiosDropzoneUploadService,
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
};
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
This keeps the dropzone UI independent from the HTTP library.
|
|
288
|
+
|
|
289
|
+
## Custom adapter at component level
|
|
290
|
+
|
|
291
|
+
You can also replace the adapter for a specific dropzone:
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
@Component({
|
|
295
|
+
// ...
|
|
296
|
+
providers: [
|
|
297
|
+
{
|
|
298
|
+
provide: NGX_DROPZONE_UPLOAD_SERVICE,
|
|
299
|
+
useClass: AxiosDropzoneUploadService,
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
})
|
|
303
|
+
export class MyUploaderComponent {}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
This is useful when one part of an application uses a different upload backend.
|
|
307
|
+
|
|
308
|
+
## Configuration API
|
|
309
|
+
|
|
310
|
+
| Input | Type | Default | Description |
|
|
311
|
+
| ----------------------- | ------------------------------------ | --------- | ---------------------------------- |
|
|
312
|
+
| `accept` | `string` | `''` | MIME/extension filters |
|
|
313
|
+
| `multiple` | `boolean` | `true` | Allow multiple files |
|
|
314
|
+
| `maxFileSize` | `number` | unlimited | Maximum size per file in bytes |
|
|
315
|
+
| `maxFiles` | `number` | unlimited | Maximum number of files |
|
|
316
|
+
| `disabled` | `boolean` | `false` | Disable interaction |
|
|
317
|
+
| `showFileList` | `boolean` | `true` | Show selected files |
|
|
318
|
+
| `uploadUrl` | `string` | `''` | Upload endpoint |
|
|
319
|
+
| `uploadAlias` | `string` | `'file'` | Multipart file field name |
|
|
320
|
+
| `autoUpload` | `boolean` | `false` | Automatically queue accepted files |
|
|
321
|
+
| `maxParallel` | `number` | `3` | Maximum simultaneous uploads |
|
|
322
|
+
| `uploadMode` | `'all' \| 'single'` | `'all'` | Manual upload UI behavior |
|
|
323
|
+
| `uploadData` | `Record<string, ...>` | `{}` | Extra multipart fields |
|
|
324
|
+
| `uploadHeaders` | `Record<string, string \| string[]>` | `{}` | Extra request headers |
|
|
325
|
+
| `uploadWithCredentials` | `boolean` | `false` | Send credentials |
|
|
326
|
+
|
|
327
|
+
## Tests
|
|
328
|
+
|
|
329
|
+
The upload implementation is covered at two levels.
|
|
330
|
+
|
|
331
|
+
### HTTP adapter tests
|
|
332
|
+
|
|
333
|
+
`ngx-dropzone-upload.spec.ts` verifies:
|
|
334
|
+
|
|
335
|
+
- multipart file alias
|
|
336
|
+
- extra form data
|
|
337
|
+
- POST request
|
|
338
|
+
- upload progress calculation
|
|
339
|
+
- final response event
|
|
340
|
+
|
|
341
|
+
### Component upload tests
|
|
342
|
+
|
|
343
|
+
`dropzone-upload.spec.ts` verifies:
|
|
344
|
+
|
|
345
|
+
- automatic upload
|
|
346
|
+
- `maxParallel` concurrency
|
|
347
|
+
- queued uploads starting after a slot is released
|
|
348
|
+
- per-file manual upload
|
|
349
|
+
- progress propagation
|
|
350
|
+
- failed upload state
|
|
351
|
+
- retry UI
|
|
352
|
+
|
|
353
|
+
The existing `file-matching.spec.ts` continues to cover file type/extension matching and byte formatting.
|
|
354
|
+
|
|
355
|
+
## Design decisions
|
|
356
|
+
|
|
357
|
+
### Why an `InjectionToken`?
|
|
358
|
+
|
|
359
|
+
The dropzone should not be coupled to a transport library.
|
|
360
|
+
|
|
361
|
+
Bad:
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
class Dropzone {
|
|
365
|
+
constructor(private http: HttpClient) {}
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Better:
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
class Dropzone {
|
|
373
|
+
constructor(
|
|
374
|
+
@Inject(NGX_DROPZONE_UPLOAD_SERVICE)
|
|
375
|
+
private uploader: NgxDropzoneUploadService,
|
|
376
|
+
) {}
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Now the UI depends on a small capability (`upload`) instead of Angular `HttpClient`.
|
|
381
|
+
|
|
382
|
+
### Why an Observable?
|
|
383
|
+
|
|
384
|
+
An upload is a stream of events rather than a single value:
|
|
385
|
+
|
|
386
|
+
```text
|
|
387
|
+
progress -> progress -> progress -> response
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
It also gives custom adapters a clean cancellation mechanism: unsubscribing from the observable cancels the transport.
|
|
391
|
+
|
|
392
|
+
### Why `maxParallel` is handled by the component
|
|
393
|
+
|
|
394
|
+
The upload adapter is responsible for **one file**.
|
|
395
|
+
|
|
396
|
+
The dropzone is responsible for **orchestration**:
|
|
397
|
+
|
|
398
|
+
```text
|
|
399
|
+
Files
|
|
400
|
+
|
|
|
401
|
+
+-- pending queue
|
|
402
|
+
|
|
|
403
|
+
+-- upload #1
|
|
404
|
+
+-- upload #2
|
|
405
|
+
+-- upload #3
|
|
406
|
+
|
|
|
407
|
+
+-- pending queue...
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
This keeps the adapter simple and lets every transport share the same concurrency behavior.
|
|
411
|
+
|
|
412
|
+
## Existing behavior retained
|
|
413
|
+
|
|
414
|
+
The upload feature is layered on top of the existing selection implementation. File acceptance, maximum size/count validation, recursive directory traversal, keyboard interaction, previews, and preview URL cleanup remain part of the component.
|
|
415
|
+
|
|
416
|
+
## Accessibility
|
|
417
|
+
|
|
418
|
+
The dropzone remains keyboard accessible through Enter/Space. Upload progress uses the ARIA `progressbar` role and exposes the current percentage.
|
|
419
|
+
|
|
420
|
+
## Dark mode and RTL
|
|
421
|
+
|
|
422
|
+
The existing CSS uses logical properties and `light-dark()`, so RTL and dark/light themes continue to work automatically.
|