export const genres = [ { _id: "5b21ca3eeb7f6fbccd471818", name: "Action" }, { _id: "5b21ca3eeb7f6fbccd471814", name: "Comedy" }, { _id: "5b21ca3eeb7f6fbccd471820", name: "Thriller" } ]; export function getGenres() { return genres.filter(g => g); }
بررسی ساختار کامپوننت ListGroup
شبیه به کامپوننت صفحه بندی که در قسمت قبل ایجاد کردیم، میخواهیم کامپوننت ListGroup نیز به طور کامل از اشیاء movie مستقل باشد؛ تا در آینده بتوان از آن در جاهای دیگری نیز استفاده کرد. به همین جهت فایل جدید src\components\common\listGroup.jsx را ایجاد کرده و سپس با استفاده از میانبرهای imrc و cc در VSCode، ساختار ابتدایی این کامپوننت را ایجاد میکنیم. هرچند میتوان این کامپوننت را به صورت «Stateless Functional Component» نیز طراحی کرد؛ چون state و متد دیگری بجز render نخواهد داشت و تمام اطلاعات خودش را از والد خود دریافت میکند.
سپس به کامپوننت movies مراجعه کرده و این کامپوننت خالی را import میکنیم:
import ListGroup from "./common/listGroup";
برای این منظور ابتدا React.Fragment موجود را با یک div با "className="row جایگزین میکنیم. سپس داخل این row، دو ستون را تعریف خواهیم کرد که در اولی، المان جدید ListGroup قرار میگیرد و در دومی، مابقی عناصری که تاکنون اضافه کردهایم؛ مانند جدول، صفحه بندی و نمایش تعداد آیتمها:
return ( <div className="row"> <div className="col-2"> <ListGroup /> </div> <div className="col"> ... </div> </div> );
import { getGenres } from "../services/fakeGenreService"; // ... class Movies extends Component { state = { // ... genres: getGenres() };
class Movies extends Component { state = { movies: [], pageSize: 4, currentPage: 1, genres: [] }; componentDidMount() { this.setState({ movies: getMovies(), genres: getGenres() }); }
پس از آن میتوان ویژگی جدید items این کامپوننت را به آرایهی genres دریافتی از state، تنظیم کرد:
<ListGroup items={this.state.genres} />
بهتر است هر زمانیکه کاربر، آیتمی را از این لیست انتخاب کرد، توسط بروز رخدادی مانند onItemSelect از وقوع آن مطلع شد و سپس نسبت به آن توسط متد handleGenreSelect، واکنش نشان داد؛ مانند فیلتر کردن لیست فیلمها بر اساس آیتم انتخابی و نمایش آن. به همین جهت ویژگی onItemSelect را به تعریف المان ListGroup اضافه میکنیم:
<ListGroup items={this.state.genres} onItemSelect={this.handleGenreSelect} />
handleGenreSelect = genre => { console.log("handleGenreSelect", genre); };
پیاده سازی نمایش آیتمها در کامپوننت ListGroup
پیاده سازی ابتدایی کامپوننت ListGroup را در اینجا مشاهده میکنید:
import React, { Component } from "react"; class ListGroup extends Component { render() { return ( <ul className="list-group"> {this.props.items.map(item => ( <li key={item._id} className="list-group-item"> {item.name} </li> ))} </ul> ); } } export default ListGroup;
تا اینجا اگر برنامه را ذخیره کرده و در مرورگر نمایش دهیم، به خروجی زیر میرسیم:
البته به نظر عرض ستون آن نامناسب است. به همین جهت به کامپوننت movies مراجعه کرده و col-2 ستون آنرا به col-3 تبدیل میکنیم.
پویا سازی انتخاب نام خواص شیء دریافتی، در کامپوننت ListGroup
در حال حاضر پیاده سازی کامپوننت ListGroup، به شیءای دقیقا با خواص id_ و name وابستهاست و اگر شیء دیگری را که دارای خواصی معادل این نامها نیست، به آن ارسال کنیم، دیگر کار نخواهد کرد. به همین جهت در محل تعریف المان این کامپوننت در کامپوننت movies، دو ویژگی دیگر نام خواص شیء مدنظر را تنظیم میکنیم تا بتوانیم با هر نوع شیءای در اینجا کار کنیم:
<ListGroup items={this.state.genres} textProperty="name" valueProperty="_id" onItemSelect={this.handleGenreSelect} />
import React, { Component } from "react"; class ListGroup extends Component { render() { return ( <ul className="list-group"> {this.props.items.map(item => ( <li key={item[this.props.valueProperty]} className="list-group-item"> {item[this.props.textProperty]} </li> ))} </ul> ); } } export default ListGroup;
تعیین مقادیر پیشفرضی برای خواص props
با زیاد شدن تعداد خواص props، اینترفیس کامپوننتها پیچیدهتر میشوند. در یک چنین حالتی میتوان در کامپوننتها defaultProps را تعریف کرد و توسط آن مقادیر پیشفرضی را برای خواص props درنظر گرفت. به این صورت در حین تعریف المان این کامپوننت، اگر مقادیر مدنظر با مقادیر پیشفرض تعیین شده یکی باشند، دیگر نیازی به ذکر این پارامترها نخواهد بود. برای مثال در انتهای کامپوننت ListGroup، خاصیت جدید defaultProps را تعریف میکنیم (املای آن باید دقیقا به همین شکل باشد؛ و گرنه شناخته نخواهد شد). سپس در اینجا خواصی را که میخواهیم مقادیر پیشفرضی را برای آنها تعیین کنیم، ذکر خواهیم کرد:
ListGroup.defaultProps = { textProperty: "name", valueProperty: "_id" }; export default ListGroup;
<ListGroup items={this.state.genres} onItemSelect={this.handleGenreSelect} />
مدیریت انتخاب گروههای فیلمها
در ادامه میخواهیم رخداد onClick بر روی هر li این لیست را مدیریت کنیم و سبب بروز رخدادی به نام onItemSelect شویم که در ابتدای بحث، آنرا به عنوان خروجی این کامپوننت تعریف کردیم. این رخداد نیز در کامپوننت movies به متد handleGenreSelect متصل است. به همین جهت تعریف ویژگی onClick را که سبب انتقال شیء جاری رندر شده، توسط رویداد onItemSelect به خارج از آن میشود، به المان li کامپوننت ListGroup اضافه میکنیم:
<li key={item[this.props.valueProperty]} className="list-group-item" onClick={() => this.props.onItemSelect(item)} style={{ cursor: "pointer" }} > {item[this.props.textProperty]} </li>
پس از فعالسازی امکان کلیک بر روی هر آیتم لیست رندر شده، اکنون میخواهیم با انتخاب هر گروه، این گروه در این لیست، به صورت انتخاب شده، همانند شماره صفحهی انتخاب شدهی در کامپوننت صفحه بندی، تغییر رنگ دهد و متمایز نمایش داده شود تا مشخص باشد که هم اکنون با کدام آیتم در حال کار هستیم. برای اینکار تنها کافی است کلاس active را به صورت پویا به className هر li، اضافه یا کم کنیم. البته برای این منظور این کامپوننت باید از آیتم انتخاب شده مطلع باشد؛ به همین جهت selectedItem را در لیست ویژگیهای اینترفیس تعریف این المان اضافه میکنیم. برای اینکار ابتدا selectedGenre را با هربار فراخوانی handleGenreSelect که به onItemSelect کامپوننت متصل است، با فراخوانی متد setState به روز رسانی میکنیم:
handleGenreSelect = genre => { console.log("handleGenreSelect", genre); this.setState({selectedGenre: genre}); };
class Movies extends Component { state = { // ... selectedGenre: {} };
<ListGroup items={this.state.genres} onItemSelect={this.handleGenreSelect} selectedItem={this.state.selectedGenre} />
<li key={item[this.props.valueProperty]} className={ item === this.props.selectedItem ? "list-group-item active" : "list-group-item" } style={{ cursor: "pointer" }} onClick={() => this.props.onItemSelect(item)} > {item[this.props.textProperty]} </li>
مدیریت فیلتر کردن اطلاعات گروه فیلم انتخابی
در قسمت قبل، در ابتدای متد رندر کامپوننت movies، از متد paginate برای صفحه بندی اطلاعات استفاده کردیم. فیلتر گروه جاری انتخاب شده را باید پیش از این متد قرار دارد؛ چون تعداد صفحات و اطلاعات نمایش داده شدهی در هر کدام باید بر اساس لیست فیلمهای فیلتر شده باشد.
برای انجام اینکار تغییرات زیر را اعمال خواهیم کرد:
الف) بجای متد paginate، از متد getPagedData زیر استفاده میکنیم:
getPagedData() { const { pageSize, currentPage, selectedGenre, movies: allMovies } = this.state; const filteredMovies = selectedGenre && selectedGenre._id ? allMovies.filter(m => m.genre._id === selectedGenre._id) : allMovies; const first = (currentPage - 1) * pageSize; const last = first + pageSize; const pagedMovies = filteredMovies.slice(first, last); return { totalCount: filteredMovies.length, data: pagedMovies }; }
- در حین Object Destructuring، نام خاصیت movies را نیز به allMovies تغییر دادهایم تا واضحتر باشد.
- در ادامه با استفاده از متد filter جاوااسکریپت، بر اساس id هر گروه انتخاب شده، اشیاء مرتبط با آن، از allMovies جدا شده و بازگشت داده میشود. البته اگر id هم انتخاب نشده باشد (اولین بار نمایش صفحه)، تمام رکوردها یعنی allMovies، مورد استفاده قرار میگیرد.
- پس از آن، همان کدهای صفحه بندی اطلاعات را که در قسمت قبل بررسی کردیم، مشاهده میکنید که اینبار بجای allMovies قسمت قبل، بر روی filteredMovies اعمال شدهاست.
- در آخر، این متد، یک شیء را با دو خاصیت که بیانگر تعداد کل رکوردهای انتخاب شده و دادههای فیلتر شدهی صفحه بندی شدهاست، بازگشت میدهد.
ب) تغییرات متد رندر کامپوننت movies به صورت زیر است:
- ابتدا متد getPagedData فوق، فراخوانی شده و شیء دریافتی از آن با استفاده از ویژگی Object Destructuring، به دو خاصیت totalCount و movies انتساب داده میشود:
render() { const { length: count } = this.state.movies; if (count === 0) return <p>There are no movies in the database.</p>; const { totalCount, data: movies } = this.getPagedData();
- همچنین کامپوننت صفحه بندی، اینبار باید totalCount آیتمهای فیلتر شده را نمایش دهد و نه totalCount تمام فیلمهای موجود را:
<Pagination itemsCount={totalCount}
<p>Showing {totalCount} movies in the database.</p>
handleGenreSelect = genre => { console.log("handleGenreSelect", genre); this.setState({ selectedGenre: genre, currentPage: 1 }); };
افزودن گزینهی نمایش تمام اطلاعات به لیست گروههای فیلمها
در ادامه قصد داریم به بالای لیست گروههای موجود، گزینهی All Genres را نیز اضافه کنیم تا با کلیک بر روی آن، مجددا بتوان لیست تمام فیلمهای موجود را مشاهده کرد.
برای این منظور در جائیکه لیست getGenres را دریافت و نمایش میدهیم، یعنی متد componentDidMount، اندکی تغییر ایجاد کرده و یک آرایهی جدید را ایجاد میکنیم؛ بطوریکه اولین عنصر آن، گزینهی جدید All Genres باشد و سپس توسط spread operator، مابقی عناصر آرایهی گروهها را به این آرایهی جدید اضافه میکنیم:
componentDidMount() { const genres = [{ _id: "", name: "All Genres" }, ...getGenres()]; this.setState({ movies: getMovies(), genres }); }
const filteredMovies = selectedGenre && selectedGenre._id ? allMovies.filter(m => m.genre._id === selectedGenre._id) : allMovies;