Тонкости при работе с @Input
@Input и set
Входящие свойства можно использовать совместно с set. Этим вы сможете на лету формировать нужные вам кастомные свойства в компоненте.
<app-list-errors [errors]="errors"></app-list-errors>
export class ListErrorsComponent { formattedErrors:Array<string> = []; @Input() set errors(errorList:Errors) { this.formattedErrors = Object.keys(errorList.errors || {}) .map(key => `${key} ${errorList.errors[key]}`); } get errorList() { return this.formattedErrors; } }
Директивы
Более подробно про директивы можно узнать в стетье - директивы в Angular
Структурная директива и subject
Реализуем структурную директиву, назначение которой будет в том, чтобы скрывать/открывать блок с навигацией для авторизированного/неавторизированного пользователя. Обратите внимание, что в самой директиве мы подпишемся на ReplaySubject
- позволяет указать сколько значений мы хотим запомнить, например, это актуально при создании новой подписки.
// user.service.ts private isAuthenticatedSubject = new ReplaySubject<boolean>(1); public isAuthenticated = this.isAuthenticatedSubject.asObservable(); // ... // ex. set isAuthenticated to true this.isAuthenticatedSubject.next(true);
<ul *appShowAuthed="false" class="nav navbar-nav pull-xs-right">
Реализация структурной директивы:
@Directive({selector: '[appShowAuthed]'}) export class ShowAuthedDirective implements OnInit { @Input() set appShowAuthed(condition:boolean) { this.condition = condition; } condition:boolean; constructor(private templateRef:TemplateRef<any>, private userService:UserService, private viewContainer:ViewContainerRef) { } ngOnInit() { this.userService.isAuthenticated.subscribe( (isAuthenticated) => { if (isAuthenticated && this.condition || !isAuthenticated && !this.condition) { this.viewContainer.createEmbeddedView(this.templateRef); } else { this.viewContainer.clear(); } } ); } }
Резолвер
Многие игнорируют данную функциональность, например, при роутинге на странцу article нужно получить данные предварительно от бэкэнда по статье. Самый простой и используемый способ это подписаться на route
в компоненте статьи, получить id
статьи и сделать запрос на сервер. Все работает, но есть более удобный и специально созданный для таких случаев инструмент - резолвер. Рассмотрим пример:
Роутинг для статьи, обратите внимание на свойство resolve
:
const routes:Routes = [ { path: ':slug', component: ArticleComponent, resolve: { article: ArticleResolver } } ];
// article-resolver.service.ts @Injectable() export class ArticleResolver implements Resolve<Article> { constructor(private articlesService:ArticlesService, private router:Router, private userService:UserService) { } resolve(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):Observable<any> { return this.articlesService.get(route.params['slug']) .pipe(catchError((err) => this.router.navigateByUrl('/'))); } }
Получаем статью в сервисе:
// articles.service.ts get(slug):Observable<Article> { return this.apiService.get('/articles/' + slug) .pipe(map(data => data.article)); }
Получаем статью в компоненте ArticleComponent
:
ngOnInit() { // Retreive the prefetched article this.route.data.subscribe( (data:{ article:Article }) => { this.article = data.article; // Load the comments on this article this.populateComments(); } );
Interceptor
Например, при каждом запросе на сервер нам нужно отправлять в заголовке токен. Вместо того чтобы дублировать код в сервисах мы можем определить один перехватчик, который будет работать как прокладка (расширять параметры запроса) для всех запрососв.
// http.token.interceptor.ts @Injectable() export class HttpTokenInterceptor implements HttpInterceptor { constructor(private jwtService:JwtService) { } intercept(req:HttpRequest<any>, next:HttpHandler):Observable>HttpEvent<any>> { const headersConfig = { 'Content-Type': 'application/json', 'Accept': 'application/json' }; const token = this.jwtService.getToken(); if (token) { headersConfig['Authorization'] = `Token ${token}`; } const request = req.clone({setHeaders: headersConfig}); return next.handle(request); } }
Регистрируем перехватчик в корневом модуле:
// src\app\core\core.module.ts @NgModule({ imports: [ CommonModule ], providers: [ { provide: HTTP_INTERCEPTORS, useClass: HttpTokenInterceptor, multi: true }, ApiService,
Обрабатываем ошибки в сервисах
Можно создать отдельный метод в сервисе, который будет обрабатывать и отдавать Observable
с ошибкой:
import {Observable, throwError} from 'rxjs'; import {catchError} from 'rxjs/operators'; public put(path:string, body:object = {}):Observable<any> { return this.httpClient .put(BASE_URL + path, JSON.stringify(body), this.options) .pipe(catchError(this.formatErrors)); } public formatErrors(error:any):Observable<any> { return throwError(error.error); }
ng-content
Вы вполне можете определять какую-либо верстку внутри определения компонента (в каком либо другом компоненте) и использовать тег ng-content
, чтобы определить место, где вы хотите вывести ранее определенную верстку внутри самого компонента:
<!-- article.component.html --> <app-article-meta [article]="article"> <span [hidden]="!canModify"> <a class="btn btn-sm btn-outline-secondary" [routerLink]="['/editor', article.slug]"> <i class="ion-edit"></i> Edit Article </a> <button class="btn btn-sm btn-outline-danger" [ngClass]="{disabled: isDeleting}" (click)="deleteArticle()"> <i class="ion-trash-a"></i> Delete Article </button> </span> <span [hidden]="canModify"> <app-follow-button [profile]="article.author" (toggle)="onToggleFollowing($event)"> </app-follow-button> <app-favorite-button [article]="article" (toggle)="onToggleFavorite($event)"> {{ article.favorited ? 'Unfavorite' : 'Favorite' }} Article <span class="counter">({{ article.favoritesCount }})</span> </app-favorite-button> </span> </app-article-meta> <!-- Используем ng-content внутри шаблона компонента ArticleMetaComponent --> <div class="article-meta"> <a [routerLink]="['/profile', article.author.username]"> <img [src]="article.author.image"/> </a> <div class="info"> <a class="author" [routerLink]="['/profile', article.author.username]"> {{ article.author.username }} </a> <span class="date"> {{ article.createdAt | date: 'longDate' }} </span> </div> <ng-content></ng-content> </div>
RxJS
concatMap
Допустим, у вас есть две асинхронные операции и вам нужно вторую асинхронную операцию вызвать с данными, которые получены от первой асинхронной операции. Для этого оптимальным решением будет использование оператора concatMap
. Например, дожидаемся, когда придут данные от роутинга и делаем запрос на сервер, чтобы получить данные о пользователе:
ngOnInit() { this.route.data.pipe( concatMap((data:{ profile:Profile }) => { this.profile = data.profile; // Load the current user's data. return this.userService.currentUser.pipe(tap( (userData:User) => { this.currentUser = userData; this.isUser = (this.currentUser.username === this.profile.username); } )); }) ).subscribe(); }
mergeMap vs flatMap vs concatMap vs switchMap
AsyncPipe
Иногда следует позаботиться об отписке от Observable. AsyncPipe подписывается на Observable и возвращает последнее выданное им значение. Когда компонент уничтожается, асинхронный pipe автоматически отписывается.
export class AsyncPipeCardComponent implements OnInit { messageSubscription: Observable<string>; constructor(private upperCaseService: UpperCaseService) {} ngOnInit() { this.messageSubscription = this.upperCaseService.getUpperCaseMessage(); } }
<h4>{{messageSubscription | async}}</h4>
Комментарии: