اسکرول بی نهایت در اپلیکیشن های انگولار — از صفر تا صد
قابلیت اسکرول بی نهایت (Infinite Scroll) به این صورت است که در زمان اسکرول شدن صفحه به صورت پیوسته صفحه دادههای جدید را بارگذاری میکند و تنها زمانی متوقف میشود که همه دادههای ممکن بارگذاری شده باشند. در این مقاله با روش افزودن امکان اسکرول بینهایت در اپلیکیشنهای انگولار آشنا خواهیم شد. افزودن این امکان به اپلیکیشن با استفاده از انگولار کار آسانی محسوب میشود. کتابخانههای زیادی برای افزودن این امکان به اپلیکیشنهای انگولار معرفی شدهاند. در این مقاله یک اپلیکیشن گالری تصاویر تکصفحهای میسازیم که امکان جستجو و اسکرول بینهایت دارد و صفحه دیگری نیز طراحی میکنیم که یک اسلایدشو از عکسهای تصادفی نمایش میدهد.
ما از مزیت Angular Material استفاده میکنیم تا ظاهر عناصر را بهبود بخشیم. همچنین کتابخانههای grid و یک کتابخانه اسلاید شو به نام ng-simple-slideshow برای نمایش تصاویر تصادفی استفاده میکنیم. این اپلیکیشن یک منو در سمت چپ نیز خواهد داشت. منبع عکسهای ما API وبسایتی به نام Pexels است. برای دسترسی به تصاویر این وبسایت به یک کلید API نیاز دارید که در این آدرس (+) میتوانید به دست آورید. این وبسایت محدودیت 200 فراخوانی API در ساعت دارد و از این رو نباید درخواستهای زیادی ارائه کنید. برای افزودن امکان اسکرول بینهایت به اپلیکیشن از پکیج ngx-infinite-scroll که برای انگولار ساخته شده است بهره میگیریم.
شروع
برای شروع به ساختن اپلیکیشن کار خود را از نصب کردن Angular CLI با اجرای دستور زیر آغاز میکنیم:
npm i @angular/cli
پس از نصب آن دستور زیر را اجرا میکنیم تا یک پروژه انگولار جدید برای اپلیکیشن گالری تصاویر خود بسازیم:
ng new image-gallery
یک Flux store نیز برای ذخیرهسازی حالت منوی میسازیم.
در ادامه کتابخانههای مورد نیاز اپلیکیشن را ایجاد میکنیم. دستور زیر را اجرا کنید تا کتابخانههایی که برای نمایش عکسها و نشان دادن اسلایدشو لازم هستند نصب شوند:
npm i @angular/cdk @angular/material ng-simple-slideshow ngx-infinite-scroll @ngrx/store
در ادامه با دستور زیر 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 به هدر همه درخواستها استفاده میشود. سرویس Photo جایی است که کد ایجاد فراخوانیها به API Pexels قرار میگیرد. کد زیر را به فایل environment.ts اضافه کنید تا بتوانیم کلید API را به فایلهای دیگر نیز ایمپورت کنیم:
1export const environment = {
2 production: false,
3 pexelsApiKey: 'your pexels api key'
4};
کد زیر را به فایل http-req-interceptor.ts اضافه کنید:
1import { Injectable } from '@angular/core';
2import {
3 HttpEvent,
4 HttpInterceptor,
5 HttpHandler,
6 HttpResponse,
7 HttpRequest
8} from '@angular/common/http';
9import { Observable } from 'rxjs';
10import { environment } from '../environments/environment'
11import { tap } from 'rxjs/operators';
12@Injectable()
13export class HttpReqInterceptor implements HttpInterceptor {
14 constructor() { }
15intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
16 let modifiedReq = req.clone({});
17 modifiedReq = modifiedReq.clone({
18 setHeaders: {
19 'Authorization': environment.pexelsApiKey
20 }
21 });
22return next.handle(modifiedReq).pipe(tap((event: HttpEvent<any>) => {
23 if (event instanceof HttpResponse) {
24}
25 }));
26 }
27}
این کد توکن ما را به هدر درخواست Authorization برای همه درخواستهای با بلوک زیر الصاق میکند:
1let modifiedReq = req.clone({});
2 modifiedReq = modifiedReq.clone({
3 setHeaders: {
4 'Authorization': environment.pexelsApiKey
5 }
6});
ارسال درخواست API
کد زیر را به فایل photo.service.ts اضافه کنید:
1import { Injectable } from '@angular/core';
2import { HttpClient } from '@angular/common/http';
3@Injectable({
4 providedIn: 'root'
5})
6export class PhotoService {
7constructor(
8 private http: HttpClient
9 ) { }
10randomPhotos(page: number = 1) {
11 return this.http.get(`https://api.pexels.com/v1/curated?per_page=15&page=${page}`)
12 }
13searchPhotos(query: string, page: number = 1) {
14 return this.http.get(`https://api.pexels.com/v1/search?query=${encodeURIComponent(query)}&per_page=15&page=${page}`)
15 }
16}
این کد به ما امکان میدهد که درخواستهایی به API وبسایت Pexels ارسال کنیم. ما در اپلیکیشن خود از تصاویر منتخب و نقطه انتهایی جستجو با صفحهبندی استفاده میکنیم. سپس کد زیر را به فایل home-page.component.ts اضافه کنید:
1import { Component, OnInit } from '@angular/core';
2import { NgForm } from '@angular/forms';
3import { PhotoService } from '../photo.service';
4@Component({
5 selector: 'app-home-page',
6 templateUrl: './home-page.component.html',
7 styleUrls: ['./home-page.component.scss']
8})
9export class HomePageComponent implements OnInit {
10 query: any = <any>{};
11 photoUrls: string[] = [];
12 page: number = 1;
13constructor(
14 private photoService: PhotoService
15 ) { }
16ngOnInit() {
17 this.getPhotos();
18 }
19getPhotos() {
20 this.photoService.randomPhotos(this.page)
21 .subscribe(res => {
22 this.photoUrls = this.photoUrls.concat((res as any).photos.map(p => p.src.landscape));
23 })
24 }
25searchPhotos(searchForm: NgForm) {
26 if (searchForm.invalid) {
27 return;
28 }
29 this.page = 1;
30 this.photoUrls = [];
31 this.requestSearchPhotos();
32 }
33requestSearchPhotos() {
34 this.photoService.searchPhotos(this.query.search, this.page)
35 .subscribe(res => {
36 this.photoUrls = this.photoUrls.concat((res as any).photos.map(p => p.src.landscape));
37 })
38 }
39onScroll() {
40 this.page++
41 if (!this.query.search) {
42 this.getPhotos();
43 }
44 else {
45 this.requestSearchPhotos();
46 }
47}
48}
این همان جایی است که عکسها را از نقطه انتهایی تصاویر منتخب میگیریم. آدرس آن به صورت زیر است:
https://api.pexels.com/v1/curated?per_page=15&page=1
به این وسیله میتوانیم URL-های تصاویر را با فراخوانی map روی فیلد photos پاسخ به دست آوریم. اگر یک عبارت جستجو وارد شده باشد، از نقطه انتهایی تصاویر مورد جستجو در آدرس زیر استفاده میکنیم:
https://api.pexels.com/v1/search?query=example+query&per_page=15&page=1
تا همان کار قبلی را با تابع map اجرا کنیم. ما امکان اسکرول بینهایت را داریم و از این رو زمانی که کاربر تا انتهای صفحه اسکرول میکند، شماره صفحه را اضافه کرده و URL تصاویر بیشتری را به ارائه اضافه میکنیم.
فرم جستجو
کد زیر را به فایل home-page.component.html اضافه کنید:
1<form #searchForm='ngForm' (ngSubmit)='searchPhotos(searchForm)'>
2 <mat-form-field>
3 <input matInput placeholder="Search Photos" required #search='ngModel' name='search' [(ngModel)]='query.search'>
4 <mat-error *ngIf="search.invalid && (search.dirty || search.touched)">
5 <div *ngIf="search.errors.required">
6 Search query is required.
7 </div>
8 </mat-error>
9 </mat-form-field>
10 <br>
11 <button mat-raised-button type='submit'>Search</button>
12</form>
13<br>
14<div infiniteScroll [infiniteScrollDistance]="2" [infiniteScrollThrottle]="50" (scrolled)="onScroll()">
15 <mat-grid-list cols="2" rowHeight="2:1">
16 <mat-grid-tile *ngFor='let p of photoUrls'>
17 <img [src]='p' class="tile-image" >
18 </mat-grid-tile>
19 </mat-grid-list>
20</div>
کد فوق، فرم جستجو را نشان میدهد و شبکه تصاویر ما را در یک div اسکرول بینهایت قرار میدهد. بدین ترتیب تصاویر جدید زمانی که کاربر اسکرول کند بارگذاری خواهند شد. infiniteScrollDistance درصد فاصله از انتهای صفحه است. بدین ترتیب 2 به این معنی است که اسکرول بینهایت زمانی که کاربر تا 98% صفحه جاری اسکرول کرد، تحریک خواهد شد.
infiniteScrollThrottle تعداد میلیثانیههایی است که طول میکشد تا اسکرول بینهایت پس از توقف اسکرول کردن کاربر تحریک شود. Scrolled زمانی تحریک خواهد شد که کاربر به انتهای صفحه اسکرول کند. همه اینها اختیاری هستند و میتوانید مطابق میل خود تنظیم کنید. کد زیر را به فایل home-page.component.scss اضافه کنید:
1.tile-image {
2 width: 100%;
3 height: auto;
4}
بدین ترتیب تصاویر کادر شبکه را پر میکنند. کدهای زیر را در فایل random-slideshow-page.component.ts قرار دهید:
1import { Component, OnInit } from '@angular/core';
2import { PhotoService } from '../photo.service';
3@Component({
4 selector: 'app-random-slideshow-page',
5 templateUrl: './random-slideshow-page.component.html',
6 styleUrls: ['./random-slideshow-page.component.scss']
7})
8export class RandomSlideshowPageComponent implements OnInit {
9 photoUrls: string[] = [];
10constructor(
11 private photoService: PhotoService
12 ) { }
13ngOnInit() {
14 this.getPhotos();
15 }
16getPhotos() {
17 this.photoService.randomPhotos(1)
18 .subscribe(res => {
19 this.photoUrls = (res as any).photos.map(p => p.src.landscape);
20 })
21 }
22}
این همان جایی است که عکسهای تصادفی از نقطه انتهایی عکسهای منتخب به دست میآیند. کدهای زیر را به فایل In random-photos-page.component.html اضافه کنید:
1<div class="center">
2 <h1>Random Photos</h1>
3</div>
4<slideshow [imageUrls]="photoUrls" [height]="height" [minHeight]="'60vh'" [autoPlay]="true" [showArrows]="false">
5</slideshow>
کد فوق اسلاید شویی از عکسها نمایش میدهد. سپس یک فایل به نام menu-reducer.ts بسازید و کد زیر را به برای ذخیره حالت منو به آن اضافه کنید:
1const TOGGLE_MENU = 'TOGGLE_MENU';
2function menuReducer(state, action) {
3 switch (action.type) {
4 case TOGGLE_MENU:
5 state = action.payload;
6 return state;
7 default:
8 return state
9 }
10}
11export { menuReducer, TOGGLE_MENU };
کد زیر را در فایل reducers/index.ts قرار دهید:
1import { menuReducer } from './menu-reducer';
2export const reducers = {
3 menu: menuReducer,
4};
کد فوق به StoreModule از ngrx/store@ امکان میدهد که از reducer منو برای ذخیرهسازی حالت استفاده کند.
تغییر حالت منو
کدهای زیر را به فایل app.component.ts اضافه کنید:
1import { Component, HostListener } from '@angular/core';
2import { Store, select } from '@ngrx/store';
3import { TOGGLE_MENU } from './reducers/menu-reducer';
4@Component({
5 selector: 'app-root',
6 templateUrl: './app.component.html',
7 styleUrls: ['./app.component.scss']
8})
9export class AppComponent {
10 menuOpen: boolean;
11constructor(
12 private store: Store<any>,
13 ) {
14 store.pipe(select('menu'))
15 .subscribe(menuOpen => {
16 this.menuOpen = menuOpen;
17 })
18 }
19@HostListener('document:click', ['$event'])
20 public onClick(event) {
21 const isOutside = !event.target.className.includes("menu-button") &&
22 !event.target.className.includes("material-icons") &&
23 !event.target.className.includes("mat-drawer-inner-container")
24 if (isOutside) {
25 this.menuOpen = false;
26 this.store.dispatch({ type: TOGGLE_MENU, payload: this.menuOpen });
27 }
28 }
29}
کد فوق به صورت خودکار منوی سمت چپ را در زمان تغییر یافتن صفحه و همچنین در صورتی که روی دکمه منو کلیک نکنید، میبندد. حالت منو را از store میگیریم و اگر منو نیاز به بسته شدن در نتیجه ناوبری یا کلیک کردن خارج از منو داشته باشد، منو را میبندیم و حالت آن را در Store ذخیره میکنیم. کدهای زیر را به فایل top-bar.component.ts اضافه کنید:
1import { Component, OnInit } from '@angular/core';
2import { Store, select } from '@ngrx/store';
3import { TOGGLE_MENU } from '../reducers/menu-reducer';
4@Component({
5 selector: 'app-top-bar',
6 templateUrl: './top-bar.component.html',
7 styleUrls: ['./top-bar.component.scss']
8})
9export class TopBarComponent implements OnInit {
10 menuOpen: boolean;
11constructor(
12 private store: Store<any>
13 ) {
14 store.pipe(select('menu'))
15 .subscribe(menuOpen => {
16 this.menuOpen = menuOpen;
17 })
18 }
19ngOnInit() {
20 }
21toggleMenu() {
22 this.store.dispatch({ type: TOGGLE_MENU, payload: !this.menuOpen });
23 }
24}
کد فوق امکان بستن و باز کردن منو و ذخیره حالت آن در Store را فراهم میسازد. کدهای زیر را به فایل app.component.html اضافه میکنیم:
1<mat-sidenav-container class="example-container">
2 <mat-sidenav mode="side" [opened]='menuOpen'>
3 <ul>
4 <li>
5 <b>
6 Image Gallery App
7 </b>
8 </li>
9 <li>
10 <a routerLink='/'>Home</a>
11 </li>
12 <li>
13 <a routerLink='/random'>Random Photos Slideshow</a>
14 </li>
15 </ul>
16</mat-sidenav>
17 <mat-sidenav-content>
18 <app-top-bar></app-top-bar>
19 <div id='content'>
20 <router-outlet></router-outlet>
21 </div>
22 </mat-sidenav-content>
23</mat-sidenav-container>
کد فوق منوی سمت چپ را برای ناوبری نمایش میدهد و router-outlet به کاربران امکان دیدن صفحهها را در زمان کلیک کردن روی لینکهای بالا یا تایپ کردن مستقیم URL میدهد. کد زیر را به فایل app.component.scss اضافه کنید:
1#content {
2 padding: 20px;
3 min-height: 130vh;
4}
5ul {
6 list-style-type: none;
7 margin: 0;
8 li {
9 padding: 20px 5px;
10 }
11}
این کد مقداری فاصلهبندی اضافه میکند و حاشیههای صفحهها را حذف میکند. در نهایت کد زیر را به فایل app.module.ts اضافه کنید:
1import { BrowserModule } from '@angular/platform-browser';
2import { NgModule } from '@angular/core';
3import {
4 MatButtonModule,
5 MatCheckboxModule,
6 MatInputModule,
7 MatMenuModule,
8 MatSidenavModule,
9 MatToolbarModule,
10 MatTableModule,
11 MatDialogModule,
12 MatDatepickerModule,
13 MatSelectModule,
14 MatCardModule,
15 MatFormFieldModule,
16 MatGridListModule
17} from '@angular/material';
18import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
19import { AppRoutingModule } from './app-routing.module';
20import { AppComponent } from './app.component';
21import { StoreModule } from '@ngrx/store';
22import { reducers } from './reducers';
23import { TopBarComponent } from './top-bar/top-bar.component';
24import { FormsModule } from '@angular/forms';
25import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
26import { HomePageComponent } from './home-page/home-page.component';
27import { RandomSlideshowPageComponent } from './random-slideshow-page/random-slideshow-page.component';
28import { InfiniteScrollModule } from 'ngx-infinite-scroll';
29import { SlideshowModule } from 'ng-simple-slideshow';
30import { HttpReqInterceptor } from './http-req-interceptor';
31import { PhotoService } from './photo.service';
32@NgModule({
33 declarations: [
34 AppComponent,
35 TopBarComponent,
36 HomePageComponent,
37 RandomSlideshowPageComponent
38 ],
39 imports: [
40 BrowserModule,
41 AppRoutingModule,
42 FormsModule,
43 MatButtonModule,
44 StoreModule.forRoot(reducers),
45 BrowserAnimationsModule,
46 MatButtonModule,
47 MatCheckboxModule,
48 MatFormFieldModule,
49 MatInputModule,
50 MatMenuModule,
51 MatSidenavModule,
52 MatToolbarModule,
53 MatTableModule,
54 HttpClientModule,
55 MatDialogModule,
56 MatDatepickerModule,
57 MatSelectModule,
58 MatCardModule,
59 MatGridListModule,
60 InfiniteScrollModule,
61 SlideshowModule,
62 ],
63 providers: [
64 {
65 provide: HTTP_INTERCEPTORS,
66 useClass: HttpReqInterceptor,
67 multi: true
68 },
69 PhotoService
70 ],
71 bootstrap: [AppComponent]
72})
73export class AppModule { }
کد فوق شامل همه کتابخانهها، HTTP interceptor و سرویسهایی است که برای کارکرد اپلیکیشن نیاز داریم. در انتها اپلیکیشنی مانند زیر به دست میآوریم:
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- مجموعه آموزشهای برنامهنویسی
- نکاتی برای بهبود تجربه کاربری اپلیکیشن انگولار با NgRx — راهنمای کاربردی
- اعتبارسنجی در فرم های دینامیک انگولار — از صفر تا صد
- ساخت داشبورد سازمانی مقیاس پذیر با انگولار
==