С Angular легко создать красивое фото-приложение. Существует множество библиотек для добавления слайд-шоу из фотографий, большинство из которых просты в использовании. Кроме того, аккуратно отображать изображение легко с помощью существующих библиотек, таких как сетка дизайна Angular Material.

В этой истории мы создадим приложение фотогалереи с одной страницей с возможностью поиска и бесконечной прокруткой и другой страницей, показывающей слайд-шоу из случайных фотографий. Мы будем использовать Angular Material, чтобы воспользоваться его красивыми элементами формы и сеткой, а также библиотеку слайд-шоу под названием ng-simple-slideshow для отображения слайд-шоу. Слева будет меню. Источник наших фотографий будет из API Pexels. Вам понадобится ключ API для бесплатного доступа к API, зарегистрировавшись на https://www.pexels.com/api/. Он ограничен 200 вызовами API в час, поэтому не делайте слишком много запросов.

Чтобы начать сборку приложения, мы начнем с установки Angular CLI, запустив npm i @angular/cli. После его установки мы запускаем ng new image-gallery, чтобы создать новый Angular для нашего приложения галереи изображений. Также мы создаем хранилище потоков для хранения состояния меню. Затем мы устанавливаем библиотеки для приложения. Мы запускаем npm i @angular/cdk @angular/material ng-simple-slideshow ngx-infinite-scroll @ngrx/store, чтобы установить библиотеки, необходимые для отображения фотографий и слайд-шоу фотографий. Затем мы запускаем ng add @ngrx/store, чтобы добавить код для

Затем мы добавляем скелетный код для кода, который будем писать. Для этого мы запускаем следующие команды:

ng g component homePage
ng g component randomSlideshowPage
ng g component topBar
ng g class httpReqInterceptor
ng g service photo

Это создаст компоненты, используемые для отображения. Класс httpReqInterceptor используется для присоединения ключа API к заголовку каждого запроса. Сервис фотографий - это место, где будет находиться код для вызовов API Pexels.

В environment.ts мы помещаем:

export const environment = {
  production: false,
  pexelsApiKey: 'your pexels api key'
};

чтобы мы могли импортировать ваш ключ API в другие файлы.

В http-req-interceptor.ts мы добавляем:

import { Injectable } from '@angular/core';
import {
    HttpEvent,
    HttpInterceptor,
    HttpHandler,
    HttpResponse,
    HttpRequest
} from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../environments/environment'
import { tap } from 'rxjs/operators';
@Injectable()
export class HttpReqInterceptor implements HttpInterceptor {
    constructor() { }
    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        let modifiedReq = req.clone({});
        modifiedReq = modifiedReq.clone({
            setHeaders: {
                'Authorization': environment.pexelsApiKey
            }
        });
return next.handle(modifiedReq).pipe(tap((event: HttpEvent<any>) => {
            if (event instanceof HttpResponse) {
}
        }));
    }
}

чтобы прикрепить наш токен к заголовку Authorization request каждого запроса с этим блоком:

let modifiedReq = req.clone({});
  modifiedReq = modifiedReq.clone({
    setHeaders: {
      'Authorization': environment.pexelsApiKey
    }
});

В photo.service.ts мы помещаем:

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({
  providedIn: 'root'
})
export class PhotoService {
  constructor(
    private http: HttpClient
  ) { }
  randomPhotos(page: number = 1) {
    return this.http.get(`https://api.pexels.com/v1/curated?per_page=15&page=${page}`)
  }
  searchPhotos(query: string, page: number = 1) {
    return this.http.get(`https://api.pexels.com/v1/search?query=${encodeURIComponent(query)}&per_page=15&page=${page}`)
  }
}

чтобы мы могли делать запросы к API Pexels. Мы использовали тщательно отобранные фотографии и конечную точку поиска с разбивкой на страницы в нашем приложении.

Далее в home-page.component.ts мы помещаем:

import { Component, OnInit } from '@angular/core';
import { NgForm } from '@angular/forms';
import { PhotoService } from '../photo.service';
@Component({
  selector: 'app-home-page',
  templateUrl: './home-page.component.html',
  styleUrls: ['./home-page.component.scss']
})
export class HomePageComponent implements OnInit {
  query: any = <any>{};
  photoUrls: string[] = [];
  page: number = 1;
  constructor(
    private photoService: PhotoService
  ) { }
  ngOnInit() {
    this.getPhotos();
  }
  getPhotos() {
    this.photoService.randomPhotos(this.page)
      .subscribe(res => {
        this.photoUrls = this.photoUrls.concat((res as any).photos.map(p => p.src.landscape));
      })
  }
  searchPhotos(searchForm: NgForm) {
    if (searchForm.invalid) {
      return;
    }
    this.page = 1;
    this.photoUrls = [];
    this.requestSearchPhotos();
  }
  requestSearchPhotos() {
    this.photoService.searchPhotos(this.query.search, this.page)
      .subscribe(res => {
        this.photoUrls = this.photoUrls.concat((res as any).photos.map(p => p.src.landscape));
      })
  }
  onScroll() {
    this.page++
    if (!this.query.search) {
      this.getPhotos();
    }
    else {
      this.requestSearchPhotos();
    }
  }
}

