티스토리 뷰

728x90

0. 이 포스트는 가장 많이 사용되는 Reactive Form에 대한 정리 시리즈이다.

 

1. 포스트로 완결성을 위해 별로 중요하지 않은 몇 가지를 작성한다.

 

2. form 전체에 error가 없을 때 submit버튼이 활성화 되도록 하기

  2-1 카드 컴포넌트의 submit 버튼을 disabled 속성에 form group의 valid속성을 사용했다.

 


...

  <button class="btn btn-primary mr-2" [disabled]="!creditFormGroup.valid" type="submit">Submit</button>
  <button class="btn btn-warning" type="reset">Reset</button>
</form>

 

 

 

3. Reset 버튼을 누르면 모든 값을 초기화하는 방법은 2가지가 있는데,

  3-1 현재의 reest 버튼 처럼 type을 reset으로 두는 것이다. 버튼을 누르면 모두 초기화된다.

  3-2 혹시 브라우저의 호환성이 문제가 되면 아래처럼 click이벤트를 달아 주고

    3-2-1 카드 컴포넌트 클래스에 onReset() 메소드 내에서 아래 처럼 reset를 실행한다.

 

  onReset() {
    this.creditFormGroup.reset()
  }
  <button class="btn btn-primary mr-2" [disabled]="!creditFormGroup.valid" type="submit">Submit</button>
  <button class="btn btn-warning" (click)="onReset()">Reset</button>
</form>

 

4. 에러가 발생한 컴포넌트의 input의 테두리에 경고를 표시하는 코드작성

  4-1 수정할 부분은 두 부분이다.

    4-1-1 검증 컴포넌트에 에러가 있고, 작성된 적이 있는지를 체크하는 boolean flag를 만든다.

    4-1-2 추가된 부분은 _hasError 속성과 getter 부분이다.

 

import { Component, Input } from '@angular/core';
import { FormControl } from '@angular/forms';
import { ValidatorService } from '../validator.service';

@Component({
  selector: 'app-validator',
  templateUrl: './validator.component.html',
  styleUrls: ['./validator.component.css']
})
export class ValidatorComponent {

  @Input() control: FormControl = new FormControl()

  _errorMessage: string = ""
  _hasError: boolean = false

  constructor(private validatorService: ValidatorService) { }

  get errorMessage() {

    // if there is an error in the control
    for (let property in this.control.errors) {
      if (this.control.touched) {
        if (this.control.errors.hasOwnProperty(property)) {
          this._errorMessage = this.validatorService.getErrorMessage(
            property, this.control.errors[property]
          )
          return this._errorMessage
        }
      }
    }
    // returns emtpy string only when there is no error
    return '';
  }

  get hasError(): boolean {
    return this.errorMessage && this.control.touched
  }

}

 

    4-1-3 해당 변수를 신용카드 template에서 참조하여 input의 ngClass에 적용한다.

    4-1-4 각 검증 컴포넌트를 참조하기 위해서 #로 시작하는 template varable을 지정하였고,

    4-1-5 이 tempate variable의 hasError 속성을 통하여 각 input에 테두리 색을 지정하였다.

 

<form [formGroup]="creditFormGroup">
  <div class="form-group" >
    <label class="form-label">Name On Card</label>
    <input type="text" class="form-control" formControlName="nameOnCard"
      [ngClass]="{ 'border border-danger': nameOnCard?.hasError }"
    >
    <app-validator [control]="creditFormGroup.get('nameOnCard')" #nameOnCard></app-validator>
  </div>
  <div class="form-group">
    <label class="form-label">Card Number</label>
    <input type="text" class="form-control" mask="0000-0000-0000-0000" formControlName="cardNumber"
      [ngClass]="{ 'border border-danger': cardNumber?.hasError }"
    >
    <app-validator [control]="creditFormGroup.get('cardNumber')" #cardNumber></app-validator>
  </div>
  <div class="form-row">
    <div class="form-group col-6">
      <label class="form-label">Expiration</label>
      <input type="text" class="form-control" mask="00/00" formControlName="expiration"
        [ngClass]="{ 'border border-danger': expiration?.hasError }"
      >
      <app-validator [control]="creditFormGroup.get('expiration')" #expiration></app-validator>
    </div>
    <div class="form-group col-6">
      <label class="form-label">Security Code</label>
      <input type="text" class="form-control" mask="000" formControlName="securityCode"
        [ngClass]="{ 'border border-danger': securityCode?.hasError }"
      >
      <app-validator [control]="creditFormGroup.get('securityCode')" #securityCode></app-validator>
    </div>
  </div>
  <button class="btn btn-primary mr-2" [disabled]="!creditFormGroup.valid" type="submit">Submit</button>
  <button class="btn btn-warning" type="reset" (click)="onReset()">Reset</button>
