صفحات مودال در بوت استرپ 3
برای نمایش مودال تو در تو فکر نکنم نیازی به این کتابخانه "jquery.bootstrap-modal-ajax-form.js " باشد . کتابخانه جاوااسکریت بوت استراپ کفایت میکند .
این برنامه از چهار کامپوننت تشکیل شدهاست:
- کامپوننت App که در برگیرندهی سه کامپوننت زیر است:
- کامپوننت BasketItemsCounter: جمع تعداد آیتمهای انتخابی توسط کاربر را نمایش میدهد؛ به همراه دکمهای برای خالی کردن لیست انتخابی.
- کامپوننت ShopItemsList: لیست محصولات موجود در فروشگاه را نمایش میدهد. با کلیک بر روی هر آیتم آن، آیتم انتخابی به لیست انتخابهای او اضافه خواهد شد.
- کامپوننت BasketItemsList: لیستی را نمایش میدهد که حاصل انتخابهای کاربر در کامپوننت ShopItemsList است (یا همان سبد خرید). در ذیل این لیست، جمع نهایی قیمت قابل پرداخت نیز درج میشود. همچنین اگر کاربر بر روی دکمهی remove هر ردیف کلیک کند، یک واحد از چند واحد انتخابی، حذف خواهد شد.
بنابراین در اینجا سه کامپوننت مجزا را داریم که با هم تبادل اطلاعات میکنند. یکی جمع تعداد محصولات خریداری شده را، دیگری لیست محصولات موجود را و آخری لیست خرید نهایی را نمایش میدهد. همچنین این سه کامپوننت، فرزند یک دیگر هم محسوب نمیشوند و انتقال اطلاعات بین اینها نیاز به بالا بردن state هر کدام و قرار دادن آنها در کامپوننت App را دارد تا بتوان پس از آن از طریق props آنها را بین سه کامپوننت فوق که اکنون فرزند کامپوننت App محسوب میشوند، به اشتراک گذاشت. روش بهتر اینکار، استفاده از یک مخزن حالت سراسری است تا حالتهای این کامپوننتها را نگهداری کرده و دادهها را بین آنها به اشتراک بگذارد که در اینجا برای حل این مساله از کتابخانههای mobx و mobx-react استفاده خواهیم کرد.
برپایی پیشنیازها
برای پیاده سازی برنامهی فوق، یک پروژهی جدید React را ایجاد میکنیم:
> create-react-app state-management-with-mobx-part4 > cd state-management-with-mobx-part4
> npm install --save bootstrap mobx mobx-react mobx-react-devtools mobx-state-tree
- برای استفاده از شیوهنامههای بوت استرپ، بستهی bootstrap نیز در اینجا نصب میشود.
- اصل کار برنامه توسط دو کتابخانهی mobx و کتابخانهی متصل کنندهی آن به برنامههای react که mobx-react نام دارد، انجام خواهد شد.
- چون میخواهیم از افزونهی mobx-devtools نیز استفاده کنیم، نیاز است دو بستهی mobx-react-devtools و همچنین mobx-state-tree را که جزو وابستگیهای آن است، نصب کنیم.
سپس بستههای زیر را که در قسمت devDependencies فایل package.json درج خواهند شد، باید نصب شوند:
> npm install --save-dev babel-eslint customize-cra eslint eslint-config-react-app eslint-loader eslint-plugin-babel eslint-plugin-css-modules eslint-plugin-filenames eslint-plugin-flowtype eslint-plugin-import eslint-plugin-no-async-without-await eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-react-redux eslint-plugin-redux-saga eslint-plugin-simple-import-sort react-app-rewired typescript
- افزودن فایل جدید config-overrides.js به ریشهی پروژه، تا پشتیبانی ازlegacy" decorators spec" فعال شود.
- اصلاح فایل package.json و ویرایش قسمت scripts آن برای استفادهی از react-app-rewired، تا امکان تغییر تنظیمات webpack به صورت پویا در زمان اجرای برنامه، میسر شود.
- همچنین فایل غنی شدهی eslintrc.json. را نیز به ریشهی پروژه اضافه میکنیم.
تهیه سرویس لیست محصولات موجود در فروشگاه
این برنامه از یک لیست درون حافظهای، برای تهیهی لیست محصولات موجود در فروشگاه استفاده میکند. به همین جهت پوشهی service را افزوده و سپس فایل جدید src\services\productsService.js را با محتوای زیر، ایجاد میکنیم:
const products = [ { id: 1, name: "Item 1", price: 850 }, { id: 2, name: "Item 2", price: 900 }, { id: 3, name: "Item 3", price: 1500 }, { id: 4, name: "Item 4", price: 1000 } ]; export default products;
ایجاد کامپوننت نمایش لیست محصولات
پس از مشخص شدن لیست محصولات قابل فروش، کامپوننت جدید src\components\ShopItemsList.jsx را به صورت زیر ایجاد میکنیم:
import React from "react"; import products from "../services/productsService"; const ShopItemsList = ({ onAdd }) => { return ( <table className="table table-hover"> <thead className="thead-light"> <tr> <th>Name</th> <th>Price</th> <th>Action</th> </tr> </thead> <tbody> {products.map(product => ( <tr key={product.id}> <td>{product.name}</td> <td>{product.price}</td> <td> <button className="btn btn-sm btn-info" onClick={() => onAdd(product)} > Add </button> </td> </tr> ))} </tbody> </table> ); }; export default ShopItemsList;
- در اینجا همچنین هر ردیف، به همراه یک دکمهی Add نیز هست که قرار است با کلیک بر روی آن، متد رویدادگردان onAdd فراخوانی شود. این متد نیز از طریق props این کامپوننت دریافت میشود. کتابخانههای مدیریت حالت، تمام خواص و رویدادگردانهای مورد نیاز یک کامپوننت را از طریق props، تامین میکنند.
- فعلا این کامپوننت به هیچ مخزن دادهای متصل نیست و فقط طراحی ابتدایی آن آماده شدهاست.
ایجاد کامپوننت نمایش لیست خرید کاربر (سبد خرید)
اکنون که میتوان توسط کامپوننت لیست محصولات، تعدادی از آنها را خریداری کرد، کامپوننت جدید src\components\BasketItemsList.jsx را برای نمایش لیست نهایی خرید کاربر، به صورت زیر پیاده سازی میکنیم:
import React from "react"; const BasketItemsList = ({ items, totalPrice, onRemove }) => { return ( <> <table className="table table-hover"> <thead className="thead-light"> <tr> <th>Name</th> <th>Price</th> <th>Count</th> <th>Action</th> </tr> </thead> <tbody> {items.map(item => ( <tr key={item.id}> <td>{item.name}</td> <td>{item.price}</td> <td>{item.count}</td> <td> <button className="btn btn-sm btn-danger" onClick={() => onRemove(item.id)} > Remove </button> </td> </tr> ))} <tr> <td align="right"> <strong>Total: </strong> </td> <td> <strong>{totalPrice}</strong> </td> <td></td> <td></td> </tr> </tbody> </table> </> ); }; export default BasketItemsList;
const BasketItemsList = ({ items, totalPrice, onRemove }) => {
- همچنین هر ردیف نمایش داده شده، به همراه یک دکمهی Remove آیتم انتخابی نیز هست که به متد رویدادگردان onRemove متصل شدهاست.
- در ردیف انتهایی این لیست، مقدار totalPrice که یک خاصیت محاسباتی است، درج میشود.
- فعلا این کامپوننت نیز به هیچ مخزن دادهای متصل نیست و فقط طراحی ابتدایی آن آماده شدهاست.
ایجاد کامپوننت نمایش تعداد آیتمهای خریداری شده
کاربر اگر آیتمی را از لیست محصولات انتخاب کند و یا محصول انتخاب شده را از لیست خرید حذف کند، تعداد نهایی باقی مانده را میتوان در کامپوننت src\components\BasketItemsCounter.jsx مشاهده کرد:
import React, { Component } from "react"; class BasketItemsCounter extends Component { render() { const { count, onRemoveAll } = this.props; return ( <div> <h1>Total items: {count}</h1> <button type="button" className="btn btn-sm btn-danger" onClick={() => onRemoveAll()} > Empty Basket </button> </div> ); } } export default BasketItemsCounter;
- فعلا این کامپوننت نیز به هیچ مخزن دادهای متصل نیست و فقط طراحی ابتدایی آن آماده شدهاست.
نمایش ابتدایی سه کامپوننت توسط کامپوننت App
اکنون که این سه کامپوننت تکمیل شدهاند، میتوان المانهای آنها را در فایل src\App.js درج کرد تا در صفحه نمایش داده شوند:
import React, { Component } from "react"; import BasketItemsCounter from "./components/BasketItemsCounter"; import BasketItemsList from "./components/BasketItemsList"; import ShopItemsList from "./components/ShopItemsList"; class App extends Component { render() { return ( <main className="container"> <div className="row"> <BasketItemsCounter /> </div> <hr /> <div className="row"> <h2>Products</h2> <ShopItemsList /> </div> <div className="row"> <h2>Basket</h2> <BasketItemsList /> </div> </main> ); } } export default App;
میتوان همانند Redux کل state برنامه را داخل یک شیء store ذخیره کرد و یا چون در اینجا میتوان طراحی مخزن حالت MobX را به دلخواه انجام داد، میتوان چندین مخزن حالت را تهیه و به هم متصل کرد؛ مانند تصویری که مشاهده میکنید. در اینجا:
- src\stores\counter.js: مخزن دادهی حالت کامپوننت شمارشگر است.
- src\stores\market.js: مخزن دادهی کامپوننتهای لیست محصولات و سبد خرید است.
- src\stores\index.js: کار ترکیب دو مخزن قبل را انجام میدهد.
در ادامه کدهای کامل این مخازن را مشاهده میکنید:
مخزن حالت src\stores\counter.js
import { action, observable } from "mobx"; export default class CounterStore { @observable totalNumbersInBasket = 0; constructor(rootStore) { this.rootStore = rootStore; } @action increase = () => { this.totalNumbersInBasket++; }; @action decrease = () => { this.totalNumbersInBasket--; }; }
- در اینجا خاصیت totalNumbersInBasket به صورت observable تعریف شدهاست و با تغییر آن چه به صورت مستقیم، با مقدار دهی آن و یا توسط دو action تعریف شده، سبب به روز رسانی UI خواهد شد.
- میشد این مخزن را با مخزن src\stores\market.js یکی کرد؛ اما جهت ارائهی مثالی در مورد نحوهی تعریف چند مخزن و روش برقراری ارتباط بین آنها، به صورت مجزایی تعریف شد.
مخزن حالت src\stores\market.js
import { action, computed, observable } from "mobx"; export default class MarketStore { @observable basketItems = []; constructor(rootStore) { this.rootStore = rootStore; } @action add = product => { const selectedItem = this.basketItems.find(item => item.id === product.id); if (selectedItem) { selectedItem.count++; } else { this.basketItems.push({ ...product, count: 1 }); } this.rootStore.counterStore.increase(); }; @action remove = id => { const selectedItem = this.basketItems.find(item => item.id === id); selectedItem.count--; if (selectedItem.count === 0) { this.basketItems.remove(selectedItem); } this.rootStore.counterStore.decrease(); }; @action removeAll = () => { this.basketItems = []; this.rootStore.counterStore.totalNumbersInBasket = 0; }; @computed get totalPrice() { return this.basketItems.reduce((previous, current) => { return previous + current.price * current.count; }, 0); } }
- توسط متد add آن در کامپوننت نمایش لیست محصولات، میتوان آیتمی را به این آرایه اضافه کرد. در اینجا چون شیء product مورد استفاده دارای خاصیت count نیست، روش افزودن آنرا توسط spread operator برای درج خواص شیء product اصلی و سپس تعریف آنرا مشاهده میکنید. این فراخوانی، سبب افزایش یک واحد به عدد شمارشگر نیز میشود.
- متد remove آن در کامپوننت سبد خرید، مورد استفاده قرار میگیرد تا کاربر بتواند اطلاعاتی را از این لیست حذف کند. این فراخوانی، سبب کاهش یک واحد از عدد شمارشگر نیز میشود.
- متد removeAll آن در کامپوننت شمارشگر بالای صفحه استفاده میشود تا سبب خالی شدن آرایهی آیتمهای انتخابی گردد و همچنین عدد آنرا نیز صفر کند.
- خاصیت محاسباتی totalPrice آن در پایین جدول سبد خرید، جمع کل هزینهی قابل پرداخت را مشخص میکند.
مخزن حالت src\stores\index.js
در اینجا روش یکی کردن دو مخزن حالت یاد شده را به صورت خاصیتهای عمومی یک مخزن کد ریشه، مشاهده میکنید:
import CounterStore from "./counter"; import MarketStore from "./market"; class RootStore { counterStore = new CounterStore(this); marketStore = new MarketStore(this); } export default RootStore;
export default class MarketStore { @observable basketItems = []; constructor(rootStore) { this.rootStore = rootStore; } @action removeAll = () => { this.basketItems = []; this.rootStore.counterStore.totalNumbersInBasket = 0; }; }
پس از ایجاد مخازن حالت، اکنون نیاز است آنها را در اختیار سلسه مراتب کامپوننتهای برنامه قرار دهیم. به همین جهت به فایل src\index.js مراجعه کرده و آنرا به صورت زیر تغییر میدهیم:
import "./index.css"; import "bootstrap/dist/css/bootstrap.css"; import makeInspectable from "mobx-devtools-mst"; import { Provider } from "mobx-react"; import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; import * as serviceWorker from "./serviceWorker"; import RootStore from "./stores"; const rootStore = new RootStore(); if (process.env.NODE_ENV === "development") { makeInspectable(rootStore); // https://github.com/mobxjs/mobx-devtools } ReactDOM.render( <Provider {...rootStore}> <App /> </Provider>, document.getElementById("root") ); serviceWorker.unregister();
- سپس یک وهلهی جدید از RootStore را که حاوی خاصیتهای عمومی counterStore و marketStore است، ایجاد میکنیم.
- اگر علاقمند باشید تا حین کار با MobX، جزئیات پشت صحنهی آنرا توسط افزونهی mobx-devtools ردیابی کنید، روش آنرا در اینجا با فراخوانی متد makeInspectable مشاهده میکنید. مقدار process.env.NODE_ENV نیز بر اساس پروسهی جاری node.js اجرا کنندهی برنامهی React تامین میشود. اطلاعات بیشتر
- قسمت آخر این تنظیمات، محصور کردن کامپوننت App که بالاترین کامپوننت در سلسله مراتب کامپوننتهای برنامه است، با شیء Provider میباشد. در این شیء توسط spread operator، سبب درج خواص عمومی rootStore، به عنوان مخازن قابل استفاده شدهایم. تنظیم {rootStore...} معادل عبارت زیر است:
<Provider counterStore={rootStore.counterStore} marketStore={rootStore.marketStore}>
اتصال کامپوننت ShopItemsList به مخزن حالت marketStore
پس از ایجاد rootStore و محصور کردن کامپوننت App توسط شیء Provider در فایل src\index.js، اکنون باید قسمت export default کامپوننتهای برنامه را جهت استفادهی از مخازن حالت، یکی یکی ویرایش کرد:
import { inject, observer } from "mobx-react"; import React from "react"; import products from "../services/productsService"; const ShopItemsList = ({ onAdd }) => { return ( // ... ); }; export default inject(({ marketStore }) => ({ onAdd: marketStore.add }))(observer(ShopItemsList));
اتصال کامپوننت BasketItemsList به مخزن حالت marketStore
در اینجا نیز سطر export default را جهت دریافت خاصیت marketStore، از شیء Provider تامین شدهی در فایل src\index.js، ویرایش میکنیم. به این ترتیب سه props مورد انتظار این کامپوننت، توسط خاصیتهای basketItems (آرایهی اشیاء انتخابی توسط کاربر)، totalPrice (خاصیت محاسباتی جمع کل هزینه) و متد رویدادگردان onRemove (برای حذف یک آیتم) تامین میشوند. در آخر کامپوننت را به صورت observer محصور کرده و بازگشت میدهیم تا تغییرات در مخزن حالت آن، سبب به روز رسانی UI آن شوند:
import { inject, observer } from "mobx-react"; import React from "react"; const BasketItemsList = ({ items, totalPrice, onRemove }) => { return ( // ... ); }; export default inject(({ marketStore }) => ({ items: marketStore.basketItems, totalPrice: marketStore.totalPrice, onRemove: marketStore.remove }))(observer(BasketItemsList));
اتصال کامپوننت BasketItemsCounter به دو مخزن حالت counterStore و marketStore
در اینجا روش استفادهی از decorator syntax کتابخانهی mobx-react را بر روی یک کامپوننت کلاسی مشاهده میکنید. تزئین کنندهی inject، امکان دسترسی به مخازن حالت تزریقی به شیء Provider را میسر کرده و سپس توسط آن میتوان props مورد انتظار کامپوننت را از مخازن متناظر استخراج کرده و در اختیار کامپوننت قرار داد. همچنین این کامپوننت توسط تزئین کنندهی observer نیز علامت گذاری شدهاست. در این حالت نیازی به تغییر سطر export default نیست.
import { inject, observer } from "mobx-react"; import React, { Component } from "react"; @inject(rootStore => ({ count: rootStore.counterStore.totalNumbersInBasket, onRemoveAll: rootStore.marketStore.removeAll })) @observer class BasketItemsCounter extends Component { render() { const { count, onRemoveAll } = this.props; return ( // ... ); } } export default BasketItemsCounter;
کدهای کامل این قسمت را میتوانید از اینجا دریافت کنید: state-management-with-mobx-part4.zip
استفاده از کامپوننت AutoComplete کتابخانهی Angular Material
کتابخانهی Angular Material به همراه یک کامپوننت Auto Complete نیز هست. در اینجا قصد داریم آنرا در یک صفحهی دیالوگ جدید نمایش دهیم و با انتخاب کاربری از لیست توصیههای آن و کلیک بر روی دکمهی نمایش آن کاربر، جزئیات کاربر یافت شده را نمایش دهیم.
به همین جهت ابتدا کامپوننت جدید search-auto-complete را به صورت زیر به مجموعهی کامپوننتهای تعریف شده اضافه میکنیم:
ng g c contact-manager/components/search-auto-complete --no-spec
import { SearchAutoCompleteComponent } from "./components/search-auto-complete/search-auto-complete.component"; @NgModule({ entryComponents: [ SearchAutoCompleteComponent ] }) export class ContactManagerModule { }
در ادامه برای نمایش این کامپوننت به صورت popup، دکمهی جدید جستجو را به toolbar اضافه میکنیم:
برای این منظور به فایل toolbar\toolbar.component.html مراجعه کرده و دکمهی جستجو را پیش از دکمهی نمایش منو، قرار میدهیم:
<span fxFlex="1 1 auto"></span> <button mat-button (click)="openSearchDialog()"> <mat-icon>search</mat-icon> </button> <button mat-button [matMenuTriggerFor]="menu"> <mat-icon>more_vert</mat-icon> </button>
@Component() export class ToolbarComponent { constructor( private dialog: MatDialog, private router: Router) { } openSearchDialog() { const dialogRef = this.dialog.open(SearchAutoCompleteComponent, { width: "650px" }); dialogRef.afterClosed().subscribe((result: User) => { console.log("The SearchAutoComplete dialog was closed", result); if (result) { this.router.navigate(["/contactmanager", result.id]); } }); } }
کنترلر جستجوی سمت سرور و سرویس سمت کلاینت استفاده کنندهی از آن
در اینجا کنترلر و اکشن متدی را جهت جستجوی قسمتی از نام کاربران را مشاهده میکنید:
namespace MaterialAspNetCoreBackend.WebApp.Controllers { [Route("api/[controller]")] public class TypeaheadController : Controller { private readonly IUsersService _usersService; public TypeaheadController(IUsersService usersService) { _usersService = usersService ?? throw new ArgumentNullException(nameof(usersService)); } [HttpGet("[action]")] public async Task<IActionResult> SearchUsers(string term) { return Ok(await _usersService.SearchUsersAsync(term)); } } }
از این کنترلر به نحو ذیل در برنامهی Angular برای ارسال اطلاعات و انجام جستجو استفاده میشود:
import { HttpClient, HttpErrorResponse } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { Observable, throwError } from "rxjs"; import { catchError, map } from "rxjs/operators"; import { User } from "../models/user"; @Injectable({ providedIn: "root" }) export class UserService { constructor(private http: HttpClient) { } searchUsers(term: string): Observable<User[]> { return this.http .get<User[]>(`/api/Typeahead/SearchUsers?term=${encodeURIComponent(term)}`) .pipe( map(response => response || []), catchError((error: HttpErrorResponse) => throwError(error)) ); } }
تکمیل کامپوننت جستجوی کاربران توسط یک AutoComplete
پس از این مقدمات که شامل تکمیل سرویسهای سمت سرور و کلاینت دریافت اطلاعات کاربران جستجو شده و نمایش صفحهی جستجو به صورت یک popup است، اکنون میخواهیم محتوای این popup را تکمیل کنیم. البته در اینجا فرض بر این است که مطلب «کنترل نرخ ورود اطلاعات در برنامههای Angular» را پیشتر مطالعه کردهاید و با جزئیات آن آشنایی دارید.
تکمیل قالب search-auto-complete.component.html
<h2 mat-dialog-title>Search</h2> <mat-dialog-content> <div fxLayout="column"> <mat-form-field class="example-full-width"> <input matInput placeholder="Choose a user" [matAutocomplete]="auto1" (input)="onSearchChange($event.target.value)"> </mat-form-field> <mat-autocomplete #auto1="matAutocomplete" [displayWith]="displayFn" (optionSelected)="onOptionSelected($event)"> <mat-option *ngIf="isLoading" class="is-loading"> <mat-spinner diameter="50"></mat-spinner> </mat-option> <ng-container *ngIf="!isLoading"> <mat-option *ngFor="let user of filteredUsers" [value]="user"> <span>{{ user.name }}</span> <small> | ID: {{user.id}}</small> </mat-option> </ng-container> </mat-autocomplete> </div> </mat-dialog-content> <mat-dialog-actions> <button mat-button color="primary" (click)="showUser()"> <mat-icon>search</mat-icon> Show User </button> <button mat-button color="primary" [mat-dialog-close]="true"> <mat-icon>cancel</mat-icon> Close </button> </mat-dialog-actions>
سپس نحوهی اتصال یک Input box معمولی را به کامپوننت mat-autocomplete مشاهده میکنید که شامل این موارد است:
- جعبه متنی که قرار است به یک mat-autocomplete متصل شود، توسط دایرکتیو matAutocomplete به template reference variable تعریف شدهی در آن autocomplete اشاره میکند. برای مثال در اینجا این متغیر auto1 است.
- برای انتقال دکمههای فشرده شدهی در input box به کامپوننت، از رخداد input استفاده شدهاست. این روش با هر دو نوع حالت مدیریت فرمهای Angular سازگاری دارد و کدهای آن یکی است.
در کامپوننت mat-autocomplete این تنظیمات صورت گرفتهاند:
- در لیست ظاهر شدهی توسط یک autocomplete، هر نوع ظاهری را میتوان طراحی کرد. برای مثال در اینجا نام و id کاربر نمایش داده میشوند. اما برای تعیین اینکه پس از انتخاب یک آیتم از لیست، چه گزینهای در input box ظاهر شود، از خاصیت displayWith که در اینجا به متد displayFn کامپوننت متصل شدهاست، کمک گرفته خواهد شد.
- از رخداد optionSelected برای دریافت آیتم انتخاب شده، در کدهای کامپوننت استفاده میشود.
- در آخر کار نمایش لیستی از کاربران توسط mat-optionها انجام میشود. در اینجا برای اینکه بتوان تاخیر دریافت اطلاعات از سرور را توسط یک mat-spinner نمایش داد، از خاصیت isLoading تعریف شدهی در کامپوننت استفاده خواهد شد.
تکمیل کامپوننت search-auto-complete.component.ts
کدهای کامل این کامپوننت را در ادامه مشاهده میکنید:
import { Component, OnDestroy, OnInit } from "@angular/core"; import { MatAutocompleteSelectedEvent, MatDialogRef } from "@angular/material"; import { Subject, Subscription } from "rxjs"; import { debounceTime, distinctUntilChanged, finalize, switchMap, tap } from "rxjs/operators"; import { User } from "../../models/user"; import { UserService } from "../../services/user.service"; @Component({ selector: "app-search-auto-complete", templateUrl: "./search-auto-complete.component.html", styleUrls: ["./search-auto-complete.component.css"] }) export class SearchAutoCompleteComponent implements OnInit, OnDestroy { private modelChanged: Subject<string> = new Subject<string>(); private dueTime = 300; private modelChangeSubscription: Subscription; private selectedUser: User = null; filteredUsers: User[] = []; isLoading = false; constructor( private userService: UserService, private dialogRef: MatDialogRef<SearchAutoCompleteComponent>) { } ngOnInit() { this.modelChangeSubscription = this.modelChanged .pipe( debounceTime(this.dueTime), distinctUntilChanged(), tap(() => this.isLoading = true), switchMap(inputValue => this.userService.searchUsers(inputValue).pipe( finalize(() => this.isLoading = false) ) ) ) .subscribe(users => { this.filteredUsers = users; }); } ngOnDestroy() { if (this.modelChangeSubscription) { this.modelChangeSubscription.unsubscribe(); } } onSearchChange(value: string) { this.modelChanged.next(value); } displayFn(user: User) { if (user) { return user.name; } } onOptionSelected(event: MatAutocompleteSelectedEvent) { console.log("Selected user", event.option.value); this.selectedUser = event.option.value as User; } showUser() { if (this.selectedUser) { this.dialogRef.close(this.selectedUser); } } }
- توسط متد displayFn، عبارتی که در نهایت پس از انتخاب از لیست نمایش داده شده در input box قرار میگیرد، مشخص خواهد شد.
- در متد onOptionSelected، میتوان به شیء انتخاب شدهی توسط کاربر از لیست mat-autocomplete دسترسی داشت.
- این شیء انتخاب شده را در متد showUser و توسط سرویس MatDialogRef به کامپوننت toolbar که در حال گوش فرادادن به رخداد بسته شدن کامپوننت جاری است، ارسال میکنیم. به این صورت است که کامپوننت toolbar میتواند کار هدایت به جزئیات این کاربر را انجام دهد.
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید.
برای اجرای آن:
الف) ابتدا به پوشهی src\MaterialAngularClient وارد شده و فایلهای restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشهی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایلهای restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.
پروژههای کوچک عموما دارای ساختاری مشابه تصویر ذیل میباشند:
این مورد، روش پیشنهادی در Angular Seed است و بدین صورت است که تعاریف ماژولها در فایل app.js انجام میگیرد. تعاریف و پیاده سازی تمام کنترلرها در فایل controller.js است. و همچنین دایرکتیوها و فیلترها و سرویسها هر کدام در فایلها جداگانه تعریف و پیاده سازی میشوند. این روش راه حلی سریع برای پروژههای کوچک با تعداد developerهای کم است. برای مثال زمانی که یک developer در حال ویرایش فایل controller.js است، از آن جا که فایل مورد نظر checkout خواهد شد در نتیجه سایر developerها امکان تغییر در فایل مورد نظر را نخواهند داشت. سورس فایلها به مرور زیاد خواهد شد و در نتیجه debug آن سخت میشود.
روش دوم
در این حالت تعاریف کنترلر ها، مدلها و سرویسها هرکدام در یک دایرکتوری مجزا قرار خواهد گرفت. برای هر view یک کنترلر و بنا بر نیاز مدل تعریف میکنیم. ساختار آن به صورت زیر میشود:
دایرکتیوها و فیلترها عموما در یک فایل قرار داده خواهند شد تا بنابر نیاز در جای مناسب رفرنس داده شوند. این روش ساختار مناسبتری نسبه به روش قبلی دارد اما دارای معایبی هم چون موارد زیر است:
»وابستگی بین فایلها مشخص نیست در نتیجه بدون استفاده از کتابخانه هایی نظیر requireJs با مشکل مواجه خواهید شد.
»refactoring کدها تا حدودی سخت است.
روش سوم
این ساختار مناسب برای پیاده سازی پروژهها به صورت ماژولار است و برای پروژههای بزرگ نیز بسیار مناسب است. در این حالت شما فایلهای مربوط به هر ماژول را در دایرکتوری خاص آن قرار خواهید داد. به صورت زیر:
همان طور که ملاحظه میکنید سرویس ها، کنترلرها و حتی مدلهای مربوط به هر بخش در یک مسیر جداگانه قرار میگیرند. علاوه بر آن فایل هایی که قابلیت اشتراکی دارند در مسیری به نام common وجود دارند تا بتوان در جای مناسب برای استفاده از آنها رفرنس داده شود. حتی اگر در پروژه خود فقط یک ماژول دارید باز سعی کنید از این روش برای مدیریت فایلهای خود استفاده نمایید. اگر با ngStart آشنایی داشته باشید به احتمال زیاد با این روش بیگانه نیستید.
بررسی چند نکته درباره کدهای مشترک
»اگر ماژولها وابستگی شدیدی به فایلها و سورسهای مشترک دارند باید اطمینان حاصل نمایید که این ماژولها فقط به اطلاعات مورد نیاز دسترسی دارند. این اصل interface segregation principle اصول SOLID است.
»توابعی که کاربرد زیادی دارند و اصطلاحا به عنوان Utility شناخته میشوند باید به rootScope$ اضافه شوند تا scopeهای وابسته نیز به آنها دسترسی داشته باشند. این مورد به ویژه باعث کاهش تکرار وابستگیهای مربوط به هر کنترلر میشود.
»برای جداسازی وابستگیهای بین دو component بهتر از eventها استفاده نمایید. AngularJs این امکان را با استفاده از سرویسهای on$ و emit$ و broadcast$ به راحتی میسر کرده است.
ASP.NET MVC #21
چرا به ابزارهای مدیریت حالت نیاز داریم؟
به محض رد شدن از مرز پیاده سازی امکانات اولیهی یک برنامه، نیاز به ابزارهای مدیریت حالت نمایان میشوند؛ خصوصا زمانیکه نیاز است با اطلاعات قابل توجهی سر و کار داشت. مهمترین دلیل استفادهی از یک ابزار مدیریت حالت، مدیریت منطق تجاری برنامه است. منطق نمایشی برنامه مرتبط است به نحوهی نمایش اجزای آن در صفحه؛ مانند نمایش یک صفحهی مودال، تغییر رنگ عناصر با عبور کرسر ماوس از روی آنها و در کل منطقی که مرتبط و یا وابستهی به هدف اصلی برنامه نیست. از سوی دیگر منطق تجاری برنامه مرتبط است با مدیریت، تغییر و ذخیره سازی اشیاء تجاری مورد نیاز آن؛ مانند اطلاعات حساب کاربری شخص و دریافت اطلاعات برنامه از یک API که مختص به برنامهی خاص ما است و به همین دلیل نیاز به ابزاری برای مدیریت بهینهی آن وجود دارد. برای مثال اینکه در کجا باید منطق تجاری و نمایشی را به هم متصل کرد، میتواند چالش بر انگیر باشد. چگونه باید اطلاعات کاربر را ذخیره کرد؟ چگونه React باید متوجه شود که اطلاعات ما تغییر کردهاست و در نتیجهی آن کامپوننتی را مجددا رندر کند؟ یک ابزار مدیریت حالت، تمام این مسایل را به نحو یکدستی در سراسر برنامه، مدیریت میکند.
اگر از یک ابزار مدیریت حالت استفاده نکنیم، مجبور خواهیم شد تمام اطلاعات منطق تجاری را در داخل state کامپوننتها ذخیره کنیم که توصیه نمیشود؛ چون مقیاس پذیر نیست. برای مثال فرض کنید قرار است تمام اطلاعات state را داخل یک کامپوننت ذخیره کنیم. هر زمانیکه بخواهیم این state را از طریق یک کامپوننت فرزند تغییر دهیم، نیاز خواهد بود این اطلاعات را به والد آن کامپوننت ارسال کنیم که اگر از تعداد زیادی کامپوننت تو در تو تشکیل شده باشد، زمانبر و به همراه کدهای تکراری زیادی خواهد بود. همچنین اینکار سبب رندر مجدد کل برنامه با هر تغییری در state آن میشود که غیرضروری بوده و کارآیی برنامه را کاهش میدهد. به علاوه در این بین مشخص نیست هر قسمت از state، از کدام کامپوننت تامین شدهاست. به همین جهت نیاز به روشی برای مدیریت حالت در بین کامپوننتهای برنامه وجود دارد.
داشتن تنها یک محل برای ذخیره سازی state در برنامه
همانطور که در قسمت 8 ترکیب کامپوننتها در سری React 16x بررسی کردیم، هر کامپوننت در React، دارای state خاص خودش است و این state از سایر کامپوننتها کاملا مستقل و ایزولهاست. این مورد با بزرگتر شدن برنامه و برقراری ارتباط بین کامپوننتها، مشکل ایجاد میکند. برای مثال اگر بخواهیم دکمهای را در صفحه قرار داده و توسط این دکمه درخواست صفر شدن مقدار هر کدام از شمارشگرها را صادر کنیم، با صفر کردن value هر کدام از این کامپوننتها، اتفاقی رخ نمیدهد. چون state محلی این کامپوننتها، با سایر اجزای صفحه به اشتراک گذاشته نمیشود و باید آنرا تبدیل به یک controlled component کرد، بطوریکه دارای local state خاص خودش نیست و تمام دادههای دریافتی را از طریق this.props دریافت میکند و هر زمانیکه قرار است دادهای تغییر کند، رخدادی را به والد خود صادر میکند. بنابراین این کامپوننت به طور کامل توسط والد آن کنترل میشود. تازه این روش در مورد کامپوننتهایی صدق میکند که رابطهی والد و فرزندی بین آنها وجود دارد. اگر چنین رابطهای وجود نداشت، باید state را به یک سطح بالاتر انتقال داد. برای مثال باید state کامپوننت Counters را به والد آن که کامپوننت App است، منتقل کرد. پس از آن چون کامپوننتهای ما، از کامپوننت App مشتق میشوند، اکنون میتوان این state را به تمام فرزندان App توسط props منتقل کرد و به اشتراک گذاشت. این مورد هم مانند مثال انتقال اطلاعات کاربر لاگین شدهی به سیستم، به تمام زیر قسمتهای برنامه، نیاز به ارسال اطلاعات از طریق props یک کامپوننت، به کامپوننت بعدی را دارد و به همین ترتیب برای مابقی که به props drilling مشهور است و روش پسندیدهای نیست.
Redux چیست؟ ذخیره سازی کل درخت state یک برنامه، در یک محل. به این ترتیب به یک شیء جاوا اسکریپتی بزرگ خواهیم رسید که در برگیرندهی تمام state برنامهاست. یکی از مزایای آن امکان serialize و deserialize کل این شیء، به سادگی است. برای مثال توسط متد JSON.stringify میتوان آنرا در جائی ذخیره کرد و سپس آنرا به صورت یک شیء جاو اسکریپتی در زمانی دیگر بازیابی کرد. یکی از مزایای آن، امکان بازیابی دقیق شرایط کاربری است که دچار مشکل شدهاست و سپس دیباگ و رفع مشکل او، در زمانی دیگر.
تاریخچهای از سیستمهای مدیریت حالت
همه چیز با AngularJS 1x شروع شد که از data binding دو طرفه پشتیبانی میکرد. هرچند این روش برای همگام نگه داشتن View و مدل برنامه، مفید است، اما در Viewهای پیچیده، برنامه را کند میکند. در همین زمان فیسبوک، روش مدیریت حالتی را به نام Flux ارائه داد که از data binding یک طرفه پشتیبانی میکرد. به این معنا که در این روش، همواره اطلاعات از View به مدل، جریان پیدا میکند. کار کردن با آن سادهاست؛ چون نیازی نیست حدس زده شود که اکنون جریان اطلاعات از کدام سمت است. اما مشکل آن عدم هماهنگی model و view، در بعضی از حالات است. Flux از این جهت به وجود آمد که مدیریت حالت در برنامههای React آن زمان، پیچیده بود و مقیاس پذیری کمی داشت (پیش از ارائهی Context و Hooks). در کل Flux صرفا یکسری الگوی مدیریت حالت را بیان میکند و یک کتابخانهی مجزا نیست. بر مبنای این الگوها و قراردادها، میتوان کتابخانههای مختلفی را ایجاد کرد. از این رو در سال 2015، کتابخانههای زیادی مانند Reflux, Flummox, MartyJS, Alt, Redux و غیره برای پیاده سازی آن پدید آمدند. در این بین، کتابخانهی Redux ماندگار شد و پیروز این نبرد بود!
توابع خالص و ناخالص (Pure & Impure Functions)
پیش از شروع بحث، نیاز است با یکسری از واژهها مانند توابع خالص و ناخالص آشنا شد. این نکات از این جهت مهم هستند که Redux فقط با توابع خالص کار میکند.
توابع خالص: تعدادی آرگومان را دریافت کرده و بر اساس آنها، مقداری را باز میگردانند.
// Pure const add = (a, b) => { return a + b; }
توابع ناخالص: این نوع توابع سبب تغییراتی در متغیرهایی خارج از میدان دید خود میشوند و یا به همراه یک سری اثرات جانبی (side effects) مانند تعامل با دنیای خارج (وجود یک console.log در آن تابع و یا دریافت اطلاعاتی از یک API خارجی) هستند.
// Impure const b; const add = (a) => { return a + b; }
// Impure const add = (a, b) => { console.log('lolololol'); return a + b; }
// Impure const add = (a, b) => { Api.post('/add', { a, b }, (response) => { // Do something. }); };
روشهایی برای جلوگیری از تغییرات در اشیاء در جاوا اسکریپت
ایجاد تغییرات در آرایهها و اشیاء (Mutating arrays and objects) نیز ناخالصی ایجاد میکند؛ از این جهت که سبب تغییراتی در دنیای خارج (خارج از میدان دید تابع) میشویم. به همین جهت نیاز به روشهایی وجود دارد که از این نوع تغییرات جلوگیری کرد:
// Copy object const original = { a: 1, b: 2 }; const copy = Object.assign({}, original);
برای مثال در React، برای انجام رندر نهایی، در پشت صحنه کار مقایسهی اشیاء صورت میگیرد. به همین جهت اگر همان شیءای را که ردیابی میکند تغییر دهیم، دیگر نمیتواند به صورت مؤثری فقط قسمتهای تغییر کردهی آنرا تشخیص داده و کار رندر را فقط بر اساس آنها انجام دهد و مجبور خواهد شد کل یک شیء را بارها و بارها رندر کند که اصلا بهینه نیست. به همین جهت، ایجاد تغییرات مستقیم در شیءای که به state آن انتساب داده میشود، مجاز نیست.
متد Object.assign، چندین شیء را نیز میتواند با هم یکی کند و شیء جدیدی را تشکیل دهد:
// Extend object const original = { a: 1, b: 2 }; const extension = { c: 3 }; const extended = Object.assign({}, original, extension);
// Copy object const original = { a: 1, b: 2 }; const copy = { ...original };
// Extend object const original = { a: 1, b: 2 }; const extension = { c: 3 }; const extended = { ...original, ...extension };
روشهایی برای جلوگیری از تغییرات در آرایهها در جاوا اسکریپت
متد slice آرایهها نیز بدون ذکر آرگومانی، یک کپی از آرایهی اصلی را ایجاد میکند:
// Copy array const original = [1, 2, 3]; const copy = [1, 2, 3].slice();
// Copy array const original = [1, 2, 3]; const copy = [ ...original ];
// Extend array const original = [1, 2, 3]; const extended = original.concat(4); const moreExtended = original.concat([4, 5]);
معادل قطعه کد فوق در ES-6 و به همراه spread operator آن به صورت زیر است:
// Extend array const original = [1, 2, 3]; const extended = [ ...original, 4 ]; const moreExtended = [ ...original, ...extended, 5 ];
مفاهیم ابتدایی Redux
در Redux برای ایجاد تغییرات در شیء کلی state، از مفهومی به نام dispatch actions استفاده میشود. action در اینجا به معنای رخدادن چیزی است؛ مانند کلیک بر روی یک دکمه و یا دریافت اطلاعاتی از یک API. در این حالت مقایسهای بین وضعیت قبلی state و وضعیت فعلی آن صورت میگیرد و تغییرات مورد نیاز جهت اعمال به UI، محاسبه خواهند شد.
اصلیترین جزء Redux، تابعی است به نام Reducer. این تابع، یک تابع خالص است و دو آرگومان را دریافت میکند:
تابع Reducer، بر اساس action و یا رخدادی، ابتدا کل state برنامه را دریافت میکند و سپس خروجی آن بر اساس منطق این تابع، یک state جدید خواهد بود. اکنون که این state جدید را داریم، برنامهی React ما میتواند به تغییرات آن گوش فرا داده و بر اساس آن، UI را به روز رسانی کند. به این ترتیب کار اصلی مدیریت state، به خارج از برنامهی React منتقل میشود.
در این تصویر، تابع action creator را هم ملاحظه میکند که کاملا اختیاری است. یک action میتواند یک رشته و یا یک عدد باشد. با پیچیده شدن برنامه، نیاز به ارسال یکسری متادیتا و یا اطلاعات بیشتری از اکشن رسیدهاست. کار action creator، ایجاد شیء action، به صورت یک دست و یکنواخت است تا دیگر نیازی به ایجاد دستی آن نباشد.
مزایای کار با Redux
- داشتن یک مکان مرکزی برای ذخیره سازی کلی حالت برنامه (به آن «source of truth» و یا store هم گفته میشود): به این ترتیب مشکل ارسال خواص در بین کامپوننتهای عمیق و چند سطحی، برطرف شده و هر زمانیکه نیاز بود، از آن اطلاعاتی را دریافت و یا با قالب خاصی، آنرا به روز رسانی میکنند.
- رسیدن به بهروز رسانیهای قابل پیش بینی state: هرچند در حالت کار با Redux، یک شیء بزرگ جاوا اسکریپتی، کل state برنامه را تشکیل میدهد، اما امکان کار مستقیم با آن و تغییرش وجود ندارد. به همین جهت است که برای کار با آن، باید رویدادی را از طریق actionها به تابع Reducer آن تحویل داد. چون Reducer یک تابع خالص است، با دریافت یک سری ورودی مشخص، همواره یک خروجی مشخص را نیز تولید میکند. به همین جهت قابلیت ضبط و تکرار را پیدا میکند؛ همان بحث serialize و deseriliaze، توسط ابزاری مانند: logrocket. به علاوه قابلیت undo و redo را نیز میتوان به این ترتیب پیاده سازی کرد (state جدید محاسبه شده، مشخص است، کل state قبلی را نیز داریم یا میتوان ذخیره کرد و سپس برای undo، آنرا جایگزین state جدید نمود). افزونهی redux dev tools نیز قابلیت import و export کل state را به همراه دارد.
- چون تابع Reducer، یک تابع خالص است و همواره خروجیهای مشخصی را به ازای ورودیهای مشخصی، تولید میکند، آزمایش کردن، پیاده سازی و حتی logging آن نیز سادهتر است. در این بین حتی یک افزونهی مخصوص نیز برای دیباگ آن تهیه شدهاست: redux-devtools-extension. تابع خالص، تابعی است که به همراه اثرات جانبی نیست (side effects)؛ به همین جهت عملکرد آن کاملا قابل پیش بینی بوده و آزمون پذیری آن به دلیل نداشتن وابستگیهای خارجی، بسیار بالا است.
Context API خود React چطور؟
در قسمت 33 سری React 16x، مفهوم React Context را بررسی کردیم. پس از معرفی آن با React 16.3، مقالات زیادی منتشر شدند که ... Redux مردهاست (!) و یا بجای Redux از React context استفاده کنید. اما واقعیت این است که React Redux در پشت صحنه از React context استفاده میکند و تابع connect آن دقیقا به همین زیر ساخت متصل میشود.
کار با Redux مزایایی مانند کارآیی بالاتر، با کاهش رندرهای مجدد کامپوننتها، دیباگ سادهتر با افزونههای اختصاصی و همچنین سفارشی سازی، مانند نوشتن میانافزارها را به همراه دارد. اما شاید واقعا نیازی به تمام این امکانات را هم نداشته باشید؛ اگر هدف، صرفا انتقال سادهتر اطلاعات بوده و برنامهی مدنظر نیز کوچک است. React Context برخلاف Redux، نگهدارندهی state نیست و بیشتر هدفش محلی برای ذخیره سازی اطلاعات مورد استفادهی در چندین و چند کامپوننت تو در تو است. هرچند شبیه به Redux میتوان اشارهگرهایی از متدها را به استفاده کنندگان از آن ارسال کرد تا سبب بروز رویدادها و اکشنهایی در کامپوننت تامین کنندهی Contrext شوند (یا یک کتابخانهی ابتدایی شبیه به Redux را توسط آن تهیه کرد). بنابراین برای انتخاب بین React Context و Redux باید به اندازهی برنامه، تعداد نفرات تیم، آشنایی آنها با مفاهیم Redux دقت داشت.