반응형

환경 설정

* 환경 : firebase DB & REST API

 

1. firebase에서 프로젝트를 선택(or 생성)합니다.

2. Database -> 규칙

{
  "rules": {
    ".read": "auth != null",
    ".write": "auth != null"
  }
}

3. Authntication -> 로그인 방법 -> 이메일/비밀번호 활성화


유저 생성

 

아래와 같이 구글에서 firebase auth rest api를 검색합니다.

https://firebase.google.com/docs/reference/rest/auth

 

Firebase Auth REST API  |  Firebase

API Usage You can query the Firebase Auth backend through a REST API. This can be used for various operations such as creating new users, signing in existing ones and editing or deleting these users. Throughout this document, API_KEY refers to the Web API

firebase.google.com


사이트 오른쪽 목차에서 Sign up with email / password 로 아래와 같은 조건을 만족시키는 통신을 합니다.

 

Method: POST

Content-Type: application/json

Request Body Payload

Property NameType Description
email string 유저 생성을 위한 email
password string 유저 생성을 위한 비밀번호
returnSecureToken boolean

ID 와 새로고침 토큰을 반환 하는지 안하는지, 항상 true입니다.

 

post로 https://identitytoolkit.googleapis.com/v1/accounts:signUp?key=[API_KEY] 를 합니다.

API_KEY는 화면 왼쪽 drawer 에서 톱니바퀴(설정)을 눌러 확인할 수 있습니다.

내 프로젝트 [웹 API 키]를 복사하여 이용합니다.

// auth.service.ts
signup(email: string, password: string) {
  return this.http
    .post(
    'https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser?key=AIzaSyC4NdTkUmluvTX-wfuFKVZPdFCXvZKiWEQ',
    {
      email: email,
      password: password,
      returnSecureToken: true
    }
)

Observable을 반환하기 때문에 return을 사용해줍니다.

Response Payload

Property NameType Description
idToken string 신규 유저의 Auth ID 토큰.
email string 신규 유저를 위한 email.
refreshToken string 신규 유저를 위한 Auth 새로고침 토큰.
expiresIn string ID 토큰의 만료를 초를 숫자로 나타냅니다.
localId string The uid of the newly created user.

리스폰스에 알맞는 데이터를 받기 위해 아래와 같은 인터페이스를 만들어 포스트의 리턴타입을 입력합니다.

// auth.service.ts
export interface AuthResponseData {
  idToken: string;
  email: string;
  refreshToken: string;
  expiresIn: string;
  localId: string;
}

signup(email: string, password: string) {
  return this.http
  .post<AuthResponseData>(
  'https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser?key=AIzaSyC4NdTkUmluvTX-wfuFKVZPdFCXvZKiWEQ',
  {
    email: email,
    password: password,
    returnSecureToken: true
  }
)
// auth.component.ts

onSubmit(form: NgForm) {
  if (!form.valid) {
    return;
  }
  const email = form.value.email;
  const password = form.value.password;
  
  this.authService.subscribe(
    resData => {
      console.log(resData);
    },
    errorMessage => {
      console.log(errorMessage);
    }
  );
  form.reset();
}

로딩 스피너

1. 컴포넌트 만들기

share폴더에

  loading-spinner 폴더와 ts, css파일을 생성합니다.

    loading-spinner.component.ts

    loading-spinner.component.css

 

구글에 검색합니다. css loading spinners https://loading.io/css/

해당 사이트에서 원하는 스피너를 선택한 뒤에, css에 값과 html값을 복붙합니다.

// loading-spinner.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-loading-spinner',
  template:
    '<div class="lds-ring"><div></div><div></div><div></div><div></div></div>',
  styleUrls: ['./loading-spinner.component.css']
})
export class LoadingSpinnerComponent {}

2. HTML에 추가하기

<!-- auth.component.html -->

<div class="row">
  <div class="col-xs-12 col-md-6 col-md-offset-3">
    <div *ngIf="isLoading">
      <app-loading-spinner></app-loading-spinner>
    </div>
    <form #authForm="ngForm" (ngSubmit)="onSubmit(authForm)" *ngIf="!isLoading">
      <!-- ... -->
    </form>
  </div>
</div>

form 태그에 isLoading을 이용하여 조건별로 나타날 수 있도록 합니다.

에러 핸들링

UI를 위해 error를 추가합니다.

<!-- auth.component.html -->

<div class="row">
  <div class="col-xs-12 col-md-6 col-md-offset-3">
  <div class="alert alert-danger" *ngIf="error">
      <p>{{ error }}</p>
    </div>
    <div *ngIf="isLoading">
      <app-loading-spinner></app-loading-spinner>
    </div>
    <form #authForm="ngForm" (ngSubmit)="onSubmit(authForm)" *ngIf="!isLoading">
      <!-- ... -->
    </form>
  </div>
</div>
// auth.component.ts

isLoading = false;
error: string = null;

onSubmit(form: NgForm) {
  if (!form.valid) {
    return;
  }
  const email = form.value.email;
  const password = form.value.password;
  this.isLoading = true;
  
  this.authService.subscribe(
    resData => {
      console.log(resData);
      this.isLoading = false;
    },
    errorMessage => {
      console.log(errorMessage);
      this.error = errorMessage;
      this.isLoading = false;
    }
  );
  form.reset();
}

 

firebase auth rest api 에서 조금 더 아래로 내리면 아래와 같은 내용을 발견할 수 있습니다.

 

Common error codes

  • EMAIL_EXISTS: The email address is already in use by another account.
  • OPERATION_NOT_ALLOWED: Password sign-in is disabled for this project.
  • TOO_MANY_ATTEMPTS_TRY_LATER: We have blocked all requests from this device due to unusual activity. Try again later.

errorRes를 보면, 

error안에 error가 있고, message를 보면, EAMIL_EXISTS와 같은 코드를 받을 수 있습니다.

 

에러 코드를 이용하면 사용자에게 쉽게 안내할 수 있습니다.

errorRes => {
  console.log(errorRes);
  switch (errorRes.error.error.message) {
  case 'EMAIL_EXISTS':
  this.error = 'This email exists already';
  break;
  case 'EMAIL_NOT_FOUND':
  this.error = 'This email does not exist.';
  break;
  case 'INVALID_PASSWORD':
  this.error = 'This password is not correct.';
  break;
  }
}

로그인 만료 시간 핸들링

로그인 하기 전에는 브라우져로 다시 들어갈 경우, 지속적으로 로그인 상태가 됩니다.

이런 상황을 방지하기 위한 방법 입니다.

 

Response Payload

Property NameTypeDescription

expiresIn string The number of seconds in which the ID token expires.

expiresIn은 ID토큰의 만료 시간입니다.

const expirationDate = new Date(new Date().getTime() + expiresIn * 1000);

곱하기 1000을 해서 밀리세컨드(ms = 1/1000 초)를 하여 생성되는 아이디의 초과 시간을 Local에 저장해 둡니다.


Auto Loggin

 

로컬 스토리지에 userData저장.

localStorage.setItem('userData', JSON.stringify(user)); // JSON.stringify()를 꼭사용해주세요.

 

불러오기

autoLogin() {

	// userData snapshot을 불러옵니다.
    const userData: {
      email: string;
      id: string;
      _token: string;
      _tokenExpirationDate: string;
    } = JSON.parse(localStorage.getItem('userData')); // JSON.parse로 반드시 파싱합니다.
    if (!userData) { // 데이터가 없을경우 끗.
      return;
    }

    const loadedUser = new User(
      userData.email,
      userData.id,
      userData._token,
      new Date(userData._tokenExpirationDate)
    );

	// 로그인 제한시간을 확인합니다.
    if (loadedUser.token) {
      this.user.next(loadedUser);
      const expirationDuration =
        new Date(userData._tokenExpirationDate).getTime() -
        new Date().getTime();
      this.autoLogout(expirationDuration);
    }
  }

Guard (Router Protection)

// auth.guard.ts
import {
  CanActivate,
  ActivatedRouteSnapshot,
  RouterStateSnapshot
} from '@angular/router';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { AuthService } from './auth.service';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    router: RouterStateSnapshot
  ):
    | boolean
    | Promise<boolean>
    | Observable<boolean> {
    return this.authService.user.pipe(
      map(user => {
        return !!user;
      })
    );
  }
}

Activate 인터페이슨는 route와 state(RouterStateSnapshot)을 필요로 합니다.

또한 boolean, Promise<boolean>, Observable<boolean>의 형태로 반환해야 합니다.

Authenticated User는 pipe(map())으로 묶어서 반환합니다.

 

// app-routing.module.ts

canActivate: [AuthGuard]//를 추가합니다

리다이렉트 시키기!

URLTree를 이용하여 원하는 곳으로 이동시킬 수 있습니다.