</form>

 

5. 결과화면

 

 

6. 신용카드 이미지를 가지고 마지막 화면을 구성한다.

  6-0 인터넷을 찾아보면 자유롭게 사용가능하게 공개해 놓은 카드 이미지가 있다.

  6-1 우선 card 컴포넌트를 하나 만들고 공개된 내용을 바로 사용한다.

  6-2 card 컴포넌트의 css 파일 내용이다. 라이센스 부분이 쓰여져 있다.

 

/* Copyright (c) 2020 by Adam Quinlan (https://codepen.io/quinlo/pen/YONMEa)

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */

:host {
  display: block;
  height: 250px;
}

.container {
  width: 100%;
  max-width: 400px;
  max-height: 251px;
  height: 54vw;
  padding: 20px;
}

#ccsingle {
  position: absolute;
  right: 15px;
  top: 20px;
}

#ccsingle svg {
  width: 100px;
  max-height: 60px;
}

.creditcard svg#cardfront,
.creditcard svg#cardback {
  width: 100%;
  -webkit-box-shadow: 1px 5px 6px 0px black;
  box-shadow: 1px 5px 6px 0px black;
  border-radius: 22px;
}

#generatecard {
  float: right;
  font-size: 12px;
  color: #fff;
  padding: 2px 4px;
  background-color: #909090;
  border-radius: 4px;
  float: right;
}

/* CHANGEABLE CARD ELEMENTS */
.creditcard .lightcolor,
.creditcard .darkcolor {
  -webkit-transition: fill 0.5s;
  transition: fill 0.5s;
}

.creditcard .lightblue {
  fill: #03a9f4;
}

.creditcard .lightbluedark {
  fill: #0288d1;
}

.creditcard .red {
  fill: #ef5350;
}

.creditcard .reddark {
  fill: #d32f2f;
}

.creditcard .purple {
  fill: #ab47bc;
}

.creditcard .purpledark {
  fill: #7b1fa2;
}

.creditcard .cyan {
  fill: #26c6da;
}

.creditcard .cyandark {
  fill: #0097a7;
}

.creditcard .green {
  fill: #66bb6a;
}

.creditcard .greendark {
  fill: #388e3c;
}

.creditcard .lime {
  fill: #d4e157;
}

.creditcard .limedark {
  fill: #afb42b;
}

.creditcard .yellow {
  fill: #ffeb3b;
}

.creditcard .yellowdark {
  fill: #f9a825;
}

.creditcard .orange {
  fill: #ff9800;
}

.creditcard .orangedark {
  fill: #ef6c00;
}

.creditcard .grey {
  fill: #bdbdbd;
}

.creditcard .greydark {
  fill: #616161;
}

/* FRONT OF CARD */
#svgname {
  text-transform: uppercase;
}

#cardfront .st2 {
  fill: #ffffff;
}

#cardfront .st3 {
  font-family: 'Source Code Pro', monospace;
  font-weight: 600;
}

#cardfront .st4 {
  font-size: 54.7817px;
}

#cardfront .st5 {
  font-family: 'Source Code Pro', monospace;
  font-weight: 400;
}

#cardfront .st6 {
  font-size: 33.1112px;
}

#cardfront .st7 {
  opacity: 0.6;
  fill: #ffffff;
}

#cardfront .st8 {
  font-size: 24px;
}

#cardfront .st9 {
  font-size: 36.5498px;
}

#cardfront .st10 {
  font-family: 'Source Code Pro', monospace;
  font-weight: 300;
}

#cardfront .st11 {
  font-size: 16.1716px;
}

#cardfront .st12 {
  fill: #4c4c4c;
}

/* BACK OF CARD */
#cardback .st0 {
  fill: none;
  stroke: #0f0f0f;
  stroke-miterlimit: 10;
}

