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

IgniteUI / igniteui-angular / 12197035990

06 Dec 2024 10:18AM CUT coverage: 91.581% (-0.03%) from 91.613%
12197035990

Pull #15150

github

web-flow
Merge 01de31589 into 07d155736
Pull Request #15150: fix(*): icon service doesn't work with scoped themes

12977 of 15215 branches covered (85.29%)

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

3 existing lines in 2 files now uncovered.

26303 of 28721 relevant lines covered (91.58%)

33939.44 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
    standalone: true,
72
    imports: [IgxRippleDirective]
73
})
74
export class IgxCheckboxComponent implements EditorProvider, AfterViewInit, ControlValueAccessor {
2✔
75

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

588
    /**
589
     * @hidden
590
     * @internal
591
     */
592
    public get ariaChecked() {
593
        if (this.indeterminate) {
57,965✔
594
            return 'mixed';
1,044✔
595
        } else {
596
            return this.checked;
56,921✔
597
        }
598
    }
599

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

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

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

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

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

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

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

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

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

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