constructor(private authService: AuthService, private router: Router) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    router: RouterStateSnapshot
  ):
    | boolean
    | UrlTree
    | Promise<boolean | UrlTree>
    | Observable<boolean | UrlTree> {
    return this.authService.user.pipe(
      take(1), // 불필요한 subscribe를 방지합니다. 1번만 실행.
      map(user => {
        const isAuth = !!user;
        if (isAuth) {
          return true;
        }
        return this.router.createUrlTree(['/auth']); // URLTree를 생성하여 login하도록 리다이렉트.
      })
      // angular 옛 버전에서는 직접 리다이렉트 주소를 기입해야 합니다.
      // tap(isAuth => {
      //   if (!isAuth) {
      //     this.router.navigate(['/auth']); // 일일히 리다이렉트 주소 입력.
      //   }
      // })
    );

 

 

반응형

'프로그래밍 > Angular' 카테고리의 다른 글

Service VS EventEmitter  (0) 2020.02.19
Dynamic Component  (0) 2019.09.13
Http  (0) 2019.08.20
Pipe  (0) 2019.08.15
Form - 복습 및 보충 내용  (0) 2019.08.14
반응형

http.get

import { HttpClient } from '@angular/common/http';

private fetchPosts() {
    this.http
      .get('https://PROJECT_NAME.firebaseio.com/posts.json')
      .subscribe(posts => {
        console.log(posts);
      });
  }

HttpClient를 이용하여, 매우 간단하게 가져올 수 있습니다.


자료 가공하기

import { map } from 'rxjs/operators';

id라는 key를 추가하여 post를 get하는 코드입니다.

private fetchPosts() {
    this.http
      .get('https://ng-complete-guide-be717.firebaseio.com/posts.json')
      .pipe(
        map(responseData => {
          const postsArray = [];
          for (const key in responseData) {
            if (responseData.hasOwnProperty(key)) {
              postsArray.push({ ...responseData[key], id: key });
            }
          }
          return postsArray;
        })
      )
      .subscribe(posts => {
        // ...
        console.log(posts);
      });
  }

get()와 subscribe()사이에 pipemap으로 변경하였습니다.

 

pipe는 앞에서 사용해 보았듯이, 

여러가지 operator로 observable 데이터를 가공(연산)할 수 있도록 합니다.

 

여기선 map Operator를 사용하였습니다.

한번더 observable로 레핑(wrapping)하는 역할입니다.

 

id는 랜덤으로 설정되는 글자이기 때문에 string으로 하였습니다.

 

결과는 캡쳐화면처럼 id의 key:value를 response에 추가해 줍니다.


Type사용하기.

export interface Post {
  title: string;
  content: string;
  id?: string; // optional 물음표 보이시죠? 있을수도 있고 없을 수도 있습니다.
}

먼저 Post를 선언합니다.

 

private fetchPosts() {
		...
      .pipe(
        map((responseData: {[key: string]: Post}) => {
          const postsArray = [];
		    ...
          return postsArray;
        })
      )
		...
  }

map의 데이터의 타입을 지정해줍니다. key는 string으로 되어있고, value는 Post로 합니다.

 

이렇게 하여 response데이터의 타입을 알려주게 됩니다.

또한 맵 안에 있는 postsArray도 타입을 지정할 수 있게됩니다.

private fetchPosts() {
		...
      .pipe(
        map((responseData: {[key: string]: Post}) => {
          const postsArray: Post[] = [];
		    ...
          return postsArray;
        })
      )
		...
  }

 

조금 더 편리한 방법은 response body의 타입을 지정해 주는 방식입니다.

private fetchPosts() {
    this.http
      .get<{[key: string]: Post}>('https://PROJECT_NAME.firebaseio.com/posts.json')
      .pipe(
        map(responseData => {
          const postsArray: Post[] = [];
          for (const key in responseData) {
            if (responseData.hasOwnProperty(key)) {
              postsArray.push({ ...responseData[key], id: key });
            }
          }
          return postsArray;
        })
      )
      .subscribe(posts => {
        // ...
        console.log(posts);
      });
  }

get에 <>를 추가하여 제네릭을 추가합니다.

따로 map과 같은 operator를 사용할 때마다 지정하지 않아도 body와 동일한 타입으로 인식합니다.


Post에도 response body 타입 지정해보기.

onCreatePost(postData: { title: string; content: string }) {
    // Send Http request
    this.http
      .post(
        'https://PROJECT_NAME.firebaseio.com/posts.json',
        postData
      )
      .subscribe(responseData => {
        console.log(responseData);
      });
  }

위와 같은 코드가 있을 경우, subscribe의

responseData는 (parameter) responseData: Object로 나타납니다.

 

 

하지만 .post<name: string> 을 추가할 경우,

onCreatePost(postData: { title: string; content: string }) {
    // Send Http request
    this.http
      .post<{name: string}>(
        'https://PROJECT_NAME.firebaseio.com/posts.json',
        postData
      )
      .subscribe(responseData => {
        console.log(responseData);
      });
  }

responseData는 (parameter) responseData: {name: string;}으로 나타납니다.


Error Handling

 

http에서 지원하는 error사용.

.subscribe(
  (posts: Post[])=> {
    this.isFetching = false;
    this.loadedPosts = posts;
  }, (error) => {
    console.log(error);
    this.error = error.message;
});

console.log(error)의 내용 입니다.

여러곳에서 에러를 다루어야할 경우, Subject를 생성합니다.

// service.ts
import { Subject} form 'rxjs';

error = new Subject<string>();

.subscribe(
	...
  },
  error => {
  	this.error.next(error.message);
  }
)

다른 곳에서 service.error.subscribe()를 통하여 에러메시지를 핸들링 할 수 있습니다.

 

catchError를 이용하여, analytics server등을 던질 수 있습니다.

import { throwError } form 'rxjs';

  ...
,
catchError(errorRes => {
	return throwError(errorRes);
});

제네릭 에러를 핸들링하는 작업이 필요할 경우 사용하면 됩니다.


Header Setting

import { HttpHeaders } from '@angular/common/http';

.get<{ [key: string]: Post }>(
  'https://ng-complete-guide-be717.firebaseio.com/posts.json',
  {
    headers: new HttpHeaders({'Custom-Header': 'Hello'})
  }
)

hangular/common/http 에서 지원하는 HttpHeaders를 이용하면, 원하는 Header를 추가할 수 있습니다.

해더에 추가한 대로, Custom-Header에 Hello라는 key-value pair로 나오죠?


파라미터 추가하기

 

import { HttpPramas } from '@angular.common,http';

params: new HttpParams().set('print', 'pretty')
let searchParams = new HttpParams(); // multi. 파라미터 추가하기.
searchParams = searchParams.append('print', 'pretty');
searchParams = searchParams.append('custom', 'key');

javascript로 데이터 받기(data type 변경하기)

2번째 argu.에 observe를 추가합니다.

this.http
  .post<{ name: string }>(
    'https://ng-complete-guide-c56d3.firebaseio.com/posts.json',
    postData,
    {observe: 'response'}
)

tap operator

tap 연산자는 Observable을 디버깅 할 수 있도록 정확한 값이나, 다른 사이드 이팩트에 유용합니다.

Note: Observable의 측면에서 subscribe와는 다릅니다. 설령 subscribe하지 않은 Observable이 tap에 의해서 return되어도 , Observer에 의한 사이드 이팩트는 절대 발생하지 않습니다. tap은 단순히 존재하는 실행만 엿볼뿐, subscribe처럼 이벤트를 실행하지 않습니다.

 

Response body Type 변경하기

deletePosts() {
  return this.http.delete(
    'https://ng-complete-guide-c56d3.firebaseio.com/posts.json', {
    observe: 'events',
    responseType: 'json'
    }
  );
}

interceptors

모든 

// auth-intercptor.service.ts

import { HttpInterceptor, HttpRequest, HttpHandler } from '@angular/common/http';

export class AuthInterceptorService implements HttpInterceptor {
    intercept(req: HttpRequest<any>, next: HttpHandler) {
      console.log('Resquest in an Its way');
      return next.handle(req);
    }
}
// app.module.ts

providers: [
  {
    provide: HTTP_INTERCEPTORS, // 토큰
    useClass: AuthInterceptorService,
    multi: true
  } 
], 

Header 추가하기

intercept(req: HttpRequest<any>, next: HttpHandler) {
  console.log(req.url);
  const modifiedRequest = req.clone({headers: req.headers.append('Auth', 'xyz')})
  console.log('Resquest in an Its way');
  return next.handle(modifiedRequest);
}

로깅 인터셉터

// logging-interceptor.service.ts

import { HttpInterceptor, HttpRequest, HttpHandler, HttpEventType } from '@angular/common/http';
import { tap } from 'rxjs/operators';

export class LoggingIntercptorService implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler) {
    console.log('Outgoing request');
    console.log(req.url);
    console.log(req.headers);
    return next.handle(req).pipe(
      tap(event => {
        if (event.type === HttpEventType.Response) {
          console.log('Incoming response');
          console.log(event.body);
      }
    }));
  }
}
// app.module.ts

providers: [
    {
      provide: HTTP_INTERCEPTORS, // 토큰
      useClass: AuthInterceptorService,
      multi: true
    },
    {
      provide: HTTP_INTERCEPTORS,
      useClass: LoggingIntercptorService,
      multi: true
    }
  ], 