#cardback .st2 {
  fill: #111111;
}

#cardback .st3 {
  fill: #f2f2f2;
}

#cardback .st4 {
  fill: #d8d2db;
}

#cardback .st5 {
  fill: #c4c4c4;
}

#cardback .st6 {
  font-family: 'Source Code Pro', monospace;
  font-weight: 400;
}

#cardback .st7 {
  font-size: 27px;
}

#cardback .st8 {
  opacity: 0.6;
}

#cardback .st9 {
  fill: #ffffff;
}

#cardback .st10 {
  font-size: 24px;
}

#cardback .st11 {
  fill: #eaeaea;
}

#cardback .st12 {
  font-family: 'Rock Salt', cursive;
}

#cardback .st13 {
  font-size: 37.769px;
}

/* FLIP ANIMATION */
.container {
  perspective: 1000px;
}

.creditcard {
  display: flex;
  flex-direction: row;
  justify-content: center;
  width: 100%;
  /* max-width: 400px; */
  -webkit-transform-style: preserve-3d;
  transform-style: preserve-3d;
  transition: -webkit-transform 0.6s;
  -webkit-transition: -webkit-transform 0.6s;
  transition: transform 0.6s;
  transition: transform 0.6s, -webkit-transform 0.6s;
  cursor: pointer;
}

.creditcard .front,
.creditcard .back {
  position: absolute;
  width: 100%;
  max-width: 400px;
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  -webkit-font-smoothing: antialiased;
  color: #47525d;
}

.creditcard .back {
  -webkit-transform: rotateY(180deg);
  transform: rotateY(180deg);
}

.creditcard.flipped {
  -webkit-transform: rotateY(180deg);
  transform: rotateY(180deg);
}

 

  6-3 card 컴포넌트 template이다. 중요한 부분은 정보가 표출될 부분에 적절한 속성이름을 써준다.

    6-3-1 당연하겠지만 신용카드 컴포넌트의 속성이름과 동일하게 하였다.

 

