پیشنیازهای نمایش اطلاعات گرید به همراه صفحه بندی اطلاعات
در مطلب «Angular CLI - قسمت ششم - استفاده از کتابخانههای ثالث» نحوهی نصب و معرفی کتابخانهی ngx-bootstrap را بررسی کردیم. دقیقا همان مراحل، در اینجا نیز باید طی شوند و از این مجموعه تنها به کامپوننت Pagination آن نیاز داریم. همان قسمت ذیل گرید تصویر فوق که شماره صفحات را جهت انتخاب، نمایش دادهاست.
بنابراین ابتدا فرض بر این است که دو بستهی بوت استرپ و ngx-bootstrap را نصب کردهاید:
> npm install bootstrap --save > npm install ngx-bootstrap --save
"apps": [ { "styles": [ "../node_modules/bootstrap/dist/css/bootstrap.min.css", "styles.css" ],
import { PaginationModule } from "ngx-bootstrap"; @NgModule({ imports: [ PaginationModule.forRoot() ]
>ng g m SimpleGrid -m app.module --routing
کامپوننتی هم که مثال جاری را نمایش میدهد به صورت ذیل به ماژول SimpleGrid فوق اضافه شدهاست:
>ng g c SimpleGrid/products-list
تهیه معادلهای قراردادهای سمت سرور در سمت Angular
در قسمت قبل، تعدادی قرارداد مانند پارامترهای دریافتی از سمت کلاینت و ساختار اطلاعات ارسالی به سمت کلاینت را تعریف کردیم. اکنون جهت کار strongly typed با آنها در سمت یک برنامهی تایپ اسکریپتی Angular، کلاسهای معادل آنها را تهیه میکنیم.
ساختار شیء محصول دریافتی از سمت سرور
>ng g cl SimpleGrid/app-product
export class AppProduct { constructor( public productId: number, public productName: string, public price: number, public isAvailable: boolean ) {} }
ساختار معادل پارامترهای صفحه بندی و مرتب سازی ارسالی به سمت سرور
>ng g cl SimpleGrid/PagedQueryModel
export class PagedQueryModel { constructor( public sortBy: string, public isAscending: boolean, public page: number, public pageSize: number ) {} }
ساختار معادل اطلاعات صفحه بندی شدهی دریافتی از سمت سرور
>ng g cl SimpleGrid/PagedQueryResult
export class PagedQueryResult<T> { constructor(public totalItems: number, public items: T[]) {} }
ساختار ستونهای گرید نمایشی
>ng g cl SimpleGrid/GridColumn
export class GridColumn { constructor( public title: string, public propertyName: string, public isSortable: boolean ) {} }
تهیه سرویس ارسال اطلاعات صفحه بندی به سرور و دریافت اطلاعات از آن
پس از تدارک این مقدمات، اکنون کار تعریف سرویسی که این اطلاعات را به سمت سرور ارسال میکند و نتیجه را باز میگرداند، به صورت ذیل خواهد بود:
>ng g s SimpleGrid/products-list -m simple-grid.module
پیش از تکمیل این سرویس، نیاز است متدی را جهت تبدیل یک شیء، به معادل کوئری استرینگ آن تهیه کنیم:
toQueryString(obj: any): string { const parts = []; for (const key in obj) { if (obj.hasOwnProperty(key)) { const value = obj[key]; if (value !== null && value !== undefined) { parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(value)); } } } return parts.join("&"); }
[HttpGet("[action]")] public PagedQueryResult<Product> GetPagedProducts(ProductQueryViewModel queryModel)
http://localhost:5000/api/Product/GetPagedProducts?sortBy=productId&isAscending=true&page=2&pageSize=7
پس از این تعریف، سرویس ProductsListService به صورت ذیل تکمیل خواهد شد:
@Injectable() export class ProductsListService { private baseUrl = "api/Product"; constructor(private http: Http) {} getPagedProductsList( queryModel: PagedQueryModel ): Observable<PagedQueryResult<AppProduct>> { return this.http .get(`${this.baseUrl}/GetPagedProducts?${this.toQueryString(queryModel)}`) .map(res => { const result = res.json(); return new PagedQueryResult<AppProduct>( result.totalItems, result.items ); }); }
سپس در متد map آن، res.json دقیقا همان ساختار PagedQueryResult سمت سرور را به همراه دارد. اینجا است که فرصت خواهیم داشت نمونهی سمت کلاینت آنرا که در ابتدای بحث تهیه کردیم، وهله سازی کرده و بازگشت دهیم (نگاشت فیلدهای دریافتی از سمت سرور به سمت کلاینت).
تکمیل کامپوننت نمایش گرید
قسمت آخر این مطلب، استفادهی از این ساختارها و سرویسها و نمایش اطلاعات دریافتی از آنها است. برای این منظور ابتدا نیاز است سرستونهای این گرید را تهیه کرد:
<table class="table table-striped table-hover table-bordered table-condensed"> <thead> <tr> <th class="text-center" style="width:3%">#</th> <th *ngFor="let column of columns" class="text-center"> <div *ngIf="column.isSortable" (click)="sortBy(column.propertyName)" style="cursor: pointer"> {{ column.title }} <i *ngIf="queryModel.sortBy === column.propertyName" class="glyphicon" [class.glyphicon-sort-by-order]="queryModel.isAscending" [class.glyphicon-sort-by-order-alt]="!queryModel.isAscending"></i> </div> <div *ngIf="!column.isSortable" style="cursor: pointer"> {{ column.title }} </div> </th> </tr> </thead>
export class ProductsListComponent implements OnInit { columns: GridColumn[] = [ new GridColumn("Id", "productId", true), new GridColumn("Name", "productName", true), new GridColumn("Price", "price", true), new GridColumn("Available", "isAvailable", true) ];
همچنین در کدهای قالب این کامپوننت، مدیریت کلیک بر روی یک سر ستون را نیز مشاهده میکنید:
export class ProductsListComponent implements OnInit { itemsPerPage = 7; queryModel = new PagedQueryModel("productId", true, 1, this.itemsPerPage); sortBy(columnName) { if (this.queryModel.sortBy === columnName) { this.queryModel.isAscending = !this.queryModel.isAscending; } else { this.queryModel.sortBy = columnName; this.queryModel.isAscending = true; } this.getPagedProductsList(); } }
کار رندر بدنهی اصلی گرید توسط همین چند سطر در قالب آن مدیریت میشود:
<tbody> <tr *ngFor="let item of queryResult.items; let i = index"> <td class="text-center">{{ itemsPerPage * (currentPage - 1) + i + 1 }}</td> <td class="text-center">{{ item.productId }}</td> <td class="text-center">{{ item.productName }}</td> <td class="text-center">{{ item.price | number:'.0' }}</td> <td class="text-center"> <input id="item-{{ item.productId }}" type="checkbox" [checked]="item.isAvailable" disabled="disabled" /> </td> </tr> </tbody> </table>
در اینجا حلقهای بر روی queryResult.items تشکیل شدهاست. این منبع داده به صورت ذیل در کامپوننت متناظر مقدار دهی میشود:
export class ProductsListComponent implements OnInit { itemsPerPage = 7; currentPage: number; numberOfPages: number; isLoading = false; queryModel = new PagedQueryModel("productId", true, 1, this.itemsPerPage); queryResult = new PagedQueryResult<AppProduct>(0, []); constructor(private productsListService: ProductsListService) {} ngOnInit() { this.getPagedProductsList(); } private getPagedProductsList() { this.isLoading = true; this.productsListService .getPagedProductsList(this.queryModel) .subscribe(result => { this.queryResult = result; this.isLoading = false; }); } }
قسمت آخر کار، افزودن کامپوننت نمایش شماره صفحات است:
<div align="center"> <pagination [maxSize]="8" [boundaryLinks]="true" [totalItems]="queryResult.totalItems" [rotate]="false" previousText="‹" nextText="›" firstText="«" lastText="»" (numPages)="numberOfPages = $event" [(ngModel)]="currentPage" (pageChanged)="onPageChange($event)"></pagination> </div> <pre class="card card-block card-header">Page: {{currentPage}} / {{numberOfPages}}</pre>
export class ProductsListComponent implements OnInit { itemsPerPage = 7; currentPage: number; numberOfPages: number; onPageChange(event: any) { this.queryModel.page = event.page; this.getPagedProductsList(); } }
هر زمانیکه کاربر بر روی شمارهای کلیک میکند، رخداد onPageChange فراخوانی شده و در اینحالت تنها کافی است شماره صفحهی درخواستی queryModel جاری را به روزرسانی کرده و سپس آنرا در اختیار متد getPagedProductsList جهت دریافت اطلاعات این صفحهی درخواستی قرار دهیم.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
کتابخانهی React Nice Dates
معرفی پروژه فروشگاهی Iris Store
در پروژهی iOS، در فایل AppDelegate.cs، بعد از Forms.Init، کد زیر را کپی کنید:
SfListViewRenderer.Init();
همین کد را در MainPage.xaml.cs در پروژه UWP، قبل از LoadApplication قرار دهید. نیازی به انجام کاری در Android نیست.
سپس Product Key این محصول را به دست آورده و در پروژه XamApp، فولدر Views در فایل SyncfusionLicense قرار دهید.
حال برای نمایش لیستی از محصولات، ابتدا کلاس Product را ایجاد میکنیم. چه در زمانیکه یک Rest api را در سمت سرور فراخوانی میکنیم و چه زمانیکه با دیتابیس بر روی گوشی یعنی Sqlite کار میکنیم، در نهایت لیستی از یک کلاس را داریم (در اینجا Product).
public class Product : Bindable { public int Id { get; set; } public string Name { get; set; } public bool IsActive { get; set; } public decimal Price { get; set; } }
در یک View Model جدید با نام ProductsViewModel، در OnNavigatedToAsync، دیتا را از سرور یا دیتابیس، بر روی گوشی دریافت میکنیم؛ اما در این مثال، برای راحتی بیشتر یک List را New میکنیم:
public class ProductsViewModel : BitViewModelBase { public List<Product> Products { get; set; } public async override Task OnNavigatedToAsync(INavigationParameters parameters) { Products = new List<Product> // getting products from server or sqlite database { new Product { Id = 1, IsActive = true, Name = "Product1" , Price = 12.2m /* m => decimal */ }, new Product { Id = 2, IsActive = false, Name = "Product2" , Price = 14 }, new Product { Id = 3, IsActive = true, Name = "Product3" , Price = 11 }, }; await base.OnNavigatedToAsync(parameters); } }
حال نوبت به دادن یک Template میرسد. مثلا فرض کنید میخواهیم نام را درون یک Label نمایش دهیم و بر اساس فعال یا غیر فعال بودن Product، یک Checkbox را تغییر داده، تیک بزنیم یا نزنیم و در نهایت نمایش قیمت را در یک Label دیگر خواهیم داشت.
<sfListView:SfListView ItemsSource="{Binding Products}"> <sfListView:SfListView.ItemTemplate> <DataTemplate> <FlexLayout x:DataType="model:Product" Direction="Row"> <Label FlexLayout.Basis="50%" Text="{Binding Name}" VerticalTextAlignment="Center" /> <bitControls:BitCheckbox InputTransparent="True" FlexLayout.Basis="25%" IsChecked="{Binding IsActive}" /> <Label FlexLayout.Basis="25%" Text="{Binding Price}" VerticalTextAlignment="Center" /> </FlexLayout> </DataTemplate> </sfListView:SfListView.ItemTemplate> </sfListView:SfListView>
همانطور که میبینید، در DataTemplate از Flex Layout استفاده شده است. Flex Layout در کنار Grid, Stack, Relative, Absolute و سایر Layoutهای Xamarin Forms در پروژه قابلیت استفاده دارد و مزیتهای خاص خود را دارد.
این Data Template توسط List View، حداکثر سه بار ساخته میشود؛ چون View Model در لیست مثال خود، سه Product دارد. خود List View تکنیکهای Virtualization و Cell Reuse را بدون نیاز به هیچ کد اضافهای هندل میکند و Performance خوبی دارد. در View مربوطه یعنی ProductsView.xaml، هر Binding ای (مثل Binding Products) به View Model اشاره میکند، اما درون Data Template، هر Binding به Product ای اشاره میکند که آن ردیف List View، دارد نمایشاش میدهد. برای همین x:DataType را روی Flex Layout درون Data Template به Product وصل کردهایم. در این صورت اگر بنویسیم Binding N_ame، به ما خطا داده میشود که کلاس Product هیچ Property با نام N_ame ندارد که خطای درستی است.
روی BitCheckbox مقدار InputTransparent را برابر با True دادهایم که باعث میشود کلیک روی Checkbox عملا در نظر گرفته نشود. این منطقی است، زیرا عوض کردن مقدار Checkbox در این مثال ما ذخیره نمیشود و کاربرد نمایشی دارد و فقط باعث گیج شدن کاربر میشود.
کنترل BitCheckbox از مجموعه کنترلهای Bit است که اخیرا با BitDatePicker آن آشنا شدهاید. برای آشنایی با نحوه افزودن این کنترلها به یک پروژه، به مستندات Bit Framework مراجعه کنید. خود Syncfusion نیز Checkbox دارد.
حال فرض کنید که قرار است دکمهای برای هر ردیف List View داشته باشیم که با زدن روی آن، اطلاعات Product به سرور ارسال شود و جزئیات بیشتری دریافت و در قالب یک Alert نمایش داده شود. برای این کار، ابتدا به Data Template که Flex Layout است، یک دکمه اضافه میکنیم. سپس Command آن دکمه را به View Model بایند میکنیم. در آن Command البته احتیاج داریم بدانیم درخواست نمایش جزئیات بیشتر، برای کدام Product داده شده. این مهم با Command Parameter شدنی است.
برای پیاده سازی این مثال، در سمت View Model داریم:
public BitDelegateCommand<Product> ShowProductDetailsCommand { get; set; }public IUserDialogs UserDialogs { get; set; } async Task ShowProductDetails(Product product) { string productDetail = $"Product: {product.Name}'s more info: ..."; // get more info from server. await UserDialogs.AlertAsync(productDetail, "Product Detail"); }
کامند ShowProductDetailCommand یک پارامتر را از جنس Product میگیرد و آن Product ای است که روی دکمه آن کلیک شدهاست. با Clone کردن آخرین نسخه XamApp و درخواست نمایش صفحهی Products در App.xaml.cs به صورت زیر و اجرای برنامه، میتوانید درک بهتری از عملکرد آن داشته باشید:
await NavigationService.NavigateAsync("/Nav/Products", animated: false);
سپس در View مربوطه داریم:
...<Button Command="{Binding ShowProductDetailsCommand}" CommandParameter="{Binding .}" Text="Detail..." /> </FlexLayout> </DataTemplate>
CommandParameter اگر برابر با Binding Id میبود، به Command در سمت View Model، بجای کل Product، فقط Id آن ارسال میشد. ولی Show Product Detail Command منتظر یک Product کامل است، نه فقط Id آن. با نوشتن
CommandParameter="{Binding .}"
کل Product با کلیک روی دکمه به Command ارسال میشود.
اکنون اگر پروژه را Build کنید، خطایی را از x:DataType خواهید گرفت که منطقی است. اگر Binding Name و Binding Price دو Property با نامهای Name و Price را از کلاس Product جستجو میکنند، پس قاعدتا ShowProductDetailCommand نیز در همان کلاس مدل، یعنی Product جستجو میشود! ولی میدانیم که این Command در View Model ما یعنی ProductsViewModel است. برای حل این مشکل، به جای Binding از bit:ViewModelBinding استفاده میکنیم:
Command="{bit:ViewModelBinding ShowProductDetailsCommand}"
در این صورت، بجای جستجو کردن ShowProductDetailCommand در کلاس Product، این را در ProductsViewModel جستجو میکند که منجر به خروجی درست میشود.
این List View دارای امکاناتی چون Infinite loading، Pull to refresh و Grouping-Sorting-Filtering و ... است که میتوانید از روی مستندات خوب Syncfusion، آنها را راه اندازی کنید و اگر به مشکلی برخوردید نیز اینجا بپرسید. همچنین نگاهی به لیست 129 کنترل دیگر بیاندازید و ببینید که در برنامههای خود از کدام یک از آنها میتوانید استفاده کنید.
ایجاد فرم لاگین
فرم لاگینی را که به برنامهی نمایش لیست فیلمهای تکمیل شدهی تا قسمت 17، اضافه خواهیم کرد، یک فرم بوت استرپی است و میتوانید جزئیات بیشتر مزین سازی المانهای این نوع فرمها را با کلاسهای بوت استرپ، در مطلب «کار با شیوهنامههای فرمها در بوت استرپ 4» مطالعه کنید.
در ابتدا فایل جدید src\components\loginForm.jsx را ایجاد کرده و سپس توسط میانبرهای imrc و cc در VSCode، ساختار ابتدایی کامپوننت جدید LoginForm را ایجاد میکنیم:
import React, { Component } from "react"; class LoginForm extends Component { render() { return <h1>Login</h1>; } } export default LoginForm;
import LoginForm from "./components/loginForm"; //... function App() { return ( <React.Fragment> <NavBar /> <main className="container"> <Switch> <Route path="/login" component={LoginForm} /> <Route path="/movies/:id" component={MovieForm} /> // ... </Switch> </main> </React.Fragment> ); }
<NavLink className="nav-item nav-link" to="/login"> Login </NavLink>
اکنون نوبت به افزودن فرم بوت استرپی لاگین به فایل loginForm.jsx رسیدهاست:
import React, { Component } from "react"; class LoginForm extends Component { render() { return ( <form> <div className="form-group"> <label htmlFor="username">Username</label> <input id="username" type="text" className="form-control" /> </div> <div className="form-group"> <label htmlFor="password">Password</label> <input id="password" type="password" className="form-control" /> </div> <button className="btn btn-primary">Login</button> </form> ); } } export default LoginForm;
- ابتدا المان form به صفحه اضافه میشود.
- سپس هر ورودی، داخل یک div با کلاس form-group، محصور میشود. کار آن تبدیل یک برچسب و فیلد ورودی، به یک گروه از ورودیهای بوت استرپ است.
- در اینجا هر برچسب دارای یک ویژگی for است. اما چون قرار است عبارات jsx، به معادلهای جاوا اسکریپتی ترجمه شوند، نمیتوان از واژهی کلیدی for در اینجا استفاده کرد. به همین جهت از معادل react ای آن که htmlFor است، در کدهای فوق استفاده کردهایم؛ شبیه به نکتهای که در مورد تبدیل ویژگی class به className وجود دارد. مقدار هر ویژگی htmlFor نیز به id فیلد ورودی متناظر با آن تنظیم میشود. به این ترتیب اگر کاربر بر روی این برچسب کلیک کرده و آنرا انتخاب کند، فیلد متناظر با آن، دارای focus میشود.
- فیلدهای ورودی نیز دارای کلاس form-control هستند.
با این خروجی نهایی در مرورگر:
مدیریت ارسال فرمها
به صورت پیش فرض و استاندارد، دکمهی افزوده شدهی به المان form، سبب ارسال اطلاعات آن به سرور و سپس بارگذاری کامل صفحه میشود. این رفتاری نیست که در یک برنامهی SPA مدنظر باشد. برای مدیریت این حالت، میتوان از رخداد onSubmit هر المان فرم، استفاده کرد:
class LoginForm extends Component { handleSubmit = e => { console.log("handleSubmit", e); e.preventDefault(); // call the server }; render() { return ( <form onSubmit={this.handleSubmit}> //...
دسترسی مستقیم به المانهای فرمها
پس از فراخوانی متد preventDefault، کار مدیریت ارسال فرم به سرور را باید خودمان مدیریت کنیم و دیگر رخداد full post back استاندارد به سمت سرور را نخواهیم داشت. در جاوا اسکریپت خالص برای دریافت مقادیر وارد شدهی توسط کاربر میتوان نوشت:
const username = document.getElementById("username").value;
برای دسترسی به یک المان DOM در React، باید یک reference را به آن نسبت داد. برای این منظور یک خاصیت جدید را در سطح کلاس کامپوننت، ایجاد کرده و آنرا با React.RefObject، مقدار دهی اولیه میکنیم:
class LoginForm extends Component { username = React.createRef();
<input ref={this.username} id="username" type="text" className="form-control" />
handleSubmit = e => { e.preventDefault(); // call the server const username = this.username.current.value; console.log("handleSubmit", username); };
class LoginForm extends Component { username = React.createRef(); componentDidMount = () => { this.username.current.focus(); };
البته روش بهتری نیز برای انجام اینکار وجود دارد. المانهای JSX دارای ویژگی autoFocus نیز هستند که دقیقا همین کار را انجام میدهد:
<input autoFocus ref={this.username} id="username" type="text" className="form-control" />
تبدیل المانهای فرمها به Controlled elements
در بسیاری از اوقات، فرمهای ما state خود را از سرور دریافت میکنند. فرض کنید که در حال ایجاد یک فرم ثبت اطلاعات فیلمها هستیم. در این حالت باید بر اساس id فیلم، اطلاعات آن را از سرور دریافت و در state ذخیره کرد؛ سپس فیلدهای فرم را بر اساس آن مقدار دهی اولیه کرد. برای نمونه در فرم لاگین میتوان state را با شیء account، به صورت زیر مقدار دهی اولیه کرد:
class LoginForm extends Component { state = { account: { username: "", password: "" } };
ابتدا ویژگی value فیلد برای مثال username را به خاصیت username شیء account موجود در state متصل میکنیم:
<input value={this.state.account.username}
<input value={this.state.account.username} onChange={this.handleChange}
handleChange = e => { const account = { ...this.state.account }; //cloning an object account.username = e.currentTarget.value; this.setState({ account }); };
مدیریت دریافت اطلاعات چندین فیلد ورودی
تا اینجا موفق شدیم اطلاعات state را به تغییرات فیلد username در فرم لاگین متصل کنیم؛ اما فیلد password را چگونه باید مدیریت کرد؟ برای اینکه تمام این مراحل را مجددا تکرار نکنیم، میتوان از مقدار دهی پویای خواص در جاوا اسکریپت که توسط [] انجام میشود استفاده کرد:
handleChange = e => { const account = { ...this.state.account }; //cloning an object account[e.currentTarget.name] = e.currentTarget.value; this.setState({ account }); };
<input id="password" name="password" value={this.state.account.password} onChange={this.handleChange} type="password" className="form-control" />
یک نکته: میتوان توسط Object Destructuring، تکرار e.currentTarget را حذف کرد:
handleChange = ({ currentTarget: input }) => { const account = { ...this.state.account }; //cloning an object account[input.name] = input.value; this.setState({ account }); };
آشنایی با خطاهای متداول دریافتی در حین کار با فرمها
فرض کنید خاصیت username را از شیء account موجود در state حذف کردهایم. در زمان نمایش ابتدایی فرم، خطایی را دریافت نخواهیم کرد، اما اگر اطلاعاتی را در آن وارد کنیم، بلافاصله در کنسول توسعه دهندگان مرورگر چنین اخطاری ظاهر میشود:
Warning: A component is changing an uncontrolled input of type text to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component. More info: https://fb.me/react-controlled-components
دقیقا چنین اخطاری را با ورود null/undefined بجای "" در حین مقدار دهی اولیهی username در شیء account نیز دریافت خواهیم کرد:
Warning: `value` prop on `input` should not be null. Consider using an empty string to clear the component or `undefined` for uncontrolled components.
ایجاد یک کامپوننت ورود اطلاعات با قابلیت استفادهی مجدد
هر چند در پیاده سازی فعلی سعی کردیم با بکارگیری مقداردهی پویای خواص اشیاء، تکرار کدها را کاهش دهیم، اما باز هم به ازای هر فیلد ورودی باید این مسایل تکرار شوند:
- ایجاد یک div با کلاسهای بوت استرپی.
- ایجاد label و همچنین فیلد ورودی.
- در اینجا مقدار htmlFor باید با مقدار id فیلد ورودی یکی باشد.
- مقدار دهی ویژگیهای value و onChange نیز باید تکرار شوند.
بنابراین بهتر است این تعاریف را استخراج و به یک کامپوننت با قابلیت استفادهی مجدد منتقل کرد. به همین جهت فایل جدید src\components\common\input.jsx را در پوشهی common ایجاد کرده و سپس توسط میانبرهای imrc و sfc، این کامپوننت تابعی بدون حالت را تکمیل میکنیم:
import React from "react"; const Input = ({ name, label, value, onChange }) => { return ( <div className="form-group"> <label htmlFor={name}>{label}</label> <input value={value} onChange={onChange} id={name} name={name} type="text" className="form-control" /> </div> ); }; export default Input;
سپس به کامپوننت فرم لاگین بازگشته و ابتدا آنرا import میکنیم:
import Input from "./common/input";
render() { const { account } = this.state; return ( <form onSubmit={this.handleSubmit}> <Input name="username" label="Username" value={account.username} onChange={this.handleChange} /> <Input name="password" label="Password" value={account.password} onChange={this.handleChange} /> <button className="btn btn-primary">Login</button> </form> );
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: sample-18.zip