여러개의 인터셉터를 사용할 경우에는 순서매우 중요합니다.

providers에서 Logging은 뒤쪽에 위치해야 합니다.

 

 

반응형

'프로그래밍 > Angular' 카테고리의 다른 글

Dynamic Component  (0) 2019.09.13
Authentication & Route Protection  (0) 2019.09.06
Pipe  (0) 2019.08.15
Form - 복습 및 보충 내용  (0) 2019.08.14
FormHandling - Reavice  (0) 2019.07.02
반응형

Pipe는 보이는 값(view)에만 영향을 미칩니다.

당연히 template에 로직이 있는게 정석이죠.

 

예제) username을 대문자로 나타내시오.

 

username = 'kim' 일 경우, 템플릿에 {{username}}이라고 합니다.

pipe는 {{username | uppercase}} 로,  '|' 뒤에 붙여서 output만 변경 시킵니다.

 

pipe 연산 순서

보통 왼쪽에서 오른쪽으로 -> 연산하게 됩니다.

{{server.start | date:'fullDate' | uppercase }} // Mon Aug 09 1920 00:00:00 GMT+0900 (한국 표준시)

{{server.start | date:'fullDate' | uppercase }} // 에러가 발생합니다. Object를 uppercase할 수 없기 때문

API : https://angular.io/api?query=pipe


커스텀 pipe 만들기

 

10글자 이상일 경우 뒤에 ... 붙여서 보여주기.

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'shorten'
})
export class ShortenPipe implements PipeTransform {
  transform(value: any) {
    if (value.length > 10) {
      return value.substr(0, 10) + '...';
    }
    return value;
  }
}

 

Pipe데코레이터를 선언합니다.

impements PipeTransform 인만큼 transform매쏘드가 필요합니다. (1개 이상의 파라미터를 요구합니다.)

반드시 return값이 있어야 겠져?

 

app.moule.ts에서 declarations에 'ShortenPipe'를(class name) 추가해 줍니다

 

{{servername | shorten}} // name을 적어주면 됩니다.


pipe로 array 필터링 하기

 

input(필터)에 속성 값을 입력한 것이 나오도록 하는 필터 입니다.

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'filter'
})
export class FilterPipe implements PipeTransform {

  transform(value: any, filterString: string, propName: string): any {
    if ( value.length === 0 || filterString === '') {
      return value;
    }
    const resultArray = [];
    for (const item of value) {
      if (item[propName] === filterString) {
        resultArray.push(item);
      }
    }
    return resultArray;
  }
}

필터 값이 비어있다면, 필터링 되지 않은 전체를 보여줍니다.

여기서는 서버의 상태(status)를 필터 합니다

<div class="container">
  <div class="row">
    <div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
      <input type="text" [(ngModel)]="filterStatus">
      <hr>
      <ul class="list-group">
        <li
          class="list-group-item"
          *ngFor="let server of servers | filter:filterStatus:'status'"
          [ngClass]="getStatusClasses(server)">
          <span
            class="badge">
            {{ server.status }}
          </span>
          <strong>{{ server.name }}</strong> | {{ server.instanceType }} | {{ server.started }}
        </li>
      </ul>
    </div>
  </div>
</div>

2-way-bind할 수 있도록 [(ngModel)]을 씁니다. 당연히 ts.file에 filterStatus라는 변수가 선언되어야 겠죠?

 

*ngFor뒤쪽에 pipe를 추가합니다 (filter)

*ngFor="let server of servers | filter:filterStatus:'status'"

filter (pipeName)

:filterStatus (첫번째 파이프의 파라미터) // 여기에선 사용자가 입력한 값 입니다.

:'status' 대상값이 됩니다. 

아래와 같은 Object에서 name에 해당하는 status를 대상으로 예제를 진행 합니다.

 

{
  instanceType: 'medium',
  name: 'Production Server',
  status: 'stable',
  started: new Date(15, 1, 2017)
},

위와 같은 실행 결과가 나오게 됩니다.

 


@Pipe({
  name: 'filter',

  pure : false // 페이지의 값이 변경될 때마다 계산을 합니다. (매우 퍼포먼스가 좋지 않기 때문에 사용에 주의하시기 바랍니다.)
})

 

async와 같은 경우, 데이터가 변할 지라도, pure:false가 적용되지 않습니다. 그럴땐.

{{appStatus | async}}

server request와 같은 async 자료는 아래와 같이 표기를 해주면 됩니다. 

반응형

'프로그래밍 > Angular' 카테고리의 다른 글

Authentication & Route Protection  (0) 2019.09.06
Http  (0) 2019.08.20
Form - 복습 및 보충 내용  (0) 2019.08.14
FormHandling - Reavice  (0) 2019.07.02
Handling Form - Template Driven  (0) 2019.07.02
반응형

Template Driven

 

-HTML

form태그 (ngSubmit)='onAddItem(f)" #f="ngForm"

앵귤러 submit과, 로컬레퍼런스 f를 선언합니다.

Validation 속성 : pattern 정규식, required가 있습니다.

<!-- child HTML for form -->

<form (ngSubmit)="onAddItem(f)" #f="ngForm">
  <div class="row">
    <div class="col-sm-2 form-group">
    <label for="amount">Amount</label>
    <input
    type="number"
    id="amount"
    class="form-control"
    name="amount"
    ngModel
    required
    pattern="^[1-9]+[0-9]*$"
    >
    </div>
  </div>
  <div class="row">
    <div class="col-xs-12">
      <button class="btn btn-success" type="submit" [disabled]="!f.valid">Add</button>
      <button class="btn btn-danger" type="button">Delete</button>
      <button class="btn btn-primary" type="button">Clear</button>
    </div>
  </div>
</form>

-TS

// child TS

onAddItem(form: NgForm) {
  const value = form.value;
  const newIngredient = new Ingredient(value.amount);
  this.slService.addIngredient(newIngredient);
}

NgForm을 form.value로 값에 접근할 수 있습니다.

 

아이템 값 불러오기 - Index이용.

 

1. Array에서 인덱스 값을 가져옴.

2. service를 이용하여 인덱스 값을 넘김.

3. subscribe로 child에 값을 받음.

 

// service.ts

startedEditng = new Subject<number>();

서비스에 subscribe할 수 있도록 Subject를 만들고, index는 number이기 때문에 제네릭 타입을 number로 지정.

 

-HTML

<!-- parent HTML -->

<ul class="list-group">
  <a
    class="list-group-item"
    style="cursor: pointer"
    *ngFor="let ingredient of ingredients; let i = index"
    (click)="onEditItem(i)"
    >
    {{ ingredient.name }} ({{ ingredient.amount }})
  </a>
</ul>

*ngFor에 let i = index로 index를 지정함.

(click)이벤트를 이용하여 index값을 ts로 넘김.

// parent TS

onEditItem(index: number) {
  this.slService.startedEditng.next(index);
}

child의 ngOnInit()에서 subscribe로 해당 HTML에 값을 입력합니다.

// child TS

@ViewChild('f') slForm: NgForm; // 랜더링에 맞는 view를 가져오기 위함.
  subscription: Subscription;
  editedItemIndex: number;
  editedItem: Ingredient;
//  editMode = false;

  constructor(private slService: ShoppingListService) { }

  ngOnInit() {
    this.subscription = this.slService.startedEditng
    .subscribe(
      (index: number) => {
//      	this.editMode = true;
        this.editedItemIndex = index;
        this.editedItem = this.slService.getIngredient(index);
        this.slForm.setValue({
        name: this.editedItem.name,
        amount: this.editedItem.amount
        })
      }
    );
  }
  
  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

Subscription을 만들어 둡니다. (destroy를 위해...)

 

1. startedEditing을 subscribe합니다: 해당 parameter였던 index를 이용하여 값을 설정합니다.

2. index를 이용하여 service에서 Array[index]의 값을 받아옵니다.

ViewChild로 되어있는 NgForm에 SetValue()를 이용하여 값을 설정해 줍니다.

 

어레이 아이템 삭제하기 - view단에서 실수 요소 차단하기

 

아이템 값 불러오기 - Index이용. 에서 editedItemIndex와 같은 변수를 매소드의 const로 쓰지 않아서 이상하다고 생각하셨죠?

 

주석으로 된 editMode를 해제하고 아래와 같은 HTML을 생성합니다.

<button 
  class="btn btn-danger" 
  type="button"
  (click)="onDelete()"
  *ngIf="editMode"
  >Delete</button>

ngIf로, editMode일 때만 활성화 하도록 합니다.

onDelete() {
  this.slService.deleteIngredient(this.editedItemIndex);
  this.onClear();
}

그렇게 되면, TS파일 내부에서 editMode를 따로 확인할 필요 없으면서, 클라이언트의 실수를 줄일 수 있습니다.

물론 코딩하는 스타일마다 다르지만 지금은 Template Driven입니다. 매우 유용한 스킬이라 생각됩니다.

 


ReactiveFormsModule

 

HTML에 