<div class="creditcard">
  <div class="front is-centered">
    <div id="ccsingle"></div>
    <svg
      version="1.1"
      id="cardfront"
      xmlns="http://www.w3.org/2000/svg"
      xmlns:xlink="http://www.w3.org/1999/xlink"
      x="0px"
      y="0px"
      viewBox="0 0 750 471"
      style="enable-background:new 0 0 750 471;"
      xml:space="preserve"
    >
      <g id="Front">
        <g id="CardBackground">
          <g id="Page-1_1_">
            <g id="amex_1_">
              <path
                id="Rectangle-1_1_"
                class="lightcolor grey"
                d="M40,0h670c22.1,0,40,17.9,40,40v391c0,22.1-17.9,40-40,40H40c-22.1,0-40-17.9-40-40V40
                  C0,17.9,17.9,0,40,0z"
              />
            </g>
          </g>
          <path
            class="darkcolor greydark"
            d="M750,431V193.2c-217.6-57.5-556.4-13.5-750,24.9V431c0,22.1,17.9,40,40,40h670C732.1,471,750,453.1,750,431z"
          />
        </g>
        <text
          transform="matrix(1 0 0 1 60.106 295.0121)"
          id="svgnumber"
          class="st2 st3 st4"
        >
          {{ cardNumber }}
        </text>
        <text
          transform="matrix(1 0 0 1 54.1064 428.1723)"
          id="svgname"
          class="st2 st5 st6"
        >
          {{ nameOnCard }}
        </text>
        <text transform="matrix(1 0 0 1 54.1074 389.8793)" class="st7 st5 st8">
          cardholder name
        </text>
        <text transform="matrix(1 0 0 1 479.7754 388.8793)" class="st7 st5 st8">
          expiration
        </text>
        <text transform="matrix(1 0 0 1 65.1054 241.5)" class="st7 st5 st8">
          card number
        </text>
        <g>
          <text
            transform="matrix(1 0 0 1 574.4219 433.8095)"
            id="svgexpire"
            class="st2 st5 st9"
          >
            {{ expiration }}
          </text>
          <text
            transform="matrix(1 0 0 1 479.3848 417.0097)"
            class="st2 st10 st11"
          >
            VALID
          </text>
          <text
            transform="matrix(1 0 0 1 479.3848 435.6762)"
            class="st2 st10 st11"
          >
            THRU
          </text>
          <polygon class="st2" points="554.5,421 540.4,414.2 540.4,427.9 		" />
        </g>
        <g id="cchip">
          <g>
            <path
              class="st2"
              d="M168.1,143.6H82.9c-10.2,0-18.5-8.3-18.5-18.5V74.9c0-10.2,8.3-18.5,18.5-18.5h85.3
              c10.2,0,18.5,8.3,18.5,18.5v50.2C186.6,135.3,178.3,143.6,168.1,143.6z"
            />
          </g>
          <g>
            <g>
              <rect x="82" y="70" class="st12" width="1.5" height="60" />
            </g>
            <g>
              <rect x="167.4" y="70" class="st12" width="1.5" height="60" />
            </g>
            <g>
              <path
                class="st12"
                d="M125.5,130.8c-10.2,0-18.5-8.3-18.5-18.5c0-4.6,1.7-8.9,4.7-12.3c-3-3.4-4.7-7.7-4.7-12.3
                  c0-10.2,8.3-18.5,18.5-18.5s18.5,8.3,18.5,18.5c0,4.6-1.7,8.9-4.7,12.3c3,3.4,4.7,7.7,4.7,12.3
                  C143.9,122.5,135.7,130.8,125.5,130.8z M125.5,70.8c-9.3,0-16.9,7.6-16.9,16.9c0,4.4,1.7,8.6,4.8,11.8l0.5,0.5l-0.5,0.5
                  c-3.1,3.2-4.8,7.4-4.8,11.8c0,9.3,7.6,16.9,16.9,16.9s16.9-7.6,16.9-16.9c0-4.4-1.7-8.6-4.8-11.8l-0.5-0.5l0.5-0.5
                  c3.1-3.2,4.8-7.4,4.8-11.8C142.4,78.4,134.8,70.8,125.5,70.8z"
              />
            </g>
            <g>
              <rect x="82.8" y="82.1" class="st12" width="25.8" height="1.5" />
            </g>
            <g>
              <rect x="82.8" y="117.9" class="st12" width="26.1" height="1.5" />
            </g>
            <g>
              <rect x="142.4" y="82.1" class="st12" width="25.8" height="1.5" />
            </g>
            <g>
              <rect x="142" y="117.9" class="st12" width="26.2" height="1.5" />
            </g>
          </g>
        </g>
      </g>
      <g id="Back"></g>
    </svg>
  </div>
  <div class="back">
    <svg
      version="1.1"
      id="cardback"
      xmlns="http://www.w3.org/2000/svg"
      xmlns:xlink="http://www.w3.org/1999/xlink"
      x="0px"
      y="0px"
      viewBox="0 0 750 471"
      style="enable-background:new 0 0 750 471;"
      xml:space="preserve"
    >
      <g id="Front">
        <line class="st0" x1="35.3" y1="10.4" x2="36.7" y2="11" />
      </g>
      <g id="Back">
        <g id="Page-1_2_">
          <g id="amex_2_">
            <path
              id="Rectangle-1_2_"
              class="darkcolor greydark"
              d="M40,0h670c22.1,0,40,17.9,40,40v391c0,22.1-17.9,40-40,40H40c-22.1,0-40-17.9-40-40V40
              C0,17.9,17.9,0,40,0z"
            />
          </g>
        </g>
        <rect y="61.6" class="st2" width="750" height="78" />
        <g>
          <path
            class="st3"
            d="M701.1,249.1H48.9c-3.3,0-6-2.7-6-6v-52.5c0-3.3,2.7-6,6-6h652.1c3.3,0,6,2.7,6,6v52.5
          C707.1,246.4,704.4,249.1,701.1,249.1z"
          />
          <rect x="42.9" y="198.6" class="st4" width="664.1" height="10.5" />
          <rect x="42.9" y="224.5" class="st4" width="664.1" height="10.5" />
          <path
            class="st5"
            d="M701.1,184.6H618h-8h-10v64.5h10h8h83.1c3.3,0,6-2.7,6-6v-52.5C707.1,187.3,704.4,184.6,701.1,184.6z"
          />
        </g>
        <text
          transform="matrix(1 0 0 1 621.999 227.2734)"
          id="svgsecurity"
          class="st6 st7"
        >
          123
        </text>
        <g class="st8">
          <text
            transform="matrix(1 0 0 1 518.083 280.0879)"
            class="st9 st6 st10"
          >
            security code
          </text>
        </g>
        <rect x="58.1" y="378.6" class="st11" width="375.5" height="13.5" />
        <rect x="58.1" y="405.6" class="st11" width="421.7" height="13.5" />
        <text
          transform="matrix(1 0 0 1 59.5073 228.6099)"
          id="svgnameback"
          class="st12 st13"
        >
          {{ nameOnCard }}
        </text>
      </g>
    </svg>
  </div>
