• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dineeek / ngx-libs-workspace / 5027468065

pending completion
5027468065

Pull #11

github

GitHub
Merge 4da8dc4ab into f3f79a68f
Pull Request #11: Ngx pass code - refactor parent control

52 of 53 branches covered (98.11%)

Branch coverage included in aggregate %.

7 of 7 new or added lines in 1 file covered. (100.0%)

131 of 132 relevant lines covered (99.24%)

30.24 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.17
/libs/ngx-pass-code/src/lib/component/pass-code.component.ts
1
import {
1✔
2
  ChangeDetectionStrategy,
3
  ChangeDetectorRef,
4
  Component,
5
  Input,
6
  OnDestroy,
7
  OnInit
8
} from '@angular/core'
9
import {
1✔
10
  AbstractControl,
11
  ControlValueAccessor,
12
  FormArray,
13
  FormControl,
14
  NgControl,
15
  ValidationErrors,
16
  Validator
17
} from '@angular/forms'
18
import { distinctUntilChanged, map, Subject, takeUntil, tap } from 'rxjs'
1✔
19

20
@Component({
21
  selector: 'ngx-pass-code',
22
  templateUrl: './pass-code.component.html',
23
  styleUrls: ['./pass-code.component.scss'],
24
  changeDetection: ChangeDetectionStrategy.OnPush
25
})
26
export class PassCodeComponent
1✔
27
  implements OnInit, OnDestroy, ControlValueAccessor, Validator
