ngx-digits-only 1.0.3 → 1.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.
package/README.md CHANGED
@@ -1,19 +1,16 @@
1
1
  # ngx-digits-only
2
2
 
3
- Dependency-free Angular directive for numeric-only inputs — a lightweight alternative to `ngx-mask` when all you need is digit filtering and formatting.
3
+ A dependency-free, standalone Angular directive for smart numeric inputs — digit filtering, formatting, and validation without pulling in a full masking library like `ngx-mask`.
4
4
 
5
- ## Features
5
+ ## What does it do?
6
6
 
7
- - Restricts input to digits only, with optional decimal and negative number support
8
- - Thousands separator formatting (configurable)
9
- - Prefix / suffix support (e.g. currency symbols, units)
10
- - Configurable decimal places
11
- - Min / max value validation
12
- - Persian / Arabic (Eastern) numeral conversion and display
13
- - Full RTL / LTR layout support
14
- - Implements `ControlValueAccessor` and `Validator` — works natively with Reactive Forms and Template-driven forms
15
- - Standalone directive — no NgModule required
16
- - Zero runtime dependencies
7
+ Turns any `<input>` into a smart numeric field:
8
+
9
+ - **Blocks** letters, symbols, and anything that isn't a digit — on every keystroke and paste
10
+ - **Formats** the display with thousands separators, prefix, suffix, and pattern masks
11
+ - **Strips** all display decoration before sending the value to your model
12
+ - **Validates** min, max, maxLength, and pattern completeness automatically
13
+ - **Converts** Arabic / Persian digits to Western digits silently
17
14
 
18
15
  ## Installation
19
16
 
@@ -21,68 +18,401 @@ Dependency-free Angular directive for numeric-only inputs — a lightweight alte
21
18
  npm install ngx-digits-only
22
19
  ```
23
20
 
24
- ## Usage
21
+ ## Quick start
25
22
 
26
- Import the directive directly into your standalone component:
23
+ This is a **standalone directive** no module to import, just the directive itself.
27
24
 
28
25
  ```typescript
29
26
  import { Component } from '@angular/core';
30
27
  import { ReactiveFormsModule } from '@angular/forms';
31
- import { DigitsOnlyDirective } from 'digits-only';
28
+ import { DigitsOnlyDirective } from 'ngx-digits-only';
32
29
 
33
30
  @Component({
34
- selector: 'app-example',
35
31
  standalone: true,
36
- imports: [ReactiveFormsModule, DigitsOnlyDirective],
37
- template: `
38
- <input
39
- digitsOnly
40
- [allowDecimal]="true"
41
- [decimalPlaces]="2"
42
- [allowNegative]="false"
43
- [thousandsSeparator]="true"
44
- [min]="0"
45
- [max]="1000000"
46
- [formControl]="amountControl"
47
- />
48
- `
32
+ imports: [
33
+ ReactiveFormsModule, // or FormsModule if you use ngModel
34
+ DigitsOnlyDirective,
35
+ ],
36
+ template: `<input digitsOnly formControlName="quantity" />`,
49
37
  })
50
- export class ExampleComponent {
51
- amountControl = new FormControl('');
52
- }
38
+ export class ExampleComponent {}
53
39
  ```
54
40
 
55
- ### In an NgModule-based app
56
-
57
- Standalone directives can still be used inside NgModule-based components — just add it to that component's own `imports` array if the component itself is standalone, or import it directly where the input lives:
41
+ In an NgModule-based app, standalone directives can be added directly to that module's `imports` array too:
58
42
 
59
43
  ```typescript
