diff --git a/packages/angular/package.json b/packages/angular/package.json index 87a6220c..42d1640b 100644 --- a/packages/angular/package.json +++ b/packages/angular/package.json @@ -29,7 +29,7 @@ "@firebase-ui/translations": "workspace:*" }, "dependencies": { - "@tanstack/angular-form": "^1.1.0", + "@tanstack/angular-form": "^1.21.1", "@firebase-ui/styles": "workspace:*", "tslib": "^2.8.1", "clsx": "^2.1.1", diff --git a/packages/angular/src/lib/auth/forms/sign-in-auth-form/sign-in-auth-form.component.ts b/packages/angular/src/lib/auth/forms/sign-in-auth-form/sign-in-auth-form.component.ts index 6b8549a5..5d63b160 100644 --- a/packages/angular/src/lib/auth/forms/sign-in-auth-form/sign-in-auth-form.component.ts +++ b/packages/angular/src/lib/auth/forms/sign-in-auth-form/sign-in-auth-form.component.ts @@ -14,91 +14,68 @@ * limitations under the License. */ -import { Component, EventEmitter, Output } from "@angular/core"; +import { Component, EventEmitter, Output, OnInit } from "@angular/core"; import { CommonModule } from "@angular/common"; -import { injectForm, TanStackField } from "@tanstack/angular-form"; +import { injectForm, TanStackField, TanStackAppField } from "@tanstack/angular-form"; import { injectSignInAuthFormSchema, injectTranslation, injectUI } from "../../../provider"; -import { ButtonComponent } from "../../../components/button/button.component"; import { TermsAndPrivacyComponent } from "../../../components/terms-and-privacy/terms-and-privacy.component"; import { FirebaseUIError, signInWithEmailAndPassword } from "@firebase-ui/core"; +import { + FormInputComponent, + FormSubmitComponent, + FormErrorMessageComponent, +} from "../../../components/form/form.component"; +import { UserCredential } from "firebase/auth"; + @Component({ selector: "fui-sign-in-auth-form", standalone: true, - imports: [CommonModule, TanStackField, ButtonComponent, TermsAndPrivacyComponent], + imports: [ + CommonModule, + TanStackField, + TanStackAppField, + TermsAndPrivacyComponent, + FormInputComponent, + FormSubmitComponent, + FormErrorMessageComponent, + ], template: `
- - - +
- - - + + @if (forgotPassword) { + + } +
- -
{{ formError }}
+ +
- @if(register) { -
- -
+ @if (register) { + }
`, }) -export class SignInAuthFormComponent { +export class SignInAuthFormComponent implements OnInit { private ui = injectUI(); private formSchema = injectSignInAuthFormSchema(); @@ -112,6 +89,7 @@ export class SignInAuthFormComponent { @Output() forgotPassword = new EventEmitter(); @Output() register = new EventEmitter(); + @Output() signIn?: EventEmitter; formError: string | null = null; @@ -120,59 +98,32 @@ export class SignInAuthFormComponent { email: "", password: "", }, - validators: { - onBlur: this.formSchema(), - onSubmit: this.formSchema(), - }, - }) as any; // TODO(ehesp): Fix this - types go too deep - - async handleSubmit(event: SubmitEvent) { - event.preventDefault(); - event.stopPropagation(); - - const email = this.form.state.values.email; - const password = this.form.state.values.password; + }); - if (!email || !password) { - return; - } - - await this.validateAndSignIn(email, password); + handleSubmit(event: SubmitEvent) { + event.preventDefault() + event.stopPropagation() + this.form.handleSubmit() } - async validateAndSignIn(email: string, password: string) { - try { - const validationResult = this.formSchema().safeParse({ - email, - password, - }); - - if (!validationResult.success) { - const validationErrors = validationResult.error.format(); - - if (validationErrors.email?._errors?.length) { - this.formError = validationErrors.email._errors[0]; - return; - } - - if (validationErrors.password?._errors?.length) { - this.formError = validationErrors.password._errors[0]; - return; - } - - this.formError = this.unknownErrorLabel(); - return; - } - - this.formError = null; - await signInWithEmailAndPassword(this.ui(), email, password); - } catch (error) { - if (error instanceof FirebaseUIError) { - this.formError = error.message; - return; - } - - this.formError = this.unknownErrorLabel(); - } + ngOnInit() { + this.form.update({ + validators: { + onBlur: this.formSchema(), + onSubmit: this.formSchema(), + onSubmitAsync: async ({ value }) => { + try { + const credential = await signInWithEmailAndPassword(this.ui(), value.email, value.password); + this.signIn?.emit(credential); + } catch (error) { + if (error instanceof FirebaseUIError) { + return error.message; + } + + return this.unknownErrorLabel(); + } + }, + }, + }); } } diff --git a/packages/angular/src/lib/components/form/form.component.ts b/packages/angular/src/lib/components/form/form.component.ts new file mode 100644 index 00000000..103292a9 --- /dev/null +++ b/packages/angular/src/lib/components/form/form.component.ts @@ -0,0 +1,113 @@ +import { Component, HostBinding, input, Input } from '@angular/core' +import { AnyFieldApi, injectField, injectForm } from '@tanstack/angular-form' +import { cn } from '../../utils'; +import { ButtonComponent } from '../button/button.component'; + +@Component({ + selector: 'fui-form-metadata', + standalone: true, + template: ` + @if(field.state.meta.isTouched && field.state.meta.errors.length > 0) { +
+ +
+ } + `, +}) +export class FormMetadataComponent { + @Input() field: AnyFieldApi; +} + +@Component({ + selector: 'fui-form-input', + standalone: true, + imports: [FormMetadataComponent], + template: ` + + `, +}) +export class FormInputComponent { + field = injectField() + + label = input.required(); +} + +@Component({ + selector: 'button[fui-form-action]', + standalone: true, + template: ` + + `, +}) +export class FormActionComponent { + @Input() + @HostBinding("class") + className: string = ""; + + @HostBinding("attr.class") + get getButtonClasses(): string { + return cn("fui-form__action", this.className); + } + + @HostBinding('attr.type') + readonly type = 'button'; + + field = injectField() +} + +@Component({ + selector: 'fui-form-submit', + standalone: true, + imports: [ButtonComponent], + template: ` + + `, +}) +export class FormSubmitComponent { + @Input() + className: string = ""; + + @HostBinding('attr.type') + readonly type = 'submit'; + + form = injectForm() + + get buttonClasses(): string { + return cn("fui-form__action", this.className); + } + + get isSubmitting(): boolean { + return this.form.state.isSubmitting; + } +} + +@Component({ + selector: 'fui-form-error-message', + standalone: true, + template: ` + @if (form.state.errorMap.onSubmit) { +
+ {{ form.state.errorMap.onSubmit.toString() }} +
+ } + `, +}) +export class FormErrorMessageComponent { + form = injectForm() +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c5e9f1bd..781e4f8c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -305,8 +305,8 @@ importers: specifier: workspace:* version: link:../translations '@tanstack/angular-form': - specifier: ^1.1.0 - version: 1.19.5(@angular/common@20.3.0(@angular/core@20.3.0(@angular/compiler@20.3.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.0(@angular/compiler@20.3.0)(rxjs@7.8.2)(zone.js@0.15.1)) + specifier: ^1.21.1 + version: 1.21.1(@angular/common@20.3.0(@angular/core@20.3.0(@angular/compiler@20.3.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.0(@angular/compiler@20.3.0)(rxjs@7.8.2)(zone.js@0.15.1)) clsx: specifier: ^2.1.1 version: 2.1.1 @@ -2882,8 +2882,8 @@ packages: peerDependencies: '@angular/core': '>=19.0.0' - '@tanstack/angular-form@1.19.5': - resolution: {integrity: sha512-oqgl3lEyuTqASroxmYv+O9U0GViIIqaX/WfNeKjR0pwbNeZ3hW8we6CJklQN44qtZzoC8RQTaZTCNBc5+zmBWQ==} + '@tanstack/angular-form@1.21.1': + resolution: {integrity: sha512-ZtQtCQxvt273sSdT3awFqh3Op0wvdPeLYRliiWrehgHo6MR311scStl0BG9cvQq9d2zkQM5F/+EYQF3Vo0tJpw==} peerDependencies: '@angular/core': '>=19.0.0' @@ -2893,14 +2893,18 @@ packages: '@angular/common': '>=19.0.0' '@angular/core': '>=19.0.0' + '@tanstack/devtools-event-client@0.2.4': + resolution: {integrity: sha512-oqRF1KNYtVUcJV/xXDf3OdJ+wynIcrVxML5a+JBaNFgnyclu14gV1sxi8QfuNMznreyvNxajJbZMS8HHtO+MTA==} + engines: {node: '>=18'} + '@tanstack/form-core@0.41.4': resolution: {integrity: sha512-XZJtN7mWJmi3apsc2J+GpWbcsXbv0pWBkZKP47ZW1QD/2Tj1UWsM6JjcaAkzIlrBdaoEFYmrHToLKr/Ddk8BVg==} '@tanstack/form-core@0.42.1': resolution: {integrity: sha512-jTU0jyHqFceujdtPNv3jPVej1dTqBwa8TYdIyWB5BCwRVUBZEp1PiYEBkC9r92xu5fMpBiKc+JKud3eeVjuMiA==} - '@tanstack/form-core@1.19.5': - resolution: {integrity: sha512-MhHk/f3fOVhm2kHAEOg+yJklSY84C3qSwwYwAUzAQw6i5ZQquBdB2aYzg89FFXNkhmLcEl9hJCqRZVw4NHzDMQ==} + '@tanstack/form-core@1.21.1': + resolution: {integrity: sha512-JoMIRa/VpMRtlFp9LGun8otLHycYI2jE7Pg5e9ziNxoSkAIlG9FENeZ3E62et3bEU64hU2yG4DlUqyDjj1wwoA==} '@tanstack/react-form@0.41.4': resolution: {integrity: sha512-uIfIDZJNqR1dLW03TNByK/woyKd2jfXIrEBq6DPJbqupqyfYXTDo5TMd/7koTYLO4dgTM5wd+2v3uBX3M2bRaA==} @@ -10097,11 +10101,11 @@ snapshots: transitivePeerDependencies: - '@angular/common' - '@tanstack/angular-form@1.19.5(@angular/common@20.3.0(@angular/core@20.3.0(@angular/compiler@20.3.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.0(@angular/compiler@20.3.0)(rxjs@7.8.2)(zone.js@0.15.1))': + '@tanstack/angular-form@1.21.1(@angular/common@20.3.0(@angular/core@20.3.0(@angular/compiler@20.3.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.0(@angular/compiler@20.3.0)(rxjs@7.8.2)(zone.js@0.15.1))': dependencies: '@angular/core': 20.3.0(@angular/compiler@20.3.0)(rxjs@7.8.2)(zone.js@0.15.1) '@tanstack/angular-store': 0.7.5(@angular/common@20.3.0(@angular/core@20.3.0(@angular/compiler@20.3.0)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.0(@angular/compiler@20.3.0)(rxjs@7.8.2)(zone.js@0.15.1)) - '@tanstack/form-core': 1.19.5 + '@tanstack/form-core': 1.21.1 tslib: 2.8.1 transitivePeerDependencies: - '@angular/common' @@ -10113,6 +10117,8 @@ snapshots: '@tanstack/store': 0.7.5 tslib: 2.8.1 + '@tanstack/devtools-event-client@0.2.4': {} + '@tanstack/form-core@0.41.4': dependencies: '@tanstack/store': 0.7.5 @@ -10121,8 +10127,9 @@ snapshots: dependencies: '@tanstack/store': 0.7.5 - '@tanstack/form-core@1.19.5': + '@tanstack/form-core@1.21.1': dependencies: + '@tanstack/devtools-event-client': 0.2.4 '@tanstack/store': 0.7.5 '@tanstack/react-form@0.41.4(react-dom@19.1.1(react@19.1.1))(react@19.1.1)(typescript@5.9.2)':