1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-20 10:13:31 +00:00

[CL-707] Migrate CL codebase to signals (#15340)

This commit is contained in:
Vicki League
2025-07-16 08:39:37 -04:00
committed by GitHub
parent 97ec9a6339
commit 6811ea4c0b
124 changed files with 944 additions and 809 deletions

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, Input } from "@angular/core";
import { Component, input } from "@angular/core";
import { AbstractControl, UntypedFormGroup } from "@angular/forms";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -18,11 +18,10 @@ import { I18nPipe } from "@bitwarden/ui-common";
imports: [I18nPipe],
})
export class BitErrorSummary {
@Input()
formGroup: UntypedFormGroup;
readonly formGroup = input<UntypedFormGroup>();
get errorCount(): number {
return this.getErrorCount(this.formGroup);
return this.getErrorCount(this.formGroup());
}
get errorString() {

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, HostBinding, Input } from "@angular/core";
import { Component, HostBinding, input } from "@angular/core";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -18,37 +18,38 @@ let nextId = 0;
export class BitErrorComponent {
@HostBinding() id = `bit-error-${nextId++}`;
@Input() error: [string, any];
readonly error = input<[string, any]>();
constructor(private i18nService: I18nService) {}
get displayError() {
switch (this.error[0]) {
const error = this.error();
switch (error[0]) {
case "required":
return this.i18nService.t("inputRequired");
case "email":
return this.i18nService.t("inputEmail");
case "minlength":
return this.i18nService.t("inputMinLength", this.error[1]?.requiredLength);
return this.i18nService.t("inputMinLength", error[1]?.requiredLength);
case "maxlength":
return this.i18nService.t("inputMaxLength", this.error[1]?.requiredLength);
return this.i18nService.t("inputMaxLength", error[1]?.requiredLength);
case "min":
return this.i18nService.t("inputMinValue", this.error[1]?.min);
return this.i18nService.t("inputMinValue", error[1]?.min);
case "max":
return this.i18nService.t("inputMaxValue", this.error[1]?.max);
return this.i18nService.t("inputMaxValue", error[1]?.max);
case "forbiddenCharacters":
return this.i18nService.t("inputForbiddenCharacters", this.error[1]?.characters.join(", "));
return this.i18nService.t("inputForbiddenCharacters", error[1]?.characters.join(", "));
case "multipleEmails":
return this.i18nService.t("multipleInputEmails");
case "trim":
return this.i18nService.t("inputTrimValidator");
default:
// Attempt to show a custom error message.
if (this.error[1]?.message) {
return this.error[1]?.message;
if (error[1]?.message) {
return error[1]?.message;
}
return this.error;
return error;
}
}
}

View File

@@ -1,4 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line
import { ModelSignal, Signal } from "@angular/core";
// @ts-strict-ignore
export type InputTypes =
| "text"
@@ -14,13 +17,13 @@ export type InputTypes =
export abstract class BitFormFieldControl {
ariaDescribedBy: string;
id: string;
id: Signal<string>;
labelForId: string;
required: boolean;
hasError: boolean;
error: [string, any];
type?: InputTypes;
spellcheck?: boolean;
type?: ModelSignal<InputTypes>;
spellcheck?: ModelSignal<boolean | undefined>;
readOnly?: boolean;
focus?: () => void;
}

View File

@@ -9,9 +9,10 @@ import {
ElementRef,
HostBinding,
HostListener,
Input,
ViewChild,
signal,
input,
Input,
} from "@angular/core";
import { I18nPipe } from "@bitwarden/ui-common";
@@ -38,10 +39,11 @@ export class BitFormFieldComponent implements AfterContentChecked {
@ViewChild(BitErrorComponent) error: BitErrorComponent;
@Input({ transform: booleanAttribute })
disableMargin = false;
readonly disableMargin = input(false, { transform: booleanAttribute });
/** If `true`, remove the bottom border for `readonly` inputs */
// TODO: Skipped for signal migration because:
// Your application code writes to the input. This prevents migration.
@Input({ transform: booleanAttribute })
disableReadOnlyBorder = false;
@@ -76,7 +78,7 @@ export class BitFormFieldComponent implements AfterContentChecked {
@HostBinding("class")
get classList() {
return ["tw-block"]
.concat(this.disableMargin ? [] : ["tw-mb-4", "bit-compact:tw-mb-3"])
.concat(this.disableMargin() ? [] : ["tw-mb-4", "bit-compact:tw-mb-3"])
.concat(this.readOnly ? [] : "tw-pt-2");
}

View File

@@ -414,13 +414,18 @@ export const Select: Story = {
export const AdvancedSelect: Story = {
render: (args) => ({
props: args,
props: {
formObj: fb.group({
select: "value1",
}),
...args,
},
template: /*html*/ `
<bit-form-field>
<bit-form-field [formGroup]="formObj">
<bit-label>Label</bit-label>
<bit-select>
<bit-option label="Select"></bit-option>
<bit-option label="Other"></bit-option>
<bit-select formControlName="select">
<bit-option label="Select" value="value1"></bit-option>
<bit-option label="Other" value="value2"></bit-option>
</bit-select>
</bit-form-field>
`,

View File

@@ -5,7 +5,7 @@ import {
Host,
HostBinding,
HostListener,
Input,
model,
OnChanges,
Output,
} from "@angular/core";
@@ -18,12 +18,15 @@ import { BitFormFieldComponent } from "./form-field.component";
@Directive({
selector: "[bitPasswordInputToggle]",
host: {
"[attr.aria-pressed]": "toggled()",
},
})
export class BitPasswordInputToggleDirective implements AfterContentInit, OnChanges {
/**
* Whether the input is toggled to show the password.
*/
@HostBinding("attr.aria-pressed") @Input() toggled = false;
readonly toggled = model(false);
@Output() toggledChange = new EventEmitter<boolean>();
@HostBinding("attr.title") title = this.i18nService.t("toggleVisibility");
@@ -33,8 +36,8 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan
* Click handler to toggle the state of the input type.
*/
@HostListener("click") onClick() {
this.toggled = !this.toggled;
this.toggledChange.emit(this.toggled);
this.toggled.update((toggled) => !toggled);
this.toggledChange.emit(this.toggled());
this.update();
}
@@ -46,7 +49,7 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan
) {}
get icon() {
return this.toggled ? "bwi-eye-slash" : "bwi-eye";
return this.toggled() ? "bwi-eye-slash" : "bwi-eye";
}
ngOnChanges(): void {
@@ -55,16 +58,16 @@ export class BitPasswordInputToggleDirective implements AfterContentInit, OnChan
ngAfterContentInit(): void {
if (this.formField.input?.type) {
this.toggled = this.formField.input.type !== "password";
this.toggled.set(this.formField.input.type() !== "password");
}
this.button.icon = this.icon;
this.button.icon.set(this.icon);
}
private update() {
this.button.icon = this.icon;
this.button.icon.set(this.icon);
if (this.formField.input?.type != null) {
this.formField.input.type = this.toggled ? "text" : "password";
this.formField.input.spellcheck = this.toggled ? false : undefined;
this.formField.input.type.set(this.toggled() ? "text" : "password");
this.formField?.input?.spellcheck?.set(this.toggled() ? false : undefined);
}
}
}

View File

@@ -60,15 +60,15 @@ describe("PasswordInputToggle", () => {
describe("initial state", () => {
it("has correct icon", () => {
expect(button.icon).toBe("bwi-eye");
expect(button.icon()).toBe("bwi-eye");
});
it("input is type password", () => {
expect(input.type).toBe("password");
expect(input.type!()).toBe("password");
});
it("spellcheck is disabled", () => {
expect(input.spellcheck).toBe(undefined);
expect(input.spellcheck!()).toBe(undefined);
});
});
@@ -78,15 +78,15 @@ describe("PasswordInputToggle", () => {
});
it("has correct icon", () => {
expect(button.icon).toBe("bwi-eye-slash");
expect(button.icon()).toBe("bwi-eye-slash");
});
it("input is type text", () => {
expect(input.type).toBe("text");
expect(input.type!()).toBe("text");
});
it("spellcheck is disabled", () => {
expect(input.spellcheck).toBe(false);
expect(input.spellcheck!()).toBe(false);
});
});
@@ -97,15 +97,15 @@ describe("PasswordInputToggle", () => {
});
it("has correct icon", () => {
expect(button.icon).toBe("bwi-eye");
expect(button.icon()).toBe("bwi-eye");
});
it("input is type password", () => {
expect(input.type).toBe("password");
expect(input.type!()).toBe("password");
});
it("spellcheck is disabled", () => {
expect(input.spellcheck).toBe(undefined);
expect(input.spellcheck!()).toBe(undefined);
});
});
});

View File

@@ -1,20 +1,21 @@
import { Directive, HostBinding, Input, OnInit, Optional } from "@angular/core";
import { Directive, OnInit, Optional } from "@angular/core";
import { BitIconButtonComponent } from "../icon-button/icon-button.component";
@Directive({
selector: "[bitPrefix]",
host: {
"[class]": "classList",
},
})
export class BitPrefixDirective implements OnInit {
@HostBinding("class") @Input() get classList() {
return ["tw-text-muted"];
}
readonly classList = ["tw-text-muted"];
constructor(@Optional() private iconButtonComponent: BitIconButtonComponent) {}
ngOnInit() {
if (this.iconButtonComponent) {
this.iconButtonComponent.size = "small";
this.iconButtonComponent.size.set("small");
}
}
}

View File

@@ -1,20 +1,21 @@
import { Directive, HostBinding, Input, OnInit, Optional } from "@angular/core";
import { Directive, OnInit, Optional } from "@angular/core";
import { BitIconButtonComponent } from "../icon-button/icon-button.component";
@Directive({
selector: "[bitSuffix]",
host: {
"[class]": "classList",
},
})
export class BitSuffixDirective implements OnInit {
@HostBinding("class") @Input() get classList() {
return ["tw-text-muted"];
}
readonly classList = ["tw-text-muted"];
constructor(@Optional() private iconButtonComponent: BitIconButtonComponent) {}
ngOnInit() {
if (this.iconButtonComponent) {
this.iconButtonComponent.size = "small";
this.iconButtonComponent.size.set("small");
}
}
}