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

IgniteUI / igniteui-angular / 13005189282

28 Jan 2025 06:56AM CUT coverage: 91.628% (+0.008%) from 91.62%
13005189282

Pull #15298

github

web-flow
Merge a168f378d into 0a93e080d
Pull Request #15298: feat(checkbox): extract common logic in a directive

12981 of 15212 branches covered (85.33%)

90 of 94 new or added lines in 4 files covered. (95.74%)

1 existing line in 1 file now uncovered.

26375 of 28785 relevant lines covered (91.63%)

34338.64 hits per line

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

95.12
/projects/igniteui-angular/src/lib/checkbox/checkbox-base.directive.ts
1
import {
2
    Directive,
3
    EventEmitter,
4
    HostListener,
5
    HostBinding,
6
    Input,
7
    Output,
8
    ViewChild,
9
    ElementRef,
10
    ChangeDetectorRef,
11
    Renderer2,
12
    Optional,
13
    Self,
14
    booleanAttribute,
15
    inject,
16
    DestroyRef,
17
    Inject,
18
    AfterViewInit,
19
} from '@angular/core';
20
import { NgControl, Validators } from '@angular/forms';
21
import { IBaseEventArgs, getComponentTheme, mkenum } from '../core/utils';
22
import { noop, Subject } from 'rxjs';
23
import { takeUntil } from 'rxjs/operators';
24
import {
25
    IgxTheme,
26
    THEME_TOKEN,
27
    ThemeToken,
28
} from '../services/theme/theme.token';
29

30
export const LabelPosition = /*@__PURE__*/ mkenum({
2✔
31
    BEFORE: 'before',
32
    AFTER: 'after'
33
});
34
export type LabelPosition = typeof LabelPosition[keyof typeof LabelPosition];
35

36
export interface IChangeCheckboxEventArgs extends IBaseEventArgs {
37
    checked: boolean;
38
    value?: any;
39
}
40

41
let nextId = 0;
2✔
42

43
@Directive()
44
export class CheckboxBaseDirective implements AfterViewInit {
2✔
45
    /**
46
     * An event that is emitted after the checkbox state is changed.
47
     * Provides references to the `IgxCheckboxComponent` and the `checked` property as event arguments.
48
     */
49
    // eslint-disable-next-line @angular-eslint/no-output-native
50
    @Output() public readonly change: EventEmitter<IChangeCheckboxEventArgs> =
8,174✔
51
        new EventEmitter<IChangeCheckboxEventArgs>();
52

53
    /**
54
     * @hidden
55
     * @internal
56
     */
57
    public destroy$ = new Subject<boolean>();
8,174✔
58

59
    /**
60
     * Returns reference to the native checkbox element.
61
     *
62
     * @example
63
     * ```typescript
64
     * let checkboxElement =  this.component.checkboxElement;
65
     * ```
66
     */
67
    @ViewChild('checkbox', { static: true })
68
    public nativeInput: ElementRef;
69

70
    /**
71
     * Returns reference to the native label element.
72
     * ```typescript
73
     *
74
     * @example
75
     * let labelElement =  this.component.nativeLabel;
76
     * ```
77
     */
78
    @ViewChild('label', { static: true })
79
    public nativeLabel: ElementRef;
80

81
    public cssClass: string;
82
    public disabled: boolean;
83
    public readonly: boolean;
84
    public indeterminate: boolean;
85
    public focused: boolean;
86
    public invalid: boolean;
87

88
    @Input({ transform: booleanAttribute })
89
    public get checked() {
90
        return this._checked;
173,113✔
91
    }
92

93
    public set checked(value: boolean) {
94
        if (this._checked !== value) {
10,351✔
95
            this._checked = value;
4,060✔
96
            this._onChangeCallback(this._checked);
4,060✔
97
        }
98
    }
99

100
    /**
101
     * Returns reference to the `nativeElement` of the igx-checkbox/igx-switch.
102
     *
103
     * @example
104
     * ```typescript
105
     * let nativeElement = this.component.nativeElement;
106
     * ```
107
     */
108
    public get nativeElement() {
109
        return this.nativeInput.nativeElement;
116,139✔
110
    }
111

112
    /**
113
     * Returns reference to the label placeholder element.
114
     * ```typescript
115
     *
116
     * @example
117
     * let labelPlaceholder =  this.component.placeholderLabel;
118
     * ```
119
     */
120
    @ViewChild('placeholderLabel', { static: true })
121
    public placeholderLabel: ElementRef;
122

123
    /**
124
     * Sets/gets the `id` of the checkbox component.
125
     * If not set, the `id` of the first checkbox component will be `"igx-checkbox-0"`.
126
     *
127
     * @example
128
     * ```html
129
     * <igx-checkbox id="my-first-checkbox"></igx-checkbox>
130
     * ```
131
     * ```typescript
132
     * let checkboxId =  this.checkbox.id;
133
     * ```
134
     */
135
    @HostBinding('attr.id')
136
    @Input()
137
    public id = `igx-checkbox-${nextId++}`;
8,174✔
138

139
    /**
140
     * Sets/gets the id of the `label` element.
141
     * If not set, the id of the `label` in the first checkbox component will be `"igx-checkbox-0-label"`.
142
     *
143
     * @example
144
     * ```html
145
     * <igx-checkbox labelId="Label1"></igx-checkbox>
146
     * ```
147
     * ```typescript
148
     * let labelId =  this.component.labelId;
149
     * ```
150
     */
151
    @Input() public labelId = `${this.id}-label`;
8,174✔
152

153
    /**
154
     * Sets/gets the `value` attribute.
155
     *
156
     * @example
157
     * ```html
158
     * <igx-checkbox [value]="'CheckboxValue'"></igx-checkbox>
159
     * ```
160
     * ```typescript
161
     * let value =  this.checkbox.value;
162
     * ```
163
     */
164
    @Input() public value: any;
165

166
    /**
167
     * Sets/gets the `name` attribute.
168
     *
169
     * @example
170
     * ```html
171
     * <igx-checkbox name="Checkbox1"></igx-checkbox>
172
     * ```
173
     * ```typescript
174
     * let name =  this.checkbox.name;
175
     * ```
176
     */
177
    @Input() public name: string;
178

179
    /**
180
     * Sets/gets the value of the `tabindex` attribute.
181
     *
182
     * @example
183
     * ```html
184
     * <igx-checkbox [tabindex]="1"></igx-checkbox>
185
     * ```
186
     * ```typescript
187
     * let tabIndex =  this.checkbox.tabindex;
188
     * ```
189
     */
190
    @Input() public tabindex: number = null;
8,174✔
191

192
    /**
193
     *  Sets/gets the position of the `label`.
194
     *  If not set, the `labelPosition` will have value `"after"`.
195
     *
196
     * @example
197
     * ```html
198
     * <igx-checkbox labelPosition="before"></igx-checkbox>
199
     * ```
200
     * ```typescript
201
     * let labelPosition =  this.checkbox.labelPosition;
202
     * ```
203
     */
204
    @Input()
205
    public labelPosition: LabelPosition | string = LabelPosition.AFTER;
8,174✔
206

207
    /**
208
     * Enables/Disables the ripple effect.
209
     * If not set, `disableRipple` will have value `false`.
210
     *
211
     * @example
212
     * ```html
213
     * <igx-checkbox [disableRipple]="true"></igx-checkbox>
214
     * ```
215
     * ```typescript
216
     * let isRippleDisabled = this.checkbox.desableRipple;
217
     * ```
218
     */
219
    @Input({ transform: booleanAttribute })
220
    public disableRipple = false;
8,174✔
221

222
    /**
223
     * Sets/gets the `aria-labelledby` attribute.
224
     * If not set, the `aria-labelledby` will be equal to the value of `labelId` attribute.
225
     *
226
     * @example
227
     * ```html
228
     * <igx-checkbox aria-labelledby="Checkbox1"></igx-checkbox>
229
     * ```
230
     * ```typescript
231
     * let ariaLabelledBy = this.checkbox.ariaLabelledBy;
232
     * ```
233
     */
234
    @Input('aria-labelledby')
235
    public ariaLabelledBy = this.labelId;
8,174✔
236

237
    /**
238
     * Sets/gets the value of the `aria-label` attribute.
239
     *
240
     * @example
241
     * ```html
242
     * <igx-checkbox aria-label="Checkbox1"></igx-checkbox>
243
     * ```
244
     * ```typescript
245
     * let ariaLabel = this.checkbox.ariaLabel;
246
     * ```
247
     */
248
    @Input('aria-label')
249
    public ariaLabel: string | null = null;
8,174✔
250

251
    constructor(
252
        protected cdr: ChangeDetectorRef,
8,174✔
253
        protected renderer: Renderer2,
8,174✔
254
        @Inject(THEME_TOKEN)
255
        protected themeToken: ThemeToken,
8,174✔
256
        @Optional() @Self() public ngControl: NgControl
8,174✔
257
    ) {
258
        if (this.ngControl !== null) {
8,174✔
259
            this.ngControl.valueAccessor = this;
44✔
260
        }
261

262
        this.theme = this.themeToken.theme;
8,174✔
263

264
        const { unsubscribe } = this.themeToken.onChange((theme) => {
8,174✔
265
            if (this.theme !== theme) {
8,174!
NEW
266
                this.theme = theme;
×
NEW
267
                this.cdr.detectChanges();
×
268
            }
269
        });
270

271
        this.destroyRef.onDestroy(() => unsubscribe);
8,174✔
272
    }
273

274
    /**
275
     * Sets/gets whether the checkbox is required.
276
     * If not set, `required` will have value `false`.
277
     *
278
     * @example
279
     * ```html
280
     * <igx-checkbox required></igx-checkbox>
281
     * ```
282
     * ```typescript
283
     * let isRequired = this.checkbox.required;
284
     * ```
285
     */
286
    // @Input({ transform: booleanAttribute })
287
    // public get required(): boolean {
288
    //     return this._required || this.nativeElement.hasAttribute('required');
289
    // }
290
    // public set required(value: boolean) {
291
    //     this._required = value;
292
    // }
293
    @Input({ transform: booleanAttribute })
294
    public get required(): boolean {
295
        return this._required || this.nativeElement.hasAttribute('required');
116,206✔
296
    }
297
    public set required(value: boolean) {
298
        this._required = value;
60✔
299
    }
300

301
    /**
302
     * @hidden
303
     * @internal
304
     */
305
    public ngAfterViewInit() {
306
        if (this.ngControl) {
8,172✔
307
            this.ngControl.statusChanges
44✔
308
                .pipe(takeUntil(this.destroy$))
309
                .subscribe(this.updateValidityState.bind(this));
310

311
            if (
44✔
312
                this.ngControl.control.validator ||
79✔
313
                this.ngControl.control.asyncValidator
314
            ) {
315
                this._required = this.ngControl?.control?.hasValidator(
9✔
316
                    Validators.required
317
                );
318
                this.cdr.detectChanges();
9✔
319
            }
320
        }
321

322
        this.setComponentTheme();
8,172✔
323
    }
324

325
    /**
326
     * @hidden
327
     * @internal
328
     */
329
    public inputId = `${this.id}-input`;
8,174✔
330

331
    /**
332
     * @hidden
333
     */
334
    protected _onChangeCallback: (_: any) => void = noop;
8,174✔
335

336
    /**
337
     * @hidden
338
     */
339
    private _onTouchedCallback: () => void = noop;
8,174✔
340

341
    /**
342
     * @hidden
343
     * @internal
344
     */
345
    protected _checked = false;
8,174✔
346

347
    /**
348
     * @hidden
349
     * @internal
350
     */
351
    protected theme: IgxTheme;
352

353
    /**
354
     * @hidden
355
     * @internal
356
     */
357
    public _required = false;
8,174✔
358
    private elRef = inject(ElementRef);
8,174✔
359
    protected destroyRef = inject(DestroyRef);
8,174✔
360

361
    private setComponentTheme() {
362
        if (!this.themeToken.preferToken) {
8,172✔
363
            const theme = getComponentTheme(this.elRef.nativeElement);
8,172✔
364

365
            if (theme && theme !== this.theme) {
8,172!
NEW
366
                this.theme = theme;
×
NEW
367
                this.cdr.markForCheck();
×
368
            }
369
        }
370
    }
371

372
    /** @hidden @internal */
373
    @HostListener('keyup', ['$event'])
374
    public onKeyUp(event: KeyboardEvent) {
375
        event.stopPropagation();
14✔
376
        this.focused = true;
14✔
377
    }
378

379
    /** @hidden @internal */
380
    @HostListener('click', ['$event'])
381
    public _onCheckboxClick(event: PointerEvent | MouseEvent) {
382
        // Since the original checkbox is hidden and the label
383
        // is used for styling and to change the checked state of the checkbox,
384
        // we need to prevent the checkbox click event from bubbling up
385
        // as it gets triggered on label click
386
        // NOTE: The above is no longer valid, as the native checkbox is not labeled
387
        // by the SVG anymore.
388
        if (this.disabled || this.readonly) {
152✔
389
            // readonly prevents the component from changing state (see toggle() method).
390
            // However, the native checkbox can still be activated through user interaction (focus + space, label click)
391
            // Prevent the native change so the input remains in sync
392
            event.preventDefault();
71✔
393
            return;
71✔
394
        }
395

396
        this.nativeElement.focus();
81✔
397

398
        this.indeterminate = false;
81✔
399
        this.checked = !this.checked;
81✔
400
        this.updateValidityState();
81✔
401

402
        // K.D. March 23, 2021 Emitting on click and not on the setter because otherwise every component
403
        // bound on change would have to perform self checks for weather the value has changed because
404
        // of the initial set on initialization
405
        this.change.emit({
81✔
406
            checked: this.checked,
407
            value: this.value,
408
            owner: this,
409
        });
410
    }
411

412
    /**
413
     * @hidden
414
     * @internal
415
     */
416
    public get ariaChecked() {
417
        if (this.indeterminate) {
57,735✔
418
            return 'mixed';
1,046✔
419
        } else {
420
            return this.checked;
56,689✔
421
        }
422
    }
423

424
    /** @hidden @internal */
425
    public _onCheckboxChange(event: Event) {
426
        // We have to stop the original checkbox change event
427
        // from bubbling up since we emit our own change event
428
        event.stopPropagation();
60✔
429
    }
430

431
    /** @hidden @internal */
432
    @HostListener('blur')
433
    public onBlur() {
434
        this.focused = false;
51✔
435
        this._onTouchedCallback();
51✔
436
        this.updateValidityState();
51✔
437
    }
438

439
    /** @hidden @internal */
440
    public writeValue(value: boolean) {
441
        this._checked = value;
53✔
442
    }
443

444
    /** @hidden @internal */
445
    public get labelClass(): string {
446
        switch (this.labelPosition) {
58,095✔
447
            case LabelPosition.BEFORE:
448
                return `${this.cssClass}__label--before`;
6✔
449
            case LabelPosition.AFTER:
450
            default:
451
                return `${this.cssClass}__label`;
58,089✔
452
        }
453
    }
454

455
    /** @hidden @internal */
456
    public registerOnChange(fn: (_: any) => void) {
457
        this._onChangeCallback = fn;
66✔
458
    }
459

460
    /** @hidden @internal */
461
    public registerOnTouched(fn: () => void) {
462
        this._onTouchedCallback = fn;
80✔
463
    }
464

465
    /** @hidden @internal */
466
    public setDisabledState(isDisabled: boolean) {
467
        this.disabled = isDisabled;
45✔
468
    }
469

470
    /** @hidden @internal */
471
    public getEditElement() {
472
        return this.nativeInput.nativeElement;
26✔
473
    }
474

475
    /**
476
     * @hidden
477
     * @internal
478
     */
479
    protected updateValidityState() {
480
        if (this.ngControl) {
187✔
481
            if (
77✔
482
                !this.disabled &&
270✔
483
                !this.readonly &&
484
                (this.ngControl.control.touched || this.ngControl.control.dirty)
485
            ) {
486
                // the control is not disabled and is touched or dirty
487
                this.invalid = this.ngControl.invalid;
42✔
488
            } else {
489
                //  if the control is untouched, pristine, or disabled, its state is initial. This is when the user did not interact
490
                //  with the checkbox or when the form/control is reset
491
                this.invalid = false;
35✔
492
            }
493
        } else {
494
            this.checkNativeValidity();
110✔
495
        }
496
    }
497

498
    /**
499
     * A function to assign a native validity property of a checkbox.
500
     * This should be used when there's no ngControl
501
     *
502
     * @hidden
503
     * @internal
504
     */
505
    private checkNativeValidity() {
506
        if (
110✔
507
            !this.disabled &&
238✔
508
            this._required &&
509
            !this.checked &&
510
            !this.readonly
511
        ) {
512
            this.invalid = true;
8✔
513
        } else {
514
            this.invalid = false;
102✔
515
        }
516
    }
517
}
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