Email & Password confirmation fields
This commit is contained in:
parent
356394dcd8
commit
5d4ef6f958
@ -1,6 +1,6 @@
|
|||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label" [for]="inputId">{{title}}</label>
|
<label class="form-label" [for]="inputId">{{title}}</label>
|
||||||
<input #inputField type="password" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)">
|
<input #inputField type="password" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [disabled]="disabled">
|
||||||
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
<small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small>
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
{{error}}
|
{{error}}
|
||||||
|
@ -5,13 +5,31 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<pngx-input-text i18n-title title="Email" formControlName="email" [error]="error?.email"></pngx-input-text>
|
<pngx-input-text i18n-title title="Email" formControlName="email" (keyup)="onEmailKeyUp($event)" [error]="error?.email"></pngx-input-text>
|
||||||
<pngx-input-password i18n-title title="Password" formControlName="password" [error]="error?.password"></pngx-input-password>
|
<div ngbAccordion>
|
||||||
|
<div ngbAccordionItem="first" [collapsed]="!showEmailConfirm" class="border-0 bg-transparent">
|
||||||
|
<div ngbAccordionCollapse>
|
||||||
|
<div ngbAccordionBody class="p-0 pb-3">
|
||||||
|
<pngx-input-text i18n-title title="Confirm Email" formControlName="email_confirm" (keyup)="onEmailConfirmKeyUp($event)" [error]="error?.email_confirm"></pngx-input-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pngx-input-password i18n-title title="Password" formControlName="password" (keyup)="onPasswordKeyUp($event)" [error]="error?.password"></pngx-input-password>
|
||||||
|
<div ngbAccordion>
|
||||||
|
<div ngbAccordionItem="first" [collapsed]="!showPasswordConfirm" class="border-0 bg-transparent">
|
||||||
|
<div ngbAccordionCollapse>
|
||||||
|
<div ngbAccordionBody class="p-0 pb-3">
|
||||||
|
<pngx-input-password i18n-title title="Confirm Password" formControlName="password_confirm" (keyup)="onPasswordConfirmKeyUp($event)" [error]="error?.password_confirm"></pngx-input-password>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<pngx-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></pngx-input-text>
|
<pngx-input-text i18n-title title="First name" formControlName="first_name" [error]="error?.first_name"></pngx-input-text>
|
||||||
<pngx-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></pngx-input-text>
|
<pngx-input-text i18n-title title="Last name" formControlName="last_name" [error]="error?.first_name"></pngx-input-text>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
|
||||||
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
|
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive || saveDisabled">Save</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
::ng-deep {
|
||||||
|
.accordion-body .mb-3 {
|
||||||
|
margin: 0 !important; // hack-ish, for animation
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ import { ProfileEditDialogComponent } from './profile-edit-dialog.component'
|
|||||||
import { ProfileService } from 'src/app/services/profile.service'
|
import { ProfileService } from 'src/app/services/profile.service'
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||||
import {
|
import {
|
||||||
|
NgbAccordionModule,
|
||||||
NgbActiveModal,
|
NgbActiveModal,
|
||||||
NgbModal,
|
NgbModal,
|
||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
@ -14,6 +15,7 @@ import { TextComponent } from '../input/text/text.component'
|
|||||||
import { PasswordComponent } from '../input/password/password.component'
|
import { PasswordComponent } from '../input/password/password.component'
|
||||||
import { of, throwError } from 'rxjs'
|
import { of, throwError } from 'rxjs'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { By } from '@angular/platform-browser'
|
||||||
|
|
||||||
const profile = {
|
const profile = {
|
||||||
email: 'foo@bar.com',
|
email: 'foo@bar.com',
|
||||||
@ -41,6 +43,7 @@ describe('ProfileEditDialogComponent', () => {
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
NgbModalModule,
|
NgbModalModule,
|
||||||
|
NgbAccordionModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
profileService = TestBed.inject(ProfileService)
|
profileService = TestBed.inject(ProfileService)
|
||||||
@ -86,4 +89,66 @@ describe('ProfileEditDialogComponent', () => {
|
|||||||
component.cancel()
|
component.cancel()
|
||||||
expect(closeSpy).toHaveBeenCalled()
|
expect(closeSpy).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should show additional confirmation field when email changes, warn with error & disable save', () => {
|
||||||
|
expect(component.form.get('email_confirm').enabled).toBeFalsy()
|
||||||
|
const getSpy = jest.spyOn(profileService, 'get')
|
||||||
|
getSpy.mockReturnValue(of(profile))
|
||||||
|
component.ngOnInit()
|
||||||
|
component.form.get('email').patchValue('foo@bar2.com')
|
||||||
|
component.onEmailKeyUp({ target: { value: 'foo@bar2.com' } } as any)
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(component.form.get('email_confirm').enabled).toBeTruthy()
|
||||||
|
expect(fixture.debugElement.nativeElement.textContent).toContain(
|
||||||
|
'Emails must match'
|
||||||
|
)
|
||||||
|
expect(component.saveDisabled).toBeTruthy()
|
||||||
|
|
||||||
|
component.form.get('email_confirm').patchValue('foo@bar2.com')
|
||||||
|
component.onEmailConfirmKeyUp({ target: { value: 'foo@bar2.com' } } as any)
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(fixture.debugElement.nativeElement.textContent).not.toContain(
|
||||||
|
'Emails must match'
|
||||||
|
)
|
||||||
|
expect(component.saveDisabled).toBeFalsy()
|
||||||
|
|
||||||
|
component.form.get('email').patchValue(profile.email)
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(component.form.get('email_confirm').enabled).toBeFalsy()
|
||||||
|
expect(fixture.debugElement.nativeElement.textContent).not.toContain(
|
||||||
|
'Emails must match'
|
||||||
|
)
|
||||||
|
expect(component.saveDisabled).toBeFalsy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should show additional confirmation field when password changes, warn with error & disable save', () => {
|
||||||
|
expect(component.form.get('password_confirm').enabled).toBeFalsy()
|
||||||
|
const getSpy = jest.spyOn(profileService, 'get')
|
||||||
|
getSpy.mockReturnValue(of(profile))
|
||||||
|
component.ngOnInit()
|
||||||
|
component.form.get('password').patchValue('new*pass')
|
||||||
|
component.onPasswordKeyUp({ target: { value: 'new*pass' } } as any)
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(component.form.get('password_confirm').enabled).toBeTruthy()
|
||||||
|
expect(fixture.debugElement.nativeElement.textContent).toContain(
|
||||||
|
'Passwords must match'
|
||||||
|
)
|
||||||
|
expect(component.saveDisabled).toBeTruthy()
|
||||||
|
|
||||||
|
component.form.get('password_confirm').patchValue('new*pass')
|
||||||
|
component.onPasswordConfirmKeyUp({ target: { value: 'new*pass' } } as any)
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(fixture.debugElement.nativeElement.textContent).not.toContain(
|
||||||
|
'Passwords must match'
|
||||||
|
)
|
||||||
|
expect(component.saveDisabled).toBeFalsy()
|
||||||
|
|
||||||
|
component.form.get('password').patchValue(profile.password)
|
||||||
|
fixture.detectChanges()
|
||||||
|
expect(component.form.get('password_confirm').enabled).toBeFalsy()
|
||||||
|
expect(fixture.debugElement.nativeElement.textContent).not.toContain(
|
||||||
|
'Passwords must match'
|
||||||
|
)
|
||||||
|
expect(component.saveDisabled).toBeFalsy()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,25 +1,39 @@
|
|||||||
import { Component, OnInit } from '@angular/core'
|
import { Component, OnDestroy, OnInit } from '@angular/core'
|
||||||
import { FormControl, FormGroup } from '@angular/forms'
|
import { FormControl, FormGroup } from '@angular/forms'
|
||||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { ProfileService } from 'src/app/services/profile.service'
|
import { ProfileService } from 'src/app/services/profile.service'
|
||||||
import { ToastService } from 'src/app/services/toast.service'
|
import { ToastService } from 'src/app/services/toast.service'
|
||||||
|
import { Subject, takeUntil } from 'rxjs'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pngx-profile-edit-dialog',
|
selector: 'pngx-profile-edit-dialog',
|
||||||
templateUrl: './profile-edit-dialog.component.html',
|
templateUrl: './profile-edit-dialog.component.html',
|
||||||
styleUrls: ['./profile-edit-dialog.component.scss'],
|
styleUrls: ['./profile-edit-dialog.component.scss'],
|
||||||
})
|
})
|
||||||
export class ProfileEditDialogComponent implements OnInit {
|
export class ProfileEditDialogComponent implements OnInit, OnDestroy {
|
||||||
public networkActive: boolean = false
|
public networkActive: boolean = false
|
||||||
public error: any
|
public error: any
|
||||||
|
private unsubscribeNotifier: Subject<any> = new Subject()
|
||||||
|
|
||||||
public form = new FormGroup({
|
public form = new FormGroup({
|
||||||
email: new FormControl(''),
|
email: new FormControl(''),
|
||||||
|
email_confirm: new FormControl({ value: null, disabled: true }),
|
||||||
password: new FormControl(null),
|
password: new FormControl(null),
|
||||||
|
password_confirm: new FormControl({ value: null, disabled: true }),
|
||||||
first_name: new FormControl(''),
|
first_name: new FormControl(''),
|
||||||
last_name: new FormControl(''),
|
last_name: new FormControl(''),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
private currentPassword: string
|
||||||
|
private newPassword: string
|
||||||
|
private passwordConfirm: string
|
||||||
|
public showPasswordConfirm: boolean = false
|
||||||
|
|
||||||
|
private currentEmail: string
|
||||||
|
private newEmail: string
|
||||||
|
private emailConfirm: string
|
||||||
|
public showEmailConfirm: boolean = false
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private profileService: ProfileService,
|
private profileService: ProfileService,
|
||||||
public activeModal: NgbActiveModal,
|
public activeModal: NgbActiveModal,
|
||||||
@ -27,20 +41,103 @@ export class ProfileEditDialogComponent implements OnInit {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.profileService.get().subscribe((profile) => {
|
this.networkActive = true
|
||||||
|
this.profileService
|
||||||
|
.get()
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe((profile) => {
|
||||||
|
this.networkActive = false
|
||||||
this.form.patchValue(profile)
|
this.form.patchValue(profile)
|
||||||
|
this.currentEmail = profile.email
|
||||||
|
this.form.get('email').valueChanges.subscribe((newEmail) => {
|
||||||
|
this.newEmail = newEmail
|
||||||
|
this.onEmailChange()
|
||||||
})
|
})
|
||||||
|
this.currentPassword = profile.password
|
||||||
|
this.form.get('password').valueChanges.subscribe((newPassword) => {
|
||||||
|
this.newPassword = newPassword
|
||||||
|
this.onPasswordChange()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.unsubscribeNotifier.next(true)
|
||||||
|
this.unsubscribeNotifier.complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
get saveDisabled(): boolean {
|
||||||
|
return this.error?.password_confirm || this.error?.email_confirm
|
||||||
|
}
|
||||||
|
|
||||||
|
onEmailKeyUp(event: KeyboardEvent) {
|
||||||
|
this.newEmail = (event.target as HTMLInputElement)?.value
|
||||||
|
this.onEmailChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
onEmailConfirmKeyUp(event: KeyboardEvent) {
|
||||||
|
this.emailConfirm = (event.target as HTMLInputElement)?.value
|
||||||
|
this.onEmailChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
onEmailChange() {
|
||||||
|
this.showEmailConfirm = this.currentEmail !== this.newEmail
|
||||||
|
if (this.showEmailConfirm) {
|
||||||
|
this.form.get('email_confirm').enable()
|
||||||
|
if (this.newEmail !== this.emailConfirm) {
|
||||||
|
if (!this.error) this.error = {}
|
||||||
|
this.error.email_confirm = $localize`Emails must match`
|
||||||
|
} else {
|
||||||
|
delete this.error?.email_confirm
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.form.get('email_confirm').disable()
|
||||||
|
delete this.error?.email_confirm
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPasswordKeyUp(event: KeyboardEvent) {
|
||||||
|
this.newPassword = (event.target as HTMLInputElement)?.value
|
||||||
|
this.onPasswordChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
onPasswordConfirmKeyUp(event: KeyboardEvent) {
|
||||||
|
this.passwordConfirm = (event.target as HTMLInputElement)?.value
|
||||||
|
this.onPasswordChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
onPasswordChange() {
|
||||||
|
this.showPasswordConfirm = this.currentPassword !== this.newPassword
|
||||||
|
console.log(this.currentPassword, this.newPassword, this.passwordConfirm)
|
||||||
|
|
||||||
|
if (this.showPasswordConfirm) {
|
||||||
|
this.form.get('password_confirm').enable()
|
||||||
|
if (this.newPassword !== this.passwordConfirm) {
|
||||||
|
if (!this.error) this.error = {}
|
||||||
|
this.error.password_confirm = $localize`Passwords must match`
|
||||||
|
} else {
|
||||||
|
delete this.error?.password_confirm
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.form.get('password_confirm').disable()
|
||||||
|
delete this.error?.password_confirm
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
const profile = Object.assign({}, this.form.value)
|
const profile = Object.assign({}, this.form.value)
|
||||||
this.profileService.update(profile).subscribe({
|
this.networkActive = true
|
||||||
|
this.profileService
|
||||||
|
.update(profile)
|
||||||
|
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||||
|
.subscribe({
|
||||||
next: () => {
|
next: () => {
|
||||||
this.toastService.showInfo($localize`Profile updated successfully`)
|
this.toastService.showInfo($localize`Profile updated successfully`)
|
||||||
this.activeModal.close()
|
this.activeModal.close()
|
||||||
},
|
},
|
||||||
error: (error) => {
|
error: (error) => {
|
||||||
this.toastService.showError($localize`Error saving profile`, error)
|
this.toastService.showError($localize`Error saving profile`, error)
|
||||||
|
this.networkActive = false
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user