FormGorup 바인딩 - [formGroup]="formName"

<form [formGroup]="recipeForm" (ngSubmit)="onSubmit()"> ... </form>

FormControl 바인딩 - formControlName 속성

<input 
  type="text"
  id="name"
  formControlName="name"
  class="form-control">

 

버튼을 눌렀을 때, 중복이 submit이 될 때 - type="button"

<button 
  type="button"
  class="btn btn-success"
  (click)="onAddIngredient()"
  >Add Ingredient</button>

Fixing a Bug

form array의 controls에 접속하는 코드에서 문제가 발생할 수 있습니다:

*ngFor="let ingredientCtrl of recipeForm.get('ingredients').controls; let i = index"

이 코드는 Angular 최신버전에서 실패 할겁니다.

.ts 파일에서 매소드를 만들어 controls를 얻을 수 있습니다.

getControls() {
	return (<FormArray>this.recipeForm.get('ingredients')).controls;
}

템플릿에서는 이렇게 작성해 주셔야 합니다:

*ngFor="let ingredientCtrl of getControls(); let i = index"

대체방안으로, getter를 사용하는 캐스팅 구문을 사용할 수 있습니다:

get controls() {
	return (this.recipeForm.get('ingredients') as FormArray).controls;
}

그리고 템플릿은 이렇게 변경해주세요:

*ngFor="let ingredientCtrl of controls; let i = index"

image 미리보기 기능

<div class="row">
  <div class="col-xs-12">
    <div class="form-group">
      <label for="imagePath">Image URL</label>
      <input 
        type="text"
        id="imagePath"
        formControlName="imagePath"
        class="form-control"
        #imagePath>
    </div>
  </div>
</div>
<div class="row">
  <div class="col-xs-12">
	<img [src]="imagePath.value" class="img-responsive">
  </div>
</div>

로컬 레퍼런스 #imagePath

[src]="imagePath.value" 이렇게 사용합니다.


FromArray의 모든 아이템 삭제하기

 

Angular 8에서, 모든 아이템을 제거하는 새로운 방식이 있습니다.

(<FormArray>this.recipeForm.get('ingredients')).clear();

clear()매쏘드는 FormControls(혹은 FormGorups)에 등록된 모든 곳을 돌면서 자동적으로 제거해 줍니다.

removeAt()으로 일일이 루프를 돌면서 할 필요 없습니다.

반응형

'프로그래밍 > Angular' 카테고리의 다른 글

Http  (0) 2019.08.20
Pipe  (0) 2019.08.15
FormHandling - Reavice  (0) 2019.07.02
Handling Form - Template Driven  (0) 2019.07.02
Observable & Subject  (0) 2019.06.26
반응형

qpp.module.ts 에서 imports에 - ReactiveFormsModule추가하기

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    ReactiveFormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

 

 

이미지 처럼 3개의 필드가 있는 모양을 TypeScript를 이용하여 만들것입니다.


FormGroup 생성하기.

import { Component, OnInit } from '@angular/core';
import { FormGroup, FormControl } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  genders = ['male', 'female'];
  signupForm: FormGroup;

  ngOnInit() {
    // initialize 
    this.signupForm = new FormGroup({
      'username': new FormControl(null),
      'email': new FormControl(null),
      'gender': new FormControl('male')
    });
  }
}

FormGroup에 각 항목 마다 key-value pair로 값을 지정해 줍니다.

key = 항목 명

value = FormControl객체


HTML Input과 바인딩(syncronize)하기

<form>
  <div class="form-group">
    <label for="username">Username</label>
    <input
      type="text"
      id="username"
      class="form-control">
    </div>
    <div class="form-group">
    <label for="email">email</label>
    <input
      type="text"
      id="email"
      class="form-control">
    </div>
    <div class="radio" *ngFor="let gender of genders">
    <label>
      <input
        type="radio"
        [value]="gender">{{ gender }}
    </label>
  </div>
  <button class="btn btn-primary" type="submit">Submit</button>
</form>

 

HTML에 directive를 오버라이드 합니다. - <form>태그에 formGroup 연결

<form [formGroup]="signupForm">

위에서 선언한 signForm을 formGorup속성으로 넣어줍니다.

 

각 항목마다 formControlName을 사용하여, Angular에게 내가 사용하는 typeScript의 input 이름을 알려줍니다.

<input
  type="text"
  id="username"
  class="form-control"
  formControlName="username">

물론 속성 값으로   [formControlName]="'username'" 를 사용해도 됩니다. string을 전달해야 하기 떄문에 ""안에 ''를 넣어야 합니다.

 

브라우저로 들어가 제대로 매칭이 되었는지 확인해 봅니다.

그림과 같이 ng-untouched와 같은 클래스가 있습니다.

개발 도구를 이용하면 ng로 시작하는 클래스가 있음으로, type스크립트가 작동한다는 것을 알 수 있습니다.


Validation 추가하기 - formControl 생성시 2번째 argu.에 추가합니다.

this.signupForm = new FormGroup({ //first: default Value, second: validation
      'username': new FormControl(null, Validators.required),
      'email': new FormControl(null, [Validators.required, Validators.email]),
      'gender': new FormControl('male')
    });

[]를 이용하여 복수의 validation을 추가합니다.

 

HTML에 안내 문 추가 하기

<span 
  *ngIf="!signupForm.get('username').valid && signupForm.get('username').touched"
  >Please enter a valid username!</span>

signupForm 객체에 들어가 valid, touched와 같은 validation 값에 접근할 수 있습니다.

 

구릅핑 하기 - Form Group

ts파일에 userData로 그릅핑 하겠습니다. - nested Group

'userData': new FormGroup({
  'username': new FormControl(null, Validators.required),
  'email': new FormControl(null, [Validators.required, Validators.email])
}),      

html도 같이 변경해 주어야 합니다.

<div formGroupName="userData">
	... <!-- username과 email <input> -->
</div>

validation도 역시 다르게 접근해야 합니다.

<span *ngIf="!signupForm.get('userData.username').valid && signupForm.get('userData.username').touched">...</span>

폼 컨트롤 어레이 추가하기 - FormArray

 

사용자가 원하는 만큼 text항목을 추가합니다. 

취미 리스트나 todo list에 사용할 수 있습니다.

 

ts에서 formGroup에 FormArray를 추가합니다.

this.signupForm = new FormGroup({
  'userData': new FormGroup({
    'username': new FormControl(null, Validators.required),
    'email': new FormControl(null, [Validators.required, Validators.email])
  }),      
  'gender': new FormControl('male'),
  'hobbies': new FormArray([])
});

hobbies라는 FormArray를 빈 상태로 추가합니다. - 취미가 없는 사람이 있을 수 있기 때문입니다.

<div formArrayName="hobbies">
  <h4>Youre Hobbies</h4>
    <button 
    class="btn btn-default"
    type="button"
    (click)="onAddHobby()">Add Hobby</button>
      <div 
      class="form-group"
      *ngFor="let hobbyContrl of signupForm.get('hobbies').controls; let i = index">
      <input type="text" class="form-control" [formControlName]="i">
  </div>
</div>

HTML에 formArrayName으로 바인딩 합니다.

onAddHobby라는 매쏘드로 사용자에게 반응합니다.

 

몇개의 취미가 있을지 모르기 때문에 ngFor을 이용하여 FormArray에 값을 추가합니다.

 

<Add Hobby 버튼>을 누를때 마다 onAddHobby()매쏘드가 작동하여 FormControl push합니다.

onAddHobby() {
  const control = new FormControl(null, Validators.required);
  (<FormArray>this.signupForm.get('hobbies')).push(control);
}

 

ngFor에 의하여 <input type="text"> 가 나타납니다.

생성과 동시에 formControlName의 매칭을 추가해야 하기 때문에, [formControlName]="i"를 사용합니다.

[]로 속성으로 만들어야 "i" index를 formControlName으로 줄 수 있기 때문입니다.


Custom Validators

특정 username을 사용하지 못하도록 합니다.

 

변수를 사용하여 지정합니다.

forbiddenUsernames = ['Chris', 'Anna'];

매쏘드를 만듭니다.

forbiddenNames(control: FormControl): {[s: string]: boolean} {
  if(this.forbiddenUsernames.indexOf(control.value) !== -1) return {'nameIsForbidden': true};
  return null;
}

foo(argu): {[s: string]:boolean} {} 

foo(argu)뒤에 오는 :{...}은 반환인자 입니다.

foo(): {} foo() 매쏘드는 구조체를 반환합니다.

key/value pair를 반환합니다.

key는 string이고, value는 boolean 입니다.


Indexable Types

...더보기

위 indexable Types 링크를 누르면 이전에 어떻게 interface를 사용할 수 있는지 나와있습니다.

a[10], ageMap["daniel"] 처럼 "index into"로 타입을 기술 할 수 있습니다.

Indexable(색인가능한) 타입은 객체에 색인할 수 있도록하는 인덱스 특징(index signature 이하 인덱스 특징)을 갖고 있으며, 인덱싱 할 때 객체와 상응하는 리턴 타입이 함께합니다. 아래 예제를 보겠습니다.

