Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(checkbox): migration to signals #474

Merged
merged 2 commits into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, Input } from '@angular/core';
import { Component, input, model } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { BrnCheckboxComponent } from './brn-checkbox.component';

Expand All @@ -7,15 +7,14 @@ import { BrnCheckboxComponent } from './brn-checkbox.component';
standalone: true,
template: `
<label>
Airplane mode is: {{ airplaneMode ? 'on' : 'off' }}
<brn-checkbox [disabled]="disabled" [(ngModel)]="airplaneMode"></brn-checkbox>
Airplane mode is: {{ airplaneMode() ? 'on' : 'off' }}
<brn-checkbox [disabled]="disabled()" [(ngModel)]="airplaneMode"></brn-checkbox>
</label>
`,
imports: [BrnCheckboxComponent, FormsModule],
})
export class BrnCheckboxNgModelSpecComponent {
@Input()
public disabled = false;
@Input()
public airplaneMode = false;
public readonly disabled = input(false);

public readonly airplaneMode = model(false);
}
14 changes: 7 additions & 7 deletions libs/ui/checkbox/brain/src/lib/brn-checkbox-ng-model.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,40 +24,40 @@ describe('BrnCheckboxComponentNgModelIntegration', () => {
expect(labelElement).toBeInTheDocument();
await user.click(labelElement);
await screen.findByDisplayValue('on');
expect(container.fixture.componentInstance.airplaneMode).toBe(true);
expect(container.fixture.componentInstance.airplaneMode()).toBe(true);
});

it('should set input as default correctly and click should toggle then', async () => {
const { labelElement, user, container } = await setup(true);

await user.click(labelElement);
await screen.findByDisplayValue('off');
expect(container.fixture.componentInstance.airplaneMode).toBe(false);
expect(container.fixture.componentInstance.airplaneMode()).toBe(false);

await user.click(labelElement);
await screen.findByDisplayValue('on');
expect(container.fixture.componentInstance.airplaneMode).toBe(true);
expect(container.fixture.componentInstance.airplaneMode()).toBe(true);
});

it('should set input as default correctly and enter should toggle then', async () => {
const { user, container } = await setup(true);

await user.keyboard('[Tab][Enter]');
expect(container.fixture.componentInstance.airplaneMode).toBe(false);
expect(container.fixture.componentInstance.airplaneMode()).toBe(false);

await user.keyboard('[Enter]');
expect(container.fixture.componentInstance.airplaneMode).toBe(true);
expect(container.fixture.componentInstance.airplaneMode()).toBe(true);
});

