11< app-documentation-page
22 title ="Auth Challenge Component "
3- description ="The AuthChallengeComponent is a Dialog that validates user credentials (password) and manages 2FA/recovery code inputs before allowing sensitive actions. "
3+ description ="The AuthChallengeComponent validates user credentials (password) and manages 2FA/recovery code inputs before allowing sensitive actions. It can be used as a Dialog, dynamically attached via CdkPortalOutlet, or embedded directly in HTML . "
44>
55 < div controls >
6- < p > Customize the dialog to preview different configurations:</ p >
6+ < p > Customize the component to preview different configurations:</ p >
77
88 < div style ="display: grid; gap: 16px; margin-bottom: 24px ">
99 < mat-checkbox [(ngModel)] ="showPasswordField ">
2424 < input matInput [(ngModel)] ="boldText " />
2525 </ mat-form-field >
2626
27+ < mat-form-field appearance ="outline ">
28+ < mat-label > Trailing text</ mat-label >
29+ < input matInput [(ngModel)] ="trailingText " />
30+ </ mat-form-field >
31+
2732 < div >
2833 < button
2934 mat-flat-button
5257
5358 < div usage style ="font-size: 14px ">
5459 < p >
55- This component is a < strong > MatDialog</ strong > . It manages the UI
56- switching and validation logic between the password field, 2FA code, and
57- recovery code inputs. You must pass a parent < code > [formGroup]</ code > into
58- it via the dialog data so it can attach its controls.
60+ This component manages the UI switching and validation logic between the
61+ password field, 2FA code, and recovery code inputs. You must pass a parent
62+ < code > [formGroup]</ code > into it so it can attach its controls.
5963 </ p >
6064
61- < p > To implement this component, use the code highlighted in yellow.</ p >
65+ < p >
66+ It can be rendered as a < strong > MatDialog</ strong > , injected dynamically
67+ via < strong > CdkPortalOutlet</ strong > , or used as a standard HTML tag.
68+ < em
69+ > Note: When using MatDialog, pass configuration properties inside the
70+ < code > data</ code > object. Otherwise, bind them as standard
71+ < code > @Input()</ code > properties.</ em
72+ >
73+ </ p >
6274
6375 < h4 > 1. Form Configuration</ h4 >
6476 < p >
6577 Initialize the controls in your parent component. Note that
6678 < code > Validators.required</ code > for the 2FA fields is managed
67- automatically by the dialog component based on the fields being shown.
79+ automatically by the component based on the fields currently being shown.
6880 </ p >
6981 < pre > < code class ="language-typescript "> this.form = this.fb.group({
7082 // ... other controls like 'id'
@@ -73,12 +85,7 @@ <h4>1. Form Configuration</h4>
7385 < span style ="background-color: #fff59d; color: #000; "> twoFactorRecoveryCode: [null, [Validators.minLength(10), Validators.maxLength(10)]],</ span >
7486});</ code > </ pre >
7587
76- < h4 > 2. Opening and Handling the Dialog</ h4 >
77- < p >
78- Use the following method to open the dialog, listen for the verify button
79- click, trigger your backend request, and handle the final success/cancel
80- state. You can copy and paste this directly into your component.
81- </ p >
88+ < h4 > 2A. Usage via MatDialog</ h4 >
8289 < pre > < code class ="language-typescript "> openAuthChallenge() {
8390 // 1. Open the dialog
8491 const dialogRef = this._matDialog.open<AuthChallengeComponent>(
@@ -88,6 +95,7 @@ <h4>2. Opening and Handling the Dialog</h4>
8895 parentForm: this.form,
8996 actionDescription: 'unlink the alternate sign in account',
9097 boldText: 'Google',
98+ trailingText: 'to continue.',
9199 showTwoFactorField: this.twoFactorState, // true or false
92100 } as AuthChallengeFormData,
93101 }
@@ -102,37 +110,53 @@ <h4>2. Opening and Handling the Dialog</h4>
102110 .subscribe({
103111 next: (response: any) => {
104112 if (response.success) {
105- // Close the dialog and pass true for success
106113 dialogRef.close(true);
107114 } else {
108- // Pass backend response back to the dialog to display errors
109- dialogRef.componentInstance.loading = false;
110115 dialogRef.componentInstance.processBackendResponse(response);
111116 }
112117 },
113118 });
114119
115- // 3. Listen for when the dialog actually closes
120+ // 3. Listen for cancel or close
116121 dialogRef.afterClosed().subscribe((success) => {
117122 this.form.reset();
118-
119- if (success) {
120- // Action completed successfully
121- this.success = true;
122- } else {
123- // User canceled or closed the dialog
124- this.cancel = true;
125- }
126123 });
127124}</ code > </ pre >
128125
129- < h4 > 3. Interface Definitions </ h4 >
126+ < h4 > 2B. Usage via CdkPortalOutlet </ h4 >
130127 < p >
131- The endpoint payload/response object needs to extend the
132- < strong > < code > AuthChallenge</ code > </ strong > interface. The dialog
133- configuration uses the
134- < strong > < code > AuthChallengeFormData</ code > </ strong > interface.
128+ First, add the outlet container to your HTML and query it in your TS file.
135129 </ p >
130+ < pre > < code class ="language-html "> <!-- In your parent HTML -->
131+ <ng-container #authChallengeOutlet cdkPortalOutlet></ng-container></ code > </ pre >
132+
133+ < pre > < code class ="language-typescript "> // In your parent TS file
134+ @ViewChild('authChallengeOutlet', { static: false, read: CdkPortalOutlet }) outlet!: CdkPortalOutlet;
135+
136+ showAuthenticationChallenge(): void {
137+ const portal = new ComponentPortal<AuthChallengeComponent>(AuthChallengeComponent);
138+ const componentRef = this.outlet.attachComponentPortal(portal);
139+
140+ // Bind inputs directly to the instance
141+ componentRef.instance.parentForm = this.form;
142+ componentRef.instance.actionDescription = 'complete your password reset';
143+ componentRef.instance.showPasswordField = false;
144+
145+ componentRef.instance.submitAttempt.subscribe(() => {
146+ // Handle submission and call componentRef.instance.processBackendResponse(res) on error.
147+ // On success, navigate away.
148+ this.router.navigate(['/success']);
149+ });
150+
151+ componentRef.instance.cancelAttempt.subscribe(() => {
152+ this.form.reset();
153+ // TIP: If staying on the same page, use this.outlet.detach() to close the view.
154+ // If routing away, DO NOT detach it manually to avoid a UI flash. Angular will clean it up!
155+ this.router.navigate(['/signin']);
156+ });
157+ }</ code > </ pre >
158+
159+ < h4 > 3. Interface Definitions</ h4 >
136160 < pre > < code class ="language-typescript "> export interface AuthChallenge {
137161 success?: boolean;
138162 invalidPassword?: boolean;
@@ -146,14 +170,14 @@ <h4>3. Interface Definitions</h4>
146170
147171export interface AuthChallengeFormData {
148172 actionDescription?: string;
173+ boldText?: string;
174+ trailingText?: string;
149175 showPasswordField?: boolean;
150176 showTwoFactorField?: boolean;
151177 codeControlName?: string;
152178 recoveryControlName?: string;
153179 passwordControlName?: string;
154180 parentForm?: UntypedFormGroup;
155- boldText?: string;
156- trailingText?: string;
157181}</ code > </ pre >
158182
159183 < h4 > 4. Backend Implementation</ h4 >
@@ -193,9 +217,14 @@ <h4>4. Backend Implementation</h4>
193217 (e.g. < code > 'unlink the alternate sign in account'</ code > ).
194218 </ li >
195219 < li >
196- < code style ="font-weight: bold "> memberName </ code > : < code > string</ code > .
220+ < code style ="font-weight: bold "> boldText </ code > : < code > string</ code > .
197221 The target of the action to be bolded (e.g. < code > 'Google'</ code > ).
198222 </ li >
223+ < li >
224+ < code style ="font-weight: bold "> trailingText</ code > :
225+ < code > string</ code > . Optional text that appears after the bolded text
226+ (e.g. < code > 'to complete your password reset'</ code > ).
227+ </ li >
199228 < li >
200229 < code style ="font-weight: bold "> showPasswordField</ code > :
201230 < code > boolean</ code > . Whether to display the password input field.
0 commit comments