60
- @Component({
61
- standalone: true,
62
- imports: [DigitsOnlyDirective],
63
- ...
44
+ @NgModule({
45
+ imports: [
46
+ FormsModule,
47
+ ReactiveFormsModule,
48
+ DigitsOnlyDirective,
49
+ ],
50
+ declarations: [MyComponent],
64
51
  })
52
+ export class MyFeatureModule {}
65
53
  ```
66
54
 
67
- If your component is *not* standalone (declared in an `NgModule`), you can import the directive into that NgModule's `imports` array directly — standalone directives are valid entries there too.
55
+ ## Your first input
68
56
 
69
- ## API
57
+ ```html
58
+ <input digitsOnly />
59
+ ```
60
+
61
+ That's it. The field now only accepts digits `0–9`. Nothing else gets through.
62
+
63
+ ```html
64
+ <input digitsOnly formControlName="quantity" />
65
+ <input digitsOnly [(ngModel)]="quantity" name="quantity" />
66
+ ```
70
67
 
71
- | Input | Type | Default | Description |
68
+ ## All inputs at a glance
69
+
70
+ | Input | Type | Default | What it does |
72
71
  |---|---|---|---|
73
- | `allowDecimal` | `boolean` | `false` | Allow decimal point input |
74
- | `decimalPlaces` | `number` | `2` | Max digits after decimal point |
75
- | `allowNegative` | `boolean` | `false` | Allow leading minus sign |
76
- | `thousandsSeparator` | `boolean` | `false` | Format with thousands separators as user types |
77
- | `prefix` | `string` | `''` | Text prepended to the display value |
78
- | `suffix` | `string` | `''` | Text appended to the display value |
79
- | `min` | `number` | `undefined` | Minimum allowed value (adds validator) |
80
- | `max` | `number` | `undefined` | Maximum allowed value (adds validator) |
81
- | `easternNumerals` | `boolean` | `false` | Display Persian/Arabic-Indic digits instead of Western |
72
+ | `decimalPlaces` | `number \| null` | `0` | Decimal places: `0` = integers, `N` = exactly N, `null` = unlimited |
73
+ | `thousandSeparator` | `'' \| ',' \| '.' \| ' ' \| '_'` | `''` | Visual grouping character |
74
+ | `prefix` | `string` | `''` | Text shown before the number (e.g. `$`) |
75
+ | `suffix` | `string` | `''` | Text shown after the number (e.g. `%`) |
76
+ | `allowNegative` | `boolean` | `false` | Allow a leading minus sign |
77
+ | `leadingZeros` | `boolean` | `false` | Preserve leading zeros like `007` |
78
+ | `maxLength` | `number \| null` | `null` | Max raw digit count |
79
+ | `min` | `number \| null` | `null` | Minimum value (validation) |
80
+ | `max` | `number \| null` | `null` | Maximum value (validation) |
81
+ | `outputType` | `'number' \| 'string'` | `'number'` | Type emitted to the model |
82
+ | `pattern` | `NamedPattern \| string` | `''` | Display format mask |
83
+ | `convertEasternNumerals` | `boolean` | `true` | Convert Arabic/Persian digits |
84
+
85
+ ## decimalPlaces
86
+
87
+ Controls how many digits are allowed after the decimal point.
88
+
89
+ ```html
90
+ <!-- 0 (default) — integers only, decimal point blocked -->
91
+ <input digitsOnly formControlName="quantity" />
92
+ <!-- user types: 1234 model: 1234 -->
93
+
94
+ <!-- N — exactly N decimal places -->
95
+ <input digitsOnly [decimalPlaces]="2" formControlName="price" />
96
+ <!-- user types: 99.99 model: 99.99 -->
97
+
98
+ <!-- null — unlimited decimal places, only single-dot rule enforced -->
99
+ <input digitsOnly [decimalPlaces]="null" formControlName="exchangeRate" />
100
+ <!-- user types: 1.23456789 model: 1.23456789 -->
101
+ ```
102
+
103
+ A keystroke that would exceed the allowed decimal places is blocked outright.
104
+ When `null`, any number of decimal digits is accepted but only one decimal point is allowed.
105
+
106
+ > `null` follows the same "no limit" convention used by `maxLength`, `min`, and `max`.
107
+
108
+ ## thousandSeparator
109
+
110
+ Display-only formatting — the model always gets the plain number.
111
+
112
+ ```html
113
+ <input digitsOnly thousandSeparator="," formControlName="salary" />
114
+ <!-- display: 1,234,567 model: 1234567 -->
115
+
116
+ <input digitsOnly thousandSeparator="." [decimalPlaces]="2" formControlName="amount" />
117
+ <!-- display: 1.234.567,89 model: 1234567.89 -->
118
+ <!-- setting "." as the thousands separator auto-switches the decimal char to "," -->
119
+ ```
120
+
121
+ Also accepts `' '` (space) and `'_'` (underscore).
122
+
123
+ ## prefix and suffix
124
+
125
+ ```html
126
+ <input digitsOnly prefix="$" formControlName="price" />
127
+ <!-- display: $1234 model: 1234 -->
128
+
129
+ <input digitsOnly prefix="€" suffix=" EUR" [decimalPlaces]="2" formControlName="amount" />
130
+ <!-- display: €1,234.56 EUR model: 1234.56 -->
131
+ ```
132
+
133
+ Both are stripped before the value reaches your model.
134
+
135
+ > **Angular Material note:** The label correctly floats only when there is actual numeric
136
+ > content. An empty field always writes `""` to the native input so `MatInput`'s empty
137
+ > check passes correctly — even when `prefix` or `suffix` is set.
138
+
139
+ ## allowNegative
140
+
141
+ ```html
142
+ <input digitsOnly [allowNegative]="true" formControlName="temperature" />
143
+ <!-- user can type: -42 model: -42 -->
144
+ ```
145
+
146
+ The minus sign is only valid as the first character, and works correctly even with `prefix` set.
147
+
148
+ ## leadingZeros
149
+
150
+ By default, leading zeros are stripped in number mode.
151
+
152
+ ```html
153
+ <input digitsOnly outputType="string" [leadingZeros]="true" [maxLength]="5" formControlName="zip" />
154
+ <!-- user types: 01234 model: '01234' (not 1234) -->
155
+ ```
156
+
157
+ When `pattern` is set, leading zeros are always preserved automatically.
158
+
159
+ ## maxLength
160
+
161
+ ```html
162
+ <input digitsOnly [maxLength]="5" formControlName="pin" />
163
+ ```
164
+
165
+ When `pattern` is set, `maxLength` is derived automatically from the number of `0` slots — any manual value is ignored.
166
+
167
+ ## min and max
168
+
169
+ Only apply in `outputType="number"` mode.
170
+
171
+ ```html
172
+ <input digitsOnly [min]="0" [max]="999" formControlName="score" #scoreCtrl="ngModel" />
173
+
174
+ <p *ngIf="scoreCtrl.errors?.['min']">Minimum value is {{ scoreCtrl.errors?.['min'].min }}</p>
175
+ <p *ngIf="scoreCtrl.errors?.['max']">Maximum value is {{ scoreCtrl.errors?.['max'].max }}</p>
176
+ ```
177
+
178
+ Error payloads: `{ min: 0, actual: -5 }` / `{ max: 999, actual: 1200 }`.
179
+
180
+ ## outputType — the most important decision
181
+
182
+ **`outputType="number"` (default)** — model gets a JS `number` or `null`. Use for prices, quantities, anything you'll do arithmetic with.
183
+
184
+ **`outputType="string"`** — model gets a JS `string` or `null`. Use for card numbers, phone numbers, postal codes, SSNs — identifiers, not quantities.
185
+
186
+ Why it matters:
187
+
188
+ - Leading zeros are lost in number mode: `01234` → `1234`
189
+ - 16-digit card numbers exceed `Number.MAX_SAFE_INTEGER` (`9007199254740991`) and get silently corrupted if stored as a number
190
+
191
+ ```typescript
192
+ // WRONG
193
+ cardNumber: number = 4111111111111111; // stored as 4111111111111112 — last digit wrong
194
+
195
+ // CORRECT
196
+ cardNumber: string = '4111111111111111'; // exact
197
+ ```
198
+
199
+ ## pattern — auto formatting
200
+
201
+ ```html
202
+ <input digitsOnly pattern="0000 0000 0000 0000" formControlName="card" />
203
+ <!-- display: 4111 1111 1111 1111 model: '4111111111111111' -->
204
+ ```
205
+
206
+ Setting `pattern` automatically:
207
+
208
+ | Effect | Why |
209
+ |---|---|
210
+ | Forces `outputType="string"` | Patterns are for identifiers, never arithmetic |
211
+ | Derives `maxLength` from slot count | No need to set it separately |
212
+ | Forces `leadingZeros="true"` | Every digit position is significant |
213
+ | Disables `thousandSeparator` | Patterns have their own separators |
214
+ | Disables `decimalPlaces` | Patterns are for integers/identifiers |
215
+ | Disables `allowNegative` | Identifiers are never negative |
216
+
217
+ ### Named patterns
218
+
219
+ | Name | Slots | Example |
220
+ |---|---|---|
221
+ | `card` | 16 | `4111 1111 1111 1111` |
222
+ | `card-amex` | 15 | `3782 822463 10005` |
223
+ | `expiry` | 4 | `12/26` |
224
+ | `cvv` | 3 | `123` |
225
+ | `cvv-amex` | 4 | `1234` |
226
+ | `phone-us` | 10 | `(555) 867-5309` |
227
+ | `ssn` | 9 | `123-45-6789` |
228
+ | `date` | 8 | `01/01/1990` |
229
+ | `time` | 4 | `09:30` |
230
+ | `sort-code` | 6 | `20-00-00` |
231
+ | `bsb` | 6 | `062-000` |
232
+
233
+ ```html
234
+ <input digitsOnly pattern="card" formControlName="cardNumber" />
235
+ <input digitsOnly pattern="expiry" formControlName="expiry" />
236
+ <input digitsOnly pattern="ssn" formControlName="ssn" />
237
+ ```
238
+
239
+ ### Custom patterns
240
+
241
+ Any string not in the named list is used as a raw pattern — `0` for a digit slot, anything else as a literal separator.
242
+
243
+ ```html
244
+ <input digitsOnly pattern="000 000" formControlName="otp" />
245
+ <!-- display: 123 456 model: '123456' -->
246
+
247
+ <input digitsOnly pattern="0000-0000-0000" formControlName="account" />
248
+ <!-- display: 1234-5678-9012 model: '123456789012' -->
249
+ ```
250
+
251
+ Separators are only inserted once more digits are coming, so you never get a trailing separator mid-type.
252
+
253
+ ## convertEasternNumerals
254
+
255
+ Arabic and Persian keyboards produce different Unicode digit characters:
256
+
257
+ | Script | Characters |
258
+ |---|---|
259
+ | Western | `0 1 2 3 4 5 6 7 8 9` |
260
+ | Arabic-Indic | `٠ ١ ٢ ٣ ٤ ٥ ٦ ٧ ٨ ٩` |
261
+ | Persian/Farsi | `۰ ۱ ۲ ۳ ۴ ۵ ۶ ۷ ۸ ۹` |
262
+
263
+ By default (`convertEasternNumerals="true"`), both typing and pasting Eastern digits work transparently, with the Arabic decimal (`٫`) and thousands (`٬`) separators normalized automatically.
264
+
265
+ ```html
266
+ <input digitsOnly [convertEasternNumerals]="false" formControlName="code" />
267
+ <!-- Eastern digits now blocked — only ASCII 0-9 accepted -->
268
+ ```
269
+
270
+ Conversion is unicode-range-safe — it can never accidentally transform a comma, dollar sign, or letter.
271
+
272
+ ## Validation errors
273
+
274
+ Implements Angular's `Validator` interface — errors land on `control.errors` automatically.
275
+
276
+ | Error key | When it fires | Payload |
277
+ |---|---|---|
278
+ | `min` | Value below `[min]` | `{ min, actual }` |
279
+ | `max` | Value above `[max]` | `{ max, actual }` |
280
+ | `maxLength` | Raw digit count exceeds `[maxLength]` | `{ maxLength, actual }` |
281
+ | `patternIncomplete` | Pattern mode, not all slots filled | `{ required, actual, pattern }` |
282
+
283
+ ```html
284
+ <input digitsOnly pattern="card" formControlName="cardNumber" />
285
+ <p *ngIf="form.get('cardNumber')?.errors?.['patternIncomplete']">
286
+ Enter all {{ form.get('cardNumber')?.errors?.['patternIncomplete']?.required }} digits
287
+ </p>
288
+ ```
289
+
290
+ Directive errors merge with Angular's own validators (e.g. `Validators.required`) on the same `control.errors` object.
291
+
292
+ ## Reactive Forms example
293
+
294
+ ```typescript
295
+ this.form = this.fb.group({
296
+ cardNumber: [null, Validators.required],
297
+ expiry: [null, Validators.required],
298
+ cvv: [null, Validators.required],
299
+ amount: [null, Validators.required],
300
+ });
301
+ ```
302
+
303
+ ```html
304
+ <form [formGroup]="form" (ngSubmit)="pay()">
305
+ <input digitsOnly pattern="card" formControlName="cardNumber" placeholder="4111 1111 1111 1111" />
306
+ <input digitsOnly pattern="expiry" formControlName="expiry" placeholder="MM/YY" />
307
+ <input digitsOnly pattern="cvv" formControlName="cvv" placeholder="123" />
308
+ <input digitsOnly [decimalPlaces]="2" thousandSeparator="," prefix="$" [min]="0.01" formControlName="amount" />
309
+ <button type="submit" [disabled]="form.invalid">Pay</button>
310
+ </form>
311
+ ```
312
+
313
+ ```typescript
314
+ onSubmit() {
315
+ console.log(this.form.value);
316
+ // {
317
+ // cardNumber: '4111111111111111', // string, no spaces
318
+ // expiry: '1226', // string, no slash
319
+ // cvv: '123',
320
+ // amount: 49.99 // number
321
+ // }
322
+ }
323
+ ```
324
+
325
+ ## Type safety — NamedPattern
326
+
327
+ ```typescript
328
+ import { NamedPattern } from 'ngx-digits-only';
329
+
330
+ export class MyComponent {
331
+ selectedPattern: NamedPattern = 'card'; // autocomplete + compile-time typo checking
332
+ customPattern = '000-0000'; // custom strings still accepted
333
+ }
334
+ ```
335
+
336
+ ```html
337
+ <input digitsOnly [pattern]="selectedPattern" formControlName="id" />
338
+ ```
339
+
340
+ `NamedPattern` uses the `(string & {})` trick internally so IDE autocomplete for named patterns survives alongside free-form custom pattern strings.
341
+
342
+ ## Standalone Eastern numeral utilities
343
+
344
+ Zero-dependency functions, usable outside Angular entirely (React, Vue, Node, plain TS):
345
+
346
+ ```typescript
347
+ import { convertEasternDigits, hasEasternDigits, getDigitScript, toWesternNumber } from 'ngx-digits-only';
348
+
349
+ convertEasternDigits('١٢٣'); // '123'
350
+ hasEasternDigits('١٢٣'); // true
351
+ getDigitScript('۴۵۶'); // 'persian'
352
+ toWesternNumber('١٢٣٫٤٥'); // 123.45
353
+ ```
354
+
355
+ ## Common mistakes
356
+
357
+ **Using `number` for a card number** — exceeds `Number.MAX_SAFE_INTEGER` and corrupts digits. Use `outputType="string"` or, better, `pattern="card"` which handles it automatically.
358
+
359
+ **Setting `[maxLength]` alongside `pattern`** — ignored; pattern derives it from slot count.
360
+
361
+ **Expecting `min`/`max` with `outputType="string"`** — silently ignored in string mode; only works with `outputType="number"`.
362
+
363
+ **Expecting `decimalPlaces` to work with `pattern`** — ignored when a pattern is set; patterns are always integer-only identifiers.
364
+
365
+ **Forgetting `FormsModule`/`ReactiveFormsModule`** — required alongside `DigitsOnlyDirective` for `ngModel`/`formControlName` to work.
366
+
367
+ ## FAQ
368
+
369
+ **Can I combine `prefix`/`suffix` with `pattern`?** Yes — they wrap the formatted pattern display; the model still gets the raw digits.
370
+
371
+ **Can I pre-fill from the server?** Yes, via `fb.group()` initial values or `patchValue()` — `writeValue()` formats the display automatically.
372
+
373
+ **Why `null` instead of `0` for an empty field?** By design, to distinguish "user typed zero" from "user left it blank."
374
+
375
+ **Why is `decimalPlaces` typed as `number | null` now?** `null` means unlimited decimal places — same convention as `maxLength`, `min`, and `max`. `0` still means integers only and is still the default, so existing code is unaffected.
376
+
377
+ **Does it work with Angular Material?** Yes — including correct label floating behaviour. The directive writes `""` to the native input when the field is empty, even when `prefix` or `suffix` is set, so `MatInput` detects the empty state correctly.
378
+
379
+ **Does it work with Angular signal-based forms?** Yes — it implements `ControlValueAccessor`, compatible with any Angular form API.
380
+
381
+ **Can I use it without a form?** Yes:
382
+ ```html
383
+ <input digitsOnly (input)="onInput($event.target.value)" />
384
+ ```
385
+
386
+ ## Changelog
387
+
388
+ ### 1.1.0
389
+
390
+ **Added**
391
+ - `decimalPlaces` now accepts `null` for unlimited decimal places (single-dot rule still enforced). Existing `0` / `N` behavior is unchanged — no breaking change.
392
+
393
+ **Fixed**
394
+ - Angular Material (`MatInput`) label was permanently floated when `prefix` or `suffix` was set on an empty field. The directive now writes `""` to the native input when empty so Material's empty check passes correctly.
395
+
396
+ ### 1.0.5
397
+
398
+ **Added**
399
+ - RTL / LTR direction support — automatic via `getComputedStyle`, no extra input required
400
+ - `convertEasternNumerals` input — Arabic-Indic and Persian digit conversion on keystroke and paste
401
+ - `pattern` input with 11 named aliases and custom raw pattern support
402
+ - `outputType` input — `'number'` or `'string'`
403
+ - `NamedPattern` exported type — IDE autocomplete and compile-time safety
404
+ - `allowNegative` now works correctly when `prefix` is set
405
+
406
+ **Fixed**
407
+ - Eastern digits (Arabic/Persian) were blocked on `keydown` because `SINGLE_DIGIT` only matches ASCII — fixed with Unicode range check
408
+
409
+ ### 1.0.0
410
+
411
+ Initial release.
82
412
 
83
413
  ## Compatibility
84
414
 
85
- Requires Angular `>=16.0.0` (standalone APIs). Works in both standalone and NgModule-based applications.
415
+ Requires Angular `>=16.0.0`.
86
416
 
87
417
  ## License
88
418