interface StringArray {
    [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

위쪽, StringArray 인터페이스는 하나의 인덱스 특징을 갖고 있습니다.
이 인덱스 서명은 StringArray가 number로 인덱싱 되어있음을 나타내고, string을 반환한다고 보여주고 있습니다.


forbiddenUsernames의 index를 돌면서 해당 FormControl의  값을 찾습니다.

존재하지 않으면 -1을 반환하는 indexOf를 이용하기떄문에 !== -1를 이용하여 true, false를 결정합니다.

 

forbiddenNames를 호출할 당시의 this는 우리가 생각하는 this가 아니므로 작동하지 않습니다.

'username': new FormControl(null, [Validators.required, this.forbiddenNames.bind(this)]),

validator를 추가할때 bind(this)를 넘겨주는 꼼수을 사용합니다.


Async Vaildator

HTTP 통신을 통해서 Valid 여부를 확인 할 수 있습니다.

 

서버의 응답을 처리할 때 사용하는 기능입니다. 

  static asyncInvalidProjectName(control: FormControl): Promise<any> | Observable<any> {
    const promise = new Promise((resolve, reject) => {
      setTimeout(() => {
        if (control.value === 'Testproject') {
          resolve({'invalidProjectName': true});
        } else {
          resolve(null);
        }
      }, 2000);
    })
    return promise;
  }

 

setTimeout()를 서버에서 응답으로 가정했습니다.

반응형

'프로그래밍 > Angular' 카테고리의 다른 글

Pipe  (0) 2019.08.15
Form - 복습 및 보충 내용  (0) 2019.08.14
Handling Form - Template Driven  (0) 2019.07.02
Observable & Subject  (0) 2019.06.26
Router 정리  (0) 2019.06.25
반응형

 

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';


import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

template driven 에서는 FormsModule을 사용합니다.

 

template driven에 들어가기 앞서 용어와 개념을 정리해보았습니다.

 

투 웨이 바인딩(2-way-binding) 이해하기

 

What are the difference between one way binding and two way binding?

Answer (1 of 5): A two-way binding involves an implementation of a single “watcher set” for both the model and the component, and updating either one when the other changes. Angular 1 uses this method by default using ng-Model. Watchers are created to hand

www.quora.com

 

이벤트 바인딩

<button (click) = “TheEventMethod()”>Save</button>

DOM에서 JavaScript function을 작동 시키도록 event를 알립니다.

 

속성(프로퍼티) 바인딩

<p[size] = “fontSizePx”></p>

JavaSciprt로 설정된 fontSizePx라는 변수값을 가져옵니다.

 

2-way-바인딩

<input type='text' placeholder= "Enter stock symbol" [(ngModel)] = "lastStockSymbol">

이벤트 바인딩과 속성 바인딩을 동시에 사용하는 것이 2-way-바인딩 입니다.

클릭 이벤트 function을 상징하는 ()와 속성 바인딩 []두개가 동시에 쓰인것이 보이죠?

<input type='text' placeholder= "Enter stock symbol" [value]="lastStockSymbol" (input) = "lasStockSymbol=$event.target.value">
<br>The value of lastStockSymbol is {{lastStockSymbol}}

service를 이용하여 DOM을 update함으로써, 2-way-binding을 구현할 수 있습니다.

 


#foo 로컬 레퍼런스

 

1. regist - input input태그를 이용하여 Angular에게 알리기를 실행해 봅니다..

 

input 태그에 directive를 추가합니다. ngModel :2-way-binding이 됩니다.

html directive를 추가해줍니다

<form>
  <div id="user-data">
    <div class="form-group">
      <label for="username">Username</label>
      <input 
        type="text"
        id="username" 
        class="form-control"
        ngModel
        name="username">
    </div>
  </div>
</form>

submit할 수 있도록 만들어 보겠습니다.

 

form태그 안에 submit 버튼을 만들어 type="submit"을 만들게 클릭하게 한다면, HTML의 defalut로 작동하는 call이 trigger되어 request를 보내기 때문에, form태그에 (ngSubmit)="onSubmit()"을 추가합니다.

<form (ngSubmit)="onSubmit()">...</form>

ts

onSubmit() {
  console.log("submitted!")
}

또한 form의 reference를 받으려면 #f를 추가하면 됩니다.

 

HTML

<form (ngSubmit)="onSubmit(f)" #f="ngForm">...</form>

#f 는 local reference입니다.

Angular가 자동적으로 Javascript 객체를 생성해줍니다.

 

typescript

// <form (ngSubmit)="onSubmit(f)" #f></form>
onSubmit(form: ElementRef) {
  console.log(form);
}

// <form (ngSubmit)="onSubmit(f)" #f="ngForm"></form>
onSubmit(form: NgForm) {
  console.log(form);
}

form을 보면 아래와 같이 접근할 수 있습니다.

NgForm.value={

  key: value

}

 

2-way-바인딩을 조금더 편하게 사용하기 위해 @ViewChild를 이용합니다.

import { Component, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';

export class AppComponent {
  @ViewChild('f') singupForms: NgForm;
  
  onSubmit() {
    console.log(this.singupForms);
  }
}

ViewChild는 로컬 레퍼런스와 element를 TypeScript 코드로 변경 하기에 유용하기 때문입니다.


Form State

 

form태그 안쪽에 있는 input에 다음과 같은 directive를 사용할 수 있습니다.

또한  required, eail과 같은 vaild설정을 할 수 있습니다.

 

local reference 사용하기.

 

<input 
  type="email" 
  id="email" 
  class="form-control"
  ngModel
  required
  email
  #email="ngModel">
<span class="help-block" *ngIf="!email.vaild && email.touched">Please enter for vaild</span>

email input태그를 선택 후 아무것도 입력하지 않고 벗어나면, please enter for vaild라는 문구가 나오게 됩니다.

#email="ngModel" 을 사용함으로써 local reference로 HTML에서 email을 사용할 수 있습니다.

 

이와 마찬가지로,

submit 버튼에 disabled를 추가할 수 있습니다.

<button 
  class="btn btn-primary" 
  type="submit"
  [disabled]="!f.vaild">
Submit</button>

CSS 스타일을 관리합니다.

input.ng-invaild { ... }

Angular가 class를 추가하여 form에 위와 같은 상태를 갖고 있습니다.

 


default값 설정하기! - one-way-binding

기본값을 지정할 수 있습니다. [ngModel]

 

HTML

<select 
  id="secret" 
  class="form-control"
  [ngModel]="defaultQuestion"
  name="secret">
  <option value="pet">Your first Pet?</option>
  <option value="teacher">Your first teacher?</option>
</select>

ts

export class AppComponent {

  defaultQuestion = "pet";

}

그림과 같이 pet option태그가 설정되어 있습니다.

 


2-way-바인딩

 

<div class="form-group">
  <textarea 
    name="questionAnswer" 
    rows="3"
    [(ngModel)]="answer"></textarea>
</div>
<p>Your reply: {{answer}}</p>
export class AppComponent {
  answer = '';
}

 

No-바인딩 : Angular에게 input이 동작만 전달합니다.

1-way-바인딩 : 기본값(default value)을 이용하여 값에 나타납니다.

2-way-바인딩 : 사용자 입력과 동시에 value와 화면에 나타납니다.

 


그룹핑 하기 - ngMoudelGroup

<form (ngSubmit)="onSubmit()" #f="ngForm">
  <div
    id="user-data"
    ngModelGroup="userData"
    #userData="ngModelGroup">
      <div class="form-group">
        <label for="username">Username</label>
        <input
        type="text"
        id="username"
        ngModel
      </div>
      <div class="form-group">
        <label for="email">Mail</label>
        <input
        type="email"
        id="email"
        class="form-control"
        ngModel
        name="email">
      </div>
  </div>
</form>

ngForm.value.userData 가 그룹으로 묶여 있음을 확인할 수 있습니다.


2-way-바인딩 다른 방법

 

ngForm.setValue();

this.signupForm.setValue({
  userData: {
    username: suggestedName,
    email: ''
  },
  secret: 'pet',
  questionAnswer: '',
  gender: 'male'
});

모든 값이 덮어쓰기가 됩니다.

 

ngForm.form.patchValue();

this.signupForm.form.patchValue({
  userData: {
    username: suggestedName
  }
});

한개의 값만 변경합니다.


값 빼내기

 

user = {
    username: '',
    email: '',
    secretQuestion: '',
    answer: '',
    gender: ''
  };

onSubmit() {
  this.user.username = this.signupForm.value.userData.username;
  this.user.email = this.signupForm.value.userData.email;
  this.user.secretQuestion = this.signupForm.value.secret;
  this.user.answer = this.signupForm.value.questionAnswer;
  this.user.gender = this.signupForm.value.gender;

  this.signupForm.reset();
}

위와 같이 객체를 만들어 사용합니다.

 

반응형

'프로그래밍 > Angular' 카테고리의 다른 글

Form - 복습 및 보충 내용  (0) 2019.08.14
FormHandling - Reavice  (0) 2019.07.02
Observable & Subject  (0) 2019.06.26
Router 정리  (0) 2019.06.25
Routes  (0) 2019.06.17
반응형

어씽크(asynchronous) 핸들러입니다. 솔찍히 Promise와 모가 다른지 모르겠지만... 말이죠.

 

Ajax와 같은 기존에 사용하던 기능이 있는데 생소한 Observable을 사용하려니 이만저만 힘든게 아니라는 생각이 들었습니다.

하지만 Router를 이용하면서 이미 Observable를 이용했다고 하니 외부 라이브러리를 사용할 필요가 없다는 것을 알았습니다.

 

Route.params.subscribe()에서 params가 Observable이였습니다.

 

다음은 Observable을 사용하는 간단한 예제입니다.

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

import { interval, Subscription } from 'rxjs';

export class HomeComponent implements OnInit, OnDestroy {

  private firstObsSubscription: Subscription;

  ngOnInit() {
    this.firstObsSubscription = interval(1000).subscribe(count => {
      console.log(count);
    });
  }

  ngOnDestroy(): void {
    this.firstObsSubscription.unsubscribe();
  }

}

직접 unsubscribe해야 합니다. route.params에서는 Angular가 뒤에서 unsubscribe까지 지원해 주었습니다.

 

data(customedName), error, complete를 이용하기 예제

const customIntervalObservable = Observable.create(observer => {
  let count = 0;
  setInterval(() => {
    observer.next(count);
    if (count == 5) {
      observer.complete();
    }
    if (count > 3) {
      observer.error(new Error('Count is greater 3!'));
    }
  count++;
  }, 1000);
      
this.firstObsSubscription = customIntervalObservable.subscribe(data => {
    console.log(data);
  }, error => {
    console.log(error);
    alert(error.message);
  }, () => {
    console.log('Completed!');
  });
}

error혹은 complete을 만나면 event emit은 끝납니다.

 

Operators를 이용해서 filter, map등 이용할 수 있습니다.

import { map, filter } from 'rxjs/operators';

customIntervalObservable.pipe(filter(data => {
      return data > 0;
    }), map((data: number) => {
      return 'Round: ' + (data+1);
    })).subscribe...

Subject는 service를 이용하여 컴포넌트간 통신을 할 때 유용합니다.

 

Observable대신, Subject로 할 수 있습니다. 두 개의 차이는 Passvie와 Active로 나눌 수 있습니다.

Subject를 사용하면 수동으로 next()를 사용해야 하는데,


다만 @Output를 사용할 때, EventEmitter대신 Subject를 사용시 주의해야 합니다.

 

Useful Resources:
Official Docs: https://rxjs-dev.firebaseapp.com/
RxJS Series: https://academind.com/learn/javascript/understanding-rxjs/
Updating to RxJS 6: https://academind.com/learn/javascript/rxjs-6-what-changed/

 

RxJS 6 - What Changed?

RxJS 6 changes everything! Or does it? It certainly breaks your imports and operator usage. Time for some (quick!) fixes!

academind.com

 

자료 출처 : https://www.udemy.com/share/1000imAkEeeVhSQ34=/

반응형

'프로그래밍 > Angular' 카테고리의 다른 글

FormHandling - Reavice  (0) 2019.07.02
Handling Form - Template Driven  (0) 2019.07.02
Router 정리  (0) 2019.06.25
Routes  (0) 2019.06.17
Providers - Hierarchical Injector  (0) 2019.06.16
반응형

Router를 정리하고 직접 구현에 적용해 보았습니다.

<ts.file>

 

app 모듈 등록시 '' path를 'localhost:4200'일 때만 적용하려면,

const appRoutes: Routes = 
[ 
  { path: '', component: HomeComponent},
  { path: 'users', component: UsersComponent},
  { path: 'servers', component: ServersComponent}, 
];

path: '' 일경우, 아래와 같이 전체 경로 주소임을 나타내는 full을 추가해 준다.

{path: '', redirectTo: '/recipes', pathMatch: 'full'},

동적경로를 추가 하고 싶을 때, :foo 와 같이 콜론(:)을 앞에 붙여 추가한다.

{path: ':id'}

Route.navigate() 매쏘드 사용법 - from[localhost:4200/recipes] to[localhost:4200/recipes/0/edit]

id: number;

constructor(private router: Router, 
			private route: ActivatedRoute) { }

this.router.navigate(["/recipes", this.id, "edit"]) // 절대경로
this.router.navigate([this,id, "edit"], {relativeTo: this.route}) // 상대경로
this.router.navigate(["../", this,id, "edit"], {relativeTo: this.route}) // 한단계 이전 위치에서 상대경로 이동

 

<HTML>

 

routerLinkActive : 선택된 Element에 active class를 추가시켜줌으로써, css효과를 추가해 준다.

routerLink : /로 시작하지 않으면, 상대 주소기 때문에, 현 위치를 기준으로 아래 주소를 써주면 된다.

(ex: localhost:4200/recipes/0 으로 이동하는 링크 [현 위치: localhost:4200/recipes]의 child)

<a
  style="cursor: pointer;"
  [routerLink]="[index]"
  routerLinkActive="active"
  class="list-group-item clearfix">
  <div class="pull-left">
    <h4 class="list-group-item-heading">{{ recipe.name }}</h4>
    <p class="list-group-item-text">{{ recipe.description }}</p>
  </div>
  <span class="pull-right">
        <img
          [src]="recipe.imagePath"
          alt="{{ recipe.name }}"
          class="img-responsive"
          style="max-height: 50px;">
      </span>
</a>

[routerLink]="[index]"는 자동적으로 localhost:4200/recipes/index로 이동하게 된다.

 

 

반응형

'프로그래밍 > Angular' 카테고리의 다른 글

Handling Form - Template Driven  (0) 2019.07.02
Observable & Subject  (0) 2019.06.26
Routes  (0) 2019.06.17
Providers - Hierarchical Injector  (0) 2019.06.16
custom attribute Directive  (0) 2019.06.09
반응형

1. routerModule 등록

2. routerLink 사용하기

3. router 코딩하기

4. 상대주소 사용하기

5. 유저 불러오기(예제)

6. Query Parameter & Fragment

7. Child(Nested)Routes

8. Redirecting & Wildcard Routes

9. Outsourcing the Foute Configuration

10. Guard

11. Data with resolve Guard


1. app.module.ts (등록하기)

import { Routes, RouterModule } from '@angular/router';

const appRoutes: Routes = [
  { path: '', component: HomeComponent},
  { path: 'users', component: UsersComponent},
  { path: 'servers', component: ServersComponent},
]

imports: [
  BrowserModule,
  FormsModule,
  RouterModule.forRoot(appRoutes)

path는 맨 앞의 / 를 제외합니다. ("localhost:4200/")가 이미 앞에 있음으로, "/"를 생략하고 입력해 줍니다.

RouterModule의 forRoot에 선언한 route를 등록합니다.

 

main.html

<div class="row">
  <div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
    <app-home></app-home>
  </div>
</div>
<div class="row">
  <div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
    <app-users></app-users>
  </div>
</div>
<div class="row">
  <div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
    <app-servers></app-servers>
  </div>
</div>

아래와 같이 변경 간단하게 줄일 수 있습니다.

<div class="row">
  <div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
    <router-outlet></router-outlet>
  </div>
</div>

 

router-outlet은 directive랍니다.

 

하지만 이렇게만 바꾸면, 새로운 페이지가 로딩됩니다.

물론, 네비게이션 클릭시, 
혹은 주소 입력시 서버에 해당 주소로 이동하도록 요청을 보내고, 
서버에서 페이지를 내려 받기 때문에, 
리로딩이 되는것이 맞습니다.(주소창에 해당 주소를 치는것과 같은 효과) 하지만 베스트가 아닙니다.

스테이트를 잃어버리기 때문에 사용자에게 제공하려는 서비스가 아닙니다.


main.html(네비게이션)

<div class="row">
  <div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
    <ul class="nav nav-tabs">
      <li role="presentation" class="active"><a href="/">Home</a></li>
      <li role="presentation"><a href="/servers">Servers</a></li>
      <li role="presentation"><a href="/users">Users</a></li>
    </ul>
  </div>
</div>

href 속성 대신, routerLink라는 directive를 사용합니다.

<ul class="nav nav-tabs">
  <li role="presentation" class="active"><a routerLink="/">Home</a></li>
  <li role="presentation"><a routerLink="/servers">Servers</a></li>
  <li role="presentation"><a [routerLink]="'/users'">Users</a></li>
</ul>

[routerLink]으로 property(속성)으로 사용할 경우 자바스크립트에선 invaild name이기 때문에 ''로 한번 더 감싸야 합니다.

 

또 다른 방법으로, 

<li role="presentation"><a [routerLink]="['/users']">Users</a></li>

[] 배열안에 여러가지를 추가 할 수 있습니다. 이 방법을 사용한다면, 매우 쉽게 복잡한 경로를 추가할 수 있습니다.

 

routerLink 사용 시 주의 사항,

절대 경로 : "/"로 /로 시작.

상대 경로 : "./어딘가"이나, "../저긴가"와 같이 /로 시작하지 않습니다.

 

css클라스를 이용하지 않고,

routerLinkActive를 이용한다면, bootstrap없이 사용가능합니다.

 

각 리스트 아이템: <li>에 routerLinkActive를 추가해줍니다.

<li 
	role="presentation" 
	routerLinkActive="active"
	[routerLinkActiveOptions]="{exact: true}">
  <a routerLink="/">Home</a>
</li>

만약 [routerLinkActiveOptions]="{exact: true}"가 없다면,

bootstap은 "/" 가 앞에 있기 때문에 home의 네비게이션이 active인 상태로 만들어 버립니다.

 

exact는 정확히 / 만 있을 경우를 해당 주소로 인식하게 만드는 기능을 합니다.


3. ts파일로 작동하기

import { Router } from "@angular/router";

constructor(private router: Router) { }

onLoadServers() {
  // 복잡한 경로 계산이 가능해짐.
  this.router.navigate(['/servers'])
}

4. 상대주소

  constructor(
    private router: Router,
    private route: ActivatedRoute
  ) { }

  onReload() {
    this.router.navigate(['somewhere'],{relativeTo: this.route})
  }

somewehere이라는 상대주소를 사용했습니다.

router.navigate의 2번째 파라미터에 {}자바스크립트 객체를 넣고,

relativeTo속성에 현재 위치가 어디인지를 나타냅니다.

현재 어떤 라우터가 작동하고있는지(ActivatedRoute) 알려줍니다.

 

*relativeTo가 없을 경우, root에서 추가하기 때문에, 원하지 않는 곳으로 이동할 수 있습니다.


5. 유저 불러오기

 

app.module.ts

const appRoutes: Routes = [
  { path: 'users/:id/:name', component: UsersComponent},// dynamic part of the path :'s meaning
]

path에 ":"는 다이나믹한 값을 받아올 수 있게 합니다.

 

ts

export class UserComponent implements OnInit {
  user: {id: number, name: string};

  constructor(private route: ActivatedRoute) { }

  ngOnInit() {
    this.user = {
      id: this.route.snapshot.params['id'],
      name: this.route.snapshot.params['name']
    };
    this.route.params
      .subscribe(
        (params: Params) => {
          this.user.id = params['id'];
          this.user.name = params['name'];
        }
      );
  }
}

HTML

<p>User with ID {{user.id}} loaded.</p>
<p>User name is {{user.name}}</p>
<hr>
<a [routerLink]="['/users',10, 'Anna']">Load Anna (10)</a>

Snapshot: 일회성 사용시(the no-observable alternative)

특정 컴퍼넌트의 재사용이 필요 없다면, snapshot을 사용하세요.

router는 항상 새로운 인스턴스를 생성합니다.

 

router.snapshot은 router 최초 값(initial value)으로 작동합니다.

스냅샷을 이용한다면, subscribing이나 observable operator를 추가하지 않고도 쉽게 읽고 쓰기를 진행할 수 있습니다.

this.route.params
  .subscribe(
    (params: Params) => {
      this.user.id = params['id'];
      this.user.name = params['name'];
    }
  );

위 코드 없이 Load Anna (10) 링크를 클릭하면,

URL은 바뀌지만, HTML의 내용은 변하지 않음을 확인할 수 있습니다.

 

Angular는 UserComponent(동일 컴포넌트)에서 <a> link를 누를경우 re-rendering을 하지 않습니다. 

이는 Angular의 default 동작으로, Angular는 컴포넌트 전체를 destroy하고 recreate하지 않는 답니다.

그러기에 rendering을 다시 할 수 있도록, subscribe를 사용합니다.

 

*참고: this.route.params는 observables입니다.

기본적으로, observables는 third-party 패키지로 추가된 기능입니다.

Angular의 기능은 아니지만 어씽크(asynchronous) 작업을 쉽게 지원하기 때문에 많이 사용하고 있습니다.

link를 누르는 것은 어씽크 작업입니다.

불러온 route의 현재 패러미터값은 언제 변경될 모르고 얼마나 걸릴지도 모르기 때문입니다.

또한 링크를 누르지 않을 수 도 있기 때문에, 무한정 기다리거나 코드를 없애서도 안됩니다.

 

*참고: OnDestroy()

꼭 해야할 필요는 없지만, 알아두어야 할 기능이 있습니다. life cycle의 ngDestroy()입니다.

Angular는 background에서 해당 컴포넌트를 destroy합니다. 동시에 subscription을 제거해줍니다.

만약 하지 않는다면, subscription을 제거하지 않는다면, 해당 컴포넌트는 제거되지만, subscription은 계속 남아 있게 됩니다.

import { Subscription } from 'rxjs/Subscription';

export class UserComponent implements OnInit, OnDestroy {
  user: {id: number, name: string};
  paramsSubscription: Subscription;

  constructor(private route: ActivatedRoute) { }

  ngOnInit() {
    this.paramsSubscription = this.route.params
      .subscribe(
        (params: Params) => {
          this.user.id = params['id'];
          this.user.name = params['name'];
        }
      );
  }

  ngOnDestroy() {
    this.paramsSubscription.unsubscribe();
  }

}

6. Query Parameter & Fragment

 

?mode=editing#loading

을 추가하려면 어떻게 해야할까요?

 

1. app.module.ts / path 추가

const appRoutes: Routes = [
  { path: 'servers/:id/edit', component: EditServerComponent}
]

2. servers.component.html / routerLink, queryParams, fragment

<div class="row">
  <div class="col-xs-12 col-sm-4">
    <div class="list-group">
      <a
        [routerLink]="['/servers', 5, 'edit']"
        [queryParams]="{allowEdit: '1'}"
        fragment="loading"
        href="#"
        class="list-group-item"
        *ngFor="let server of servers">
        {{ server.name }}
      </a>
    </div>
  </div>
</div>

routerLink는 path에 추가된 조건과 동일하게 맞추어 줍니다.

 

queryParams는 routerLink의 'edit'뒤에 파라미터를 추가할 수 있지만, queryParams라는 directive를 사용해보겠습니다.

javascript 객체에 key-value 페어를 이용합니다.

fragment도 추가할 수 있습니다.

 

3. home.component.html & Home.component.ts / 코딩으로 routing

 

HTML

<button class="btn btn-primary" (click)="onLoadServers(1)">Load Server 1</button>

1 이라는 서버로 이동하는 매쏘드로 변경합니다.

 

onLoadServers(id: number) {
  this.router.navigate(['/servers', id, 'edit'], {queryParams: {allowEdit: '1'}, fragment: 'loading'})
}

2번과 같이 key-value로 추가합니다.

 

relativeTo를 사용했지만, 위와 같이도 가능합니다.


7. Child(Nested)Routes


app.module.ts

const appRoutes: Routes = [
  { path: '', component: HomeComponent},
  { path: 'users', component: UsersComponent},
	  { path: 'users/:id/:name', component: UsersComponent},
  
  { path: 'servers', component: ServersComponent},
	  { path: 'servers/:id', component: ServerComponent},
	  { path: 'servers/:id/edit', component: EditServerComponent}
]
const appRoutes: Routes = [
  { path: '', component: HomeComponent},
  { path: 'users', component: UsersComponent, children: [
    { path: ':id/:name', component: UserComponent},
  ]},
  { path: 'servers', component: ServersComponent, children: [
    { path: ':id', component: ServerComponent},
    { path: ':id/edit', component: EditServerComponent}
  ]}
]

children으로 중복되는 경로를 보기 쉽게 해줍니다.

component가 선언되어 있기 때문에, 해당 HTML도 변경되어야합니다.

 

servers.component.HTML

<div class="col-xs-12 col-sm-4">
  <router-outlet></router-outlet>
    <!-- <button class="btn btn-primary" (click)="onReload()">Reload Page</button> -->
    <!-- <app-edit-server></app-edit-server> -->
    <!-- <hr> -->
    <!-- <app-server></app-server> -->
</div>

users.componente.HTML

<div class="col-xs-12 col-sm-10 col-md-8 col-sm-offset-1 col-md-offset-2">
  <router-outlet></router-outlet>
</div>

8. Redirecting & Wildcard Routes

 

app.module.ts

const appRoutes: Routes = [
  { path: '', component: HomeComponent},
  { path: 'users', component: UsersComponent, children: [
    { path: ':id/:name', component: UserComponent},
  ]},
  { path: 'servers', component: ServersComponent, children: [
    { path: ':id', component: ServerComponent},
    { path: ':id/edit', component: EditServerComponent}
  ]},
  { path: 'not-found', component: PageNotFoundComponent},
  { path: '**', redirectTo: '/not-found'}
]

path: '**' 와일드 카드는 무조건 맨 아래에 있어야 합니다.

만약 맨 위에 위치하게 된다면, 모든 경로는 리다이랙트 됩니다.

 

*Tip:

{ path: '', redirectTo: '/somewhere-else', pathMatch: 'full' } 을 사용해야 합니다.

Angulart의 매칭 방법(matching strategy)은 "prefix"입니다.

Angular는 입력한 URL이 해당 path로 시작하는지 체크합니다. 물론 매번 ''으로 시작하기 때문에(중요: whitespace는 존재하지 않음으로, 언제나 "nothing"입니다.)

그러므로 매칭 방법을 "full"로 변경해야 합니다.


9. Outsourcing the Foute Configuration

 

9.1 module.ts파일을 만듭니다.

@NgModule({
	imports: [
		RouterModule.forRoot(appRoutes)
	],
	exports: [RouterModule]
})

export class AppRoutingModule {

}

9.2 app.module.ts

imports: [
    BrowserModule,
    FormsModule,
    AppRoutingModule
  ]

imports에 추가합니다.

 

routing 정보를 파일로 만들면, 파일 정리 및 가독성을 높일 수 있습니다.


10. Guard

Authentication 을 Fake로 간단히 만들고, 어떤 페이지에서 이탈할 경우, 정말 나갈 것인지 묻도록 합니다.

 

10.1. auth.service | 로그인 기능

export class AuthService {
    loggedIn = false;

    isAuthenticated() {
        const promise = new Promise(
            (resolve, reject) => {
                setTimeout(()=> {
                    resolve(this.loggedIn);
                }, 800);
            }
        );
        return promise;
    }

    login() {
        this.loggedIn = true;
    }

    logout() {
        this.loggedIn = false;
    }
}

로그 in/out은 home.component를 통해 간단하게 조작합니다.

onLogin() {
    this.authService.login();
  }

  onLogout() {
    this.authService.logout();
  }

10.2 auth-guard.service | 접근 통제

import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router, CanActivateChild } from "@angular/router";
import { Observable } from "rxjs";
import { Injectable } from "@angular/core";
import { AuthService} from "./auth.service";

@Injectable()
export class AuthGuard implements CanActivate, CanActivateChild {
    constructor(private authServlice: AuthService, private router: Router) {}
    canActivate(route: ActivatedRouteSnapshot,
                state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
        return this.authServlice.isAuthenticated()
            .then(
                (authenticated: boolean) => {
                    if (authenticated) {
                        return true;
                    } else {
                        this.router.navigate(['/']);
                    }
                }
            )
        }
    canActivateChild(route: ActivatedRouteSnapshot,
                    state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
    return this.canActivate(route, state);
    }
}

canActivateChild매쏘드는 일정 페이지 이상의 접근을 제한합니다.

app-routing.module.ts

{ path: 'servers',
      //   canActivate: [AuthGuard], 
      canActivateChild: [AuthGuard],
      component: ServersComponent, 
      children: [
      { path: ':id', component: ServerComponent},
      { path: ':id/edit', component: EditServerComponent, canDeactivate: [CanDeactivateGuard]}
    ]},

10.3 이탈 방지 코드

can-deactivate-guard.service.ts

import { Observable } from "rxjs/Observable";
import { ActivatedRouteSnapshot, RouterStateSnapshot } from "@angular/router";

export interface CanComponentDeactivate {
    canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}

export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
    canDeactivate(component: CanComponentDeactivate,
        currentRoute: ActivatedRouteSnapshot,
        currentState: RouterStateSnapshot,
        nextState?: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
            return component.canDeactivate();
        }
}

ㅠㅠ 실력이 없어서... 위 코드를 따라하기만 하네요... 설명하고 싶은데 못하겠어요... 어플 만들고나서 이해가 필요한 부분이네요!

그 때쯤이면 어느정도 실력이 쌓여서 지금 풀어보는 것보다 더 이해도가 높겟쪄? ㅎㅎ

 

edit-server.component에서 실수로 뒤로가기를 누르거나, 업데이트 등 할 때 사용자에게 한번 더 묻습니다.

canDeactivate(): Observable<boolean> | Promise<boolean> | boolean {
    if (!this.allowEdit) {
      return true;
    }
    if ((this.serverName !== this.server.name || this.serverStatus !== this.server.status) && !this.chagesSaved) {
      return confirm('Do you want to discard the changes?');
    } else {
      return true;
    }
  }

물론 app-routing.module.ts에 추가하고, app.module에 provider로 등록해야합니다.

providers: [ServersService, AuthService, AuthGuard, CanDeactivateGuard],

11. Data with resolve Guard

11.1. static data를 route로 전달하기

 

11.1.1. routing.module.ts 에 코드 추가 data: {message: '원하는 문구'}

{ path: 'not-found', component: ErrorPageComponent, data: {message: 'Page not found'} },

11.1.2. route로 data 불러오기

export class ErrorPageComponent implements OnInit {

  errorMessage: string;

  constructor(private route: ActivatedRoute) { }

  ngOnInit() {
    this.route.data.subscribe(
      (data: Data) => {
        this.errorMessage = data['message'];
      }
    )
  }
}

 

11.2. Resolving 다이나믹 데이터

11.2.1. resolver 만들기

import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot, Router } from "@angular/router";
import { Observable } from "rxjs";
import { ServersService } from "../servers.service";
import { Injectable } from "@angular/core";

interface Server {id: number, name: string, status: string}

@Injectable()
export class ServerResolver implements Resolve<Server> {
  constructor(private serversService: ServersService) {}
  resolve(route: ActivatedRouteSnapshot, status: RouterStateSnapshot): Observable<Server> | Promise<Server> | Server {
    return this.serversService.getServer(+route.params['id']);
  }
}

interface Resolve 를 구현 했기 대문에 route와 status의 스냅샷이 필요합니다.

+route.params['id']는 getServer가 number 타입을 원하기 때문에 꼼수로 앞에다 +를 추가하였습니다.

 

11.2.2. app.module.ts 에 providers 추가하기

providers: [ServersService, AuthService, AuthGuard, CanDeactivateGuard, ServerResolver],

 

11.2.3. routing.module.ts에 resolve추가

{ path: ':id', component: ServerComponent, resolve: {server: ServerResolver} },

 

11.2.4. 코드 적용하기

  ngOnInit() {
    this.route.data.subscribe(
      (data: Data) => {
        this.server = data['server'];
      }
    )
    // const id = +this.route.snapshot.params['id'];
    // this.server = this.serversService.getServer(id);
    // this.route.params
    //   .subscribe(
    //     (params: Params) => {
    //         this.server = this.serversService.getServer(+params['id']);
    //     }
    //   )
  }

기존의 코드를 주석처리 합니다.

route를 통해 data가 변경될 때 마다 받아 옵니다.

 

만약 /servers입력시,  index.html로 돌아간다면

@NgModule({
    imports: [
        RouterModule.forRoot(appRoutes, {useHash: true})
    ],
    exports: [RouterModule]
})

hash가 주소창 앞에 붙여

#이전의 주소와 #이후의 주소로 구별하여, 실제 server에 접근하지 않도록 방지해 줍니다.

 

 

 

 

 

 

 

 

 

 

 

출처: https://www.udemy.com/share/1000imAkEeeVhSQ34=/

Angular 8 (formerly Angular 2) - The Complete Guide

제제작자 Maximilian Schwarzmülle

routing-final.zip
0.06MB

 

반응형

'프로그래밍 > Angular' 카테고리의 다른 글

Observable & Subject  (0) 2019.06.26
Router 정리  (0) 2019.06.25
Providers - Hierarchical Injector  (0) 2019.06.16
custom attribute Directive  (0) 2019.06.09
style 스타일링  (0) 2018.09.05
반응형

providers의 메타 데이터는 객체를 지정합니다.

아래와 같은 방식으로 FooService와 BarService 객체를 지정합니다.

@Component({
  selector: 'app-active-users',
  templateUrl: './active-users.component.html',
  styleUrls: ['./active-users.component.css'],
  providers: [FooService, BarService]
})

 

provider를 어디에 지정하느냐에 따라 인스턴스 공유 범위가 달라집니다.

 

AppModule : Application에 같은 인스턴스를 사용할 수 있습니다.

 

AppComponent : 모든 컴포넌트와 같은 인스턴스를 사용할 수 있습니다.

 

하위 컴포넌트 : 해당 컴포넌트와 그 아래 모든 자식 컴포넌트와 같은 인스턴스를 사용할 수 있습니다.

 

 

 

개별적으로 인스턴스를 선언하여 사용할 경우, constructor에 선언하여 사용합니다.

export class Foo {
  constructor(private barService: BarService){}
}

 

Service 객체를 사용하면, 이벤트를 emit없이 손쉽게 객체간 communication이 가능합니다.

 

Logging과 같은 경우 common으로 주로 사용하기 때문에, 매우 유익하겠죠?

반응형

'프로그래밍 > Angular' 카테고리의 다른 글

Router 정리  (0) 2019.06.25
Routes  (0) 2019.06.17
custom attribute Directive  (0) 2019.06.09
style 스타일링  (0) 2018.09.05
FormsModule With ViewChild  (0) 2018.09.05

+ Recent posts