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: `
`,
})
-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) {
+
+
+ {{ field.state.meta.errors.join(", ") }}
+
+
+ }
+ `,
+})
+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)':