С 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-перехватчик и службы, необходимые для работы приложения.
Наконец, в готовом приложении у нас есть следующее: