ngx-kit 6.0.2 → 6.1.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.
@@ -1,14 +1,30 @@
1
1
  # ngx-kit/dropzone
2
2
 
3
- A file drag-and-drop area: type/size/count validation, image previews, recursive folder traversal (when a user drags a folder), and full keyboard accessibility.
3
+ A standalone Angular dropzone with file validation, previews, recursive folder support, keyboard accessibility, and a production-ready upload pipeline.
4
4
 
5
- ## Install
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
6
 
7
- ```bash
8
- npm install ngx-kit
9
- ```
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`
10
26
 
11
- ## Usage
27
+ ## Basic selection
12
28
 
13
29
  ```html
14
30
  <ngx-dropzone
@@ -17,44 +33,390 @@ npm install ngx-kit
17
33
  [maxFileSize]="5 * 1024 * 1024"
18
34
  [maxFiles]="10"
19
35
  (filesAdded)="onFilesAdded($event)"
20
- (filesRejected)="onFilesRejected($event)"
21
- >
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">
22
52
  Drop files here or click to browse
23
53
  </ngx-dropzone>
24
54
  ```
25
55
 
26
- ## API
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
27
61
 
28
- | Input | Type | Default | Description |
29
- | --- | --- | --- | --- |
30
- | `accept` | `string` | `''` | Like `input[type=file]`: `"image/*"`, `".pdf,.docx"`, or a comma-separated combination |
31
- | `multiple` | `boolean` | `true` | Allow selecting multiple files |
32
- | `maxFileSize` | `number` (bytes) | unlimited | Max size per file |
33
- | `maxFiles` | `number` | unlimited | Max number of allowed files |
34
- | `disabled` | `boolean` | `false` | |
35
- | `showFileList` | `boolean` | `true` | Show the list/preview of selected files inside the component itself |
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.
36
66
 
37
- | Output | Type | Description |
38
- | --- | --- | --- |
39
- | `filesAdded` | `File[]` | Newly accepted files |
40
- | `filesRejected` | `{ file: File; reason: 'type' \| 'size' \| 'count' }[]` | Rejected files and why |
41
- | `filesChange` | `File[]` | The full current list, after every change (add or remove) |
67
+ ## Manual upload: one "Upload all" button
42
68
 
43
- ### Custom icon
69
+ This is the default manual mode.
44
70
 
45
71
  ```html
46
- <ngx-dropzone>
47
- <svg dropzone-icon>...</svg>
48
- Your own text
49
- </ngx-dropzone>
72
+ <ngx-dropzone
73
+ [uploadUrl]="'/api/files/upload'"
74
+ [uploadAlias]="'files'"
75
+ [autoUpload]="false"
76
+ [uploadMode]="'all'"
77
+ [maxParallel]="4"></ngx-dropzone>
50
78
  ```
51
79
 
52
- ## Technical notes
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
53
417
 
54
- - An enter/leave counter prevents the "dragging" state from flickering as the pointer passes over children inside the drop area.
55
- - Dropping a **folder** is also supported (`webkitGetAsEntry` + recursive traversal) — browsers without support fall back to grabbing files directly.
56
- - Preview object URLs are automatically `revokeObjectURL`'d when a file is removed or the component is destroyed (no memory leak).
418
+ The dropzone remains keyboard accessible through Enter/Space. Upload progress uses the ARIA `progressbar` role and exposes the current percentage.
57
419
 
58
420
  ## Dark mode and RTL
59
421
 
60
- Supported automatically.
422
+ The existing CSS uses logical properties and `light-dark()`, so RTL and dark/light themes continue to work automatically.