화면구성
위와 같이 보이도록 bootstrap을 이용해서 form을 구성해보자
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
<h3>Hero Registration</h3> <!--<form>--> <div class="form-group mt-1"> <label for="name">Name</label> <input type="text" class="form-control" placeholder="Enter Name" id="name"> </div> <div class="form-group mt-1"> <label for="email">Email Address</label> <input type="email" class="form-control" placeholder="Enter Email" id="email"> </div> <div class="d-flex flex-column mt-1"> <div>성별</div> <div> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" name="sex" value="male" id="male"> <label class="form-check-label" for="male">남자</label> </div> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" name="sex" value="female" id="female"> <label class="form-check-label" for="female">여자</label> </div> </div> </div> <div class="form-group mt-1"> <label for="country">country</label> <select class="form-control" id="country"> <option value="Japan">Japan</option> <option value="American">American</option> <option value="Korean">Korean</option> </select> </div> <div class="form-group mt-1"> <label for="email">Address</label> <textarea class="form-control" placeholder="Enter address" id="address" rows="3"></textarea> </div> <div class="d-flex flex-column mt-1"> <div>power</div> <div> <div class="form-check" *ngFor="let p of powers"> <input type="checkbox" class="form-check-input"> <label class="form-check-label">{{p}}</label> </div> </div> </div> <div class="m-3 d-flex justify-content-center"> <button type="submit" class="btn btn-outline-primary">등록</button> </div> <!--</form>--> |
form 태그가 들어가면 template-driven from 혹은 reactive form을 구성해야하는데 당장 에러가 나므로 일단 먼저 뷰만 만들기 위해서 주석 처리하였다.
bootstrap의 form-group과 form-control을 이용해서 form을 스타일링하였다.
mt-1은 margin-top의 약자이고 1은 0.25rem, 2는 0.5rem, 3은 1rem이다.
d-flex는 display: flex 를 의미하고 flex-column은 flex-direction: column을 의미한다. justify-content-center는 justify-content: center를 의미한다.
power의 경우는 스트링 배열 데이터로 만들고 사용자가 선택하도록 만들어야 하므로 모델데이터가 필요하므로 한줄을 추가한다.
1 |
powers = ['flying', 'penetration', 'hacking', 'strength']; |
Reactive Style Form 구성
먼저 admin 모듈에 ReactiveFormsModule을 추가한다.
1 2 3 4 5 |
imports: [ CommonModule, ReactiveFormsModule, RouterModule.forChild(routes) ], |
reactive 의 의미는 모델 데이터를 만들면 뷰가 모델에 반응해서 렌더링된다는 의미이다.
그러므로 먼저 모델데이터를 구성해야 한다.
생성자에서 FormBuilder를 서비스로 주입받고, 이것을 이용해서 FormGroup을 만든다.name, email, sex, country, address, power 까지 6가지 모델 데이터를 만들었다.
이 모델 데이터는 초기화 값과 validation을 설정하는데 여기서는 validation은 뒤에서 살펴보고 초기화만 한다.
power의 경우는 array타입이며 초기 데이터는 [false, false, false, false ] 이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
form: FormGroup; powers = ['flying', 'penetration', 'hacking', 'strength']; constructor(private fb: FormBuilder) { this.form = this.fb.group({ name: null, email: null, sex: null, country: null, address: null, power: this.fb.array(this.powers.map(x => !1)) }); } } |
이 모델 데이터를 기반으로 뷰에 바인딩하자.
먼저 form에는 [formGroup]=”form” 을 추가하였다.
그리고, input에는 formControlName=”모델데이터” 로 바인딩하였다. required 같은 속성은 필요하지 않다. 이미 모델 데이터에서 결정되었기 때문이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
<h3>Hero Registration</h3> <form [formGroup]="form" (ngSubmit)="register()" novalidate> <div class="form-group mt-1"> <label for="name">Name</label> <input type="text" class="form-control" placeholder="Enter Name" id="name" formControlName="name"> </div> <div class="form-group mt-1"> <label for="email">Email Address</label> <input type="email" class="form-control" placeholder="Enter Email" id="email" formControlName="email"> </div> <div class="d-flex flex-column mt-1"> <div>성별</div> <div> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" name="sex" value="male" id="male" formControlName="sex"> <label class="form-check-label" for="male">남자</label> </div> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" name="sex" value="female" id="female" formControlName="sex"> <label class="form-check-label" for="female">여자</label> </div> </div> </div> <div class="form-group mt-1"> <label for="country">country</label> <select class="form-control" id="country" formControlName="country"> <option value="Japan">Japan</option> <option value="American">American</option> <option value="Korean">Korean</option> </select> </div> <div class="form-group mt-1"> <label for="email">Address</label> <textarea class="form-control" placeholder="Enter address" id="address" rows="3" formControlName="address"></textarea> </div> <div class="d-flex flex-column mt-1"> <div>power</div> <div> <div class="form-check" *ngFor="let p of powers; let i = index" formArrayName="power"> <input type="checkbox" class="form-check-input" [formControlName]="i"> <label class="form-check-label">{{p}}</label> </div> </div> </div> <div class="m-3 d-flex justify-content-center"> <button type="submit" class="btn btn-outline-primary">등록</button> </div> </form> <p>{{form.value | json}}</p> |
하단에 디버깅 메시지를 찍어 놓았기 때문에 상단에 값을 입력하면서 하단 디버깅 메시지를 살펴보자.
값을 입력한 하나의 예는 다음과 같다. power 는 첫번째 세번째를 선택하였다.
{ “name”: “hong”, “email”: “hong@gmail.com”, “sex”: “male”, “country”: “American”, “address”: “address1”, “power”: [ true, false, true, false ] }
validation 하기
name은 requried 속성과, 그리고, 최소 5자 최대 20자로 제한한다.
모델 데이터에 이 속성을 추가하면 뷰에서는 required, minlength, maxlength 를 추가하지 않아도 된다. 모델 => 뷰 로 리액트 되기 때문이다.
이메일 속성은 별도의 로직으로 체크해야하는데 여기서는 일단 외부 모듈로 처리하였다.
1 2 3 4 5 6 7 8 9 10 |
constructor(private fb: FormBuilder) { this.form = this.fb.group({ name: [null, Validators.compose([Validators.required, Validators.minLength(5), Validators.maxLength(20)])], email: [null, Validators.compose([Validators.required, Validators.email])], sex: [null, Validators.required], country: [null, Validators.required], address: null, power: this.fb.array(this.powers.map(x => !1)) }); } |
이제 뷰단에서 validation 체크 로직을 추가하자.
먼저 속성이 3가지가 추가된 이름 부터 살펴보다
name 모델에 접근하는 방법은 form.controls[‘name’]과 같이 접근한다. touched가 있으므로 터치가 일어나면 그때 validation을 체크한다.
1 2 3 4 5 6 7 |
<div class="form-group mt-1"> <label for="name">Name</label> <input type="text" class="form-control" placeholder="Enter Name" id="name" formControlName="name"> <div *ngIf="!form.controls['name'].valid && form.controls['name'].touched" class="alert alert-danger">이름을 입력하세요.</div> <div *ngIf="!form.controls['name'].valid && form.controls['name'].touched" class="alert alert-danger">5자이상 입력하세요.</div> <div *ngIf="!form.controls['name'].valid && form.controls['name'].touched" class="alert alert-danger">20자이하로 입력하세요.</div> </div> |
그런데 에러문구가 3개가 다 보이는데, 차례대로 하나씩 보이게 하기 위해서는 아래와 같이 처리해야 한다.
이제 테스트해보면 클릭했다 다른데 클릭하면 이름을 입력하세요라고 에러문구 하나만 뜰 것이고, 입력을 시작하면 5자이상 입력하세요라고 뜨고 20자가 넘으면 20자이하로 입력하세요라고 나오는것을 확인할 수 있다.
1 2 3 4 5 6 7 |
<div class="form-group mt-1"> <label for="name">Name</label> <input type="text" class="form-control" placeholder="Enter Name" id="name" formControlName="name"> <div *ngIf="form.controls['name'].hasError('required') && form.controls['name'].touched" class="alert alert-danger">이름을 입력하세요.</div> <div *ngIf="form.controls['name'].hasError('minlength') && form.controls['name'].touched" class="alert alert-danger">5자이상 입력하세요.</div> <div *ngIf="form.controls['name'].hasError('maxlength') && form.controls['name'].touched" class="alert alert-danger">20자이하로 입력하세요.</div> </div> |
이메일 속성값도 추가한다. 커스텀 속성값을 추가하는 방법은 form.controls[’email’].errors?.email 이다. 여기서 ?는 옵셔널 연산자이다. errors가 초기에 null인경우에 이렇게 사용한다.
1 2 3 4 5 6 |
<div class="form-group mt-1"> <label for="email">Email Address</label> <input type="email" class="form-control" placeholder="Enter Email" id="email" formControlName="email"> <div *ngIf="form.controls['email'].hasError('required') && form.controls['email'].touched" class="alert alert-danger">이메일을 입력하세요.</div> <div *ngIf="form.controls['email'].hasError('email') && form.controls['email'].touched" class="alert alert-danger">유효한 이메일을 입력하세요.</div> </div> |
sex, country도 추가한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<div class="d-flex flex-column mt-1"> <div>성별</div> <div> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" name="sex" value="male" id="male" formControlName="sex"> <label class="form-check-label" for="male">남자</label> </div> <div class="form-check form-check-inline"> <input class="form-check-input" type="radio" name="sex" value="female" id="female" formControlName="sex"> <label class="form-check-label" for="female">여자</label> </div> <div *ngIf="!form.controls['sex'].valid && form.controls['sex'].touched" class="alert alert-danger">성별을 선택하세요.</div> </div> </div> <div class="form-group mt-1"> <label for="country">country</label> <select class="form-control" id="country" formControlName="country"> <option value="Japan">Japan</option> <option value="American">American</option> <option value="Korean">Korean</option> </select> <div *ngIf="!form.controls['country'].valid && form.controls['country'].touched" class="alert alert-danger">국가를 선택하세요.</div> </div> |
만일 sex, country가 required 속성인데, submit 버튼을 누르면 submit이 되는가? 안되는가?
html5라면 submit이 안되겠지만 html5 validation을 사용하지 않고 있으므로 submit이 일어난다.
register() 버튼에 form 유효성 체크 로직을 추가하자.
1 2 3 4 5 6 |
register() { console.log('register'); if (!this.form.valid) { return; } } |
폼을 유효하지 않게 만들고 submit 버튼을 누르면 리턴되서 아무액션이 일어나지 않는다. 이렇게 되면 사용자가 무슨 폼을 입력을 안했는지 알 수 없으므로 모든 form에 대한 validation을 체크해야 한다.
1 2 3 4 5 6 7 8 9 10 11 |
register() { console.log('register'); if (!this.form.valid) { // to validate all form fields Object.keys(this.form.controls).forEach(key => { const control = this.form.controls[key]; control.markAsTouched({onlySelf: true}); }); return; } } |
그러면 이제 모든 폼에 에러 문구가 나타날 것이다.
서버 연동
등록버튼을 클릭하면 서버로 전송하기 위한 hero json 객체를 생성하자.
power의 경우는 DB에 스트링 타입이므로 스트링 배열을 콤마로 분리된 스트링으로 변환해줘야 한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
register() { console.log('register'); if (!this.form.valid) { // to validate all form fields Object.keys(this.form.controls).forEach(key => { const control = this.form.controls[key]; control.markAsTouched({onlySelf: true}); }); return; } const power = this.form.controls['power'].value .map((item, index) => item ? this.powers[index] : false) .filter(item => item ? true : false); console.log(power); const sendForm = Object.assign({}, this.form.value); // DB에는 스트링 배열 타입이 아니라 콤마로 분리된 스트링으로 넣어야 한다. sendForm.power = power.toString(); console.log(sendForm); } |
form값을 모두 deep copy해서 가져온다.
power의 경우 예를 들어 1, 3번째를 체크했다고 하면 처음에 [true, false, true, false] 가 될것이다.
그러면 map을 이용해서 실제 데이터와 매핑한다. 그러면 결과는 [‘flying’, false, ‘hacking’, false] 가 된다.
다시 filter를 이용해서 값이 있는 경우만 필터링하면 결과는 [‘flying’, ‘hacking’]이 된다.
이제 서비스를 추가하고, 실제 서버와 연동하자.
1 |
ng g service admin/admin |
admin 모듈에 admin.service.ts를 추가하였다.
admin 모듈에 HttpCliemtModule을 추가한다.
1 2 3 4 5 6 |
imports: [ CommonModule, HttpClientModule, ReactiveFormsModule, RouterModule.forChild(routes) ], |
이제 서비스에 http 서비스를 주입받고 http 헤더에 content-type을 설정한다. admin 서비스는 admin 모듈이 생성될때 등록되어야 하므로 Injectable 부분은 지우고, admin 모듈의 providers에 추가한다. 이 부분이 의미하는것은 admin 모듈이 생성시에 admin 서비스를 등록하라는 의미이다.
1 2 3 4 5 6 7 8 9 10 11 12 |
@Injectable() export class AdminService { headers = new HttpHeaders(); constructor(private http: HttpClient) { this.headers.append('Content-Type', 'application/json'); } addHero(hero: Hero): Observable<ResultVo> { return this.http.post<ResultVo>(`${environment.HOST}/api/hero`, hero, {headers: this.headers}); } } |
1 |
providers: [AdminService] |
서비스에 addHero() 함수를 구현한다.
1 2 3 |
addHero(hero: Hero): Observable<ResultVo> { return this.http.post<ResultVo>(`${environment.HOST}/api/hero`, hero, {headers: this.headers}); } |
이제 컴포넌트 register() 함수의 맨 아랫부분에 서비스를 호출하는 코드를 넣어보자.
1 2 3 4 |
this.adminService.addHero(sendForm) .subscribe(body => { console.log(body); }); |
정상적으로 동작하는데, 정상적인 insert가 일어나면 폼을 초기화하고 등록되었다는 메시지를 띄워보자.
먼저 첫번째로 메시지를 띄우자. toast로 검색하면 주간다운로드 횟수가 많은것과 마지막 퍼블리싱 날짜가 최근까지 유지되는 모듈을 설치하는게 좋다.
한달전에 퍼블리싱 된 angular2-toaster를 설치하자.
먼저 https://www.npmjs.com/package/angular2-toaster 여기서 매뉴얼을 차근히 읽어보고 설치해야 한다.
1 |
npm install angular2-toaster --save |
toaster 서비스는 모든 모듈에서 사용이 되어야 하므로 root에서 등록을 하고, app.module 에 모듈을 추가한다.
BrowserAnimationsModule, ToasterModule.forRoot() 두가지를 추가하였다.
1 2 3 4 5 6 7 8 9 |
imports: [ BrowserModule, BrowserAnimationsModule, FormsModule, NgbModalModule.forRoot(), RouterModule.forRoot(routes), HttpClientModule, ToasterModule.forRoot() ], |
styles.scss에 css를 import 한다.
1 |
@import "../node_modules/angular2-toaster/toaster.css"; |
app.component에 config를 설정한다.
1 2 3 4 5 6 |
public config: ToasterConfig = new ToasterConfig({ showCloseButton: true, tapToDismiss: false, timeout: 2000 }); |
app.component.html에 컨테이너를 설정한다.
1 |
<toaster-container [toasterconfig]="config"></toaster-container> |
이제 컴포넌트에서 호출하자. 호출하고 폼을 초기화하는 코드도 삽입한다.
1 2 3 4 5 6 7 |
this.adminService.addHero(sendForm) .subscribe(body => { console.log(body); this.toaster.pop('success', 'success', '등록되었습니다!'); // form 초기화 this.form.reset({}); }); |
이제 폼을 초기화하자.