it('should do nothing when disabled', async () => {
const { labelElement, user, container } = await setup(false, true);

await user.click(labelElement);
await screen.findByDisplayValue('off');
expect(container.fixture.componentInstance.airplaneMode).toBe(false);
expect(container.fixture.componentInstance.airplaneMode()).toBe(false);

await user.click(labelElement);
await screen.findByDisplayValue('off');
expect(container.fixture.componentInstance.airplaneMode).toBe(false);
expect(container.fixture.componentInstance.airplaneMode()).toBe(false);
});
});
49 changes: 19 additions & 30 deletions libs/ui/checkbox/brain/src/lib/brn-checkbox.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,10 @@ import {
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
HostListener,
type OnDestroy,
Output,
PLATFORM_ID,
Renderer2,
ViewChild,
ViewEncapsulation,
booleanAttribute,
computed,
Expand All @@ -20,7 +17,9 @@ import {
inject,
input,
model,
output,
signal,
viewChild,
} from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { ChangeFn, TouchFn } from '@spartan-ng/ui-forms-brain';
Expand Down Expand Up @@ -73,11 +72,11 @@ const CONTAINER_POST_FIX = '-checkbox';
<ng-content />
`,
host: {
'[attr.tabindex]': '_disabled() ? "-1" : "0"',
'[attr.tabindex]': 'state().disabled() ? "-1" : "0"',
'[attr.data-state]': '_dataState()',
'[attr.data-focus-visible]': 'focusVisible()',
'[attr.data-focus]': 'focused()',
'[attr.data-disabled]': '_disabled()',
'[attr.data-disabled]': 'state().disabled()',
'[attr.aria-labelledby]': 'null',
'[attr.aria-label]': 'null',
'[attr.aria-describedby]': 'null',
Expand Down Expand Up @@ -136,51 +135,43 @@ export class BrnCheckboxComponent implements AfterContentInit, OnDestroy {

public readonly required = input(false, { transform: booleanAttribute });

private readonly _disabled = signal(false);
/** Only used as input */
public readonly disabled = input(false, { transform: booleanAttribute });

protected readonly state = computed(() => ({
disabled: signal(this.disabled()),
}));

// eslint-disable-next-line @typescript-eslint/no-empty-function
protected _onChange: ChangeFn<BrnCheckboxValue> = () => {};
// eslint-disable-next-line @typescript-eslint/no-empty-function
private _onTouched: TouchFn = () => {};

@ViewChild('checkBox', { static: true })
public checkbox?: ElementRef<HTMLInputElement>;
public readonly checkbox = viewChild.required<ElementRef<HTMLInputElement>>('checkBox');

@Output()
public readonly changed = new EventEmitter<BrnCheckboxValue>();
public readonly changed = output<BrnCheckboxValue>();

constructor() {
effect(() => {
const parent = this._renderer.parentNode(this._elementRef.nativeElement);
if (!parent) return;
// check if parent is a label and assume it is for this checkbox
if (parent?.tagName === 'LABEL') {
this._renderer.setAttribute(parent, 'data-disabled', this._disabled() ? 'true' : 'false');
this._renderer.setAttribute(parent, 'data-disabled', this.state().disabled() ? 'true' : 'false');
return;
}
if (!this._isBrowser) return;

const label = parent?.querySelector(`label[for="${this.id()}"]`);
if (!label) return;
this._renderer.setAttribute(label, 'data-disabled', this._disabled() ? 'true' : 'false');
this._renderer.setAttribute(label, 'data-disabled', this.state().disabled() ? 'true' : 'false');
});

effect(
() => {
// sync disabled input
this._disabled.set(this.disabled());
},
{ allowSignalWrites: true },
);
}

@HostListener('click', ['$event'])
@HostListener('keyup.space', ['$event'])
@HostListener('keyup.enter', ['$event'])
toggle(event: Event) {
if (this._disabled()) return;
if (this.state().disabled()) return;
event.preventDefault();
const previousChecked = this.checked();
this.checked.set(previousChecked === 'indeterminate' ? true : !previousChecked);
Expand Down Expand Up @@ -208,15 +199,13 @@ export class BrnCheckboxComponent implements AfterContentInit, OnDestroy {
}
});

if (!this.checkbox) return;

this.checkbox.nativeElement.indeterminate = this.checked() === 'indeterminate';
if (this.checkbox.nativeElement.indeterminate) {
this.checkbox.nativeElement.value = 'indeterminate';
this.checkbox().nativeElement.indeterminate = this.checked() === 'indeterminate';
if (this.checkbox().nativeElement.indeterminate) {
this.checkbox().nativeElement.value = 'indeterminate';
} else {
this.checkbox.nativeElement.value = this.checked() ? 'on' : 'off';
this.checkbox().nativeElement.value = this.checked() ? 'on' : 'off';
}
this.checkbox.nativeElement.dispatchEvent(new Event('change'));
this.checkbox().nativeElement.dispatchEvent(new Event('change'));
}

ngOnDestroy() {
Expand All @@ -241,7 +230,7 @@ export class BrnCheckboxComponent implements AfterContentInit, OnDestroy {

/** Implemented as a part of ControlValueAccessor. */
setDisabledState(isDisabled: boolean): void {
this._disabled.set(isDisabled);
this.state().disabled.set(isDisabled);
}

/**
Expand Down
36 changes: 10 additions & 26 deletions libs/ui/checkbox/helm/src/lib/hlm-checkbox.component.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,4 @@
import {
Component,
EventEmitter,
Output,
booleanAttribute,
computed,
effect,
forwardRef,
input,
model,
signal,
} from '@angular/core';
import { Component, booleanAttribute, computed, forwardRef, input, model, output, signal } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { BrnCheckboxComponent } from '@spartan-ng/ui-checkbox-brain';
import { hlm } from '@spartan-ng/ui-core';
Expand All @@ -32,7 +21,7 @@ export const HLM_CHECKBOX_VALUE_ACCESSOR = {
[name]="name()"
[class]="_computedClass()"
[checked]="checked()"
[disabled]="_disabled()"
[disabled]="state().disabled()"
[required]="required()"
[aria-label]="ariaLabel()"
[aria-labelledby]="ariaLabelledby()"
Expand All @@ -59,7 +48,7 @@ export class HlmCheckboxComponent {
'group inline-flex border border-foreground shrink-0 cursor-pointer items-center rounded-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring' +
' focus-visible:ring-offset-2 focus-visible:ring-offset-background data-[state=checked]:text-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-background',
this.userClass(),
this._disabled() ? 'cursor-not-allowed opacity-50' : '',
this.state().disabled() ? 'cursor-not-allowed opacity-50' : '',
),
);

Expand All @@ -80,25 +69,20 @@ export class HlmCheckboxComponent {
public readonly name = input<string | null>(null);
public readonly required = input(false, { transform: booleanAttribute });

protected readonly _disabled = signal(false);
public readonly disabled = input(false, { transform: booleanAttribute });

private disableInput = effect(
() => {
this._disabled.set(this.disabled());
},
{ allowSignalWrites: true },
);
protected readonly state = computed(() => ({
disabled: signal(this.disabled()),
}));

// icon inputs
public readonly checkIconName = input<string>('lucideCheck');
public readonly checkIconClass = input<string>('');
public readonly checkIconClass = input<ClassValue>('');

@Output()
public changed = new EventEmitter<boolean>();
public readonly changed = output<boolean>();

protected _handleChange(): void {
if (this._disabled()) return;
if (this.state().disabled()) return;

const previousChecked = this.checked();
this.checked.set(previousChecked === 'indeterminate' ? true : !previousChecked);
Expand Down Expand Up @@ -128,6 +112,6 @@ export class HlmCheckboxComponent {
}

setDisabledState(isDisabled: boolean): void {
this._disabled.set(isDisabled);
this.state().disabled.set(isDisabled);
}
}