Здесь мы получаем фотографии из конечной точки кураторских фотографий, расположенной по адресу https://api.pexels.com/v1/curated?per_page=15&page=1, чтобы получить URL-адреса изображений, вызвав map в поле фотографий отклик. Если введен поисковый запрос, мы будем использовать конечную точку поиска фотографий по адресу https://api.pexels.com/v1/search?query=example+query&per_page=15&page=1 и сделаем то же самое с функцией map. У нас есть бесконечная прокрутка, поэтому, когда пользователь прокручивает страницу вниз, мы увеличиваем номер страницы и продолжаем добавлять URL-адреса изображений в наш массив.

В home-page.component.html мы помещаем:

<form #searchForm='ngForm' (ngSubmit)='searchPhotos(searchForm)'>
    <mat-form-field>
        <input matInput placeholder="Search Photos" required #search='ngModel' name='search' [(ngModel)]='query.search'>
        <mat-error *ngIf="search.invalid && (search.dirty || search.touched)">
            <div *ngIf="search.errors.required">
                Search query is required.
            </div>
        </mat-error>
    </mat-form-field>
    <br>
    <button mat-raised-button type='submit'>Search</button>
</form>
<br>
<div infiniteScroll [infiniteScrollDistance]="2" [infiniteScrollThrottle]="50" (scrolled)="onScroll()">
    <mat-grid-list cols="2" rowHeight="2:1">
        <mat-grid-tile *ngFor='let p of photoUrls'>
            <img [src]='p' class="tile-image" >
        </mat-grid-tile>
    </mat-grid-list>
</div>

чтобы отобразить форму поиска и обернуть нашу сетку фотографий бесконечной прокруткой div, чтобы загружать новые изображения, когда пользователь прокручивает страницу вниз.

В home-page.component.scss мы добавляем:

.tile-image {
  width: 100%;
  height: auto;
}

чтобы изображения заполняли сетку.

В random-slideshow-page.component.ts мы помещаем:

import { Component, OnInit } from '@angular/core';
import { PhotoService } from '../photo.service';
@Component({
  selector: 'app-random-slideshow-page',
  templateUrl: './random-slideshow-page.component.html',
  styleUrls: ['./random-slideshow-page.component.scss']
})
export class RandomSlideshowPageComponent implements OnInit {
  photoUrls: string[] = [];
  constructor(
    private photoService: PhotoService
  ) { }
  ngOnInit() {
    this.getPhotos();
  }
  getPhotos() {
    this.photoService.randomPhotos(1)
      .subscribe(res => {
        this.photoUrls = (res as any).photos.map(p => p.src.landscape);
      })
  }
}

Здесь мы получаем случайные фотографии из конечной точки кураторских фотографий.

В random-photos-page.component.html мы добавляем:

<div class="center">
    <h1>Random Photos</h1>
</div>
<slideshow [imageUrls]="photoUrls" [height]="height" [minHeight]="'60vh'" [autoPlay]="true" [showArrows]="false">
</slideshow>

для показа слайд-шоу из фотографий.

Затем мы создаем файл с именем menu-reducer.ts и добавляем следующее:

const TOGGLE_MENU = 'TOGGLE_MENU';
function menuReducer(state, action) {
    switch (action.type) {
        case TOGGLE_MENU:
            state = action.payload;
            return state;
        default:
            return state
    }
}
export { menuReducer, TOGGLE_MENU };

для сохранения состояния меню.

В reducers/index.ts мы помещаем:

import { menuReducer } from './menu-reducer';
export const reducers = {
  menu: menuReducer,
};

чтобы разрешить StoreModule из @ngrx/store использовать редуктор меню для сохранения состояния.

В app.component.ts мы добавляем:

