ایجاد کامپوننت جدید tableHeader
برای پویاسازی تعاریف ستونها و همچنین کم کردن مسئولیتهای کامپوننت MoviesTable، فایل جدید src\components\common\tableHeader.jsx را ایجاد میکنیم تا در برگیرندهی کامپوننت جدید TableHeader شود. پس از ایجاد این فایل، با استفاده از میانبرهای imrc و cc، ساختار ابتدایی کامپوننت TableHeader را تشکیل میدهیم. سپس به کامپوننت MoviesTable بازگشته و متد raiseSort آنرا cut و به اینجا منتقل میکنیم. همچنین نیاز است کل thead جدول فیلمها را نیز به اینجا منتقل کنیم. اما چون میخواهیم این تعاریف پویا باشند، باید امکان تعریف پویای ستونها را نیز به آن اضافه کنیم. بنابراین اینترفیس این کامپوننت به صورت زیر است:
- ورودیهای آن: آرایهی ستونهای جدول و همچنین شیء sortColumn و رخداد onSort که در متد raiseSort استفاده میشوند.
با این توضیحات، کامپوننت TableHeader چنین شکلی را پیدا میکند:
import React, { Component } from "react"; class TableHeader 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); }; render() { return ( <thead> <tr> {this.props.columns.map(column => ( <th onClick={() => this.raiseSort(column.path)}>{column.label}</th> ))} </tr> </thead> ); } } export default TableHeader;
سپس در متد رندر آن، بر اساس آرایهی columns که از props این کامپوننت دریافت خواهد شد، لیست thهای هدر را به صورت پویا رندر میکنیم. در اینجا ساختار مورد نیاز شیء column را نیز مشاهده میکنید. نیاز است یک برچسب نمایش داده شود و همچنین برای اینکه this.raiseSort نیز بتواند مجددا کار کند، نیاز است نام خاصیتی که قرار است مرتب سازی بر اساس آن انجام شود نیز مشخص باشد. بنابراین تا اینجا شیء column باید دارای دو خاصیت label و path باشد.
پس از تعریف ابتدایی کامپوننت TableHeader، به کامپوننت MoviesTable بازگشته و شروع به استفادهی از آن میکنیم:
import TableHeader from "./common/tableHeader";
در ادامه باید آرایهی columns را که به صورت props به کامپوننت TableHeader ارسال میشود، تعریف و مقدار دهی کنیم که تشکیل شدهاست از اشیایی با خواص path و label:
columns = [ { path: "title", label: "Title" }, { path: "genre.name", label: "Genre" }, { path: "numberInStock", label: "Stock" }, { path: "dailyRentalRate", label: "Rate" }, {}, {} ];
اکنون میتوان کل تعریف thead موجود در این کامپوننت را به طور کامل با کامپوننت TableHeader ای که import کردیم، جایگزین کنیم:
render() { const { movies, onDelete, onLike, onSort, sortColumn } = this.props; return ( <table className="table"> <TableHeader columns={this.columns} sortColumn={sortColumn} onSort={onSort} /> <tbody>
در این حالت اگر برنامه را اجرا کنید، بدون مشکل خروجی نهایی را رندر میکند؛ اما در کنسول توسعه دهندگان مرورگر یک چنین خطایی را نیز لاگ خواهد کرد:
index.js:1375 Warning: Each child in a list should have a unique "key" prop. Check the render method of `TableHeader`. See https://fb.me/react-warning-keys for more information.
class MoviesTable extends Component { columns = [ { path: "title", label: "Title" }, { path: "genre.name", label: "Genre" }, { path: "numberInStock", label: "Stock" }, { path: "dailyRentalRate", label: "Rate" }, { key: "like" }, { key: "delete" } ];
render() { return ( <thead> <tr> {this.props.columns.map(column => ( <th key={column.path || column.key} style={{ cursor: "pointer" }} onClick={() => this.raiseSort(column.path)} > {column.label} </th> ))} </tr> </thead> );
استخراج TableBody از جدول کامپوننت MoviesTable
اکنون با استخراج TableHeader از کامپوننت MoviesTable، به همان مشکل مخلوط بودن درجهی abstractions رسیدهایم. از یک طرف با یک abstraction سطح بالا مانند TableHeader در این کامپوننت سر و کار داریم و از طرف دیگر، نمایش تمام جزئیات درونی رندر جدول نیز پیش روی ما است. همچنین رندر ستونهای آن نیز پویا نیست و هنوز بر اساس خاصیت this.columns تعریف شده، واکنش نشان نمیدهد. به همین جهت tbody این جدول را نیز به یک کامپوننت مستقل تبدیل میکنیم. برای این منظور فایل جدید src\components\common\tableBody.jsx را اضافه میکنیم. سپس با استفاده از میانبرهای imrc و cc، ساختار ابتدایی کامپوننت TableBody را تشکیل میدهیم.
این کامپوننت قرار است آرایهای از اشیاء را دریافت و ردیفهایی را بر اساس آنها رندر کند. به همین جهت این آرایه را از props و با نام data دریافت میکنیم. نام data به عمد انتخاب شدهاست، تا بیانگر عمومی بودن آن باشد؛ بجای استفاده از نام ویژهی آرایهی movies، در این مثال خاص.
import React, { Component } from "react"; class TableBody extends Component { render() { const { data, columns } = this.props; return ( <tbody> {data.map(item => ( <tr> {columns.map(column => ( <td></td> ))} </tr> ))} </tbody> ); } } export default TableBody;
رندر محتویات هر سلول جدول به صورت پویا
در این مرحله میخواهیم محتویات tdها را رندر کنیم و حالت فعلی آنها یک چنین شکلی را داشته و در آن ارجاع مستقیمی به شیء movie و خواص آن وجود دارد:
{movies.map(movie => ( <tr key={movie._id}> <td>{movie.title}</td>
برای رندر خواص اشیاء آرایهی ارسالی به کامپوننت TableBody، میتوان از روش [] برای دسترسی به مقادیر خواص استفاده کرد که سبب رندر پویای این مقادیر میشود:
<td>{item[column.path]}</td>
getPropValue(obj, path) { if (!path) { return obj; } const properties = path.split("."); return this.getPropValue(obj[properties.shift()], properties.join(".")); }
<td>{getPropValue(item, column.path)}</td>
همانطور که در ابتدای این سری نیز بررسی کردیم، عبارات JSX در نهایت به اشیاء خالص جاوا اسکریپتی ترجمه میشوند. این ویژگی در حین تعریف المانهای سفارشی مانند کامپوننت Like نیز صادق است. به همین جهت در آرایهی columns که تعاریف ستونهای جدول را به همراه دارد، میتوان یک خاصیت جدید را تعریف و به آن عبارات JSX را انتساب داد. بنابراین تعاریف tdهای Like و Delete را به طور کامل cut کرده و به خاصیت جدید content این دو شیء خالی انتهای لیست آرایهی columns انتساب میدهیم:
class MoviesTable extends Component { columns = [ { path: "title", label: "Title" }, { path: "genre.name", label: "Genre" }, { path: "numberInStock", label: "Stock" }, { path: "dailyRentalRate", label: "Rate" }, { key: "like", content: movie => ( <Like liked={movie.liked} onClick={() => this.props.onLike(movie)} /> ) }, { key: "delete", content: movie => ( <button onClick={() => this.props.onDelete(movie)} className="btn btn-danger btn-sm" > Delete </button> ) } ];
مرحلهی بعد، مراجعه به کامپوننت tableBody و استفاده از خاصیت جدید content، جهت رندر محتوای آن است. در اینجا در متد renderCell بررسی میکنیم اگر ستونی دارای خاصیت content باشد، آن content را رندر میکنیم. در غیراینصورت از همان getPropValue متداول استفاده خواهد شد:
renderCell = (item, column) => { if (column.content) { return column.content(item); } return this.getPropValue(item, column.path); }; createKey = (item, column) => { return item._id + (column.path || column.key); }; render() { const { data, columns } = this.props; return ( <tbody> {data.map(item => ( <tr key={item._id}> {columns.map(column => ( <td key={this.createKey(item, column)}> {this.renderCell(item, column)} </td> ))} </tr> ))} </tbody> ); }
- همچنین در اینجا یک createKey را نیز مشاهده میکنید. المانهای هر Array.map نوشته شده، نیاز به یک ویژگی key مقدار دهی شده دارند که در دو قسمت trها و همچنین tdها تعریف شدهاست. در فرمول آن جائیکه از || استفاده شده، اگر ستونی دارای path بود، مقدار آن درج میشود، اما اگر مانند دو ستون آخر صرفا key تعریف شده بود، وجود || سبب میشود تا column.key درنظر گرفته شود و مشکلی رخ ندهد.
- علت تعریف دو متد مجزای renderCell و createKey هم کم شدن بار if/elseها، در بین کدهای درج شدهی در ردیفهای جدول است.
اکنون به کامپوننت MoviesTable مراجعه کرده و کل tbody آنرا حذف و با المان کامپوننت TableBody، جایگزین میکنیم:
//... import TableBody from "./common/tableBody"; //... class MoviesTable extends Component { // ... render() { const { movies, onSort, sortColumn } = this.props; return ( <table className="table"> <TableHeader columns={this.columns} sortColumn={sortColumn} onSort={onSort} /> <TableBody columns={this.columns} data={movies} /> </table> ); } }
اضافه کردن آیکن مرتب سازی اطلاعات به سر ستونهای جدول
در کامپوننت tableHeader، کار رندر thها انجام میشود. در اینجا پس از نام سرستون، میخواهیم آیکن نمایش صعودی و یا نزولی بودن روش مرتب سازی جاری را نمایش دهیم. برای این منظور، ابتدا متد renderSortIcon را به این کامپوننت اضافه میکنیم:
renderSortIcon = column => { const { sortColumn } = this.props; if (column.path !== sortColumn.path) { return null; } if (sortColumn.order === "asc") { return <i className="fa fa-sort-asc" />; } return <i className="fa fa-sort-desc" />; };
و سپس در متد رندر کامپوننت tableHeader، این متد را در کنار label آن ستون درج خواهیم کرد:
{column.label} {this.renderSortIcon(column)}
استخراج کل Table از جدول کامپوننت MoviesTable
در حال حاضر اگر به پیاده سازی کامپوننت MoviesTable دقت کنیم، یک تگ table به همراه دو کامپوننت TableHeader و TableBody در آن درج شدهاند. با این طراحی، اگر قصد استفادهی از این امکانات را در جای دیگری داشته باشیم، باید دقیقا همین قطعه کد را تکرار کنیم. به همین جهت کل تگ table این کامپوننت را استخراج کرده و به کامپوننت جدیدی منتقل میکنیم. به همین جهت فایل جدید src\components\common\table.jsx را ایجاد کرده و با استفاده از میانبرهای imrc و cc، ساختار ابتدایی کامپوننت Table را تشکیل میدهیم. سپس کل تگ table کامپوننت MoviesTable را cut کرده و به متد رندر کامپوننت جدید Table منتقل میکنیم. سپس اولین قدم برای سازگار کردن این محتوا با یک کامپوننت جدید، افزودن importهای زیر است:
import TableBody from "./tableBody"; import TableHeader from "./tableHeader";
import TableBody from "./tableBody"; import TableHeader from "./tableHeader"; class Table extends Component { render() { const { columns, sortColumn, onSort, data } = this.props; return ( <table className="table"> <TableHeader columns={columns} sortColumn={sortColumn} onSort={onSort} /> <TableBody columns={columns} data={data} /> </table> ); } } export default Table;
//... import Table from "./common/table"; class MoviesTable extends Component { //... render() { const { movies, onSort, sortColumn } = this.props; return ( <Table columns={this.columns} sortColumn={sortColumn} onSort={onSort} data={movies} /> ); } }
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-14.zip