Skip to content
Merged
1 change: 1 addition & 0 deletions examples/shadcn/src/components/email-link-auth-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function EmailLinkAuthForm(props: EmailLinkAuthFormProps) {

const form = useForm<EmailLinkAuthFormSchema>({
resolver: standardSchemaResolver(schema),
mode: "onChange",
defaultValues: {
email: "",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function ForgotPasswordAuthForm(props: ForgotPasswordAuthFormProps) {

const form = useForm<ForgotPasswordAuthFormSchema>({
resolver: standardSchemaResolver(schema),
mode: "onChange",
defaultValues: {
email: "",
},
Expand Down
2 changes: 2 additions & 0 deletions examples/shadcn/src/components/phone-auth-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ function VerifyPhoneNumberForm(props: VerifyPhoneNumberFormProps) {

const form = useForm<PhoneAuthVerifyFormSchema>({
resolver: standardSchemaResolver(schema),
mode: "onChange",
defaultValues: {
verificationId: props.verificationId,
verificationCode: "",
Expand Down Expand Up @@ -106,6 +107,7 @@ function PhoneNumberForm(props: PhoneNumberFormProps) {

const form = useForm<PhoneAuthNumberFormSchema>({
resolver: standardSchemaResolver(schema),
mode: "onChange",
defaultValues: {
phoneNumber: "",
},
Expand Down
1 change: 1 addition & 0 deletions examples/shadcn/src/components/sign-in-auth-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export function SignInAuthForm(props: SignInAuthFormProps) {

const form = useForm<SignInAuthFormSchema>({
resolver: standardSchemaResolver(schema),
mode: "onChange",
defaultValues: {
email: "",
password: "",
Expand Down
1 change: 1 addition & 0 deletions examples/shadcn/src/components/sign-up-auth-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export function SignUpAuthForm(props: SignUpAuthFormProps) {

const form = useForm<SignUpAuthFormSchema>({
resolver: standardSchemaResolver(schema),
mode: "onChange",
defaultValues: {
email: "",
password: "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ function SmsMultiFactorAssertionVerifyForm(props: SmsMultiFactorAssertionVerifyF

const form = useForm<{ verificationId: string; verificationCode: string }>({
resolver: standardSchemaResolver(schema),
mode: "onChange",
defaultValues: {
verificationId: props.verificationId,
verificationCode: "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ function MultiFactorEnrollmentPhoneNumberForm(props: MultiFactorEnrollmentPhoneN

const form = useForm<{ displayName: string; phoneNumber: string }>({
resolver: standardSchemaResolver(schema),
mode: "onChange",
defaultValues: {
displayName: "",
phoneNumber: "",
Expand Down Expand Up @@ -109,6 +110,7 @@ export function MultiFactorEnrollmentVerifyPhoneNumberForm(props: MultiFactorEnr

const form = useForm<{ verificationId: string; verificationCode: string }>({
resolver: standardSchemaResolver(schema),
mode: "onChange",
defaultValues: {
verificationId: props.verificationId,
verificationCode: "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function TotpMultiFactorAssertionForm(props: TotpMultiFactorAssertionForm

const form = useForm<{ verificationCode: string }>({
resolver: standardSchemaResolver(schema),
mode: "onChange",
defaultValues: {
verificationCode: "",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ function TotpMultiFactorSecretGenerationForm(props: TotpMultiFactorSecretGenerat

const form = useForm<{ displayName: string }>({
resolver: standardSchemaResolver(schema),
mode: "onChange",
defaultValues: {
displayName: "",
},
Expand Down Expand Up @@ -84,6 +85,7 @@ export function MultiFactorEnrollmentVerifyTotpForm(props: MultiFactorEnrollment

const form = useForm<{ verificationCode: string }>({
resolver: standardSchemaResolver(schema),
mode: "onChange",
defaultValues: {
verificationCode: "",
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export class EmailLinkAuthFormComponent {
effect(() => {
this.form.update({
validators: {
onBlur: this.formSchema(),
onChange: this.formSchema(),
onSubmitAsync: async ({ value }) => {
try {
await sendSignInLinkToEmail(this.ui(), value.email);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export class ForgotPasswordAuthFormComponent {
effect(() => {
this.form.update({
validators: {
onBlur: this.formSchema(),
onChange: this.formSchema(),
onSubmitAsync: async ({ value }) => {
try {
await sendPasswordResetEmail(this.ui(), value.email);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ export class SmsMultiFactorAssertionVerifyFormComponent {
effect(() => {
this.form.update({
validators: {
onBlur: this.formSchema(),
onChange: this.formSchema(),
onSubmit: this.formSchema(),
onSubmitAsync: async ({ value }) => {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export class SmsMultiFactorEnrollmentFormComponent {
effect(() => {
this.phoneForm.update({
validators: {
onBlur: this.phoneFormSchema(),
onChange: this.phoneFormSchema(),
onSubmit: this.phoneFormSchema(),
onSubmitAsync: async ({ value }) => {
try {
Expand Down Expand Up @@ -192,7 +192,7 @@ export class SmsMultiFactorEnrollmentFormComponent {
effect(() => {
this.verificationForm.update({
validators: {
onBlur: this.verificationFormSchema(),
onChange: this.verificationFormSchema(),
onSubmit: this.verificationFormSchema(),
onSubmitAsync: async ({ value }) => {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export class TotpMultiFactorAssertionFormComponent {
effect(() => {
this.form.update({
validators: {
onBlur: this.formSchema(),
onChange: this.formSchema(),
onSubmitAsync: async ({ value }) => {
try {
const assertion = TotpMultiFactorGenerator.assertionForSignIn(this.hint().uid, value.verificationCode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export class TotpMultiFactorSecretGenerationFormComponent {
effect(() => {
this.form.update({
validators: {
onBlur: this.formSchema(),
onChange: this.formSchema(),
onSubmit: this.formSchema(),
onSubmitAsync: async ({ value }) => {
try {
Expand Down Expand Up @@ -191,7 +191,7 @@ export class TotpMultiFactorVerificationFormComponent {
effect(() => {
this.form.update({
validators: {
onBlur: this.formSchema(),
onChange: this.formSchema(),
onSubmit: this.formSchema(),
onSubmitAsync: async ({ value }) => {
try {
Expand Down
4 changes: 2 additions & 2 deletions packages/angular/src/lib/auth/forms/phone-auth-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export class PhoneNumberFormComponent {
effect(() => {
this.form.update({
validators: {
onBlur: this.formSchema(),
onChange: this.formSchema(),
onSubmitAsync: async ({ value }) => {
const selectedCountry = countryData.find((c) => c.code === this.country());
const formattedNumber = formatPhoneNumber(value.phoneNumber, selectedCountry!);
Expand Down Expand Up @@ -226,7 +226,7 @@ export class VerificationFormComponent {
effect(() => {
this.form.update({
validators: {
onBlur: this.formSchema(),
onChange: this.formSchema(),
onSubmitAsync: async ({ value }) => {
try {
const credential = await confirmPhoneNumber(this.ui(), this.verificationId(), value.verificationCode);
Expand Down
52 changes: 52 additions & 0 deletions packages/angular/src/lib/auth/forms/sign-in-auth-form.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,4 +426,56 @@ describe("<fui-sign-in-auth-form />", () => {

expect(component.form.state.errors).toHaveLength(0);
});

it("should show validation messages after submit and clear after typing valid values", async () => {
const { fixture, container } = await render(SignInAuthFormComponent, {
imports: [
CommonModule,
SignInAuthFormComponent,
TanStackField,
TanStackAppField,
FormInputComponent,
FormSubmitComponent,
FormErrorMessageComponent,
FormActionComponent,
PoliciesComponent,
],
});

fixture.detectChanges();

const form = container.querySelector("form") as HTMLFormElement;

expect(screen.queryByText("Please enter a valid email address")).not.toBeInTheDocument();
expect(screen.queryByText("Password should be at least 6 characters")).not.toBeInTheDocument();

fireEvent.submit(form);
await fixture.whenStable();
fixture.detectChanges();

expect(await screen.findByText("Please enter a valid email address")).toBeInTheDocument();
expect(screen.getAllByRole("alert").some((el) => /password/i.test(el.textContent ?? ""))).toBe(true);

// Then react to explicitly invalid values.
const emailInput = container.querySelector("input[name='email']") as HTMLInputElement;
const passwordInput = container.querySelector("input[name='password']") as HTMLInputElement;
fireEvent.input(emailInput, { target: { value: "invalid-email" } });
fireEvent.input(passwordInput, { target: { value: "123" } });
fireEvent.submit(form);
await fixture.whenStable();
fixture.detectChanges();

expect(screen.getByText("Please enter a valid email address")).toBeInTheDocument();
expect(screen.getByText(/Password (should|must) be at least 6 characters/)).toBeInTheDocument();

const emailInputAfterInvalid = container.querySelector("input[name='email']") as HTMLInputElement;
const passwordInputAfterInvalid = container.querySelector("input[name='password']") as HTMLInputElement;
fireEvent.input(emailInputAfterInvalid, { target: { value: "test@example.com" } });
fireEvent.input(passwordInputAfterInvalid, { target: { value: "123456" } });
await fixture.whenStable();
fixture.detectChanges();

expect(screen.queryByText("Please enter a valid email address")).not.toBeInTheDocument();
expect(screen.queryByText(/Password (should|must) be at least 6 characters/)).not.toBeInTheDocument();
});
});
2 changes: 1 addition & 1 deletion packages/angular/src/lib/auth/forms/sign-in-auth-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export class SignInAuthFormComponent {
effect(() => {
this.form.update({
validators: {
onBlur: this.formSchema(),
onChange: this.formSchema(),
onSubmitAsync: async ({ value }) => {
try {
const credential = await signInWithEmailAndPassword(this.ui(), value.email, value.password);
Expand Down
51 changes: 51 additions & 0 deletions packages/angular/src/lib/auth/forms/sign-up-auth-form.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,4 +397,55 @@ describe("<fui-sign-up-auth-form />", () => {

expect(component.form.state.errors).toHaveLength(0);
});

it("should show validation messages after submit and clear after typing valid values", async () => {
const { fixture, container } = await render(SignUpAuthFormComponent, {
imports: [
CommonModule,
SignUpAuthFormComponent,
TanStackField,
TanStackAppField,
FormInputComponent,
FormSubmitComponent,
FormErrorMessageComponent,
FormActionComponent,
PoliciesComponent,
],
});

fixture.detectChanges();

const form = container.querySelector("form") as HTMLFormElement;

expect(screen.queryByText("Please enter a valid email address")).not.toBeInTheDocument();
expect(screen.queryByText("Password should be at least 6 characters")).not.toBeInTheDocument();

fireEvent.submit(form);
await fixture.whenStable();
fixture.detectChanges();

expect(await screen.findByText("Please enter a valid email address")).toBeInTheDocument();
expect(screen.getAllByRole("alert").some((el) => /password/i.test(el.textContent ?? ""))).toBe(true);

// Then react to explicitly invalid values.
const emailInput = container.querySelector("input[name='email']") as HTMLInputElement;
const passwordInput = container.querySelector("input[name='password']") as HTMLInputElement;
fireEvent.input(emailInput, { target: { value: "invalid-email" } });
fireEvent.input(passwordInput, { target: { value: "123" } });
await fixture.whenStable();
fixture.detectChanges();

expect(screen.getByText("Please enter a valid email address")).toBeInTheDocument();
expect(screen.getByText(/Password (should|must) be at least 6 characters/)).toBeInTheDocument();

const emailInputAfterInvalid = container.querySelector("input[name='email']") as HTMLInputElement;
const passwordInputAfterInvalid = container.querySelector("input[name='password']") as HTMLInputElement;
fireEvent.input(emailInputAfterInvalid, { target: { value: "test@example.com" } });
fireEvent.input(passwordInputAfterInvalid, { target: { value: "123456" } });
await fixture.whenStable();
fixture.detectChanges();

expect(screen.queryByText("Please enter a valid email address")).not.toBeInTheDocument();
expect(screen.queryByText(/Password (should|must) be at least 6 characters/)).not.toBeInTheDocument();
});
});
2 changes: 1 addition & 1 deletion packages/angular/src/lib/auth/forms/sign-up-auth-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export class SignUpAuthFormComponent {
effect(() => {
this.form.update({
validators: {
onBlur: this.formSchema(),
onChange: this.formSchema(),
onSubmitAsync: async ({ value }) => {
try {
const credential = await createUserWithEmailAndPassword(
Expand Down
30 changes: 5 additions & 25 deletions packages/angular/src/lib/components/form.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,13 @@ import {
import { ButtonComponent } from "./button";

@Component({
template: `<fui-form-metadata [field]="field()"></fui-form-metadata>`,
template: `<fui-form-metadata [isTouched]="isTouched()" [errors]="errors()"></fui-form-metadata>`,
standalone: true,
imports: [FormMetadataComponent],
})
class TestFormMetadataHostComponent {
field = signal({
state: {
meta: {
isTouched: true,
errors: [{ message: "Test error" }],
},
},
} as any);
isTouched = signal(true);
errors = signal([{ message: "Test error" }]);
}

@Component({
Expand Down Expand Up @@ -90,14 +84,7 @@ describe("Form Components", () => {
it("does not render error message when field has no errors", async () => {
const component = await render(TestFormMetadataHostComponent);

component.fixture.componentInstance.field.set({
state: {
meta: {
isTouched: true,
errors: [],
},
},
} as any);
component.fixture.componentInstance.errors.set([]);
component.fixture.detectChanges();

const errorElement = screen.queryByRole("alert");
Expand All @@ -107,14 +94,7 @@ describe("Form Components", () => {
it("does not render error message when field is not touched", async () => {
const component = await render(TestFormMetadataHostComponent);

component.fixture.componentInstance.field.set({
state: {
meta: {
isTouched: false,
errors: [{ message: "Test error" }],
},
},
} as any);
component.fixture.componentInstance.isTouched.set(false);
component.fixture.detectChanges();

const errorElement = screen.queryByRole("alert");
Expand Down
Loading