</div>

 

  6-4 카드 컴포넌트 클래스

    6-4-1 데이터를 받을 4개의 속성을 지정한다.

 

import { Component, OnInit, Input } from '@angular/core';

@Component({
  selector: 'app-card',
  templateUrl: './card.component.html',
  styleUrls: ['./card.component.css']
})
export class CardComponent implements OnInit {

  @Input() nameOnCard: string = ""
  @Input() cardNumber: string = ""
  @Input() expiration: string = ""
  @Input() securityCode: string = ""

  constructor() { }

  ngOnInit(): void {
  }

}

 

  6-5 신용카드 컴포넌트에 이 컴포넌트를 사용한다.

    6-5-1 credit-card-form.component.html 최종버전이다.

 

<app-card
  [nameOnCard]="creditFormGroup.controls.nameOnCard.value"
  [cardNumber]="creditFormGroup.controls.cardNumber.value"
  [expiration]="creditFormGroup.controls.expiration.value"
  [securityCode]="creditFormGroup.controls.securityCode.value"
></app-card>
<form [formGroup]="creditFormGroup">
  <div class="form-group" >
    <label class="form-label">Name On Card</label>
    <input type="text" class="form-control" formControlName="nameOnCard"
      [ngClass]="{ 'border border-danger': nameOnCard?.hasError }"
    >
    <app-validator [control]="creditFormGroup.get('nameOnCard')" #nameOnCard></app-validator>
  </div>
  <div class="form-group">
    <label class="form-label">Card Number</label>
    <input type="text" class="form-control" mask="0000-0000-0000-0000" formControlName="cardNumber"
      [ngClass]="{ 'border border-danger': cardNumber?.hasError }"
    >
    <app-validator [control]="creditFormGroup.get('cardNumber')" #cardNumber></app-validator>
  </div>
  <div class="form-row">
    <div class="form-group col-6">
      <label class="form-label">Expiration</label>
      <input type="text" class="form-control" mask="00/00" formControlName="expiration"
        [ngClass]="{ 'border border-danger': expiration?.hasError }"
      >
      <app-validator [control]="creditFormGroup.get('expiration')" #expiration></app-validator>
    </div>
    <div class="form-group col-6">
      <label class="form-label">Security Code</label>
      <input type="text" class="form-control" mask="000" formControlName="securityCode"
        [ngClass]="{ 'border border-danger': securityCode?.hasError }"
      >
      <app-validator [control]="creditFormGroup.get('securityCode')" #securityCode></app-validator>
    </div>
  </div>
  <button class="btn btn-primary mr-2" [disabled]="!creditFormGroup.valid" type="submit">Submit</button>
  <button class="btn btn-warning" type="reset" (click)="onReset()">Reset</button>
</form>

 

 

7. 몇 가지 regex 패턴을 적어 놓는다.

  7-1 신용카드 패턴, email 패턴, 비밀번호 패턴이다.

 

  static creditCardValidator(control) {
    // Visa, MasterCard, American Express, Diners Club, Discover, JCB
    if (
      control.value.match(
        /^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$/
      )
    ) {
      return null;
    } else {
      return { invalidCreditCard: true };
    }
  }

  static emailValidator(control) {
    // RFC 2822 compliant regex
    if (
      control.value.match(
        /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/
      )
    ) {
      return null;
    } else {
      return { invalidEmailAddress: true };
    }
  }

  static passwordValidator(control) {
    // {6,100}           - Assert password is between 6 and 100 characters
    // (?=.*[0-9])       - Assert a string has at least one number
    if (control.value.match(/^(?=.*[0-9])[a-zA-Z0-9!@#$%^&*]{6,100}$/)) {
      return null;
    } else {
      return { invalidPassword: true };
    }
  }
728x90
댓글