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

IgniteUI / igniteui-angular / 12237118419

09 Dec 2024 01:55PM CUT coverage: 91.585% (-0.03%) from 91.613%
12237118419

Pull #15150

github

web-flow
Merge 4d9d2b458 into 90b93b228
Pull Request #15150: fix(*): icon service doesn't work with scoped themes

12977 of 15214 branches covered (85.3%)

45 of 53 new or added lines in 5 files covered. (84.91%)

1 existing line in 1 file now uncovered.

26293 of 28709 relevant lines covered (91.58%)

33975.95 hits per line

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

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

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

33
export interface IChangeCheckboxEventArgs extends IBaseEventArgs {
34
    checked: boolean;
35
    value?: any;
36
}
37

38
let nextId = 0;
2✔
39
/**
40
 * Allows users to make a binary choice for a certain condition.
41
 *
42
 * @igxModule IgxCheckboxModule
43
 *
44
 * @igxTheme igx-checkbox-theme
45
 *
46
 * @igxKeywords checkbox, label
47
 *
48
 * @igxGroup Data entry and display
49
 *
50
 * @remarks
51
 * The Ignite UI Checkbox is a selection control that allows users to make a binary choice for a certain condition.It behaves similarly
52
 * to the native browser checkbox.
53
 *
54
 * @example
55
 * ```html
56
 * <igx-checkbox [checked]="true">
57
 *   simple checkbox
58
 * </igx-checkbox>
59
 * ```
60
 */
61
@Component({
62
    selector: 'igx-checkbox',
63
    providers: [{
64
        provide: EDITOR_PROVIDER,
65
        useExisting: IgxCheckboxComponent,
66
        multi: true
67
    }],
68
    preserveWhitespaces: false,
69
    templateUrl: 'checkbox.component.html',
70
    standalone: true,
71
    imports: [IgxRippleDirective]
72
})
73
export class IgxCheckboxComponent implements EditorProvider, AfterViewInit, ControlValueAccessor {
2✔
74

75
    /**
76
     * An event that is emitted after the checkbox state is changed.
77
     * Provides references to the `IgxCheckboxComponent` and the `checked` property as event arguments.
78
     */
79
    // eslint-disable-next-line @angular-eslint/no-output-native
80
    @Output() public readonly change: EventEmitter<IChangeCheckboxEventArgs> = new EventEmitter<IChangeCheckboxEventArgs>();
8,188✔
81

82
    /**
83
     * @hidden
84
     * @internal
85
     */
86
    public destroy$ = new Subject<boolean>();
8,188✔
87

88
    /**
89
     * Returns reference to the native checkbox element.
90
     *
91
     * @example
92
     * ```typescript
93
     * let checkboxElement =  this.component.checkboxElement;
94
     * ```
95
     */
96
    @ViewChild('checkbox', { static: true })
97
    public nativeInput: ElementRef;
98

99
    /**
100
     * Returns reference to the native label element.
101
     * ```typescript
102
     *
103
     * @example
104
     * let labelElement =  this.component.nativeLabel;
105
     * ```
106
     */
107
    @ViewChild('label', { static: true })
108
    public nativeLabel: ElementRef;
109

110
    /**
111
     * Returns reference to the `nativeElement` of the igx-checkbox/igx-switch.
112
     *
113
     * @example
114
     * ```typescript
115
     * let nativeElement = this.component.nativeElement;
116
     * ```
117
     */
118
    public get nativeElement() {
119
        return this.nativeInput.nativeElement;
116,514✔
120
    }
121

122
    /**
123
     * Returns reference to the label placeholder element.
124
     * ```typescript
125
     *
126
     * @example
127
     * let labelPlaceholder =  this.component.placeholderLabel;
128
     * ```
129
     */
130
    @ViewChild('placeholderLabel', { static: true })
131
    public placeholderLabel: ElementRef;
132

133
    /**
134
     * Sets/gets the `id` of the checkbox component.
135
     * If not set, the `id` of the first checkbox component will be `"igx-checkbox-0"`.
136
     *
137
     * @example
138
     * ```html
139
     * <igx-checkbox id="my-first-checkbox"></igx-checkbox>
140
     * ```
141
     * ```typescript
142
     * let checkboxId =  this.checkbox.id;
143
     * ```
144
     */
145
    @HostBinding('attr.id')
146
    @Input()
147
    public id = `igx-checkbox-${nextId++}`;
8,188✔
148

149
    /**
150
     * Sets/gets the id of the `label` element.
151
     * If not set, the id of the `label` in the first checkbox component will be `"igx-checkbox-0-label"`.
152
     *
153
     * @example
154
     * ```html
155
     * <igx-checkbox labelId="Label1"></igx-checkbox>
156
     * ```
157
     * ```typescript
158
     * let labelId =  this.component.labelId;
159
     * ```
160
     */
161
    @Input() public labelId = `${this.id}-label`;
8,188✔
162

163
    /**
164
     * Sets/gets the `value` attribute.
165
     *
166
     * @example
167
     * ```html
168
     * <igx-checkbox [value]="'CheckboxValue'"></igx-checkbox>
169
     * ```
170
     * ```typescript
171
     * let value =  this.checkbox.value;
172
     * ```
173
     */
174
    @Input() public value: any;
175

176
    /**
177
     * Sets/gets the `name` attribute.
178
     *
179
     * @example
180
     * ```html
181
     * <igx-checkbox name="Checkbox1"></igx-checkbox>
182
     * ```
183
     * ```typescript
184
     * let name =  this.checkbox.name;
185
     * ```
186
     */
187
    @Input() public name: string;
188

189
    /**
190
     * Sets/gets the value of the `tabindex` attribute.
191
     *
192
     * @example
193
     * ```html
194
     * <igx-checkbox [tabindex]="1"></igx-checkbox>
195
     * ```
196
     * ```typescript
197
     * let tabIndex =  this.checkbox.tabindex;
198
     * ```
199
     */
200
    @Input() public tabindex: number = null;
8,188✔
201

202
    /**
203
     *  Sets/gets the position of the `label`.
204
     *  If not set, the `labelPosition` will have value `"after"`.
205
     *
206
     * @example
207
     * ```html
208
     * <igx-checkbox labelPosition="before"></igx-checkbox>
209
     * ```
210
     * ```typescript
211
     * let labelPosition =  this.checkbox.labelPosition;
212
     * ```
213
     */
214
    @Input()
215
    public labelPosition: LabelPosition | string = LabelPosition.AFTER;
8,188✔
216

217
    /**
218
     * Enables/Disables the ripple effect.
219
     * If not set, `disableRipple` will have value `false`.
220
     *
221
     * @example
222
     * ```html
223
     * <igx-checkbox [disableRipple]="true"></igx-checkbox>
224
     * ```
225
     * ```typescript
226
     * let isRippleDisabled = this.checkbox.desableRipple;
227
     * ```
228
     */
229
    @Input({ transform: booleanAttribute })
230
    public disableRipple = false;
8,188✔
231

232
    /**
233
     * Sets/gets whether the checkbox is required.
234
     * If not set, `required` will have value `false`.
235
     *
236
     * @example
237
     * ```html
238
     * <igx-checkbox required></igx-checkbox>
239
     * ```
240
     * ```typescript
241
     * let isRequired = this.checkbox.required;
242
     * ```
243
     */
244
    @Input({ transform: booleanAttribute })
245
    public get required(): boolean {
246
        return this._required || this.nativeElement.hasAttribute('required');
116,662✔
247
    }
248
    public set required(value: boolean) {
249
        this._required = value;
60✔
250
    }
251

252
    /**
253
     * Sets/gets the `aria-labelledby` attribute.
254
     * If not set, the `aria-labelledby` will be equal to the value of `labelId` attribute.
255
     *
256
     * @example
257
     * ```html
258
     * <igx-checkbox aria-labelledby="Checkbox1"></igx-checkbox>
259
     * ```
260
     * ```typescript
261
     * let ariaLabelledBy = this.checkbox.ariaLabelledBy;
262
     * ```
263
     */
264
    @Input('aria-labelledby')
265
    public ariaLabelledBy = this.labelId;
8,188✔
266

267
    /**
268
     * Sets/gets the value of the `aria-label` attribute.
269
     *
270
     * @example
271
     * ```html
272
     * <igx-checkbox aria-label="Checkbox1"></igx-checkbox>
273
     * ```
274
     * ```typescript
275
     * let ariaLabel = this.checkbox.ariaLabel;
276
     * ```
277
     */
278
    @Input('aria-label')
279
    public ariaLabel: string | null = null;
8,188✔
280

281
    /**
282
     * Returns the class of the checkbox component.
283
     *
284
     * @example
285
     * ```typescript
286
     * let class = this.checkbox.cssClass;
287
     * ```
288
     */
289
    @HostBinding('class.igx-checkbox')
290
    public cssClass = 'igx-checkbox';
8,188✔
291

292
    /**
293
     * Returns if the component is of type `material`.
294
     *
295
     * @example
296
     * ```typescript
297
     * let checkbox = this.checkbox.material;
298
     * ```
299
     */
300
    @HostBinding('class.igx-checkbox--material')
301
    protected get material() {
302
        return this.theme === 'material';
58,314✔
303
    }
304

305
    /**
306
     * Returns if the component is of type `indigo`.
307
     *
308
     * @example
309
     * ```typescript
310
     * let checkbox = this.checkbox.indigo;
311
     * ```
312
     */
313
    @HostBinding('class.igx-checkbox--indigo')
314
    protected get indigo() {
315
        return this.theme === 'indigo';
58,314✔
316
    }
317

318
    /**
319
     * Returns if the component is of type `bootstrap`.
320
     *
321
     * @example
322
     * ```typescript
323
     * let checkbox = this.checkbox.bootstrap;
324
     * ```
325
     */
326
    @HostBinding('class.igx-checkbox--bootstrap')
327
    protected get bootstrap() {
328
        return this.theme === 'bootstrap';
58,314✔
329
    }
330

331
    /**
332
     * Returns if the component is of type `fluent`.
333
     *
334
     * @example
335
     * ```typescript
336
     * let checkbox = this.checkbox.fluent;
337
     * ```
338
     */
339
    @HostBinding('class.igx-checkbox--fluent')
340
    protected get fluent() {
341
        return this.theme === 'fluent';
58,314✔
342
    }
343

344
    /**
345
     * Sets/gets whether the checkbox component is on focus.
346
     * Default value is `false`.
347
     *
348
     * @example
349
     * ```typescript
350
     * this.checkbox.focused =  true;
351
     * ```
352
     * ```typescript
353
     * let isFocused = this.checkbox.focused;
354
     * ```
355
     */
356
    @HostBinding('class.igx-checkbox--focused')
357
    public focused = false;
8,188✔
358

359
    /**
360
     * Sets/gets the checkbox indeterminate visual state.
361
     * Default value is `false`;
362
     *
363
     * @example
364
     * ```html
365
     * <igx-checkbox [indeterminate]="true"></igx-checkbox>
366
     * ```
367
     * ```typescript
368
     * let isIndeterminate = this.checkbox.indeterminate;
369
     * ```
370
     */
371
    @HostBinding('class.igx-checkbox--indeterminate')
372
    @Input({ transform: booleanAttribute })
373
    public indeterminate = false;
8,188✔
374

375
    /**
376
     * Sets/gets whether the checkbox is checked.
377
     * Default value is `false`.
378
     *
379
     * @example
380
     * ```html
381
     * <igx-checkbox [checked]="true"></igx-checkbox>
382
     * ```
383
     * ```typescript
384
     * let isChecked =  this.checkbox.checked;
385
     * ```
386
     */
387
    @HostBinding('class.igx-checkbox--checked')
388
    @Input({ transform: booleanAttribute })
389
    public get checked() {
390
        return this._checked;
173,869✔
391
    }
392
    public set checked(value: boolean) {
393
        if (this._checked !== value) {
10,367✔
394
            this._checked = value;
4,047✔
395
            this._onChangeCallback(this._checked);
4,047✔
396
        }
397
    }
398

399
    /**
400
     * Sets/gets whether the checkbox is disabled.
401
     * Default value is `false`.
402
     *
403
     * @example
404
     * ```html
405
     * <igx-checkbox disabled></igx-checkbox>
406
     * ```
407
     * ```typescript
408
     * let isDisabled = this.checkbox.disabled;
409
     * ```
410
     */
411
    @HostBinding('class.igx-checkbox--disabled')
412
    @Input({ transform: booleanAttribute })
413
    public disabled = false;
8,188✔
414

415
    /**
416
     * Sets/gets whether the checkbox is invalid.
417
     * Default value is `false`.
418
     *
419
     * @example
420
     * ```html
421
     * <igx-checkbox invalid></igx-checkbox>
422
     * ```
423
     * ```typescript
424
     * let isInvalid = this.checkbox.invalid;
425
     * ```
426
     */
427
    @HostBinding('class.igx-checkbox--invalid')
428
    @Input({ transform: booleanAttribute })
429
    public invalid = false
8,188✔
430

431
    /**
432
     * Sets/gets whether the checkbox is readonly.
433
     * Default value is `false`.
434
     *
435
     * @example
436
     * ```html
437
     * <igx-checkbox [readonly]="true"></igx-checkbox>
438
     * ```
439
     * ```typescript
440
     * let readonly = this.checkbox.readonly;
441
     * ```
442
     */
443
    @Input({ transform: booleanAttribute })
444
    public readonly = false;
8,188✔
445

446
    /**
447
     * Sets/gets whether the checkbox should disable all css transitions.
448
     * Default value is `false`.
449
     *
450
     * @example
451
     * ```html
452
     * <igx-checkbox [disableTransitions]="true"></igx-checkbox>
453
     * ```
454
     * ```typescript
455
     * let disableTransitions = this.checkbox.disableTransitions;
456
     * ```
457
     */
458
    @HostBinding('class.igx-checkbox--plain')
459
    @Input({ transform: booleanAttribute })
460
    public disableTransitions = false;
8,188✔
461

462
    /**
463
     * @hidden
464
     * @internal
465
     */
466
    public inputId = `${this.id}-input`;
8,188✔
467

468
    /**
469
     * @hidden
470
     */
471
    protected _onChangeCallback: (_: any) => void = noop;
8,188✔
472

473
    /**
474
     * @hidden
475
     */
476
    private _onTouchedCallback: () => void = noop;
8,188✔
477

478
    /**
479
     * @hidden
480
     * @internal
481
     */
482
    protected _checked = false;
8,188✔
483

484
    /**
485
     * @hidden
486
     * @internal
487
     */
488
    protected theme: IgxTheme;
489

490
    /**
491
     * @hidden
492
     * @internal
493
     */
494
    private _required = false;
8,188✔
495
    private elRef = inject(ElementRef);
8,188✔
496

497
    constructor(
498
        protected cdr: ChangeDetectorRef,
8,188✔
499
        protected renderer: Renderer2,
8,188✔
500
        @Inject(THEME_TOKEN)
501
        protected themeToken: ThemeToken,
8,188✔
502
        @Optional() @Self() public ngControl: NgControl,
8,188✔
503
    ) {
504
        if (this.ngControl !== null) {
8,188✔
505
            this.ngControl.valueAccessor = this;
44✔
506
        }
507

508
        this.theme = this.themeToken.theme;
8,188✔
509

510
        this.themeToken.onChange((theme) => {
8,188✔
511
            if (this.theme !== theme) {
8,188!
NEW
512
                this.theme = theme;
×
NEW
513
                this.cdr.detectChanges();
×
514
            }
515
        });
516
    }
517

518
    private setComponentTheme() {
519
        if(!this.themeToken.preferToken) {
8,186✔
520
            const theme = getComponentTheme(this.elRef.nativeElement);
8,186✔
521

522
            if (theme && theme !== this.theme) {
8,186!
NEW
523
                this.themeToken.set(theme);
×
NEW
524
                this.cdr.markForCheck();
×
525
            }
526
        }
527
    }
528

529
    /**
530
     * @hidden
531
     * @internal
532
    */
533
    public ngAfterViewInit() {
534
        if (this.ngControl) {
8,186✔
535
            this.ngControl.statusChanges.pipe(takeUntil(this.destroy$)).subscribe(this.updateValidityState.bind(this));
44✔
536

537
            if (this.ngControl.control.validator || this.ngControl.control.asyncValidator) {
44✔
538
                this._required = this.ngControl?.control?.hasValidator(Validators.required);
9✔
539
                this.cdr.detectChanges();
9✔
540
            }
541
        }
542

543
        this.setComponentTheme();
8,186✔
544
    }
545

546
    /** @hidden @internal */
547
    @HostListener('keyup', ['$event'])
548
    public onKeyUp(event: KeyboardEvent) {
549
        event.stopPropagation();
14✔
550
        this.focused = true;
14✔
551
    }
552

553
    /** @hidden @internal */
554
    @HostListener('click', ['$event'])
555
    public _onCheckboxClick(event: PointerEvent | MouseEvent) {
556
        // Since the original checkbox is hidden and the label
557
        // is used for styling and to change the checked state of the checkbox,
558
        // we need to prevent the checkbox click event from bubbling up
559
        // as it gets triggered on label click
560
        // NOTE: The above is no longer valid, as the native checkbox is not labeled
561
        // by the SVG anymore.
562
        if (this.disabled || this.readonly) {
152✔
563
            // readonly prevents the component from changing state (see toggle() method).
564
            // However, the native checkbox can still be activated through user interaction (focus + space, label click)
565
            // Prevent the native change so the input remains in sync
566
            event.preventDefault();
71✔
567
            return;
71✔
568
        }
569

570
        this.nativeInput.nativeElement.focus();
81✔
571

572
        this.indeterminate = false;
81✔
573
        this.checked = !this.checked;
81✔
574
        this.updateValidityState();
81✔
575

576
        // K.D. March 23, 2021 Emitting on click and not on the setter because otherwise every component
577
        // bound on change would have to perform self checks for weather the value has changed because
578
        // of the initial set on initialization
579
        this.change.emit({ checked: this.checked, value: this.value, owner: this });
81✔
580
    }
581

582
    /**
583
     * @hidden
584
     * @internal
585
     */
586
    public get ariaChecked() {
587
        if (this.indeterminate) {
57,963✔
588
            return 'mixed';
1,044✔
589
        } else {
590
            return this.checked;
56,919✔
591
        }
592
    }
593

594
    /** @hidden @internal */
595
    public _onCheckboxChange(event: Event) {
596
        // We have to stop the original checkbox change event
597
        // from bubbling up since we emit our own change event
598
        event.stopPropagation();
60✔
599
    }
600

601
    /** @hidden @internal */
602
    @HostListener('blur')
603
    public onBlur() {
604
        this.focused = false;
51✔
605
        this._onTouchedCallback();
51✔
606
        this.updateValidityState();
51✔
607
    }
608

609
    /** @hidden @internal */
610
    public writeValue(value: boolean) {
611
        this._checked = value;
53✔
612
    }
613

614
    /** @hidden @internal */
615
    public get labelClass(): string {
616
        switch (this.labelPosition) {
58,323✔
617
            case LabelPosition.BEFORE:
618
                return `${this.cssClass}__label--before`;
6✔
619
            case LabelPosition.AFTER:
620
            default:
621
                return `${this.cssClass}__label`;
58,317✔
622
        }
623
    }
624

625
    /** @hidden @internal */
626
    public registerOnChange(fn: (_: any) => void) {
627
        this._onChangeCallback = fn;
66✔
628
    }
629

630
    /** @hidden @internal */
631
    public registerOnTouched(fn: () => void) {
632
        this._onTouchedCallback = fn;
80✔
633
    }
634

635
    /** @hidden @internal */
636
    public setDisabledState(isDisabled: boolean) {
637
        this.disabled = isDisabled;
45✔
638
    }
639

640
    /** @hidden @internal */
641
    public getEditElement() {
642
        return this.nativeInput.nativeElement;
24✔
643
    }
644

645
    /**
646
     * @hidden
647
     * @internal
648
     */
649
    protected updateValidityState() {
650
        if (this.ngControl) {
187✔
651
            if (!this.disabled && !this.readonly &&
77✔
652
                (this.ngControl.control.touched || this.ngControl.control.dirty)) {
653
                // the control is not disabled and is touched or dirty
654
                this.invalid = this.ngControl.invalid;
42✔
655
            } else {
656
                //  if the control is untouched, pristine, or disabled, its state is initial. This is when the user did not interact
657
                //  with the checkbox or when the form/control is reset
658
                this.invalid = false;
35✔
659
            }
660
        } else {
661
            this.checkNativeValidity();
110✔
662
        }
663
    }
664

665
    /**
666
     * A function to assign a native validity property of a checkbox.
667
     * This should be used when there's no ngControl
668
     *
669
     * @hidden
670
     * @internal
671
     */
672
    private checkNativeValidity() {
673
        if (!this.disabled && this._required && !this.checked && !this.readonly) {
110✔
674
            this.invalid = true;
8✔
675
        } else {
676
            this.invalid = false;
102✔
677
        }
678
    }
679
}
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