Skip to content

Commit 08aeb02

Browse files
Ismaestroiramos
authored andcommitted
feat(forms): add autocomplete and scroll to first invalid directive
1 parent 0d32deb commit 08aeb02

File tree

7 files changed

+138
-19
lines changed

7 files changed

+138
-19
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ npm run update | Update the project dependencies with ng update
6969
* Angular Pipes
7070
* Interceptors and Events (Progress bar active, if a request is pending)
7171
* Modal and toasts (snakbar)!
72+
* Autocomplete enabled in forms
73+
* Scroll to first invalid input in forms (directive)
7274
* Responsive layout (flex layout module)
7375
* SASS (most common used functions and mixins) and BEM styles
7476
* Internationalization with ng-translate and ngx-translate-extract. Also use cache busting for translation files with [webpack translate loader](https://github.com/ngx-translate/http-loader#angular-cliwebpack-translateloader-example)

src/app/modules/heroes/pages/heroes-list-page/heroes-list-page.component.html

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,29 +36,27 @@ <h3 mat-line [class.cp]="hero.default" (click)="seeHeroDetails(hero);"> {{hero.n
3636
<div id="right">
3737
<h2 class="header__title">{{ 'createHero' | translate }}</h2>
3838
<div>
39-
<form [formGroup]="newHeroForm" #form="ngForm" (ngSubmit)="createNewHero(newHeroForm.value)">
39+
<form [formGroup]="newHeroForm" #form="ngForm" appScrollToFirstInvalid
40+
(ngSubmit)="createNewHero(newHeroForm.value)" autocomplete="on">
4041
<mat-form-field class="input-container">
41-
<input matInput type="text"
42+
<input matInput name="hname" type="text"
4243
placeholder="{{'name' | translate}}"
4344
formControlName="name">
44-
<mat-error *ngIf="!newHeroForm.controls.name.valid && newHeroForm.controls.name.touched">
45+
<mat-error *ngIf="!newHeroForm.controls.name.valid && form.submitted">
4546
{{'nameRequired' | translate}}
4647
</mat-error>
4748
</mat-form-field>
48-
4949
<mat-form-field class="input-container">
50-
<input matInput type="text"
50+
<input matInput type="text" name="rname"
5151
placeholder="{{'realName' | translate}}"
5252
formControlName="alterEgo">
53-
<mat-error *ngIf="!newHeroForm.controls.alterEgo.valid && newHeroForm.controls.alterEgo.touched">
53+
<mat-error *ngIf="!newHeroForm.controls.alterEgo.valid && form.submitted">
5454
{{'realNameRequired' | translate}}
5555
</mat-error>
5656
</mat-form-field>
57-
58-
<button mat-raised-button type="submit" [disabled]="!newHeroForm.valid">
57+
<button mat-raised-button type="submit" [disabled]="form.submitted && !newHeroForm.valid">
5958
{{'create' | translate}}
6059
</button>
61-
6260
<div *ngIf="error">{{error | translate}}</div>
6361
</form>
6462
</div>

src/app/modules/heroes/pages/heroes-list-page/heroes-list-page.component.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,16 @@ export class HeroesListPageComponent implements OnInit {
5151
}
5252

5353
createNewHero(newHero: Hero) {
54-
this.heroService.createHero(newHero).subscribe((newHeroWithId) => {
55-
this.heroes.push(newHeroWithId);
56-
this.myNgForm.resetForm();
57-
}, (response: Response) => {
58-
if (response.status === 500) {
59-
this.error = 'errorHasOcurred';
60-
}
61-
});
54+
if (this.newHeroForm.valid) {
55+
this.heroService.createHero(newHero).subscribe((newHeroWithId) => {
56+
this.heroes.push(newHeroWithId);
57+
this.myNgForm.resetForm();
58+
}, (response: Response) => {
59+
if (response.status === 500) {
60+
this.error = 'errorHasOcurred';
61+
}
62+
});
63+
}
6264
}
6365

6466
seeHeroDetails(hero): void {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import {Component} from '@angular/core';
2+
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
3+
import {ComponentFixture, TestBed} from '@angular/core/testing';
4+
import {ScrollToFirstInvalidDirective} from './scroll-to-first-invalid.directive';
5+
import {By} from '@angular/platform-browser';
6+
import {TestsModule} from '../modules/tests.module';
7+
8+
@Component({
9+
template: `
10+
<form id="test-form" [formGroup]="testForm" appScrollToFirstInvalid>
11+
<input id="test-input" type="text" class="input" [formControl]="testForm.controls['someText']">
12+
<button id="test-button"
13+
form="test-form"
14+
(click)="saveForm()">
15+
</button>
16+
</form>
17+
`
18+
})
19+
class TestComponent {
20+
testForm: FormGroup;
21+
22+
constructor(private formBuilder: FormBuilder) {
23+
this.testForm = this.formBuilder.group({
24+
someText: ['', [Validators.required]]
25+
});
26+
}
27+
28+
saveForm() {
29+
}
30+
}
31+
32+
describe('ScrollToFirstInvalidDirective', () => {
33+
let fixture: ComponentFixture<TestComponent>;
34+
35+
beforeEach(() => {
36+
fixture = TestBed.configureTestingModule({
37+
imports: [
38+
TestsModule
39+
],
40+
declarations: [
41+
ScrollToFirstInvalidDirective,
42+
TestComponent
43+
]
44+
}).createComponent(TestComponent);
45+
46+
fixture.detectChanges();
47+
});
48+
49+
it('should trigger focus on the input because scroll directive is executed', () => {
50+
const form = fixture.debugElement.queryAll(By.directive(ScrollToFirstInvalidDirective));
51+
const input = fixture.debugElement.query(By.css('input'));
52+
const button = fixture.debugElement.query(By.css('button'));
53+
54+
expect(form).not.toBeNull();
55+
expect(input.nativeElement.classList.contains('ng-untouched')).toBe(true);
56+
57+
button.nativeElement.click();
58+
fixture.detectChanges();
59+
60+
expect(input.nativeElement.classList.contains('ng-touched')).toBe(true);
61+
});
62+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {Directive, ElementRef, HostListener, Input} from '@angular/core';
2+
import {NgForm} from '@angular/forms';
3+
import UtilsHelper from '../../shared/helpers/utils.helper';
4+
5+
@Directive({selector: '[appScrollToFirstInvalid]'})
6+
export class ScrollToFirstInvalidDirective {
7+
@Input() formGroup: NgForm;
8+
9+
constructor(private el: ElementRef) {
10+
}
11+
12+
@HostListener('submit', ['$event'])
13+
onSubmit(event) {
14+
event.preventDefault();
15+
16+
if (!this.formGroup.valid) {
17+
const formControls = this.formGroup.controls;
18+
for (const control in formControls) {
19+
if (formControls.hasOwnProperty(control)) {
20+
formControls[control].markAsTouched();
21+
}
22+
}
23+
24+
const target = this.el.nativeElement.querySelector('.ng-invalid');
25+
if (target) {
26+
UtilsHelper.scrollToElement(target);
27+
}
28+
}
29+
}
30+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export const scrollToElement = (element) => {
2+
if (element) {
3+
const distance = window.pageYOffset - Math.abs(element.getBoundingClientRect().y);
4+
5+
window.scroll({
6+
behavior: 'smooth',
7+
left: 0,
8+
top: element.getBoundingClientRect().top + window.scrollY - 150
9+
});
10+
11+
setTimeout(() => {
12+
element.focus();
13+
element.blur(); // Trigger error messages
14+
element.focus();
15+
}, distance);
16+
}
17+
};
18+
19+
20+
export default {
21+
scrollToElement
22+
};

src/app/shared/shared.module.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {RouterModule} from '@angular/router';
1313
import {HomePageComponent} from './pages/home-page/home-page.component';
1414
import {Error404PageComponent} from './pages/error404-page/error404-page.component';
1515
import {HeroCardComponent} from './components/hero-card/hero-card.component';
16+
import {ScrollToFirstInvalidDirective} from './directives/scroll-to-first-invalid.directive';
1617

1718
@NgModule({
1819
imports: [
@@ -31,7 +32,8 @@ import {HeroCardComponent} from './components/hero-card/hero-card.component';
3132
SearchBarComponent,
3233
FooterComponent,
3334
SpinnerComponent,
34-
HeroCardComponent
35+
HeroCardComponent,
36+
ScrollToFirstInvalidDirective
3537
],
3638
exports: [
3739
CommonModule,
@@ -43,7 +45,8 @@ import {HeroCardComponent} from './components/hero-card/hero-card.component';
4345
SearchBarComponent,
4446
FooterComponent,
4547
SpinnerComponent,
46-
HeroCardComponent
48+
HeroCardComponent,
49+
ScrollToFirstInvalidDirective
4750
]
4851
})
4952

0 commit comments

Comments
 (0)