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:
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
`,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user