ng-hub-ui-forms 22.5.0 → 22.7.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 CHANGED
@@ -92,11 +92,11 @@ mode — no Bootstrap dependency.
92
92
 
93
93
  ## 🎯 Features
94
94
 
95
- - **Fields** — `hub-input` (text/number/email/password/color/switch/checkbox/counter, with input-group addons & masks, projected in-field affixes, a built-in `clearable` button and debounced typeahead `search`), `hub-otp-input`, `hub-textarea` (+ `hubAutoresize`), `hub-slider` (single / dual thumb, gradient fill), `hub-segmented` (segmented control field — single & multiple selection, horizontal & vertical, with label + validation), `hub-select` (dropdown format, grouping, typeahead, custom templates; the `buttons` / `checkbox` / `radio` formats are **deprecated** → use `hub-segmented`), `hub-datepicker` (single & range, keyboard nav, i18n).
95
+ - **Fields** — `hub-input` (text/number/email/password/color/switch/checkbox/counter, with input-group addons & masks, projected in-field affixes, a built-in `clearable` button and debounced typeahead `search`; the `file` format is **deprecated** → use `hub-file-input`), `hub-otp-input`, `hub-textarea` (+ `hubAutoresize`), `hub-slider` (single / dual thumb, gradient fill), `hub-segmented` (segmented control field — single & multiple selection, horizontal & vertical, with label + validation), `hub-select` (dropdown format, grouping, typeahead, custom templates; the `buttons` / `checkbox` / `radio` formats are **deprecated** → use `hub-segmented`), `hub-datepicker` (single & range, keyboard nav, i18n), `hub-file-input` (drag & drop, clipboard paste, type/size limits, previews, optional upload progress).
96
96
  - **Automatic error display** — bind a field and its control errors render below it; `hub-fieldset`, `form[hubForm]` and `hub-legend` surface group- and form-level (cross-field) errors the same way, with zero wiring.
97
97
  - **Containers** — `hub-fieldset` / `form[hubForm]` group fields and show their group errors; `hub-legend` renders an accessible legend.
98
- - **Configurable** — `provideHubForms({ … })` sets the invalid-feedback templates, datepicker locale/labels and more, app-wide or per instance.
99
- - **Validators & helpers** — `hubAreEqual` cross-field validator, `hubValidationError` / `hubFormText` projection directives, and a set of utility pipes.
98
+ - **Configurable** — `provideHubForms({ … })` sets the invalid-feedback templates, datepicker locale/labels, file-input labels and more, app-wide or per instance.
99
+ - **Validators & helpers** — `hubAreEqual` cross-field validator, the file validators (`hubAcceptedFiles`, `hubMaxFileSize`, `hubMinFileSize`, `hubMaxTotalSize`, `hubMaxFiles`, `hubMinFiles`), `hubValidationError` / `hubFormText` projection directives, and a set of utility pipes.
100
100
  - **Signal Forms ready** — an opt-in [`ng-hub-ui-forms/signals`](#-signal-forms-opt-in) secondary entry point integrates Angular Signal Forms; the core stays Reactive-Forms-based and Angular-21-safe.
101
101
  - **Theming** — every colour, border, radius and spacing is a `--hub-*` CSS custom property; ships shared SCSS tokens for consumers.
102
102
  - **Cross-library adapter** — `hubFormControlAdapter` lets other libraries render `hub-input` / `hub-select` on demand without hard-depending on this package (see below).
@@ -215,6 +215,97 @@ Custom option/label templates are projected straight through to the engine:
215
215
  <hub-datepicker formControlName="range" mode="range" label="Stay" />
216
216
  ```
217
217
 
218
+ ### File input
219
+
220
+ Drag & drop, clipboard paste, constraints and previews. The control value stays native — a `File`, a `File[]`, or `null` — so it goes straight into a `FormData`.
221
+
222
+ ```html
223
+ <hub-file-input
224
+ formControlName="attachments"
225
+ label="Attachments"
226
+ [multiple]="true"
227
+ accept="image/*,.pdf"
228
+ [maxSize]="5 * 1024 * 1024"
229
+ [maxFiles]="3"
230
+ preview="grid"
231
+ (rejected)="notify($event)"
232
+ />
233
+ ```
234
+
235
+ `accept`, `maxSize`, `maxFiles` and friends **filter**: an offending file never reaches the value and surfaces through `(rejected)` with a typed reason. They are enforced by hand, because the native `accept` attribute only filters the operating-system dialog — a drop or a paste bypasses it. To make the *control* invalid as well (worth doing when a value can also be patched in programmatically), add the matching validators:
236
+
237
+ ```ts
238
+ new FormControl<File[]>([], [hubMaxFiles(3), hubMaxFileSize(5 * 1024 * 1024), hubAcceptedFiles('image/*,.pdf')]);
239
+ ```
240
+
241
+ Uploading is opt-in and transport-agnostic. Implement the contract in your application — the library never ships an endpoint — and the field renders per-file progress, cancel and retry:
242
+
243
+ ```ts
244
+ @Injectable({ providedIn: 'root' })
245
+ export class ApiFileUploader implements HubFileUploader {
246
+ readonly #http = inject(HttpClient);
247
+
248
+ upload(file: File): Observable<HubFileUploadEvent> {
249
+ const body = new FormData();
250
+ body.append('file', file);
251
+
252
+ return this.#http.post('/api/files', body, { reportProgress: true, observe: 'events' }).pipe(
253
+ map((event) => {
254
+ if (event.type === HttpEventType.UploadProgress) {
255
+ // `total` is undefined when the size is unknown — pass null, not 0, so the bar
256
+ // renders indeterminate instead of looking stalled.
257
+ return { status: 'progress', loaded: event.loaded, total: event.total ?? null } as const;
258
+ }
259
+ if (event.type === HttpEventType.Response) {
260
+ return { status: 'done', response: event.body } as const;
261
+ }
262
+ return null;
263
+ }),
264
+ filter((event) => event !== null),
265
+ catchError((error) => of({ status: 'error', error } as const))
266
+ );
267
+ }
268
+ }
269
+
270
+ bootstrapApplication(App, { providers: [provideHttpClient(), provideHubFileUploader(ApiFileUploader)] });
271
+ ```
272
+
273
+ > The observable **must be cold**: one subscription is one request. `cancel()` unsubscribes, which is what aborts the underlying `XMLHttpRequest`. A shared or hot observable silently breaks cancellation.
274
+ > Bind a submit button to `uploading()` if you need to wait for the uploads: the control stays valid while they run, by design.
275
+
276
+ Whatever the uploader reports on `done` is kept on the item, so the ids the server minted are there when you submit the form:
277
+
278
+ ```ts
279
+ const uploadedIds = fileInput.files().map((item) => (item.response as { id: string }).id);
280
+ ```
281
+
282
+ Customize it without forking the template: the `--hub-file-input-*` tokens (every icon is a swappable CSS mask), the `hub-file-input-theme(...)` mixin, and three projection slots.
283
+
284
+ ```html
285
+ <hub-file-input formControlName="attachments" [multiple]="true">
286
+ <ng-template hubFileIcon let-item>
287
+ <hub-icon [name]="item.file.type === 'application/pdf' ? 'fa:solid:file-pdf' : 'fa:solid:file'" />
288
+ </ng-template>
289
+ </hub-file-input>
290
+ ```
291
+
292
+ #### Reproducing your own dropzone
293
+
294
+ The dropzone is built from a glyph, an invitation and a browse action, each themeable on its own — so a design system reproduces its own without forking the template.
295
+
296
+ - **Icon medallion** — `--hub-file-input-icon-bg`, `-icon-chip-size` and `-icon-chip-radius` put the glyph on a tinted, rounded surface. Transparent and square by default.
297
+ - **Browse as a button** — `--hub-file-input-browse-bg`, `-hover-bg`, `-padding-x`, `-padding-y`, `-radius` and an optional leading glyph (`-browse-icon`, `-browse-icon-display`, `-browse-icon-size`). A transparent underlined link by default.
298
+ - **Two invitation lines** — `[dropText]` and `[dropSubtext]` (or the `dropHere` / `dropSubtext` labels), stacked with `--hub-file-input-prompt-direction: column`. The second line is empty by default.
299
+ - **A leading notice** — `hubFileDropzoneNotice` projects markup inside the dropzone, between the glyph and the invitation, for something the invitation cannot say.
300
+
301
+ ```html
302
+ <hub-file-input dropText="Drop your documents here" dropSubtext="or click to browse" buttonLabel="Select files">
303
+ <ng-template hubFileDropzoneNotice>
304
+ <strong class="missing">{{ missingCount }} documents still missing</strong>
305
+ </ng-template>
306
+ </hub-file-input>
307
+ ```
308
+
218
309
  ### Automatic errors at every level
219
310
 
220
311
  ```html