28
{
29
  @Input() length = 0
28✔
30
  @Input() type: 'text' | 'number' | 'password' = 'text'
28✔
31
  @Input() uppercase = false
28✔
32
  @Input() autofocus = false // set focus on first input
33
  @Input() autoblur = false // remove focus from last input when filled
34

35
  passCodes!: FormArray<FormControl>
36
  isCodeInvalid = false // validation is triggered only if all controls are invalid
28✔
37

38
  private initialized = false
28✔
39
  private unsubscribe$ = new Subject<void>()
28✔
40

41
  // eslint-disable-next-line @typescript-eslint/no-empty-function
42
  onChange = (value: string | number | null) => {}
28✔
43

44
  // eslint-disable-next-line @typescript-eslint/no-empty-function
45
  onTouched = () => {}
28✔
46

47
  constructor(
48
    private controlDirective: NgControl,
28✔
49
    private cdRef: ChangeDetectorRef
28✔
50
  ) {
51
    this.controlDirective.valueAccessor = this
28✔
52
  }
53

54
  ngOnInit(): void {
55
    this.passCodes = new FormArray(
28✔
56
      [...new Array(this.length)].map(() => new FormControl(''))
163✔
57
    )
58

59
    this.setSyncValidatorsFromParent()
28✔
60
    this.updateParentControlValidation()
28✔
61
    this.propagateViewValueToModel()
28✔
62
  }
63

64
  ngOnDestroy(): void {
65
    this.unsubscribe$.next()
28✔
66
    this.unsubscribe$.complete()
28✔
67
  }
68

69
  writeValue(value: string): void {
70
    const stringifyTrimmedValue = value?.toString().trim()
35✔
71
    if (!this.initialized) {
35✔
72
      // issue - https://github.com/angular/angular/issues/29218 - have to know length property before writing any value
73
      setTimeout(() => {
28✔
74
        this.initialized = true
28✔
75
        this.propagateModelValueToView(stringifyTrimmedValue)
28✔
76
      })
77
    } else {
78
      this.propagateModelValueToView(stringifyTrimmedValue)
7✔
79
    }
80
  }
81

82
  registerOnChange(fn: any): void {
83
    this.onChange = fn
56✔
84
  }
85

86
  registerOnTouched(fn: any): void {
87
    this.onTouched = fn
56✔
88
  }
89

90
  setDisabledState(isDisabled: boolean): void {
91
    if (!this.initialized) {
7✔
92
      setTimeout(() => {
3✔
93
        this.disableControls(isDisabled)
3✔
94
      })
95

96
      return
3✔
97
    }
98

99
    this.disableControls(isDisabled)
4✔
100
  }
101

102
  validate(): ValidationErrors | null {
103
    if (this.passCodes.valid) {
119✔
104
      return null
107✔
105
    }
106

107
    const errors = this.passCodes.controls
12✔
108
      .map(control => control.errors)
83✔
109
      .filter(error => error !== null)
83✔
110

111
    return errors.length ? errors : null
12✔
112
  }
113

114
  private get parentControl(): AbstractControl<any, any> {
115
    return this.controlDirective.control as AbstractControl<any, any>
160✔
116
  }
117

118
  private setSyncValidatorsFromParent(): void {
119
    const parentValidators = this.parentControl.validator
28✔
120

121
    if (!parentValidators) {
28✔
122
      return
18✔
123
    }
124

125
    this.passCodes.controls.forEach(control => {
10✔
126
      control.setValidators(parentValidators)
70✔
127
    })
128
    this.passCodes.updateValueAndValidity({ emitEvent: false })
10✔
129
  }
130

131
  private updateParentControlValidation(): void {
132
    if (!this.parentControl) {
28!
133
      return
×
134
    }
135

136
    this.parentControl.setValidators(this.validate.bind(this))
28✔
137
    this.parentControl.updateValueAndValidity({ emitEvent: false })
28✔
138
  }
139

140
  private propagateModelValueToView(value: string): void {
141
    value ? this.setValue(value) : this.resetValue()
35✔
142
    this.updateCodeValidity()
34✔
143
    this.cdRef.markForCheck()
34✔
144
  }
145

146
  private setValue(value: string): void {
147
    if (this.type === 'number' && isNaN(parseInt(value))) {
17✔
148
      throw new TypeError(
1✔
149
        'Provided value does not match provided type property number!'
150
      )
151
    }
152

153
    const splittedValue = value.substring(0, this.length).split('') // remove chars after specified length and split
16✔
154

155
    if (splittedValue.length < this.length) {
16✔
156
      this.resetValue()
2✔
157
    }
158

159
    this.passCodes.patchValue(splittedValue, { emitEvent: false })
16✔
160
  }
161

162
  private resetValue(): void {
163
    const nullValues = Array(this.length).fill(null)
20✔
164
    this.passCodes.patchValue(nullValues, { emitEvent: false })
20✔
165
  }
166

167
  private propagateViewValueToModel(): void {
168
    this.passCodes.valueChanges
28✔
169
      .pipe(
170
        tap(() => this.updateCodeValidity()),
7✔
171
        map(codes => {
172
          const code = codes.join('')
7✔
173

174
          if (this.passCodes.invalid || !code) {
7✔
175
            return null
4✔
176
          }
177

178
          if (this.type === 'number') {
3✔
179
            return parseInt(code)
1✔
180
          }
181

182
          return this.uppercase ? code.toUpperCase() : code
2✔
183
        }),
184
        distinctUntilChanged(),
185
        takeUntil(this.unsubscribe$)
186
      )
187
      .subscribe((value: string | number | null) => this.onChange(value))
5✔
188
  }
189

190
  private updateCodeValidity(): void {
191
    const allControlsAreInvalid = this.validate()?.['length'] === this.length
41✔
192
    this.isCodeInvalid = allControlsAreInvalid && this.passCodes.dirty
41✔
193
    this.parentControl.updateValueAndValidity({ emitEvent: false })
41✔
194
  }
195

196
  private disableControls(isDisabled: boolean): void {
197
    isDisabled
7✔
198
      ? this.passCodes.disable({ emitEvent: false })
199
      : this.passCodes.enable({ emitEvent: false })
200

201
    this.parentControl.updateValueAndValidity({ emitEvent: false })
7✔
202
  }
203
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc