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

IgniteUI / igniteui-angular / 12166030900

04 Dec 2024 06:28PM UTC coverage: 91.601% (-0.02%) from 91.625%
12166030900

Pull #15140

github

web-flow
Merge 9322a9f27 into 47b2886a0
Pull Request #15140: fix(*): icon service doesn't work with scoped themes

12984 of 15223 branches covered (85.29%)

56 of 64 new or added lines in 5 files covered. (87.5%)

4 existing lines in 3 files now uncovered.

26326 of 28740 relevant lines covered (91.6%)

33986.03 hits per line

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

95.79
/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
    DestroyRef,
18
    Inject
19
} from '@angular/core';
20
import { ControlValueAccessor, NgControl, Validators } from '@angular/forms';
21
import { IgxRippleDirective } from '../directives/ripple/ripple.directive';
22
import { IBaseEventArgs, getComponentTheme, mkenum } from '../core/utils';
23
import { EditorProvider, EDITOR_PROVIDER } from '../core/edit-provider';
24
import { noop, Subject } from 'rxjs';
25
import { takeUntil } from 'rxjs/operators';
26
import { IgxTheme, THEME_TOKEN, ThemeToken } from '../services/theme/theme.token';
27

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

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

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

82
    /**
83
     * @hidden
84
     * @internal
85
     */
86
    public destroy$ = new Subject<boolean>();
8,189✔
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,708✔
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,189✔
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,189✔
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,189✔
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,189✔
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,189✔
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,856✔
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,189✔
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,189✔
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,189✔
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,411✔
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,411✔
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,411✔
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,411✔
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,189✔
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,189✔
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;
174,158✔
391
    }
392
    public set checked(value: boolean) {
393
        if (this._checked !== value) {
10,368✔
394
            this._checked = value;
4,048✔
395
            this._onChangeCallback(this._checked);
4,048✔
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,189✔
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,189✔
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,189✔
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,189✔
461

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

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

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

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

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

490
    /**
491
     * @hidden
492
     * @internal
493
     */
494
    private _required = false;
8,189✔
495
    private _prefersTokenizedTheme = false;
8,189✔
496
    private elRef = inject(ElementRef);
8,189✔
497
    private destroyRef = inject(DestroyRef);
8,189✔
498

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

510
        this.theme = this.themeToken.theme;
8,189✔
511
        this._prefersTokenizedTheme = this.themeToken.preferToken;
8,189✔
512

513
        const { unsubscribe } = this.themeToken.onChange((theme) => {
8,189✔
514
            if (this.theme !== theme) {
8,189!
NEW
515
                this.theme = theme;
×
NEW
516
                this.cdr.detectChanges();
×
517
            }
518
        });
519

520
        this.destroyRef.onDestroy(() => unsubscribe);
8,189✔
521
    }
522

523
    private setComponentTheme() {
524
        if(!this._prefersTokenizedTheme) {
8,187✔
525
            const theme = getComponentTheme(this.elRef.nativeElement);
8,187✔
526

527
            if (theme && theme !== this.theme) {
8,187!
NEW
528
                this.themeToken.set(theme);
×
NEW
529
                this.cdr.markForCheck();
×
530
            }
531
        }
532
    }
533

534
    /**
535
     * @hidden
536
     * @internal
537
    */
538
    public ngAfterViewInit() {
539
        if (this.ngControl) {
8,187✔
540
            this.ngControl.statusChanges.pipe(takeUntil(this.destroy$)).subscribe(this.updateValidityState.bind(this));
44✔
541

542
            if (this.ngControl.control.validator || this.ngControl.control.asyncValidator) {
44✔
543
                this._required = this.ngControl?.control?.hasValidator(Validators.required);
9✔
544
                this.cdr.detectChanges();
9✔
545
            }
546
        }
547

548
        this.setComponentTheme();
8,187✔
549
    }
550

551
    /** @hidden @internal */
552
    @HostListener('keyup', ['$event'])
553
    public onKeyUp(event: KeyboardEvent) {
554
        event.stopPropagation();
14✔
555
        this.focused = true;
14✔
556
    }
557

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

575
        this.nativeInput.nativeElement.focus();
81✔
576

577
        this.indeterminate = false;
81✔
578
        this.checked = !this.checked;
81✔
579
        this.updateValidityState();
81✔
580

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

587
    /**
588
     * @hidden
589
     * @internal
590
     */
591
    public get ariaChecked() {
592
        if (this.indeterminate) {
58,060✔
593
            return 'mixed';
1,046✔
594
        } else {
595
            return this.checked;
57,014✔
596
        }
597
    }
598

599
    /** @hidden @internal */
600
    public _onCheckboxChange(event: Event) {
601
        // We have to stop the original checkbox change event
602
        // from bubbling up since we emit our own change event
603
        event.stopPropagation();
60✔
604
    }
605

606
    /** @hidden @internal */
607
    @HostListener('blur')
608
    public onBlur() {
609
        this.focused = false;
51✔
610
        this._onTouchedCallback();
51✔
611
        this.updateValidityState();
51✔
612
    }
613

614
    /** @hidden @internal */
615
    public writeValue(value: boolean) {
616
        this._checked = value;
53✔
617
    }
618

619
    /** @hidden @internal */
620
    public get labelClass(): string {
621
        switch (this.labelPosition) {
58,420✔
622
            case LabelPosition.BEFORE:
623
                return `${this.cssClass}__label--before`;
6✔
624
            case LabelPosition.AFTER:
625
            default:
626
                return `${this.cssClass}__label`;
58,414✔
627
        }
628
    }
629

630
    /** @hidden @internal */
631
    public registerOnChange(fn: (_: any) => void) {
632
        this._onChangeCallback = fn;
66✔
633
    }
634

635
    /** @hidden @internal */
636
    public registerOnTouched(fn: () => void) {
637
        this._onTouchedCallback = fn;
80✔
638
    }
639

640
    /** @hidden @internal */
641
    public setDisabledState(isDisabled: boolean) {
642
        this.disabled = isDisabled;
45✔
643
    }
644

645
    /** @hidden @internal */
646
    public getEditElement() {
647
        return this.nativeInput.nativeElement;
26✔
648
    }
649

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

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

© 2026 Coveralls, Inc