import { Component, HostListener } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { TOGGLE_MENU } from './reducers/menu-reducer';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  menuOpen: boolean;
  constructor(
    private store: Store<any>,
  ) {
    store.pipe(select('menu'))
      .subscribe(menuOpen => {
        this.menuOpen = menuOpen;
      })
  }
  @HostListener('document:click', ['$event'])
  public onClick(event) {
    const isOutside = !event.target.className.includes("menu-button") &&
      !event.target.className.includes("material-icons") &&
      !event.target.className.includes("mat-drawer-inner-container")
    if (isOutside) {
      this.menuOpen = false;
      this.store.dispatch({ type: TOGGLE_MENU, payload: this.menuOpen });
    }
  }
}

чтобы автоматически закрывать левое боковое меню при смене страниц и если вы не нажимаете кнопку меню. Мы получаем состояние меню в магазине, и если меню необходимо закрыть путем навигации или щелчка за пределами меню, мы отключаем меню и устанавливаем состояние в магазине.

В top-bar.component.ts мы помещаем:

import { Component, OnInit } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { TOGGLE_MENU } from '../reducers/menu-reducer';
@Component({
  selector: 'app-top-bar',
  templateUrl: './top-bar.component.html',
  styleUrls: ['./top-bar.component.scss']
})
export class TopBarComponent implements OnInit {
  menuOpen: boolean;
  constructor(
    private store: Store<any>
  ) {
    store.pipe(select('menu'))
      .subscribe(menuOpen => {
        this.menuOpen = menuOpen;
      })
  }
  ngOnInit() {
  }
  toggleMenu() {
    this.store.dispatch({ type: TOGGLE_MENU, payload: !this.menuOpen    });
  }
}

чтобы мы могли переключать меню и сохранять состояние в магазине. Затем в top-bar.component.ts мы помещаем:

<mat-toolbar>
    <a (click)='toggleMenu()' class="menu-button">
        <i class="material-icons">
            menu
        </i>
    </a>
    Image Gallery App
</mat-toolbar>

В app.component.html у нас есть:

<mat-sidenav-container class="example-container">
  <mat-sidenav mode="side" [opened]='menuOpen'>
    <ul>
      <li>
        <b>
          Image Gallery App
        </b>
      </li>
      <li>
        <a routerLink='/'>Home</a>
      </li>
      <li>
        <a routerLink='/random'>Random Photos Slideshow</a>
      </li>
    </ul>
</mat-sidenav>
  <mat-sidenav-content>
    <app-top-bar></app-top-bar>
    <div id='content'>
      <router-outlet></router-outlet>
    </div>
  </mat-sidenav-content>
</mat-sidenav-container>

для отображения меню с левой стороны для навигации и router-outlet, чтобы пользователи могли видеть наши страницы при нажатии на ссылки выше или при прямом вводе URL-адреса.

В app.component.scss мы добавляем:

#content {
  padding: 20px;
  min-height: 130vh;
}
ul {
  list-style-type: none;
  margin: 0;
  li {
    padding: 20px 5px;
  }
}

для добавления отступов и удаления полей на страницах.

Наконец, в app.module.ts мы добавляем:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import {
  MatButtonModule,
  MatCheckboxModule,
  MatInputModule,
  MatMenuModule,
  MatSidenavModule,
  MatToolbarModule,
  MatTableModule,
  MatDialogModule,
  MatDatepickerModule,
  MatSelectModule,
  MatCardModule,
  MatFormFieldModule,
  MatGridListModule
} from '@angular/material';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { StoreModule } from '@ngrx/store';
import { reducers } from './reducers';
import { TopBarComponent } from './top-bar/top-bar.component';
import { FormsModule } from '@angular/forms';
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { HomePageComponent } from './home-page/home-page.component';
import { RandomSlideshowPageComponent } from './random-slideshow-page/random-slideshow-page.component';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { SlideshowModule } from 'ng-simple-slideshow';
import { HttpReqInterceptor } from './http-req-interceptor';
import { PhotoService } from './photo.service';
@NgModule({
  declarations: [
    AppComponent,
    TopBarComponent,
    HomePageComponent,
    RandomSlideshowPageComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    MatButtonModule,
    StoreModule.forRoot(reducers),
    BrowserAnimationsModule,
    MatButtonModule,
    MatCheckboxModule,
    MatFormFieldModule,
    MatInputModule,
    MatMenuModule,
    MatSidenavModule,
    MatToolbarModule,
    MatTableModule,
    HttpClientModule,
    MatDialogModule,
    MatDatepickerModule,
    MatSelectModule,
    MatCardModule,
    MatGridListModule,
    InfiniteScrollModule,
    SlideshowModule,
  ],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: HttpReqInterceptor,
      multi: true
    },
    PhotoService
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

чтобы включить все библиотеки, HTTP-перехватчик и службы, необходимые для работы приложения.

Наконец, в готовом приложении у нас есть следующее: