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

IgniteUI / igniteui-angular / 12932283638

23 Jan 2025 03:17PM CUT coverage: 91.645%. First build
12932283638

Pull #15298

github

web-flow
Merge 6d137128b into 69877ca54
Pull Request #15298: feat(checkbox): extract common logic in a directive

12977 of 15202 branches covered (85.36%)

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

26348 of 28750 relevant lines covered (91.65%)

34318.79 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
    selector: '[appCheckboxBase]'
45
})
46
export class CheckboxBaseDirective implements AfterViewInit {
2✔
47
    /**
48
     * An event that is emitted after the checkbox state is changed.
49
     * Provides references to the `IgxCheckboxComponent` and the `checked` property as event arguments.
50
     */
51
    // eslint-disable-next-line @angular-eslint/no-output-native
52
    @Output() public readonly change: EventEmitter<IChangeCheckboxEventArgs> =
8,174✔
53
        new EventEmitter<IChangeCheckboxEventArgs>();
54

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

264
        this.theme = this.themeToken.theme;
8,174✔
265

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

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

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

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

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

324
        this.setComponentTheme();
8,172✔
325
    }
326

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

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

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

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

349
    /**
350
     * @hidden
351
     * @internal
352
     */
353
    protected theme: IgxTheme;
354

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

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

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

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

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

398
        this.nativeInput.nativeElement.focus();
81✔
399

400
        this.indeterminate = false;
81✔
401
        this.checked = !this.checked;
81✔
402
        this.updateValidityState();
81✔
403

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

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

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

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

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

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

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

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

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

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

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

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