استخراج جدول فیلمها
در طراحی فعلی کامپوننت movies، مشکل کوچکی وجود دارد: این کامپوننت تا اینجا، ترکیبی شدهاست از دو کامپوننت صفحه بندی و نمایش لیست گروهها، به همراه جزئیات کامل یک جدول بسیار طولانی. به این مشکل، mixed levels of abstractions میگویند. در اینجا دو کامپوننت سطح بالا را داریم، به همراه یک جدول سطح پایین که تمام مشخصات آن در معرض دید هستند و با هم مخلوط شدهاند. یک چنین کدی، یکدست به نظر نمیرسد. به همین جهت اولین کاری را که در ادامه انجام خواهیم داد، تعریف یک کامپوننت جدید و انتقال تمام جزئیات جدول نمایش ردیفهای فیلمها، به آن است. برای این منظور فایل جدید src\components\moviesTable.jsx را ایجاد کرده و توسط میانبرهای imrc و cc در VSCode، ساختار ابتدایی کامپوننت MoviesTable را تولید میکنیم. این کامپوننت را در پوشهی common قرار ندادیم؛ از این جهت که قابلیت استفادهی مجدد در سایر برنامهها را ندارد. کار آن تنها مرتبط و مختص به اشیاء فیلمی است که در سرویسهای برنامه داریم. البته در ادامه، این جدول را نیز به چندین کامپوننت با قابلیت استفادهی مجدد، خواهیم شکست؛ اما فعلا در اینجا با اصل کدهای سطح پایین جدول نمایش داده شدهی در کامپوننت movies، شروع میکنیم، آنها را cut کرده و به متد رندر کامپوننت جدید MoviesTable منتقل میکنیم.
پس از انتقال کامل تگ table از کامپوننت movies به داخل متد رندر کامپوننت MoviesTable، در ابتدای آن توسط Object Destructuring، یک آرایه و دو رخدادی را که برای مقدار دهی قسمتهای مختلف آن نیاز داریم، از props فرضی، استخراج میکنیم. اینکار کمک میکند تا بتوان اینترفیس این کامپوننت را به خوبی مشخص و طراحی کرد:
class MoviesTable extends Component { render() { const { movies, onDelete, onLike } = this.props;
import Like from "./common/like";
// ... <Like liked={movie.liked} onClick={() => onLike(movie)} />
<button onClick={() => onDelete(movie)} className="btn btn-danger btn-sm" > Delete </button>
import MoviesTable from "./moviesTable";
<MoviesTable movies={movies} onDelete={this.handleDelete} onLike={this.handleLike} />
پس از این تغییرات، متد رندر کامپوننت movies چنین شکلی را پیدا کردهاست که در آن سه کامپوننت سطح بالا درج شدهاند و در یک سطح از abstraction قرار دارند و دیگر مخلوطی از المانهای سطح بالا و سطح پایین را نداریم:
return ( <div className="row"> <div className="col-3"> <ListGroup items={this.state.genres} onItemSelect={this.handleGenreSelect} selectedItem={this.state.selectedGenre} /> </div> <div className="col"> <p>Showing {totalCount} movies in the database.</p> <MoviesTable movies={movies} onDelete={this.handleDelete} onLike={this.handleLike} /> <Pagination itemsCount={totalCount} pageSize={this.state.pageSize} onPageChange={this.handlePageChange} currentPage={this.state.currentPage} /> </div> </div> );
اکنون نوبت فعالسازی کلیک بر روی سرستونهای جدول نمایش داده شده و مرتب سازی اطلاعات جدول بر اساس ستون انتخابی است. به همین جهت در کامپوننت MoviesTable، رویداد onSort را هم به لیستی از خواصی که از props انتظار داریم، اضافه میکنیم که در نهایت در کامپوننت movies، به یک متد رویدادگردان متصل میشود:
class MoviesTable extends Component { render() { const { movies, onDelete, onLike, onSort } = this.props;
return ( <table className="table"> <thead> <tr> <th style={{ cursor: "pointer" }} onClick={() => onSort("title")}>Title</th> <th style={{ cursor: "pointer" }} onClick={() => onSort("genre.name")}>Genre</th> <th style={{ cursor: "pointer" }} onClick={() => onSort("numberInStock")}>Stock</th> <th style={{ cursor: "pointer" }} onClick={() => onSort("dailyRentalRate")}>Rate</th> <th /> <th /> </tr> </thead>
<MoviesTable movies={movies} onDelete={this.handleDelete} onLike={this.handleLike} onSort={this.handleSort} />
handleSort = column => { console.log("handleSort", column); };
پیاده سازی مرتب سازی اطلاعات
تا اینجا اگر دقت کرده باشید، هر زمانیکه شماره صفحهای تغییر میکند یا گروه فیلم خاصی انتخاب میشود، ابتدا state را به روز رسانی میکنیم که در نتیجهی آن، کار رندر مجدد کامپوننت در DOM مجازی React صورت میگیرد. سپس در متد رندر، کار تغییر اطلاعات آرایهی فیلمها را جهت نمایش به کاربر، انجام میدهیم.
بنابراین ابتدا در متد رویدادگران handleSort، با فراخوانی متد setState، مقدار path دریافتی حاصل از کلیک بر روی یک سرستون را به همراه صعودی و یا نزولی بودن مرتب سازی، در state کامپوننت جاری تغییر میدهیم:
handleSort = path => { console.log("handleSort", path); this.setState({ sortColumn: { path, order: "asc" } }); };
class Movies extends Component { state = { // ... sortColumn: { path:"title", order: "asc" } };
getPagedData() { const { pageSize, currentPage, selectedGenre, movies: allMovies, sortColumn } = this.state; let filteredMovies = selectedGenre && selectedGenre._id ? allMovies.filter(m => m.genre._id === selectedGenre._id) : allMovies; filteredMovies = filteredMovies.sort((movie1, movie2) => movie1[sortColumn.path] > movie2[sortColumn.path] ? sortColumn.order === "asc" ? 1 : -1 : movie2[sortColumn.path] > movie1[sortColumn.path] ? sortColumn.order === "asc" ? -1 : 1 : 0 ); const first = (currentPage - 1) * pageSize; const last = first + pageSize; const pagedMovies = filteredMovies.slice(first, last); return { totalCount: filteredMovies.length, data: pagedMovies }; }
همچنین میخواهیم اگر با کلیک بر روی ستونی، روش و جهت مرتب سازی آن صعودی بود، نزولی شود و یا برعکس که یک روش پیاده سازی آنرا در اینجا مشاهده میکنید:
handleSort = path => { console.log("handleSort", path); const sortColumn = { ...this.state.sortColumn }; if (sortColumn.path === path) { sortColumn.order = sortColumn.order === "asc" ? "desc" : "asc"; } else { sortColumn.path = path; sortColumn.order = "asc"; } this.setState({ sortColumn }); };
بهبود کیفیت کدهای مرتب سازی اطلاعات
اگر قرار باشد کامپوننت MoviesTable را در جای دیگری مورد استفادهی مجدد قرار دهیم، زمانیکه این جدول سبب صدور رخدادی میشود، باید منطقی را که در متد handleSort فوق مشاهده میکنید، مجددا به همین شکل تکرار کنیم. بنابراین این منطق متعلق به کامپوننت MoviesTable است و زمانیکه onSort را فراخوانی میکند، بهتر است بجای ارسال path یا همان نام فیلدی که قرار است مرتب سازی بر اساس آن انجام شود، شیء sortColumn را به عنوان خروجی بازگشت دهد. به همین جهت، این منطق را به کلاس MoviesTable منتقل میکنیم:
class MoviesTable extends Component { raiseSort = path => { console.log("raiseSort", path); const sortColumn = { ...this.props.sortColumn }; if (sortColumn.path === path) { sortColumn.order = sortColumn.order === "asc" ? "desc" : "asc"; } else { sortColumn.path = path; sortColumn.order = "asc"; } this.props.onSort(sortColumn); };
<MoviesTable movies={movies} onDelete={this.handleDelete} onLike={this.handleLike} onSort={this.handleSort} sortColumn={this.state.sortColumn} />
return ( <table className="table"> <thead> <tr> <th style={{ cursor: "pointer" }} onClick={() => this.raiseSort("title")}>Title</th> <th style={{ cursor: "pointer" }} onClick={() => this.raiseSort("genre.name")}>Genre</th> <th style={{ cursor: "pointer" }} onClick={() => this.raiseSort("numberInStock")}>Stock</th> <th style={{ cursor: "pointer" }} onClick={() => this.raiseSort("dailyRentalRate")}>Rate</th> <th /> <th /> </tr> </thead>
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-13.zip