مطالب
مدیریت پیشرفته‌ی حالت در React با Redux و Mobx - قسمت پنجم - Redux Hooks
تا اینجا الگوی Redux را در برنامه‌های React بررسی کردیم که شامل این موارد است:
- با استفاده از Redux، یک شیء سراسری state، کار مدیریت state تمام برنامه را به عهده می‌گیرد که به آن «single source of truth» نیز گفته می‌شود. البته هرچند می‌توان کامپوننت‌هایی را هم در این بین داشت که state خاص خودشان را داشته باشند و آن‌را در این شیء سراسری ذخیره نکنند.
- در حین کار با Redux، تنها راه تغییر شیء سراسری state آن، صدور رخ‌دادهایی هستند که در اینجا اکشن نامیده می‌شوند. یک اکشن شیءای است که بیان می‌کند چه چیزی قرار است تغییر کند.
- برای ساده سازی ساخت این اشیاء می‌توان متدهایی را به نام action creators ایجاد کرد.
- اگر این متدهای action creator را توسط متد store.dispatch فراخوانی کنیم، سبب dispatch شیء اکشن، به یک تابع Reducer متناظری خواهند شد. این تابع Reducer است که قسمتی از state را که متناظر با نوع اکشن رسیده‌است، تغییر می‌دهد. در این حالت اگر اکشن رسیده، نوع مدنظری را نداشته باشد، خروجی تابع Reducer، همان state اصلی و بدون تغییر خواهد بود.
- Reducerها توابعی خالص هستند و نباید به همراه اثرات جانبی باشند (هر نوع تعاملی با دنیای خارج از تابع جاری) و همچنین نباید شیء state را نیز مستقیما تغییر دهند. این توابع باید یک کپی تغییر یافته‌ی از state را در صورت نیاز بازگشت دهند.
- برای مدیریت بهتر برنامه می‌توان چندین تابع Reducer را بر اساس نوع‌های اکشن‌های ویژه‌ای، پیاده سازی کرد. سپس با ترکیب آن‌ها، یک شیء rootReducer ایجاد می‌شود.
- در نهایت در الگوی Redux، یک مخزن یا store تعریف خواهد شد که تمام این اجزاء را مانند rootReducer و میان‌افزارهای تعریف شده مانند Thunk، در کنار هم قرار می‌دهد و امکان dispatch اکشن‌ها را میسر می‌کند.
- اکنون برای استفاده‌ی از Redux در یک برنامه‌ی React، نیاز است کامپوننت ریشه‌ی برنامه را توسط کامپوننت Provider آن محصور کرد تا قسمت‌های مختلف برنامه بتوانند با امکانات مخزن Redux، کار کرده و با آن ارتباط برقرار کنند.
- قسمت آخر این اتصال جائی است که کامپوننت‌های اصلی برنامه، توسط یک کامپوننت دربرگیرنده که Container نامیده می‌شود، توسط متد connect کتابخانه‌ی react-redux محصور می‌شوند. به این ترتیب این کامپوننت‌ها می‌توانند state و خواص مورد نیاز خود را از طریق props دریافت کرده (mapStateToProps) و یا رویدادها را به سمت store، ارسال کنند (mapDispatchToProps).

از زمان React 16.8، مفهوم جدیدی به نام React Hooks معرفی شد که تعدادی از مهم‌ترین‌های آن‌ها را در سری «React 16x» بررسی کردیم. توسط Hooks، کامپوننت‌های تابعی React اکنون می‌توانند به local state خود دسترسی پیدا کنند و یا با دنیای خارج ارتباط برقرار کنند. پس از آن سایر کتابخانه‌های نوشته شده‌ی برای React نیز شروع به انطباق خود با این الگوی جدید کرده‌اند؛ برای مثال کتابخانه‌ی react-redux v1.7 نیز به همراه تعدادی Hook، جهت ساده سازی آخرین قسمتی است که در اینجا بیان شد، تا بتوانند راه حل دومی برای اتصال کامپوننت‌ها و دربرگیری آن‌ها باشند که در ادامه جزئیات آن‌ها را بررسی خواهیم کرد.


بررسی useSelector Hook

useSelector Hook که توسط کتابخانه‌ی react-redux ارائه می‌شود، معادل بسیار نزدیک تابع mapStateToProps مورد استفاده‌ی در متد connect است. برای مثال در قسمت قبل، دربرگیرنده‌ی کامپوننت Posts در فایل src\containers\Posts.js، یک چنین محتوایی را دارد:
import { connect } from "react-redux";

import Posts from "../components/Posts";

const mapStateToProps = state => {
  console.log("PostsContainer->mapStateToProps", state);
  return {
    ...state.postsReducer
  };
};

export default connect(mapStateToProps)(Posts);
اینبار اگر بخواهیم کل این container را حذف کرده و از useSelector Hook استفاده کنیم، به این ترتیب عمل خواهیم کرد:
پیشتر امضای کامپوننت تابعی Posts واقع در فایل src\components\Posts.jsx، به صورت زیر تعریف شده بود که سه خاصیت را از طریق props دریافت می‌کرد:
const Posts = ({ posts, loading, error }) => {
  return (
  // ...
و این سه خاصیت دقیقا از متد mapStateToProps فوق که ملاحظه می‌کنید، تامین می‌شود. این متد خواص شیء state.postsReducer را به صورت props به کامپوننت Posts از طریق متد connect، ارسال می‌کند. کار postsReducer، فراهم آوردن و مدیریت سه خاصیت { loading: false, posts: [], error: null } است.

اکنون فایل جدید src\components\HooksPosts.jsx را ایجاد کرده و ابتدا و امضای کامپوننت تابعی Posts را به صورت زیر تغییر می‌دهیم:
import { useSelector } from "react-redux";

// ...

const HooksPosts = () => {
  const { posts, loading, error } = useSelector(state => state.postsReducer);
  return (
  // ...
متد useSelector، امکان دسترسی به state ذخیره شده‌ی در مخزن redux را میسر می‌کند. سپس باید همانند متد mapStateToProps، خواصی را که از آن نیاز داریم، دریافت کنیم که در اینجا کل خواص postsReducer دریافت شده (کل state دریافت شده و سپس خاصیت state.postsReducer آن بازگشت داده شده‌است) و در ادامه توسط Object Destructuring، به سه متغیری که پیشتر از طریق props تامین می‌شدند، انتساب داده می‌شود.

یک نکته: خروجی تابع mapStateToProps همواره باید یک شیء باشد، اما چنین محدودیتی در مورد تابع useSelector وجود ندارد و در صورت نیاز می‌توان تنها مقدار یک خاصیت از یک شیء را نیز بازگشت داد.

این کامپوننت، هیچ تغییر دیگری را نیاز ندارد و اگر اکنون به فایل src\App.js مراجعه کنیم، می‌توان دربرگیرنده‌ی کامپوننت Posts را:
import PostsContainer from "./containers/Posts";

function App() {
  return (
    <main className="container">
      <PostsContainer />
    </main>
  );
}
با کامپوننت جدید HooksPosts جایگزین کرد و دیگر نیازی به نوشتن متد connect و ساخت یک container مخصوص آن، نیست:
import HooksPosts from "./components/HooksPosts";

function App() {
  return (
    <main className="container">
      <HooksPosts />
    </main>
  );
}


بررسی useDispatch Hook

تا اینجا موفق شدیم متد mapStateToProps را با useSelector Hook جایگزین کنیم. مرحله‌ی بعد، جایگزین کردن mapDispatchToProps با هوک دیگری به نام useDispatch است. برای مثال در قسمت قبل، دربرگیرنده‌ی کامپوننت FetchPosts در فایل src\containers\FetchPosts.js، چنین تعریفی را دارد:
import { connect } from "react-redux";

import { fetchPostsAsync } from "../actions";
import FetchPosts from "../components/FetchPosts";

const mapDispatchToProps = {
  fetchPostsAsync
};

export default connect(null, mapDispatchToProps)(FetchPosts);
کار این تامین کننده، اتصال action creator ای به نام fetchPostsAsync به props کامپوننت FetchPosts است که در فایل src\components\FetchPosts.jsx به این صورت تعریف شده‌است:
const FetchPosts = ({ fetchPostsAsync }) => {
اکنون برای جایگزین کردن mapDispatchToProps با useDispatch Hook، نگارش دیگری از این کامپوننت تابعی را به نام HooksFetchPosts در فایل src\components\HooksFetchPosts.jsx ایجاد می‌کنیم:
import React from "react";
import { useDispatch } from "react-redux";

import { fetchPostsAsync } from "../actions";

const HooksFetchPosts = () => {
  const dispatch = useDispatch();
  return (
    <section className="card mt-5">
      <div className="card-header text-center">
        <button
          className="btn btn-primary"
          onClick={() => dispatch(fetchPostsAsync())}
        >
          Fetch Posts
        </button>
      </div>
    </section>
  );
};

export default HooksFetchPosts;
عملکر آن نیز بسیار ساده‌است. متد useDispatch، به ما امکان دسترسی به متد store.dispatch را می‌دهد (ارجاعی به آن‌را در اختیار ما قرار می‌دهد). اکنون اگر مانند رخ‌داد onClick تعریف شده، سبب dispatch یک action creator به نام fetchPostsAsync شویم (که اینبار باید به صورت صریح از ماژول مربوطه import شود؛ چون دیگر از طریق props تامین نمی‌شود)، سبب ارسال نتیجه‌ی آن به reducer متناظری می‌شود.

با این تغییر نیز می‌توان به فایل src\App.js مراجعه کرد و المان قبلی FetchPostsContainer را که از ماژول containers/FetchPosts تامین می‌شد، به نحو متداولی با همان کامپوننت جدید HooksFetchPosts، تعویض کرد:
import HooksFetchPosts from "./components/HooksFetchPosts";
import HooksPosts from "./components/HooksPosts";

// ...

function App() {
  return (
    <main className="container">
      <HooksFetchPosts />
      <HooksPosts />
    </main>
  );
}


یک مثال تکمیلی: بازنویسی src\components\counter.jsx با redux hooks

کامپوننت شمارشگر را در قسمت سوم این سری بررسی و تکمیل کردیم. اکنون قصد داریم فایل تامین کننده‌ی آن‌را که به صورت زیر در فایل src\containers\Counter.js تعریف شده:
import { connect } from "react-redux";

import { decrementValue, incrementValue } from "../actions";
import Counter from "../components/counter";

const mapStateToProps = (state, ownProps) => {
  console.log("CounterContainer->mapStateToProps", { state, ownProps });
  return {
    count: state.counterReducer.count
  };
};

const mapDispatchToProps = {
  incrementValue,
  decrementValue
};

export default connect(mapStateToProps, mapDispatchToProps)(Counter);
حذف کرده و با redux hooks جایگزین کنیم. برای این منظور فایل جدید src\components\HooksCounter.jsx را ایجاد می‌کنیم و سپس در ابتدا برای جایگزین کردن قسمت دریافت اطلاعات از this.pros آن:
class Counter extends Component {
  render() {
    console.log("Counter->props", this.props);
    const {
      //counterReducer: { count },
      count,
      incrementValue,
      decrementValue
    } = this.props;
به صورت زیر عمل می‌کنیم:
import React from "react";
import { useDispatch, useSelector } from "react-redux";

import { decrementValue, incrementValue } from "../actions";

const HooksCounter = ({ prop1 }) => {
  const { count } = useSelector(state => {
    console.log("HooksCounter->useSelector", { state, prop1 });
    return {
      count: state.counterReducer.count
    };
  });
  const dispatch = useDispatch();
  return (
  // ...
- متغیر count را با استفاده از useSelector، از شیء state استخراج کرده و با نام خاصیت count بازگشت می‌دهیم.
- اینبار دو action creator مورد استفاده‌ی در متدهای + و - را از ماژول action دریافت کرده‌ایم تا توسط useDispatch مورد استفاده قرار گیرند.
- همچنین دیگر نیازی به ذکر (state, ownProps) نیست. مقدار ownProps، همان props معمولی است که به کامپوننت ارسال می‌شود که برای مثال اینبار نام prop1 را دارد؛ چون هنگامیکه المان کامپوننت HooksCounter را درج و معرفی می‌کنیم، توسط کامپوننت دیگری محصور نشده‌است. تامین آن نیز در فایل src\App.js با درج متداول نام المان کامپوننت HooksCounter و ذکر ویژگی سفارشی prop1 صورت می‌گیرد:
import HooksCounter from "./components/HooksCounter";

//...

function App() {
  const prop1 = 123;
  return (
    <main className="container">
     <HooksCounter prop1={prop1} />
    </main>
  );
}
با این تغییرات، کدهای کامل src\components\HooksCounter.jsx به صورت زیر تکمیل می‌شود که قسمت‌های استفاده از متغیر count و همچنین dispatch دو action creator دریافت شده، در آن مشخص هستند:
import React from "react";
import { useDispatch, useSelector } from "react-redux";

import { decrementValue, incrementValue } from "../actions";

const HooksCounter = ({ prop1 }) => {
  const { count } = useSelector(state => {
    console.log("HooksCounter->useSelector", { state, prop1 });
    return {
      count: state.counterReducer.count
    };
  });
  const dispatch = useDispatch();
  return (
    <section className="card mt-5">
      <div className="card-body text-center">
        <span className="badge m-2 badge-primary">{count}</span>
      </div>
      <div className="card-footer">
        <div className="d-flex justify-content-center align-items-center">
          <button
            className="btn btn-secondary btn-sm"
            onClick={() => dispatch(incrementValue())}
          >
            +
          </button>
          <button
            className="btn btn-secondary btn-sm m-2"
            onClick={() => dispatch(decrementValue())}
          >
            -
          </button>
          <button className="btn btn-danger btn-sm">Reset</button>
        </div>
      </div>
    </section>
  );
};

export default HooksCounter;


مشکل! با استفاده از useSelector، تعداد رندرهای مجدد کامپوننت‌های برنامه افزایش یافته‌است!

برنامه‌ی جاری را پس از این تغییرات  اجرا کنید. با هر بار کلیک بر روی دکمه‌ی fetch posts، حتی کامپوننت شمارشگر درج شده‌ی در صفحه که ربطی به آن ندارد نیز رندر مجدد می‌شود! چرا؟ (این مورد را با مشاهده‌ی کنسول توسعه دهندگان مرورگر می‌توانید مشاهده کنید. در ابتدای متد رندر هر کدام از کامپوننت‌ها، یک console.log قرار داده شده‌است)
زمانیکه اکشنی dispatch می‌شود، useSelector hook با استفاده از مقایسه‌ی ارجاعات اشیاء (strict === reference check)، کار مقایسه‌ی مقدار قبلی و مقدار جدید را انجام می‌دهد. اگر این‌ها متفاوت باشند، کامپوننت را مجبور به رندر مجدد می‌کند. این مورد مهم‌ترین تفاوت بین useSelector hook و متد connect است. متد connect از روش shallow equality checks برای مقایسه‌ی نتایج حاصل از mapStateToProps و تصمیم در مورد رندر مجدد استفاده می‌کند. اما این مقایسه‌ها چه تفاوتی با هم دارند؟
در حالت mapStateToProps، مهم نیست که شیء بازگشت داده شده، دارای یک ارجاع جدید است یا خیر؟ shallow equality checks فقط به معنای مقایسه‌ی خاصیت به خاصیت شیء بازگشت داده شده‌، با نمونه‌ی قبلی است. اما زمانیکه از useSelector hook استفاده می‌کنیم، با بازگشت یک شیء جدید، یعنی یک ارجاع جدید را خواهیم داشت و ... این یعنی اجبار به رندر مجدد کامپوننت‌ها. به همین جهت در این حالت تعداد بار رندر کامپوننت‌ها افزایش یافته‌است، چون خروجی reducerهای تعریف شده‌ی در برنامه، همیشه یک شیء جدید را بازگشت می‌دهند.
برای رفع این مشکل می‌توان از پارامتر دوم متد useSelector که روش مقایسه‌ی اشیاء را مشخص می‌کند، استفاده کرد:
import React from "react";
import { shallowEqual, useSelector } from "react-redux";

import Post from "./Post";

const HooksPosts = () => {
  const { posts, loading, error } = useSelector(
    state => state.postsReducer,
    shallowEqual
  );
  console.log("render HooksPosts");
  return (
  // ...
استفاده از shallowEqual در اینجا سبب خواهد شد تا بجای مقایسه‌ی ارجاعات اشیاء (که همیشه متفاوت خواهند بود؛ چون هربار شیء جدیدی را بازگشت می‌دهیم)، مقادیر تک تک خواص آن‌ها با هم مقایسه شوند.
با اضافه کردن پارامتر shallowEqual به کامپوننت‌های HooksPosts و HooksCounter، دیگر با کلیک بر روی دکمه‌ی fetch posts، کار رندر مجدد کامپوننت شمارشگر، رخ نمی‌دهد.

یک نکته: روش دیگر مشاهده‌ی تعداد بار رندر شدن کامپوننت‌ها، استفاده از افزونه‌ی react dev tools و مراجعه به برگه‌ی profiler آن است. روی دکمه‌ی record آن کلیک کرده و سپس اندکی با برنامه کار کنید. اکنون کار ضبط را متوقف نمائید، تا نتیجه‌ی نهایی نمایش داده شود.

کدهای کامل این قسمت را می‌توانید از اینجا دریافت کنید: state-management-redux-mobx-part05.zip
مطالب
روش کار با فایل‌های پویای ارائه شده‌ی توسط یک برنامه‌ی ASP.NET Core در برنامه‌های React
پس از آشنایی با «روش کار با فایل‌های ایستا در برنامه‌های React»، اکنون اگر این فایل‌ها ایستا نباشند و توسط یک برنامه‌ی ASP.NET Core بازگشت داده شوند، چطور می‌توان از آن‌ها در برنامه‌های React استفاده کرد؟

برپایی پروژه‌های مورد نیاز

ابتدا یک پوشه‌ی جدید را مانند DownloadFilesSample، ایجاد کرده و در داخل آن دستور زیر را اجرا می‌کنیم:
> dotnet new react
در مورد این قالب که امکان تجربه‌ی توسعه‌ی یکپارچه‌ی ASP.NET Core و React را میسر می‌کند، در مطلب «روش یکی کردن پروژه‌های React و ASP.NET Core» بیشتر بحث کردیم.
سپس در این پوشه، پوشه‌ی ClientApp پیش‌فرض آن‌را حذف می‌کنیم؛ چون کمی قدیمی است. همچنین فایل‌های کنترلر و سرویس آب و هوای پیش‌فرض آن‌را به همراه پوشه‌ی صفحات Razor آن، حذف می‌کنیم.
به علاوه بجای تنظیم پیش فرض زیر در فایل کلاس آغازین برنامه:
spa.UseReactDevelopmentServer(npmScript: "start");
از تنظیم زیر استفاده کرده‌ایم تا با هر بار تغییری در کدهای پروژه‌ی ASP.NET، یکبار دیگر از صفر npm start اجرا نشود:
spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
بدیهی است در این حالت باید از طریق خط فرمان به پوشه‌ی clientApp وارد شد و دستور npm start را یکبار به صورت دستی اجرا کرد، تا این وب سرور بر روی پورت 3000، راه اندازی شود.

اکنون در ریشه‌ی پروژه‌ی ASP.NET Core ایجاد شده، دستور زیر را صادر می‌کنیم تا پروژه‌ی کلاینت React را با فرمت جدید آن ایجاد کند:
> create-react-app clientapp
سپس وارد این پوشه‌ی جدید شده و بسته‌های زیر را نصب می‌کنیم:
> cd clientapp
> npm install --save bootstrap axios
توضیحات:
- برای استفاده از شیوه‌نامه‌های بوت استرپ، بسته‌ی bootstrap نیز در اینجا نصب می‌شود که برای افزودن فایل bootstrap.css آن به پروژه‌ی React خود، ابتدای فایل clientapp\src\index.js را به نحو زیر ویرایش خواهیم کرد:
 import "bootstrap/dist/css/bootstrap.css";
این import به صورت خودکار توسط webpack ای که در پشت صحنه کار bundling & minification برنامه را انجام می‌دهد، مورد استفاده قرار می‌گیرد.
- برای دریافت فایل‌ها از سمت سرور، از کتابخانه‌ی معروف axios استفاده خواهیم کرد.


کدهای سمت سرور دریافت فایل‌های پویا

در اینجا کدهای سمت سرور برنامه، یک فایل PDF ساده را بازگشت می‌دهند. این محتوای باینری می‌تواند حاصل اجرای یک گزارش اکسل، PDF و یا کلا هر نوع فایلی باشد:
using Microsoft.AspNetCore.Mvc;

namespace DownloadFilesSample.Controllers
{
    [Route("api/[controller]")]
    public class ReportsController : Controller
    {
        [HttpGet("[action]")]
        public IActionResult GetPdfReport()
        {
            return File(virtualPath: "~/app_data/sample.pdf",
                        contentType: "application/pdf",
                        fileDownloadName: "sample.pdf");
        }
    }
}
فایل بازگشتی فوق که در این مثال در مسیر wwwroot\app_data\sample.pdf برنامه‌ی وب کپی شده‌است، در نهایت با آدرس api/Reports/GetPdfReport در سمت کلاینت قابل دسترسی خواهد بود.


روش دریافت محتوای باینری در برنامه‌های React

برای دریافت یک محتوای باینری از سرور توسط axios مانند تصاویر، فایل‌های PDF و اکسل و غیره، مهم‌ترین نکته، تنظیم ویژگی responseType آن به blob است:
  const getResults = async () => {
      const { headers, data } = await axios.get(apiUrl, {
        responseType: "blob"
      });
  }


ساخت URL برای دسترسی به اطلاعات باینری

تمام مرورگرهای جدید از ایجاد URL برای اشیاء Blob دریافتی از سمت سرور، توسط متد توکار URL.createObjectURL پشتیبانی می‌کنند. این متد، شیء URL را از شیء window جاری دریافت می‌کند و سپس اطلاعات باینری را دریافت کرده و آدرسی را جهت دسترسی موقت به آن تولید می‌کند. حاصل آن، یک URL ویژه‌است مانند blob:https://localhost:5001/03edcadf-89fd-48b9-8a4a-e9acf09afd67 که گشودن آن در مرورگر، یا سبب نمایش آن تصویر و یا دریافت مستقیم فایل خواهد شد.
در ادامه کدهای تبدیل blob دریافت شده‌ی از سرور را به این URL ویژه، مشاهده می‌کنید:
import axios from "axios";
import React, { useEffect, useState } from "react";

export default function DisplayPdf() {
  const apiUrl = "https://localhost:5001/api/Reports/GetPdfReport";

  const [blobInfo, setBlobInfo] = useState({
    blobUrl: "",
    fileName: ""
  });

  useEffect(() => {
    getResults();
  }, []);

  const getResults = async () => {
    try {
      const { headers, data } = await axios.get(apiUrl, {
        responseType: "blob"
      });
      console.log("headers", headers);

      const pdfBlobUrl = window.URL.createObjectURL(data);
      console.log("pdfBlobUrl", pdfBlobUrl);

      const fileName = headers["content-disposition"]
        .split(";")
        .find(n => n.includes("filename="))
        .replace("filename=", "")
        .trim();
      console.log("filename", fileName);

      setBlobInfo({
        blobUrl: pdfBlobUrl,
        fileName: fileName
      });
    } catch (error) {
      console.log(error);
    }
  };
توضیحات:
- توسط useEffect Hook و بدون ذکر وابستگی خاصی در آن، سبب شبیه سازی رویداد componentDidUpdate شده‌ایم. به این معنا که متد getResults فراخوانی شده‌ی در آن، پس از رندر کامپوننت در DOM فراخوانی می‌شود و بهترین محلی است که از آن می‌توان برای ارسال درخواست‌های Ajaxای به سمت سرور و دریافت اطلاعات از backend، استفاده کرد و سپس setState را با اطلاعات جدید فراخوانی نمود. معادل setState در اینجا نیز، همان شیء حالتی است که توسط useState Hook و متد setBlobInfo آن تعریف کرده‌ایم.
- پس از دریافت headers و data از سرور، با استفاده از متد createObjectURL، آن‌را تبدیل به یک blob URL کرده‌ایم.
- همچنین در سمت سرور، پارامتر fileDownloadName را نیز تنظیم کرده‌ایم. این نام در سمت کلاینت، توسط هدری با کلید content-disposition ظاهر می‌شود:
ontent-disposition: "attachment; filename=sample.pdf; filename*=UTF-8''sample.pdf"
 بنابراین می‌توان آن‌را تجزیه کرد و سپس filename را از آن استخراج نمود.
- اکنون که نام فایل و URL دسترسی به داده‌ی فایل باینری دریافتی از سرور را استخراج و ایجاد کرده‌ایم. با فراخوانی متد setBlobInfo، سبب تنظیم متغیر حالت blobInfo خواهیم شد. این مورد، رندر مجدد UI را سبب شده و توسط آن می‌توان برای مثال فایل PDF دریافتی را نمایش داد.


نمایش فایل PDF دریافتی از سرور، به همراه دکمه‌های دریافت، چاپ و بازکردن آن در برگه‌ای جدید

در ادامه کدهای کامل قسمت رندر این کامپوننت را مشاهده می‌کنید:
import axios from "axios";
import React, { useEffect, useState } from "react";

export default function DisplayPdf() {

  // ...

  const { blobUrl } = blobInfo;

  return (
    <>
      <h1>Display PDF Files</h1>
      <button className="btn btn-info" onClick={handlePrintPdf}>
        Print PDF
      </button>
      <button className="btn btn-primary ml-2" onClick={handleShowPdfInNewTab}>
        Show PDF in a new tab
      </button>
      <button className="btn btn-success ml-2" onClick={handleDownloadPdf}>
        Download PDF
      </button>

      <section className="card mb-5 mt-3">
        <div className="card-header">
          <h4>using iframe</h4>
        </div>
        <div className="card-body">
          <iframe
            title="PDF Report"
            width="100%"
            height="600"
            src={blobUrl}
            type="application/pdf"
          ></iframe>
        </div>
      </section>

      <section className="card mb-5">
        <div className="card-header">
          <h4>using object</h4>
        </div>
        <div className="card-body">
          <object
            data={blobUrl}
            aria-label="PDF Report"
            type="application/pdf"
            width="100%"
            height="100%"
          ></object>
        </div>
      </section>

      <section className="card mb-5">
        <div className="card-header">
          <h4>using embed</h4>
        </div>
        <div className="card-body">
          <embed
            aria-label="PDF Report"
            src={blobUrl}
            type="application/pdf"
            width="100%"
            height="100%"
          ></embed>
        </div>
      </section>
    </>
  );
}
که چنین خروجی را ایجاد می‌کند:


در اینجا با انتساب مستقیم blob URL ایجاد شده، به خواص src و یا data اشیائی مانند iframe ،object و یا embed، می‌توان سبب نمایش فایل pdf دریافتی از سرور شد. این نمایش نیز توسط قابلیت‌های توکار مرورگر صورت می‌گیرد و نیاز به نصب افزونه‌ی خاصی را ندارد.

در ادامه کدهای مرتبط با سه دکمه‌ی چاپ، دریافت و بازکردن فایل دریافتی از سرور را مشاهده می‌کنید.


مدیریت دکمه‌ی چاپ PDF

پس از اینکه به blobUrl دسترسی یافتیم، اکنون می‌توان یک iframe مخفی را ایجاد کرد، سپس src آن‌را به این آدرس ویژه تنظیم نمود و در آخر متد print آن‌را فراخوانی کرد که سبب نمایش خودکار دیالوگ چاپ مرورگر می‌شود:
  const handlePrintPdf = () => {
    const { blobUrl } = blobInfo;
    if (!blobUrl) {
      throw new Error("pdfBlobUrl is null");
    }

    const iframe = document.createElement("iframe");
    iframe.style.display = "none";
    iframe.src = blobUrl;
    document.body.appendChild(iframe);
    if (iframe.contentWindow) {
      iframe.contentWindow.print();
    }
  };


مدیریت دکمه‌ی نمایش فایل PDF در یک برگه‌ی جدید

اگر علاقمند بودید تا این فایل PDF را به صورت تمام صفحه و در برگه‌ای جدید نمایش دهید، می‌توان از متد window.open استفاده کرد:
const handleShowPdfInNewTab = () => {
    const { blobUrl } = blobInfo;
    if (!blobUrl) {
      throw new Error("pdfBlobUrl is null");
    }

    window.open(blobUrl);
  };

مدیریت دکمه‌ی دریافت فایل PDF

بجای نمایش فایل PDF می‌توان دکمه‌ای را بر روی صفحه قرار داد که با کلیک بر روی آن، این فایل توسط مرورگر به صورت متداولی جهت دریافت به کاربر ارائه شود:
  const handleDownloadPdf = () => {
    const { blobUrl, fileName } = blobInfo;
    if (!blobUrl) {
      throw new Error("pdfBlobUrl is null");
    }

    const anchor = document.createElement("a");
    anchor.style.display = "none";
    anchor.href = blobUrl;
    anchor.download = fileName;
    document.body.appendChild(anchor);
    anchor.click();
  };
در اینجا یک anchor جدید به صورت مخفی به صفحه اضافه می‌شود که href آن به blobUrl تنظیم شده‌است و همچنین از فایل fileName استخراجی نیز در اینجا جهت ارائه‌ی نام اصلی فایل دریافتی از سرور، کمک گرفته شده‌است. سپس متد click آن فراخوانی خواهد شد. این روش در مورد تدارک دکمه‌ی دریافت تمام blobهای دریافتی از سرور کاربرد دارد و منحصر به فایل‌های PDF نیست.
اگر خواستید عملیات axios.get و دریافت فایل، با هم یکی شوند، می‌توان متد handleDownloadPdf را پس از پایان کار await axios.get، فراخوانی کرد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: DownloadFilesSample.zip
برای اجرای آن، پس از صدور فرمان dotnet restore که سبب بازیابی وابستگی‌های سمت کلاینت نیز می‌شود، ابتدا به پوشه‌ی clientapp مراجعه کرده و فایل run.cmd را اجرا کنید. با اینکار react development server بر روی پورت 3000 شروع به کار می‌کند. سپس به پوشه‌ی اصلی برنامه‌ی ASP.NET Core بازگشته و فایل dotnet_run.bat را اجرا کنید. این اجرا سبب راه اندازی وب سرور برنامه و همچنین ارائه‌ی برنامه‌ی React بر روی پورت 5001 می‌شود.
مطالب
کامپوننت‌ها در AngularJS 1.5
در نسخه‌های  AngularJS 1.x عموماً با کمک کنترلرها و دایرکتیوها، می‌توانیم ویژگی‌های جدیدی را به اپلیکیشن‌هایمان اضافه کنیم؛ از دایرکتیوها برای ایجاد عناصر سفارشی HTML می‌توانستیم (می‌توانیم) استفاده کنیم. مشکل دایرکتیوها این است که برای ایجاد یک عنصر سفارشی ساده باید تنظیمات زیادی را انجام دهیم. در نسخه‌ی AngularJS 1.5 یک API جدید با نام کامپوننت معرفی شده است و این قابلیت، مدل ساده‌ی برنامه‌نویسی در کنترلرها و همچنین قدرت دایرکتیوها را در اختیارمان قرار خواهد داد. سینتکس این API خیلی شبیه به استفاده از کامپوننت‌ها در Angular 2.0 است. این یک مزیت مهم محسوب می‌شود؛ زیرا امکان مهاجرت از نسخه‌ی 1.5 به نسخه‌ی 2 را خیلی ساده خواهد کرد.

نحوه‌ی تعریف یک کامپوننت در AngularJS 1.5
همانند کنترلر و دایرکتیو، برای تعریف یک کامپوننت نیز باید از module API استفاده کنیم:

بنابراین برای ایجاد یک کامپوننت می‌توانیم به اینصورت عمل کنیم:

var app = angular.module("dntModule", []);
app.component("pmApp", {
  template: `Hello this is a simple component`
});

همانطور که مشاهده می‌کنید تابع component دو پارامتر را از ورودی دریافت خواهد کرد؛ نام کامپوننت و یک شیء برای تعیین تنظیمات کامپوننت. نام کامپوننت در اینجا به صورت camel case تعریف شده است؛ که در واقع یک convention برای Angular است. در این‌حالت برای استفاده‌ی از کامپوننت باید به اینصورت عمل کنیم:

<pm-app></pm-app>

در قسمت تنظیمات کامپوننت، در ساده‌ترین حالت یک template تعیین شده‌است که بیانگر نحوه‌ی رندر شدن یک کامپوننت می‌باشد. در اینحالت وقتی انگیولار به تگ فوق برسد، یک کامپوننت با نام pmApp را بارگذاری خواهد کرد.


ایجاد یک کامپوننت ساده

در ادامه می‌خواهیم یک کامپوننت ساده را جهت نمایش یکسری URL درون صفحه طراحی کنیم. ساختار صفحه index.html به صورت زیر خواهد بود:

<html ng-app="DNT">
<head>
    <meta charset="UTF-8">
    <title>Using Angular Component</title>
    <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css">
    <link rel="stylesheet" href="bower_components/font-awesome/css/font-awesome.min.css">
</head>
<body>
    <div class="container">
        <div class="row">
            <div class="col-md-3">
                <dnt-widget></dnt-widget>
            </div>
        </div>
    </div>
    <script src="bower_components/angular/angular.js"></script>
    <script src="scripts/app.js"></script>
    <script src="scripts/components/dnt-widget.component.js"></script>
</body>
</html>

در اینجا ابتدا توسط دایرکتیو ng-app، به Angular، ماژول‌مان را معرفی کرده‌ایم. سپس مداخل بوت‌استرپ و کتابخانه‌ی font-awesome را مشاهده می‌کنید. در ادامه، کتابخانه‌ی Angular و همچنین فایل app.js جهت معرفی ماژول برنامه معرفی شده‌است. در نهایت نیز یک فایل در مسیر ذکر شده برای قرار دادن کدهای کامپوننت در مسیر scripts/components اضافه شده‌است.

همانطور که ملاحظه می‌کنید، کامپوننت‌مان به صورت یک تگ سفارشی، درون صفحه قرار گرفته است:

<dnt-archive></dnt-archive>

در ادامه باید به Angular، نحوه‌ی تعریف این کامپوننت را اعلام کنیم. بنابراین یک فایل جاوا اسکریپتی را با نام dnt-widget.component، با محتویات زیر ایجاد کنید:

(function () {    
    "use strict";    
    var app = angular.module("DNT");    
    function DntArchiveController() {
      var model = this;      
      model.panel = {
          title: "Panel Title",
          items: [
              {
                  title: "Dotnettips", url: "https://www.dntips.ir"
              },
              {
                  title: "Google", url: "http://www.google.con"
              },
              {
                  title: "Yahoo", url: "http://www.yahoo.con"
              }
          ]
      };  
    };
    
    app.component("dntWidget", {
        templateUrl: '/scripts/components/dnt-widget.component.html',
        controllerAs: "model",
        controller: DntArchiveController
    });
} ());

توضیح کدهای فوق:

همانطور که مشاهده می‌کنید، برای پارمتر دوم کامپوننت، سه پراپرتی را تعیین کرده‌ایم:

templateUrl: به کمک این پراپرتی به Angular گفته‌ایم که محتوای قالب این کامپوننت، درون یک فایل HTML مجزا قرار دارد و به صورت linked template می‌باشد.

controllerAs: یکی از مزایای استفاده از کامپوننت‌ها، استفاده از controller as syntax می‌باشد. لازم به ذکر است اگر این پراپرتی را مقداردهی نکنیم، به صورت پیش‌فرض مقدار ctrl$ در نظر گرفته خواهد شد.

controller: مزیت دیگر کامپوننت‌ها، استفاده از کنترلرها است. با استفاده از این پراپرتی، یک کنترلر را برای کامپوننت‌مان رجیستر کرده‌ایم. در نتیجه زمانیکه‌ی Angular می‌خواهد کامپوننت‌مان را نمایش دهد، تابع تعریف شده برای این پراپرتی، جهت ایجاد یک controller instance فراخوانی خواهد شد. بنابراین هر پراپرتی یا تابعی که برای این controller instance تعریف کنیم، به راحتی درون ویوی آن جهت اعمال بایندینگ در دسترس خواهد بود (در نتیجه نیازی به scope$ نخواهد بود).

درون کنترلر نیز برای راحتی کار و همچنین به عنوان یک best practice، مقدار this را توسط یک متغیر با نام model، کپچر کرده‌ایم. در اینجا یک شیء را با نام panel نیز به مدل اضافه کرده‌ایم.


محتویات تمپلیت:

<div class="panel panel-default">
    <div class="panel-heading">
        <h3 class="panel-title">
            <span class="fa fa-archive"></span>
            {{ model.panel.title}}
        </h3>
    </div>
    <ul class="list-group">
        <li class="list-group-item" ng-repeat="item in model.panel.items">
            <span class="fa fa-industry"></span>
            <a href="{{ item.url }}">{{ item.title }}</a>
        </li>
    </ul>
</div>

ویوی کامپوننت پیچیدگی خاصی ندارد. همانطور که مشاهده می‌کنید یک پنل بوت‌استرپی را ایجاد کرده‌ایم که مقدار عنوان آن و همچنین آیتم‌های آن، از شیء اتچ شده به مدل دریافت خواهند شد. بنابراین اکنون اگر برنامه را اجرا کنید، خروجی کامپوننت را به اینصورت مشاهده خواهید کرد:



همانطور که مشاهده می‌کنید استفاده از کامپوننت‌ها در Angular 1.5 در مقایسه با ایجاد دایرکتیوها و کنترلر‌ها خیلی ساده‌تر است. در واقع امکانات این API جدید تنها به مثال فوق ختم نمی‌شود؛ بلکه این API یک سیستم مسیریابی جدید را نیز معرفی کرده است که در قسمت‌های بعدی به آن نیز خواهیم پرداخت.


جهت تکمیل بحث نیز یک تقویم شمسی ساده را در اینجا قرار داده‌ام. می‌توانید جهت مرور بحث جاری به کدهای آن مراجعه کنید. البته هدف از تعریف این پروژه تنها یک مثال ساده برای معرفی کامپوننت‌ها بود و طبیعتاً باگ‌های زیادی دارد. اگر مایل بودید می‌توانید در توسعه‌ی آن مشارکت نمائید.


کدهای این قسمت را نیز از اینجا می‌توانید دریافت کنید.

مطالب
آپلود فایل‌ها توسط برنامه‌های React به یک سرور ASP.NET Core به همراه نمایش درصد پیشرفت
قصد داریم اطلاعات یک فرم React را به همراه دو فایل الصاقی به آن، به سمت یک سرور ASP.NET Core ارسال کنیم؛ بطوریکه درصد پیشرفت ارسال فایل‌ها، زمان سپری شده، زمان باقی مانده و سرعت آپلود نیز گزارش داده شوند:



پیشنیازها
«بررسی روش آپلود فایل‌ها در ASP.NET Core»
«ارسال فایل و تصویر به همراه داده‌های دیگر از طریق jQuery Ajax »

- در مطلب اول، روش دریافت فایل‌ها از کلاینت، در سمت سرور و ذخیره سازی آن‌ها در یک برنامه‌ی ASP.NET Core بررسی شده‌است که کلیات آن در اینجا نیز صادق است.
- در مطلب دوم، روش کار با FormData استاندارد بررسی شده‌است. هرچند در مطلب جاری از jQuery استفاده نمی‌شود، اما نکات نحوه‌ی کار با شیء FormData استاندارد، در اینجا نیز یکی است.


برپایی پروژه‌های مورد نیاز

ابتدا یک پوشه‌ی جدید مانند UploadFilesSample را ایجاد کرده و در داخل آن دستور زیر را اجرا می‌کنیم:
 dotnet new react
در مورد این قالب که امکان تجربه‌ی توسعه‌ی یکپارچه‌ی ASP.NET Core و React را میسر می‌کند، در مطلب «روش یکی کردن پروژه‌های React و ASP.NET Core» بیشتر بحث کرده‌ایم.
سپس در این پوشه، پوشه‌ی ClientApp پیش‌فرض آن‌را حذف می‌کنیم؛ چون کمی قدیمی است. همچنین فایل‌های کنترلر و سرویس آب و هوای پیش‌فرض آن‌را به همراه پوشه‌ی صفحات Razor آن، حذف و پوشه‌ی خالی wwwroot را نیز به آن اضافه می‌کنیم.
همچنین بجای تنظیم پیش فرض زیر در فایل کلاس آغازین برنامه:
spa.UseReactDevelopmentServer(npmScript: "start");
از تنظیم زیر استفاده کرده‌ایم تا با هر بار تغییری در کدهای پروژه‌ی ASP.NET، یکبار دیگر از صفر npm start اجرا نشود:
spa.UseProxyToSpaDevelopmentServer("http://localhost:3000");
بدیهی است در این حالت باید از طریق خط فرمان به پوشه‌ی clientApp وارد شد و دستور npm start را یکبار به صورت دستی اجرا کرد، تا این وب سرور بر روی پورت 3000، راه اندازی شود. البته ما برنامه را به صورت یکپارچه بر روی پورت 5001 وب سرور ASP.NET Core، مرور می‌کنیم.

اکنون در ریشه‌ی پروژه‌ی ASP.NET Core ایجاد شده، دستور زیر را صادر می‌کنیم تا پروژه‌ی کلاینت React را با فرمت جدید آن ایجاد کند:
> create-react-app clientapp
سپس وارد این پوشه‌ی جدید شده و بسته‌های زیر را نصب می‌کنیم:
> cd clientapp
> npm install --save bootstrap axios react-toastify
توضیحات:
- برای استفاده از شیوه‌نامه‌های بوت استرپ، بسته‌ی bootstrap نیز در اینجا نصب می‌شود که برای افزودن فایل bootstrap.css آن به پروژه‌ی React خود، ابتدای فایل clientapp\src\index.js را به نحو زیر ویرایش خواهیم کرد:
import "bootstrap/dist/css/bootstrap.css";
این import به صورت خودکار توسط webpack ای که در پشت صحنه کار bundling & minification برنامه را انجام می‌دهد، مورد استفاده قرار می‌گیرد.
- برای نمایش پیام‌های برنامه از کامپوننت react-toastify استفاده می‌کنیم که پس از نصب آن، با مراجعه به فایل app.js نیاز است importهای لازم آن‌را اضافه کنیم:
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
همچنین نیاز است ToastContainer را به ابتدای متد render آن نیز اضافه کرد:
  render() {
    return (
      <React.Fragment>
        <ToastContainer />
- برای ارسال فایل‌ها به سمت سرور از کتابخانه‌ی معروف axios استفاده خواهیم کرد.


ایجاد کامپوننت React فرم ارسال فایل‌ها به سمت سرور

پس از این مقدمات، فایل جدید clientapp\src\components\UploadFileSimple.jsx را ایجاد کرده و به صورت زیر تکمیل می‌کنیم:
import React, { useState } from "react";
import axios from "axios";
import { toast } from "react-toastify";

export default function UploadFileSimple() {

  const [description, setDescription] = useState("");
  const [selectedFile1, setSelectedFile1] = useState();
  const [selectedFile2, setSelectedFile2] = useState();


  return (
    <form>
      <fieldset className="form-group">
        <legend>Support Form</legend>

        <div className="form-group row">
          <label className="form-control-label" htmlFor="description">
            Description
          </label>
          <input
            type="text"
            className="form-control"
            name="description"
            onChange={event => setDescription(event.target.value)}
            value={description}
          />
        </div>

        <div className="form-group row">
          <label className="form-control-label" htmlFor="file1">
            File 1
          </label>
          <input
            type="file"
            className="form-control"
            name="file1"
            onChange={event => setSelectedFile1(event.target.files[0])}
          />
        </div>

        <div className="form-group row">
          <label className="form-control-label" htmlFor="file2">
            File 2
          </label>
          <input
            type="file"
            className="form-control"
            name="file2"
            onChange={event => setSelectedFile2(event.target.files[0])}
          />
        </div>

        <div className="form-group row">
          <button
            className="btn btn-primary"
            type="submit"
          >
            Submit
          </button>
        </div>
      </fieldset>
    </form>
  );
}
کاری که تا این مرحله انجام شده، بازگشت UI فرم برنامه توسط یک functional component است.
- توسط آن یک textbox به همراه دو فیلد ارسال فایل، به فرم اضافه شده‌اند.
- مرحله‌ی بعد، دسترسی به فایل‌های انتخابی کاربر و همچنین مقدار توضیحات وارد شده‌است. به همین جهت با استفاده از useState Hook، روش دریافت و تنظیم این مقادیر را مشخص کرده‌ایم:
  const [description, setDescription] = useState("");
  const [selectedFile1, setSelectedFile1] = useState();
  const [selectedFile2, setSelectedFile2] = useState();
با React Hooks، بجای تعریف یک state، به صورت خاصیت، آن‌را صرفا use می‌کنیم و یا همان useState، که یک تابع است و باید در ابتدای کامپوننت، مورد استفاده قرار گیرد. این متد برای شروع به کار، نیاز به یک state آغازین را دارد؛ مانند انتساب یک رشته‌ی خالی به description. سپس اولین خروجی متد useState که داخل یک آرایه مشخص شده‌است، همان متغیر description است که توسط state ردیابی خواهد شد. اینبار بجای متد this.setState قبلی که یک متد عمومی بود، متدی اختصاصی را صرفا جهت تغییر مقدار همین متغیر description به نام setDescription به عنوان دومین خروجی متد useState، تعریف می‌کنیم. بنابراین متد useState، یک initialState را دریافت می‌کند و سپس یک مقدار را به همراه یک متد، جهت تغییر state آن، بازگشت می‌دهد. همین کار را برای دو فیلد دیگر نیز تکرار کرده‌ایم. بنابراین selectedFile1، فایلی است که توسط متد setSelectedFile1 تنظیم خواهد شد و این تنظیم، سبب رندر مجدد UI نیز خواهد گردید.
- پس از طراحی state این فرم، مرحله‌ی بعدی، استفاده از متدهای set تمام useStateهای فوق است. برای مثال در مورد یک textbox معمولی، می‌توان آن‌را به صورت inline تعریف کرد و با هر بار تغییری در محتوای آن، این رخ‌داد را به متد setDescription ارسال نمود تا مقدار وارد شده را به متغیر حالت description انتساب دهد:
          <input
            type="text"
            className="form-control"
            name="description"
            onChange={event => setDescription(event.target.value)}
            value={description}
          />
در مورد فیلدهای دریافت فایل‌ها، روش انجام اینکار به صورت زیر است:
          <input
            type="file"
            className="form-control"
            name="file1"
            onChange={event => setSelectedFile1(event.target.files[0])}
          />
چون المان‌های دریافت فایل می‌توانند بیش از یک فایل را نیز دریافت کنند (اگر ویژگی multiple، به تعریف تگ آن‌ها اضافه شود)، به همین جهت خاصیت files بر روی آن‌ها قابل دسترسی شده‌است. اما چون در اینجا ویژگی multiple ذکر نشده‌است، بنابراین تنها یک فایل توسط آن‌ها قابل دریافت است و به همین جهت دسترسی به اولین فایل و یا files[0] را در اینجا مشاهده می‌کنید. بنابراین با فراخوانی متد setSelectedFile1، اکنون متغیر حالت selectedFile1، مقدار دهی شده و قابل استفاده است.


تشکیل مدل ارسال داده‌ها به سمت سرور

در فرم‌های معمولی، عموما داده‌ها به صورت یک شیء JSON به سمت سرور ارسال می‌شوند؛ اما در اینجا وضع متفاوت است و به همراه توضیحات وارد شده، دو فایل باینری نیز وجود دارند.
در حالت ارسال متداول فرم‌هایی که به همراه المان‌های دریافت فایل هستند، ابتدا یک ویژگی enctype با مقدار multipart/form-data به المان فرم اضافه می‌شود و سپس این فرم به سادگی قابلیت post-back به سمت سرور را پیدا می‌کند:
<form enctype="multipart/form-data" action="/upload" method="post">
   <input id="file-input" type="file" />
</form>
اما اگر قرار باشد همین فرم را توسط جاوا اسکریپت به سمت سرور ارسال کنیم، روش کار به صورت زیر است:
let file = document.getElementById("file-input").files[0];
let formData = new FormData();
 
formData.append("file", file);
fetch('/upload/image', {method: "POST", body: formData});
ابتدا به خاصیت files و اولین فایل آن دسترسی پیدا کرده و سپس شیء استاندارد FormData را بر اساس آن و تمام فیلدهای فرم تشکیل می‌دهیم. FormData ساختاری شبیه به یک دیکشنری را دارد و از کلیدهایی که متناظر با Id المان‌های فرم و مقادیری متناظر با مقادیر آن المان‌ها هستند، تشکیل می‌شود که توسط متد append آن، به این دیکشنری اضافه خواهند شد. در آخر هم شیء formData را به سمت سرور ارسال می‌کنیم.
در یک برنامه‌ی React نیز باید دقیقا چنین مراحلی طی شوند. تا اینجا کار دسترسی به مقدار files[0] و تشکیل متغیرهای حالت فرم را انجام داده‌ایم. در مرحله‌ی بعد، شیء FormData را تشکیل خواهیم داد:
  // ...

export default function UploadFileSimple() {
  // ...

  const handleSubmit = async event => {
    event.preventDefault();

    const formData = new FormData();
    formData.append("description", description);
    formData.append("file1", selectedFile1);
    formData.append("file2", selectedFile2);


      toast.success("Form has been submitted successfully!");

      setDescription("");
  };

  return (
    <form onSubmit={handleSubmit}>
    </form>
  );
}
به همین جهت، ابتدا کار مدیریت رخ‌داد onSubmit فرم را انجام داده و توسط آن با استفاده از متد preventDefault، از post-back متداول فرم به سمت سرور جلوگیری می‌کنیم. سپس شیء FormData را بر اساس مقادیر حالت متناظر با المان‌های فرم، تشکیل می‌دهیم. کلیدهایی که در اینجا ذکر می‌شوند، نام خواص مدل متناظر سمت سرور را نیز تشکیل خواهند داد.


ارسال مدل داده‌های فرم React به سمت سرور

پس از تشکیل شیء FormData در متد مدیریت کننده‌ی handleSubmit، اکنون با استفاده از کتابخانه‌ی axios، کار ارسال این اطلاعات را به سمت سرور انجام خواهیم داد:
  // ...

export default function UploadFileSimple() {
  const apiUrl = "https://localhost:5001/api/SimpleUpload/SaveTicket";

  // ...
  const [isUploading, setIsUploading] = useState(false);

  const handleSubmit = async event => {
    event.preventDefault();

    const formData = new FormData();
    formData.append("description", description);
    formData.append("file1", selectedFile1);
    formData.append("file2", selectedFile2);

    try {
      setIsUploading(true);

      const { data } = await axios.post(apiUrl, formData, {
        headers: {
          "Content-Type": "multipart/form-data"
        }}
      });

      toast.success("Form has been submitted successfully!");

      console.log("uploadResult", data);

      setIsUploading(false);
      setDescription("");
    } catch (error) {
      setIsUploading(false);
      toast.error(error);
    }
  };


  return (
  // ...
  );
}
در اینجا نحوه‌ی ارسال شیء FormData را توسط کتابخانه‌ی axios به سمت سرور مشاهده می‌کنید. با استفاده از متد post آن، به سمت مسیر api/SimpleUpload/SaveTicket که آن‌را در ادامه تکمیل خواهیم کرد، شیء formData متناظر با اطلاعات فرم، به صورت async، ارسال شده‌است. همچنین headers آن نیز به همان «"enctype="multipart/form-data» که پیشتر توضیح داده شد، تنظیم شده‌است.
در قطعه کد فوق، متغیر جدید حالت isLoading را نیز مشاهده می‌کنید. از آن می‌توان برای فعال و غیرفعال کردن دکمه‌ی submit فرم در زمان ارسال اطلاعات به سمت سرور، استفاده کرد:
<button
   disabled={ isUploading }
   className="btn btn-primary"
   type="submit"
>
  Submit
</button>
به این ترتیب اگر فراخوانی await axios.post هنوز به پایان نرسیده باشد، مقدار isUploading مساوی true بوده و سبب غیرفعال شدن دکمه‌ی submit می‌شود.


اعتبارسنجی سمت کلاینت فایل‌های ارسالی به سمت سرور

در اینجا شاید نیاز باشد نوع و یا اندازه‌ی فایل‌های انتخابی توسط کاربر را تعیین اعتبار کرد. به همین جهت متدی را برای اینکار به صورت زیر تهیه می‌کنیم:
  const isFileValid = selectedFile => {
    if (!selectedFile) {
      // toast.error("Please select a file.");
      return false;
    }

    const allowedMimeTypes = [
      "image/png",
      "image/jpeg",
      "image/gif",
      "image/svg+xml"
    ];
    if (!allowedMimeTypes.includes(selectedFile.type)) {
      toast.error(`Invalid file type: ${selectedFile.type}`);
      return false;
    }

    const maxFileSize = 1024 * 500;
    const fileSize = selectedFile.size;
    if (fileSize > maxFileSize) {
      toast.error(
        `File size ${(fileSize / 1024).toFixed(
          2
        )} KB must be less than ${maxFileSize / 1024} KB`
      );
      return false;
    }

    return true;
  };
در اینجا ابتدا بررسی می‌شود که آیا فایلی انتخاب شده‌است یا خیر؟ سپس فایل انتخاب شده، باید دارای یکی از MimeTypeهای تعریف شده باشد. همچنین اندازه‌ی آن نیز نباید بیشتر از 500 کیلوبایت باشد. در هر کدام از این موارد، یک خطا توسط react-toastify به کاربر نمایش داده خواهد شد.

اکنون برای استفاده‌ی از این متد دو راه وجود دارد:
الف) استفاده از آن در متد مدیریت کننده‌ی submit اطلاعات:
  const handleSubmit = async event => {
    event.preventDefault();

    if (!isFileValid(selectedFile1) || !isFileValid(selectedFile2)) {
      return;
    }
در ابتدای متد مدیریت کننده‌ی handleSubmit، متد isFileValid را بر روی دو متغیر حالتی که حاوی اطلاعات فایل‌های انتخابی توسط کاربر هستند، فراخوانی می‌کنیم.

ب) استفاده‌ی از آن جهت غیرفعال کردن دکمه‌ی submit:
<button
            disabled={
              isUploading ||
              !isFileValid(selectedFile1) ||
              !isFileValid(selectedFile2)
            }
            className="btn btn-primary"
            type="submit"
>
   Submit
</button>
می‌توان دقیقا در همان زمانیکه کاربر فایلی را انتخاب می‌کند نیز به انتخاب او واکنش نشان داد. چون مقدار دهی‌های متغیرهای حالت، همواره سبب رندر مجدد فرم می‌شوند و در این حالت مقدار ویژگی disabled نیز محاسبه‌ی مجدد خواهد شد، بنابراین در همان زمانیکه کاربر فایلی را انتخاب می‌کند، متد isFileValid نیز بر روی آن فراخوانی شده و در صورت نیاز، خطایی به او نمایش داده می‌شود.


نمایش درصد پیشرفت آپلود فایل‌ها

کتابخانه‌ی axios، امکان دسترسی به میزان اطلاعات آپلود شده‌ی به سمت سرور را به صورت یک رخ‌داد فراهم کرده‌است که در ادامه از آن برای نمایش درصد پیشرفت آپلود فایل‌ها استفاده می‌کنیم:
      const startTime = Date.now();

      const { data } = await axios.post(apiUrl, formData, {
        headers: {
          "Content-Type": "multipart/form-data"
        },
        onUploadProgress: progressEvent => {
          const { loaded, total } = progressEvent;

          const timeElapsed = Date.now() - startTime;
          const uploadSpeed = loaded / (timeElapsed / 1000);

          setUploadProgress({
            queueProgress: Math.round((loaded / total) * 100),
            uploadTimeRemaining: Math.ceil((total - loaded) / uploadSpeed),
            uploadTimeElapsed: Math.ceil(timeElapsed / 1000),
            uploadSpeed: (uploadSpeed / 1024).toFixed(2)
          });
        }
      });
هر بار که متد رویدادگردان onUploadProgress فراخوانی می‌شود، به همراه اطلاعات شیء progressEvent است که خواص loaded آن به معنای میزان اطلاعات آپلود شده و total هم جمع کل اندازه‌ی اطلاعات در حال ارسال است. بر این اساس و همچنین زمان شروع عملیات، می‌توان اطلاعاتی مانند درصد پیشرفت عملیات، مدت زمان باقیمانده، مدت زمان سپری شده و سرعت آپلود اطلاعات را محاسبه کرد و سپس توسط آن، شیء state ویژه‌ای را به روز رسانی کرد که به صورت زیر تعریف می‌شود:
  const [uploadProgress, setUploadProgress] = useState({
    queueProgress: 0,
    uploadTimeRemaining: 0,
    uploadTimeElapsed: 0,
    uploadSpeed: 0
  });
هر بار به روز رسانی state، سبب رندر مجدد UI می‌شود. به همین جهت متدی را برای رندر جدولی که اطلاعات شیء state فوق را نمایش می‌دهد، به صورت زیر تهیه می‌کنیم:
  const showUploadProgress = () => {
    const {
      queueProgress,
      uploadTimeRemaining,
      uploadTimeElapsed,
      uploadSpeed
    } = uploadProgress;

    if (queueProgress <= 0) {
      return <></>;
    }

    return (
      <table className="table">
        <thead>
          <tr>
            <th width="15%">Event</th>
            <th>Status</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>
              <strong>Elapsed time</strong>
            </td>
            <td>{uploadTimeElapsed} second(s)</td>
          </tr>
          <tr>
            <td>
              <strong>Remaining time</strong>
            </td>
            <td>{uploadTimeRemaining} second(s)</td>
          </tr>
          <tr>
            <td>
              <strong>Upload speed</strong>
            </td>
            <td>{uploadSpeed} KB/s</td>
          </tr>
          <tr>
            <td>
              <strong>Queue progress</strong>
            </td>
            <td>
              <div
                className="progress-bar progress-bar-info progress-bar-striped"
                role="progressbar"
                aria-valuemin="0"
                aria-valuemax="100"
                aria-valuenow={queueProgress}
                style={{ width: queueProgress + "%" }}
              >
                {queueProgress}%
              </div>
            </td>
          </tr>
        </tbody>
      </table>
    );
  };
و این متد را به این شکل در ذیل المان fieldset فرم، اضافه می‌کنیم تا کار رندر نهایی را انجام دهد:
{showUploadProgress()}
هربار که state به روز می‌شود، مقدار شیء uploadProgress دریافت شده و بر اساس آن، 4 سطر جدول نمایش پیشرفت آپلود، تکمیل می‌شوند.
در اینجا از کامپوننت progress-bar خود بوت استرپ برای نمایش درصد آپلود فایل‌ها استفاده شده‌است. اگر style آن‌را هر بار با مقدار جدید queueProgress به روز رسانی کنیم، سبب نمایش پویای این progress-bar خواهد شد.

یک نکته: اگر می‌خواهید درصد پیشرفت آپلود را در حالت آزمایش local بهتر مشاهده کنید، دربرگه‌ی network، سرعت را بر روی 3G تنظیم کنید (مانند تصویر ابتدای بحث)؛ در غیراینصورت همان ابتدای کار به علت بالا بودن سرعت ارسال فایل‌ها، 100 درصد را مشاهده خواهید کرد.


دریافت فرم React درخواست پشتیبانی، در سمت سرور و ذخیره‌ی فایل‌های آن‌

بر اساس نحوه‌ی تشکیل FormData سمت کلاینت:
const formData = new FormData();
formData.append("description", description);
formData.append("file1", selectedFile1);
formData.append("file2", selectedFile2);
مدل سمت سرور معادل با آن به صورت زیر خواهد بود:
using Microsoft.AspNetCore.Http;

namespace UploadFilesSample.Models
{
    public class Ticket
    {
        public int Id { set; get; }

        public string Description { set; get; }

        public IFormFile File1 { set; get; }

        public IFormFile File2 { set; get; }
    }
}
که در اینجا هر selectedFile سمت کلاینت، به یک IFormFile سمت سرور نگاشت می‌شود. نام این خواص نیز باید با نام کلیدهای اضافه شده‌ی به دیکشنری FormData، یکی باشند.
پس از آن کنترلر ذخیره سازی اطلاعات Ticket را مشاهده می‌کنید:
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using UploadFilesSample.Models;

namespace UploadFilesSample.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class SimpleUploadController : Controller
    {
        private readonly IWebHostEnvironment _environment;

        public SimpleUploadController(IWebHostEnvironment environment)
        {
            _environment = environment;
        }

        [HttpPost("[action]")]
        public async Task<IActionResult> SaveTicket([FromForm]Ticket ticket)
        {
            var file1Path = await saveFileAsync(ticket.File1);
            var file2Path = await saveFileAsync(ticket.File2);

            //TODO: save the ticket ... get id

            return Created("", new { id = 1001 });
        }

        private async Task<string> saveFileAsync(IFormFile file)
        {
            const string uploadsFolder = "uploads";
            var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads");
            if (!Directory.Exists(uploadsRootFolder))
            {
                Directory.CreateDirectory(uploadsRootFolder);
            }

            //TODO: Do security checks ...!

            if (file == null || file.Length == 0)
            {
                return string.Empty;
            }

            var filePath = Path.Combine(uploadsRootFolder, file.FileName);
            using (var fileStream = new FileStream(filePath, FileMode.Create))
            {
                await file.CopyToAsync(fileStream);
            }

            return $"/{uploadsFolder}/{file.Name}";
        }
    }
}
توضیحات تکمیلی:
- تزریق IWebHostEnvironment در سازنده‌ی کلاس کنترلر، سبب می‌شود تا از طریق خاصیت WebRootPath آن، به wwwroot دسترسی پیدا کنیم و فایل‌های نهایی را در آنجا ذخیره سازی کنیم.
- همانطور که ملاحظه می‌کنید، هنوز هم model binding کار کرده و می‌توان شیء Ticket را به نحو متداولی دریافت کرد:
public async Task<IActionResult> SaveTicket([FromForm]Ticket ticket)
ویژگی FromForm نیز مرتبط است به هدر multipart/form-data ارسالی از سمت کلاینت:
      const { data } = await axios.post(apiUrl, formData, {
        headers: {
          "Content-Type": "multipart/form-data"
        }}
      });


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: UploadFilesSample.zip
برای اجرای آن، پس از صدور فرمان dotnet restore که سبب بازیابی وابستگی‌های سمت کلاینت نیز می‌شود، ابتدا به پوشه‌ی clientapp مراجعه کرده و فایل run.cmd را اجرا کنید. با اینکار react development server بر روی پورت 3000 شروع به کار می‌کند. سپس به پوشه‌ی اصلی برنامه‌ی ASP.NET Core بازگشت شده و فایل dotnet_run.bat را اجرا کنید. این اجرا سبب راه اندازی وب سرور برنامه و همچنین ارائه‌ی برنامه‌ی React بر روی پورت 5001 می‌شود.
مطالب
پیاده سازی پروژه‌های React با TypeScript - قسمت پنجم - تعیین نوع هوک useReducer
هوک استاندارد useReducer، یک نمونه‌ی پیچیده‌تر هوک useState، برای مدیریت حالت است؛ با ساختاری شبیه به Redux، اما تنها مخصوص یک کامپوننت. هوک useReducer شبیه به یک نسخه‌ی کوچک و محلی Redux است. در این قسمت، نحوه‌ی تعیین نوع‌های قسمت‌های مختلف آن‌را با TypeScript بررسی می‌کنیم.


ایجاد کامپوننت جدید ReducerButtons

برای توضیح مفاهیم این قسمت، فایل جدید src\components\ReducerButtons.tsx را به همراه محتوای زیر ایجاد می‌کنیم:
import React, { useReducer } from "react";

const initialState = { rValue: true };
type State = {  rValue: boolean; };
type Action = {   type: string; };

function reducer(state: State, action: Action) {
  switch (action.type) {
    case "one":
      return { rValue: true };
    case "two":
      return { rValue: false };
    default:
      return state;
  }
}

export const ReducerButtons = () => {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <div>
      {state?.rValue && <h1>Visible</h1>}
      <button onClick={() => dispatch({ type: "one" })}>Action One</button>
      <button onClick={() => dispatch({ type: "two" })}>Action Two</button>
    </div>
  );
};
توضیحات:
- این کامپوننت دو دکمه را رندر می‌کند تا توسط آن‌ها بتوان چندین Action مختلف را جهت مدیریت حالت، سبب شد؛ مانند Action One و Two.
- initialState را در این مثال، یک شیء ساده در نظر گرفته‌ایم که تنها دارای یک مقدار boolean است.
- سپس نیاز به یک تابع ویژه را به نام reducer، داریم که مقدار state جاری و یک action را دریافت می‌کند. این تابع بر اساس نوع Action ای که به آن می‌رسد، state جدیدی را بازگشت می‌دهد و در قسمت رندر آن، با بررسی state?.rValue، سبب نمایش عبارتی خواهیم شد.
- در حین تعریف هوک useReducer، قسمت dispatch آن، تابعی است که یک Action را اجرا می‌کند. فراخوانی آن‌را در رویداد onClick دو دکمه‌ی تعریف شده مشاهده می‌کنید. این تابع یک شیء را دریافت می‌کند که type اکشن ارسالی به تابع reducer را مقدار دهی می‌کند.

توسط useReducer برای ایجاد تغییرات در شیء state کامپوننت جاری، از مفهومی به نام dispatch actions استفاده می‌شود. action در اینجا به معنای رخ‌دادن چیزی است؛ مانند کلیک بر روی یک دکمه و یا دریافت اطلاعاتی از یک API. در این حالت مقایسه‌ای بین وضعیت قبلی state و وضعیت فعلی آن صورت می‌گیرد و تغییرات مورد نیاز جهت اعمال به UI، محاسبه خواهند شد. اصلی‌ترین جزء آن، تابعی است به نام Reducer. این تابع، یک تابع خالص است و دو آرگومان را دریافت می‌کند. تابع Reducer، بر اساس action و یا رخ‌دادی، ابتدا کل state برنامه را دریافت می‌کند و سپس خروجی آن بر اساس منطق این تابع، یک state جدید خواهد بود. اکنون که این state جدید را داریم، برنامه‌ی React ما می‌تواند به تغییرات آن گوش فرا داده و بر اساس آن، UI را به روز رسانی کند.

اولین قسمتی را که در حین کار با useReducer توسط TypeScript به آن برخورد خواهیم کرد، نمایش خطاهایی در مورد نوع‌های پارامترهای state و action تابع reducer است. در حالت متداول جاوا اسکریپتی آن، این پارامترها، بدون نوع ذکر می‌شوند که در اینجا به any تفسیر خواهند شد و یک چنین تعاریفی با توجه به strict بودن تنظیمات tsconfig.json برنامه، مجاز نیست. به همین جهت نیاز به تعریف دو type جدید به نام‌های State و Action در اینجا وجود دارد تا خطاهای بدون نوع بودن پارامترهای تابع reducer برطرف شوند. نوع Action به همراه حداقل یک خاصیت به نام action رشته‌ای است و نوع State بر اساس initialState ای که تعریف کردیم، دارای یک خاصیت متناظر boolean است.

نکته‌ی جالب دومی که در اینجا توسط TypeScript گوشزد می‌شود، الزام به ذکر حالت default مربوط به switch ای است که در تابع reducer تعریف کرده‌ایم. اگر این حالت را حذف کنیم، بلافاصله خطای زیر را در قسمت تعریف useReducer نمایش می‌دهد:


عنوان می‌کند که خروجی تابع reducer، یک state جدید است؛ اما ممکن است از نوع never هم باشد که قابلیت ترجمه‌ی به نوع state را ندارد.


بهبود تعاریف نوع‌های useReducer

تا اینجا مهم‌ترین تغییرات تایپ‌اسکریپتی صورت گرفته، تعریف نوع‌های پارامترهای تابع reducer است. اکنون فرض کنید می‌خواهیم، سومین Action را هم به کامپوننت جاری اضافه کنیم:
<button onClick={() => dispatch({ type: "three" })}>Action Three</button>
با این تغییر، برنامه بدون مشکل کامپایل می‌شود، اما ... در عمل کار خاصی را هم انجام نمی‌دهد؛ چون switch تعریف شده‌ی در تابع reducer، یک case مخصوص به آن‌را تعریف نکرده‌است. یا حتی ممکن است در حین تعریف همان دو تابع dispatch، مقدار type را به اشتباه وارد کنیم و با حالت‌های تعریف شده‌ی در تابع reducer تطابقی نداشته باشد. به همین جهت علاقمندیم تا TypeScript بتواند جلوی یک چنین اشتباهاتی را نیز بگیرد؛ برای این منظور می‌توان از union types استفاده کرد:
type Action = {
   type: "one" | "two" | "three";
};
در اینجا تمام اکشن‌های ممکن و مدنظر را لیست کرده‌ایم که سبب خواهد شد تا اگر مقدار نوع اکشنی به درستی وارد نشود، بلافاصله خطایی را دریافت کنیم:


و یا حتی اگر مقدار action.type ای را در تابع reducer به اشتباه وارد کردیم، باز هم برنامه کامپایل نمی‌شود:



تا اینجا موفق شدیم جلوی ورود actionهای پیش بینی نشده را بگیریم. اما اگر در switch تعریف شده، "case "three را هم قید نکنیم، باز هم برنامه کامپایل می‌شود و خطایی گزارش نخواهد شد. برای این منظور فقط کافی است case default را حذف کنیم. چون action.type دیگر نمی‌تواند مقدار دیگری را بجز موارد ذکر شده‌ی در نوع Action داشته باشد. با حذف case default، اینبار ذکر "case "three یا هر کدام از مقادیر سه‌گانه‌ی مجازی که در اینجا تعریف کردیم، الزامی خواهد بود. در غیراینصورت، همان خطای دریافت خروجی never را از تابع reducer دریافت می‌کنیم که با ذکر هر سه case مجاز، برطرف می‌شود.
با این تغییرات، کدهای نهایی این قسمت به صورت زیر در خواهند آمد:
import React, { useReducer } from "react";

const initialState = { rValue: true };

type State = {
  rValue: boolean;
};

type Action = {
  type: "one" | "two" | "three";
};

function reducer(state: State, action: Action) {
  switch (action.type) {
    case "one":
      return { rValue: true };
    case "two":
      return { rValue: false };
    case "three":
      return { rValue: false };
  }
}

export const ReducerButtons = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      {state?.rValue && <h1>Visible</h1>}
      <button onClick={() => dispatch({ type: "one" })}>Action One</button>
      <button onClick={() => dispatch({ type: "two" })}>Action Two</button>
      <button onClick={() => dispatch({ type: "three" })}>Action Three</button>
    </div>
  );
};
مطالب
React 16x - قسمت 19 - کار با فرم‌ها - بخش 2 - اعتبارسنجی ورودی‌های کاربران
تمام فرم‌های تعریف شده، نیاز به اعتبارسنجی اطلاعات وارد شده‌ی توسط کاربران خود را دارند. ابتدا اعتبارسنجی اطلاعات را در حین ارسال فرم و سپس آن‌را همزمان با ورود اطلاعات، بررسی می‌کنیم.


اصول کلی طراحی یک اعتبارسنج ساده

در قسمت قبل، تمام اطلاعات فرم لاگین را درون شیء account خاصیت state قرار دادیم. در اینجا نیز شبیه به چنین شیءای را برای ذخیره سازی خطاهای اعتبارسنجی فیلدهای فرم، تعریف می‌کنیم:
class LoginForm extends Component {
  state = {
    account: { username: "", password: "" },
    errors: {
      username: "Username is required"
    }
  };
خاصیت errors تعریف شده، یک شیء را باز می‌گرداند که حاوی اطلاعات و خطاهای مرتبط با اعتبارسنجی فیلدهای مشکل دار است. بنابراین نام خواص این شیء، با نام فیلدهای فرم تطابق دارند. کار کردن با یک شیء هم جهت یافتن خطاهای یک فیلد مشخص، ساده‌تر است از کار کردن با یک آرایه؛ از این جهت که نیازی به جستجوی خاصی در این شیء نبوده و با استفاده از روش دسترسی پویای به خواص یک شیء جاوا اسکریپتی مانند errors["username"]، می‌توان خطاهای مرتبط با هر فیلد را به سادگی نمایش داد.
البته در ابتدای کار، خاصیت errors را با یک شیء خالی ({}) مقدار دهی می‌کنیم و سپس در متد مدیریت ارسال فرم به سرور:
  validate = () => {
    return { username: "Username is required." };
  };

  handleSubmit = e => {
    e.preventDefault();

    const errors = this.validate();
    console.log("Validation errors", errors);
    this.setState({ errors });
    if (errors) {
      return;
    }

    // call the server
    console.log("Submitted!");
  };
- ابتدا خروجی متد validate سفارشی را بررسی می‌کنیم که خروجی آن، خطاهای ممکن است.
- اگر خطایی وجود داشت، به مرحله‌ی بعد که ارسال فرم به سمت سرور می‌باشد، نخواهیم رسید و کار را با یک return، خاتمه می‌دهیم.
- علت فراخوانی متد setState در اینجا، درخواست رندر مجدد فرم، با توجه به خطاهای اعتبارسنجی ممکنی است که به خاصیت errors، اضافه یا به روز رسانی کرده‌ایم.
- نمونه‌ای از خروجی متد validate را نیز در اینجا مشاهده می‌کنید که تشکیل شده‌است از یک شیء، که هر خاصیت آن، به نام یک فیلد موجود در فرم، اشاره می‌کند.


پیاده سازی یک اعتبارسنج ساده

در اینجا یک نمونه پیاده سازی ساده و ابتدایی منطق اعتبارسنجی فیلدهای فرم را ملاحظه می‌کنید:
  validate = () => {
    const { account } = this.state;

    const errors = {};
    if (account.username.trim() === "") {
      errors.username = "Username is required.";
    }

    if (account.password.trim() === "") {
      errors.password = "Password is required.";
    }

    return Object.keys(errors).length === 0 ? null : errors;
  };
- ابتدا توسط Object Destructuring، خاصیت account شیء منتسب به خاصیت state کامپوننت را دریافت می‌کنیم، تا مدام نیاز به نوشتن this.state.account نباشد.
- سپس یک شیء خالی error را تعریف کرده‌ایم.
- در ادامه با توجه به اینکه مقادیر المان‌های فرم در state وجود دارند، خالی بودن آن‌ها را بررسی می‌کنیم. اگر خالی بودند، یک خاصیت جدید را با همان نام المان مورد بررسی، به شیء errors اضافه کرده و پیام خطایی را درج می‌کنیم.
- در نهایت این شیء errors و یا نال را (در صورت عدم وجود خطایی) بازگشت می‌دهیم.

برای آزمایش آن، پس از اجرای برنامه، یکبار بدون وارد کردن اطلاعاتی، بر روی دکمه‌ی Login کلیک کنید؛ یکبار هم با وارد کردن اطلاعاتی در فیلدهای مختلف. در این بین کنسول توسعه دهندگان مرورگر را نیز جهت مشاهده‌ی شیء‌های error لاگ شده، بررسی نمائید.



نمایش خطاهای اعتبارسنجی فیلدهای فرم

در قسمت قبل، کامپوننت جدید src\components\common\input.jsx را جهت کاهش کدهای تکراری تعاریف المان‌های ورودی، ایجاد کردیم. در اینجا نیز می‌توان کار نمایش خطاهای اعتبارسنجی را قرار داد:
import React from "react";

const Input = ({ name, label, value, error, 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"
      />
      {error && <div className="alert alert-danger">{error}</div>}
    </div>
  );
};

export default Input;
- در اینجا ابتدا خاصیت error را به لیست خواص مورد انتظار از شیء props، اضافه کرده‌ایم.
- سپس با توجه به نکته‌ی «رندر شرطی عناصر در کامپوننت‌های React» در قسمت 5، اگر error مقداری داشته باشد و به true تفسیر شود، آنگاه به صورت خودکار، div ای که دارای کلاس‌های بوت استرپی اخطار است به همراه متن خطا، رندر خواهد شد؛ در غیراینصورت هیچ div ای به صفحه اضافه نمی‌شود.
- اکنون متد رندر کامپوننت فرم لاگین را به صورت زیر تکمیل می‌کنیم:
  render() {
    const { account, errors } = this.state;
    return (
      <form onSubmit={this.handleSubmit}>
        <Input
          name="username"
          label="Username"
          value={account.username}
          onChange={this.handleChange}
          error={errors.username}
        />
        <Input
          name="password"
          label="Password"
          value={account.password}
          onChange={this.handleChange}
          error={errors.password}
        />
        <button className="btn btn-primary">Login</button>
      </form>
    );
  }
در ابتدای متد رندر، با استفاده از Object Destructuring، خاصیت errors شیء منتسب به خاصیت state کامپوننت را دریافت کرده‌ایم. سپس با استفاده از آن، ویژگی جدید error را که به تعریف کامپوننت Input اضافه کردیم، در دو فیلد username و password، مقدار دهی می‌کنیم.

تا اینجا اگر تغییرات را ذخیره کرده و برنامه را اجرا کنیم، با کلیک بر روی دکمه‌ی Login، خطاهای اعتبارسنجی به صورت زیر ظاهر می‌شوند:


در این حالت اگر هر دو فیلد را تکمیل کرده و بر روی دکمه‌ی لاگین کلیک کنیم، به خطای زیر در کنسول توسعه دهندگان مرورگر می‌رسیم:
loginForm.jsx:55 Uncaught TypeError: Cannot read property 'username' of null
at LoginForm.render (loginForm.jsx:55)
علت اینجاست که چون فرم اعتبارسنجی شده و مشکلی وجود نداشته‌است، خروجی متد validate در این حالت، null است. بنابراین دیگر نمی‌توان به خاصیت برای مثال username آن دسترسی یافت. برای رفع این مشکل در متد handleSubmit، جائیکه errors را در خاصیت state به روز رسانی می‌کنیم، اگر errors نال باشد، بجای آن یک شیء خالی را بازگشت می‌دهیم:
this.setState({ errors: errors || {} });
این قطعه کد، به این معنا است که اگر errors مقدار دهی شده بود، از آن استفاده کن، در غیراینصورت {} (یک شیء خالی جاوا اسکریپتی) را بازگشت بده.


اعتبارسنجی فیلدهای یک فرم در حین ورود اطلاعات در آن‌ها

تا اینجا نحوه‌ی اعتبارسنجی فیلدهای ورودی را در حین submit بررسی کردیم. شبیه به همین روش را به حالت onChange و متد handleChange فرم لاگین که در قسمت قبل تکمیل کردیم نیز می‌توان اعمال کرد:
  handleChange = ({ currentTarget: input }) => {
    const errors = { ...this.state.errors }; //cloning an object
    const errorMessage = this.validateProperty(input);
    if (errorMessage) {
      errors[input.name] = errorMessage;
    } else {
      delete errors[input.name];
    }

    const account = { ...this.state.account }; //cloning an object
    account[input.name] = input.value;

    this.setState({ account, errors });
  };
- ابتدا شیء errors را clone می‌کنیم؛ چون می‌خواهیم خواصی را به آن کم و زیاد کرده و سپس بر اساس آن مجددا state را به روز رسانی کنیم.
- سپس اینبار فقط نیاز داریم اعتبار اطلاعات ورودی یک فیلد را بررسی کنیم و متد validate فعلی، فیلدهای کل فرم را با هم تعیین اعتبار می‌کند. به همین جهت متد جدید validateProperty را به صورت زیر تعریف می‌کنیم. اگر این متد خروجی داشت، خاصیت متناظر با آن‌را در شیء errors به روز رسانی می‌کنیم؛ در غیراینصورت این خاصیت را از شیء errors حذف می‌کنیم تا پیام اشتباهی را نمایش ندهد. در نهایت توسط متد setState، مقدار خاصیت errors را با شیء errors جاری به روز رسانی می‌کنیم:
  validateProperty = ({ name, value }) => {
    if (name === "username") {
      if (value.trim() === "") {
        return "Username is required.";
      }
      // ...
    }

    if (name === "password") {
      if (value.trim() === "") {
        return "Password is required.";
      }
      // ...
    }
  };
در متد validateProperty، خواص name و value از شیء input ارسالی به آن استخراج شده‌اند و سپس بر اساس آن‌ها کار اعتبارسنجی صورت می‌گیرد.

پس از ذخیره سازی این تغییرات، برای آزمایش آن، یکبار حرف a را بجای username وارد کنید و سپس آن‌را حذف کنید. بلافاصله پیام خطای مرتبطی نمایش داده خواهد شد و اگر مجددا عبارتی را وارد کنیم، این پیام محو می‌شود که معادل قسمت delete در کدهای فوق است.


معرفی Joi


تا اینجا، هدف نمایش ساختار یک اعتبارسنج ساده بود. این روش مقیاس پذیر نیست و در ادامه آن‌را با یک کتابخانه‌ی اعتبارسنجی بسیار پیشرفته به نام Joi، جایگزین خواهیم کرد که نمونه مثال‌های آن‌را در اینجا می‌توانید مشاهده کنید:
const Joi = require('@hapi/joi');
const schema = Joi.object({
    username: Joi.string().alphanum().min(3).max(30).required(),
    password: Joi.string().pattern(/^[a-zA-Z0-9]{3,30}$/),
    email: Joi.string().email({ minDomainSegments: 2, tlds: { allow: ['com', 'net'] } })
});
schema.validate({ username: 'abc', birth_year: 1994 });
ایده‌ی اصلی Joi، تعریف یک اسکیما برای object جاوا اسکریپتی خود است. در این اسکیما، تمام خواص شیء مدنظر ذکر شده و سپس توسط fluent api آن، نیازمندی‌های اعتبارسنجی هرکدام ذکر می‌شوند. برای مثال username باید رشته‌ای بوده، تنها از حروف و اعداد تشکیل شود. حداقل طول آن، 3 و حداکثر طول آن، 30 باشد و همچنین ورود آن نیز اجباری است. با استفاده از pattern آن می‌توان عبارات باقاعده را ذکر کرد و یا با متدهایی مانند email، از قالب خاص مقدار یک خاصیت، اطمینان حاصل کرد.

برای نصب آن، پس از باز کردن پوشه‌ی اصلی برنامه توسط VSCode، دکمه‌های ctrl+` را فشرده (ctrl+back-tick) و دستورات زیر را در ترمینال ظاهر شده وارد کنید:
> npm install @hapi/joi --save
> npm i --save-dev @types/hapi__joi
که در نهایت سبب نصب کتابخانه‌ی node_modules\@hapi\joi\dist\joi-browser.min.js خواهند شد و همچنین TypeScript definitions آن‌را نیز نصب می‌کنند که بلافاصله سبب فعالسازی intellisense مخصوص آن در VSCode خواهد شد. بدون نصب types آن، پس از تایپ Joi.، از مزایای تکمیل خودکار fluent api آن توسط VSCode، برخوردار نخواهیم بود.

سپس به کامپوننت فرم لاگین مراجعه کرده و در ابتدای آن، Joi را import می‌کنیم:
import Joi from "@hapi/joi";
پس از آن، اسکیمای شیء account را تعریف خواهیم کرد. اسکیما نیازی نیست جزئی از state باشد؛ چون قرار نیست تغییر کند. به همین جهت آن‌را به صورت یک خاصیت جدید در سطح کلاس کامپوننت تعریف می‌کنیم:
  schema = {
    username: Joi.string()
      .required()
      .label("Username"),
    password: Joi.string()
      .required()
      .label("Password")
  };
خاصیت اسکیما را با یک شیء با ساختار از نوع Joi.object، که خواص آن، با خواص شیء account مرتبط با فیلدهای فرم لاگین، تطابق دارد، تکمیل می‌کنیم. مقدار هر خاصیت نیز با Joi. شروع شده و سپس نوع و محدودیت‌های مدنظر اعتبارسنجی را می‌توان تعریف کرد که در اینجا هر دو مورد باید رشته‌ای بوده و به صورت اجباری وارد شوند. توسط متد label، برچسب نام خاصیت درج شده‌ی در پیام خطای نهایی را می‌توان تنظیم کرد. اگر از این متد استفاده نشود، از همان نام خاصیت ذکر شده استفاده می‌کند.
سپس ابتدای متد validate قبلی را به صورت زیر بازنویسی می‌کنیم:
  validate = () => {
    const { account } = this.state;
    const validationResult = Joi.object(this.schema).validate(account, {
      abortEarly: false
    });
    console.log("validationResult", validationResult);
ابتدا مقدار خاصیت account، از شیء state استخراج شده‌است که حاوی شیءای با اطلاعات نام کاربری و کلمه‌ی عبور است. سپس این شیء را به متد validate خاصیت اسکیمایی که تعریف کردیم، ارسال می‌کنیم. Joi، اعتبارسنجی را به محض یافتن خطایی، متوقف می‌کند. به همین جهت تنظیم abortEarly آن به false صورت گرفته‌است تا تمام خطاهای اعتبارسنجی را نمایش دهد.
اکنون اگر برنامه را اجرا کرده و بدون ورود اطلاعاتی، بر روی دکمه‌ی لاگین کلیک کنیم، خروجی زیر در کنسول توسعه دهندگان مرورگر ظاهر می‌شود:


همانطور که مشاهده می‌کنید، خروجی Joi، یک شیء است که اگر دارای خاصیت error بود، یعنی خطای اعتبارسنجی رخ‌داده‌است. سپس باید خاصیت آرایه‌ای details این شیء error را جهت یافتن خواص مشکل دار بررسی کرد. هر خاصیت در اینجا با path مشخص می‌شود. بنابراین قدم بعدی، تبدیل این ساختار، به ساختار شیء errors موجود در state کامپوننت جاری است تا مابقی برنامه بتواند بدون تغییری از آن استفاده کند.


نگاشت شیء دریافتی از Joi، به شیء errors موجود در state کامپوننت لاگین

خاصیت error شیء دریافتی از متد validate کتابخانه‌ی Joi، تنها زمانی ظاهر می‌شود که خطایی وجود داشته باشد. همچنین خاصیت details آن نیز آرایه‌ا‌ی از اشیاء با خواص message و path است. این path نیز یک آرایه است که اولین المان آن، نام خاصیت در حال بررسی است. اکنون می‌خواهیم این آرایه را تبدیل به یک شیء قابل درک برای برنامه کنیم:
  validate = () => {
    const { account } = this.state;
    const validationResult = Joi.object(this.schema).validate(account, {
      abortEarly: false
    });
    console.log("validationResult", validationResult);

    if (!validationResult.error) {
      return null;
    }

    const errors = {};
    for (let item of validationResult.error.details) {
      errors[item.path[0]] = item.message;
    }
    return errors;
  };
ابتدا بررسی می‌کنیم که آیا خاصیت error، تنظیم شده‌است یا خیر؟ اگر خیر، کار این متد به پایان می‌رسد. سپس حلقه‌ای را بر روی آرایه‌ی details، تشکیل می‌دهیم تا شیء errors مدنظر ما را با خاصیت دریافتی از path و پیام دریافتی متناظری تکمیل کند. در آخر این شیء errors با ساختار مدنظر خود را بازگشت می‌دهیم.

اکنون اگر تغییرات را ذخیره کرده و برنامه را اجرا کنیم، همانند قبل می‌توان خطاهای اعتبارسنجی در حین submit را مشاهده کرد:



بازنویسی متد validateProperty توسط Joi

تا اینجا متد validate ساده و ابتدایی خود را با استفاده از امکانات کتابخانه‌ی Joi، بازنویسی کردیم. اکنون نوبت بازنویسی متد اعتبارسنجی در حین تایپ اطلاعات است:
  validateProperty = ({ name, value }) => {
    const userInputObject = { [name]: value };
    const propertySchema = Joi.object({ [name]: this.schema[name] });
    const { error } = propertySchema.validate(userInputObject, {
      abortEarly: true
    });
    return error ? error.details[0].message : null;
  };
تفاوت این متد با متد validate، در اعتبارسنجی تنها یک خاصیت از شیء account موجود در state است؛ به همین جهت نمی‌توان کل this.state.account را به متد validate کتابخانه‌ی Joi ارسال کرد. بنابراین نیاز است بر اساس name و value رسیده‌ی از کاربر، یک شیء جدید را به صورت پویا تولید کرد. در اینجا روش تعریف { [name]: value } به computed properties معرفی شده‌ی در ES6 اشاره می‌کند. اگر در حین تعریف یک شیء، برای مثال بنویسیم {"username:"value}، این username به صورت "username" ثابتی تفسیر می‌شود و پویا نیست. اما در ES6 می‌توان با استفاده از [] ها، تعریف نام یک خاصیت را پویا کرد که نمونه‌ای از آن‌را در userInputObject و همچنین propertySchema مشاهده می‌کنید.
تا اینجا بجای this.state.account که به کل فرم اشاره می‌کند، شیء اختصاصی‌تر userInputObject را ایجاد کرده‌ایم که معادل اطلاعات فیلد ورودی کاربر است. یک چنین نکته‌ای را در مورد schema نیز باید رعایت کرد. در اینجا نمی‌توان اعتبارسنجی را با this.schema شروع کرد؛ چون این شیء نیز به اطلاعات کل فرم اشاره می‌کند و نه تک فیلدی که هم اکنون در حال کار با آن هستیم. بنابراین نیاز است یک Joi.object جدید را بر اساس name رسیده، از this.schema کلی، استخراج و تولید کرد؛ مانند propertySchema که مشاهده می‌کنید.
اکنون می‌توان متد validate این شیء اسکیمای جدید را فراخوانی کرد و خاصیت error شیء حاصل از آن‌را توسط Object Destructuring، استخراج نمود. در اینجا abortEarly به true تنظیم شده‌است (البته حالت پیش‌فرض آن است و نیازی به ذکر صریح آن نیست). علت اینجا است که نمایش خطاهای بیش از اندازه به کاربر، برای او گیج کننده خواهند بود؛ به همین جهت هربار، اولین خطای حاصل را به او نمایش می‌دهیم.


غیرفعالسازی دکمه‌ی submit در صورت شکست اعتبارسنجی‌های فرم

در ادامه می‌خواهیم دکمه‌ی submit فرم لاگین، تا زمانیکه اعتبارسنجی آن با موفقیت به پایان برسد، غیرفعال باشد:
 <button disabled={this.validate()} className="btn btn-primary">
 Login
</button>
ویژگی disabled را اگر به true تنظیم کنیم، این دکمه را غیرفعال می‌کند. در اینجا متد validate تعریف شده، نال و یا یک شیء را بازگشت می‌دهد. اگر نال را بازگشت دهد، در جاوااسکریپت به false تفسیر شده و دکمه را فعال می‌کند و برعکس؛ مانند تصویر حاصل زیر:


فراخوانی setState‌های تعریف شده‌ی در متدهای رویدادگردان این فرم هستند که سبب رندر مجدد کامپوننت شده و در این بین در متد رندر، کار بررسی مجدد مقدار نهایی متد validate صورت می‌گیرد تا بر اساس آن، فعال یا غیرفعال بودن دکمه‌ی Login، مشخص شود.

کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-19.zip
مطالب
ایجاد alert,confirm,prompt هایی متفاوت با jQuery Impromptu
alert,confirm,prompt سه متد توکار JavaScript هستند که برای نمایش پیغام ، دریافت تایید و دریافت مقدار از کاربر هستند .
گرافیک این پیغام‌ها هم وابسته به مرورگر هستند و قابل تغییر نیستند . متن عنوان و دکمه‌ها هم با توجه به زبان سیستم عامل تعیین می‌شوند و قابل تغییر توسط برنامه نویس نیستند.



حال مواقعی پیش می‌آید که نیاز داریم پیغام هایی با گرافیک و عبارات متفاوت نمایش دهیم . برای رفع این نیاز می‌توانیم از پلاگین jQuery Impromptu استفاده کنیم . البته این پلاگین قابلیت‌های دیگری هم دارد که در این مقاله با آنها آشنا می‌شویم .




از ویژگی‌های این پلاگین می‌توان حجم کم (حدود 11 کیلوبایت) و قدرت شخصی سازی بالا اشاره کرد .

طرز استفاده به این شکل است :

$.prompt( msg , options )


msg می‌تواند یک string یا یک شئ از states باشد . string ارسال شده می‌تواند شامل کدهای html باشد .

یک شئ states هم شامل مجموعه ای از وضعیت‌های prompt است . برای مثال می‌توان یک prompt ایجاد کرد که مثل یک Wizard شامل چند مرحله باشد .

options هم تنظیماتی است که می‌توان مشخص کرد . تنظیماتی مثل : prefix,classes,persistent,timeout,...


msg :


گفتیم شئ states شامل وضعیت‌های مختلف prompt است . هر وضعیت ( state ) می‌تواند شامل بخش‌های زیر باشد :

  • html : مقدار Html وضعیت

  • buttons : یک شئ شامل متن و مقدار دکمه هایی که کاربر می‌تواند کلیک کند

  • focus : ایندکس دکمه‌ی focus شده در وضعیت

  • submit : تابعی که زمانی که یکی از دکمه‌های وضعیت انتخاب شود فراخوانی می‌شود .

    اگر در این تابع false بازگشت داده شود یا متد preventDefault از event فراخوانی شود ، prompt باز می‌ماند . ( روشی برای جلوگیری از بسته شدن prompt هنگام تغییر state یا اعتبار سنجی فرم )
    همچنین شئ event شامل state ( المنت state ) و stateName ( نام state ) می‌باشد .

    پیشفرض :
    function(event, value, message, formVals){}
    value مقدار دکمه ای است که بروی آن کلیک شده ، message مقدار html تعریف شده برای state است ، formVals هم در صورتی که در html تعریف شده برای state ، المنت‌های فرم وجود داشته باشد ، شامل نام/مقادیر آنها می‌باشد . ( برای دریافت مقادیر فرم ، باید از نام المنت استفاده نمایید . )

  • position : مشخص کننده‌ی موقعیت state که شامل موارد زیر است :

    position: { container: '#container', x: 0, y: 0, width: 0, arrow: 'lm' }

    container : ‫selector المنتی است که state باید در آن مکان قرار بگیرد .
    x/y : موقعیت نسبی prompt نسبت به container
    arrow : جهت نمایش فلش prompt است که می‌تواند یکی از این مقادیر باشد : tl, tc, tr, rt, tm, tb, br, bc, bl, lb, lm, lt.



نحوه تعریف یک wizard ساده با شئ states :
var tourSubmitFunc = function (e, v, m, f) {
    if (v === -1) {
        $.prompt.prevState();
        return false;
    } else if (v === 1) {
        $.prompt.nextState();
        return false;
    }
};

var states =
    {
        state0:
            {
                html: "State1",
                buttons: { Next: 1 },
                //position: { container: '#container', x: 10, y: 0, width: 350, arrow: 'lm' },
                submit: tourSubmitFunc
            },
        state1:
            {
                html: "State2",
                buttons: { Prev: -1, Next: 1 },
                submit: tourSubmitFunc
            },
        state2:
            {
                html: "State3",
                buttons: { Prev: -1, Done: 0 },
                submit: tourSubmitFunc
            }
    };

$.prompt(states);


تا به اینجا با پارامتر اول prompt آشنا شدیم و فهمیدیم که می‌توانیم یک رشته یا یک شئ states به عنوان message به prompt ارسال کنیم .

options :

اکنون با optionهای prompt ( پارامتر دوم ) آشنا خواهیم شد .

توجه کنید که زمانی که یک رشته به prompt ارسال کنید ، مقادیر buttons,focus,submit از این تنظیمات دریافت می‌شود .
به عبارت دیگر ، زمانی که یک شئ states به prompt ارسال کنید ، از مقادیر فوق که در تنظیمات است ، استفاده نمی‌شود .


  • loaded
    یک تابع که زمانی که prompt کامل بارگزاری شده فراخوانی می‌شود .
    $.prompt("Message",
        {
            loaded: function() {
                alert("Prompt Loaded !");
            }
        });
  • submit
    یک تابع که زمانی که یکی از دکمه‌های state کلیک شود ، فراخوانی می‌شود .
    ( زمانی اتفاق میوفتد که یک رشته به عنوان متن به prompt  ارسال کرده باشید و زمانی که یک شئ از states ارسال می‌کنید ، هنگام کلیک دکمه‌های آنها ، این تابع فراخوانی نمی‌شود . )
    پیشفرض :
    function(event){}
  • statechanging
    یک تابع که زمانی که یک state در حال تعویض شدن هست فراخوانی می‌شود .
    پیشفرض :
    function(event, fromStateName, toStateName){}
    برای لغو تغییر state ، مقدار return false کنید یا متد preventDefault از event را فراخوانی کنید .

  • statechanged
    یک تابع که زمانی که یک state در حال تعویض شدن هست فراخوانی می‌شود .
    پیشفرض :
    function(event, toStateName){}
  • callback
    یک تابع که زمانی که ( یکی از دکمه‌های prompt کلیک شود و ) prompt بسته شود ، فراخوانی می‌شود .
    پیشفرض :
    function(event[, value, message, formVals]){}
    سه پارامتر آخر تنها زمانی که یک دکمه‌ی prompt کلیک شده باشد موجود هستند .

  • buttons
    یک شئ شامل مجموعه ای از دکمه‌ها .
    پیشفرض :
    { Ok : true }
    شکل دیگر تعریف دکمه به این شکل است :
    [
        {title: 'Hello World',value:true},
        {title: 'Good Bye',value:false}
    ]
  • prefix
    یک پیشوند برای همه css class‌ها و id‌های المنت‌های html که توسط prompt ایجاد می‌شود .
    پیشفرض : jqi

  • classes
    یک css class که به بالاترین سطح prompt داده می‌شود .
    در حالت پیشفرض مقداری ندارد .

  • focus
    ایندکس دکمه‌ی focus شده
    پیشفرض : 0

  • zIndex
    zIndex اعمال شده بروی prompt .
    پیشفرض : 999

  • useiframe
    استفاده از یک iframe برای overlay در IE6
    پیشفرض : false

  • top
    فاصله‌ی prompt از بالای صفحه
    پیشفرض : ‭15%

  • opacity
    میزان شفافیت لایه‌ی ای که صفحه را پوشانده است .
    پیشفرض : 0.6

  • overlayspeed
    سرعت نمایش افکت fadeIn , fadeOut لایه‌ی پوشاننده .
    مقادیر قابل قبول : ‭"slow", "fast", number(milliseconds)
    پیشفرض : "slow"

  • promptspeed
    سرعت نمایش prompt .
    مقادیر قابل قبول : ‭"slow", "fast", number(milliseconds)
    پیشفرض : "fast"

  • show
    نام یک متد jQuery برای animate کردن نمایش prompt .
    مقادیر قابل قبول : "show","fadeIn","slideDown", ...
    پیشفرض : "promptDropIn"

  • persistent
    بسته شدن prompt زمانی که بروی لایه‌ی fade کلیک شود .
    false : بسته شدن
    پیشفرض : true

  • timeout
    مدت زمانی که پس از آن ، prompt بصورت خودکار بسته می‌شود . ( milliseconds )
    پیشفرض : 0


returns
:

مقدار بازگشتی متد prompt ، یک شئ jQuery ، شامل همه‌ی محتویات تولید شده توسط prompt است .


Events using Bind
:
استفاده از Eventها با بایند کردن

  • promptloaded
    معادل loaded در optionها .

  • promptsubmit
    هنگام submit شدن ( کلیک شدن یکی از دکمه‌های ) state فعال می‌شود .

  • promptclose
    معادل callback در optionها .

  • promptstatechanging
    معادل statechanging در optionها .

  • promptstatechanged
    معادل statechanged در optionها .

ظرز بایند کردن یک event به شئ prompt :
var myPrompt = $.prompt( msg , options );
myPrompt.bind('promptloaded', function(e){});


Helper Functions :

  • ‪jQuery.prompt.setDefaults(options)
    تعیین مقادیر پیشفرض برای همه‌ی prompt‌ها .
    jQuery.prompt.setDefaults({
    prefix: 'myPrompt',
    show: 'slideDown'
    });
  • ‪jQuery.prompt.setStateDefaults(options)
    تعیین مقادیر پیشفرض برای stateها .
    jQuery.prompt.setStateDefaults({
    buttons: { Ok:true, Cancel:false },
    focus: 1
    });
  • ‪jQuery.prompt.getCurrentState()
    یک شئ jQuery از state جاری برمی‌گرداند .

  • ‪jQuery.prompt.getCurrentStateName()
    نام state جاری را برمی‌گرداند .

  • ‪jQuery.prompt.getStateContent(stateName)
    یک شئ jQuery از state مشخص شده برمی‌گرداند .

  • ‪jQuery.prompt.goToState(stateName, callback)
    state مشخص شده را در prompt نمایش می‌دهد .
    callback تابعی است که بعد از تغییر state فراخوانی می‌شود .

  • ‪jQuery.prompt.nextState(callback)
    prompt را به state بعدی منتقل می‌کند .

  • ‪jQuery.prompt.prevState(callback)
    prompt را به state قبلی منتقل می‌کند .

  • ‪jQuery.prompt.close()
    prompt را می‌بندد .


حال که با این پلاگین آشنا شدیم ، سه دستور جاوا اسکریپتی که در ابتدای مقاله ذکر کردیم را با این پلاگین پیاده سازی می‌کنیم .

  • alert
    $.prompt("یک پیام تستی",
        {
            prefix: 'dnt',
            buttons: { 'تایید': true }
        });

    نتیجه :



  • confirm
    $.prompt("درخواست تایید - موافقید ؟",
        {
            prefix: 'dnt',
            buttons: { 'تایید': true, 'انصراف': false }
        });

    نتیجه :



  • prompt
    یک فرم html مخفی برای نمایش در prompt :

    <div class="prompt-content" style="display: none;">
        <span>نام خود را وارد نمایید : </span>
        <span>
            <input type="text" name="name" />
        </span>
    </div>

       
    نمایش prompt :

    $.prompt(
        $(".prompt-content").html(),
        {
            prefix: 'dnt',
            buttons: { 'تایید': true, 'انصراف': false }
        });

    نتیجه :



در این سه مثال آخر ، از یک css متفاوت استفاده کردیم . این پلاگین یک سری کلاس دارد که نام این کلاس‌ها از ترکیب مقدار prefix که در option مشخص کردیم حاصل می‌شود .
برای مثال اگر مقدار prefix را برابر با dnt قرار بدهیم ، برای استایل دهی متن پیام باید از کلاس div.dnt .dntmessage استفاده کنید .
همانطور که در سه مثال آخر مشاهده کردید ، تغییری در استایل prompt داشتیم که با تغییر دادن استایل‌های مورد نظر انجام شد .
برای تهیه‌ی یک قالب سفارشی باید selector المنت هایی که نیاز به تغییر دارند را از قالب پیشفرض پیدا کنید ، سپس قسمت prefix از selector را به نام قالب خودتان تغییر بدید و استایل را ویرایش کنید . سپس هنگام ایجاد prompt ، مقدار prefix را برابر نام قالب قرار دهید .

مثلا می‌خواهید یک قالب به نام dnt داشته باشید . می‌خواهید متن قالب راست به چپ باشد .

div.dnt .dntmessage {
     color: #444444;
     line-height: 20px;
     padding: 10px;
/*       Edited      */
     direction:rtl;
     text-align:right;
}


البته راه بهتری هم هست که نیاز به آشنایی با فایرباگ دارد . در این روش ابتدا کل قالب jqi ( موجود در قالب پیشفرض ) را در برنامه خود کپی می‌کنیم ، مقادیر jqi را با نام قالب جایگزین می‌کنیم ، مقدار prefix را در prompt برابر با نام قالب قرار می‌هیم .
اکنون در FireFox یک prompt ایجاد می‌کنیم و توسط فایرباگ استایل هایی که با نام قالب بروی prompt اعمال شده‌اند را مطابق سلیقه تغییر می‌دهیم . در مرحله آخر به تب CSS در فایرباگ می‌رویم و کل استایل‌های مربوط به قالب را کپی و جایگزین استایل قبلی در برنامه می‌کنیم .

قالبی که بنده برای سه دستور فوق استفاده کردم ( dnt ) ، به این شکل است :
/*    Start : DotNetTips Theme     */

.dntfade {
     background-color: #AAAAAA;
     position: absolute;
}

div.dnt {
     background-color: #FFFFFF;
     border-radius: 10px 10px 10px 10px;
     border: 1px solid #FFFFFF;
     box-shadow: 0px 0px 10px 1px #6D6D6C;
     font-family: tahoma;
     font-size: 11px;
     padding: 7px;
     position: absolute;
     text-align: left;
     width: 400px;
}

div.dnt .dntcontainer {
     font-size: small;
}

div.dnt .dntclose {
     color: #BBBBBB;
     cursor: pointer;
     font-weight: bold;
     position: absolute;
     top: 4px;
     width: 18px;
}

div.dnt .dntmessage {
     color: #444444;
     line-height: 20px;
     padding: 10px;
}

div.dnt .dntbuttons {
     background-color: #F4F4F4;
     border: 1px solid #EEEEEE;
     padding: 5px 0px;
     text-align: right;
}

div.dnt button {
     background-color: #2F6073;
     border: 1px solid #F4F4F4;
     color: #FFFFFF;
     font-size: 12px;
     font-weight: bold;
     margin: 0px 10px;
     padding: 3px 10px;
}

div.dnt button:hover {
     background-color: #728A8C;
}

div.dnt button.dntdefaultbutton {
     background-color: #0099CC;
}

.dnt_state {
     direction: rtl;
     text-align: right;
}

.dnt_state button {
     font-family: tahoma;
}

.dntwarning .dnt .dntbuttons {
     background-color: #CCDDFF;
}

.dnt .dntarrow {
     border: 10px solid transparent;
     font-size: 0px;
     height: 0px;
     line-height: 0;
     position: absolute;
     width: 0px;
}

.dnt .dntarrowtl {
     border-bottom-color: #FFFFFF;
     left: 10px;
     top: -20px;
}

.dnt .dntarrowtc {
     border-bottom-color: #FFFFFF;
     left: 50%;
     margin-left: -10px;
     top: -20px;
}

.dnt .dntarrowtr {
     border-bottom-color: #FFFFFF;
     right: 10px;
     top: -20px;
}

.dnt .dntarrowbl {
     border-top-color: #FFFFFF;
     bottom: -20px;
     left: 10px;
}

.dnt .dntarrowbc {
     border-top-color: #FFFFFF;
     bottom: -20px;
     left: 50%;
     margin-left: -10px;
}

.dnt .dntarrowbr {
     border-top-color: #FFFFFF;
     bottom: -20px;
     right: 10px;
}

.dnt .dntarrowlt {
     border-right-color: #FFFFFF;
     left: -20px;
     top: 10px;
}

.dnt .dntarrowlm {
     border-right-color: #FFFFFF;
     left: -20px;
     margin-top: -10px;
     top: 50%;
}

.dnt .dntarrowlb {
     border-right-color: #FFFFFF;
     bottom: 10px;
     left: -20px;
}

.dnt .dntarrowrt {
     border-left-color: #FFFFFF;
     right: -20px;
     top: 10px;
}

.dnt .dntarrowrm {
     border-left-color: #FFFFFF;
     margin-top: -10px;
     right: -20px;
     top: 50%;
}

.dnt .dntarrowrb {
     border-left-color: #FFFFFF;
     bottom: 10px;
     right: -20px;
}

/*    End : DotNetTips Theme     */


برای مشاهده‌ی مثال‌های بیشتر به صفحه‌ی اصلی jQuery Impromptu مراجعه نمایید .
مطالب
PowerShell 7.x - قسمت پنجم - اسکریپت بلاک و توابع
همانطور که در قسمت قبل اشاره شد، توابع نیز یکی از ویژگی‌های اصلی PowerShell هستند. قبل از بررسی بیشتر توابع بهتر است ابتدا با مفهوم script block آشنا شویم. script blocks به مجموعه‌ایی از دستورات گفته میشود که داخل یک بلاک قرار میگیرند. در واقع هر چیزی داخل {} یک script block محسوب میشود (البته به جز hash tables). به عنوان مثال در کد زیر از یک script block مخصوص، با نام فیلتر استفاده شده است که یک ورودی برای پارامتر FilterScript مربوط به دستور Where-Object میباشد. چیزی که این script block را متمایز میکند، خروجی آن است. به این معنا که خروجی آن باید یک مقدار بولین باشد: 
Get-Process | Where-Object { $_.Name -eq 'Dropbox' }
script blocks را به صورت مستقیم درون command line هم میتوانیم استفاده کنیم. به محض تایپ کردن } و زدن کلید enter، امکان نوشتن اسکریپت‌های چندخطی را درون ترمینال خواهیم داشت. در نهایت با بستن script block و زدن کلید enter، از بلاک خارج خواهیم شد: 
PS /Users/sirwanafifi/Desktop> $block = {
>> $newVar = 10
>> Write-Host $newVar
>> }
با اینکار یک بلاک از کد را داخل متغیری با اسم block ذخیره کرده‌ایم. برای فراخوانی این قطعه کد میتوانیم از یک عملگر مخصوص با نام invocation operator یا call operator استفاده کنیم: 
PS /Users/sirwanafifi/Desktop> & $block
یا حتی میتوانیم از Invoke-Command نیز برای اجرای بلاک استفاده کنیم. همچنین از عملگر & برای فراخوانی یک expression رشته‌ایی نیز میتوان استفاده کرد: 
PS /Users/sirwanafifi/Desktop> & "Get-Process"
البته این نکته را در نظر داشته باشید که & قادر به پارز کردن (parse) یک expression نیست. به عنوان مثال اجرای کد زیر با خطا مواجه خواهد شد (برای حل این مشکل میتوانید بجای آن از Invoke-Expression استفاده کنید که امکان پارز کردن پارامترها را نیز دارد):
PS /Users/sirwanafifi/Desktop> & "1 + 1"
or
PS /Users/sirwanafifi/Desktop> & "Get-Process -Name Slack"

توابع
در قسمت قبل با نحوه ایجاد توابع آشنا شدیم. به این نوع توابع، basic functions گفته میشود و ساده‌ترین نوع توابع در PowerShell هستند. همچنین خیلی محدود نیز میباشند؛ یکسری ورودی/خروجی دارند. برای کنترل بیشتر روی نحوه فراخوانی توابع (به عنوان مثال دریافت ورودی از pipeline و…) باید از advanced functions یا توابع پیشرفته استفاده کنیم. در واقع به محض استفاده از اتریبیوتی با نام [()CmdletBinding] تابع ما تبدیل به یک advanced function خواهد شد. منظور از دریافت ورودی از pipeline این است که بتوانیم خروجی دستورات را به تابع‌مان pipe کنیم اینکار در basic function امکانپذیر نیست: 
Function Add-Something {
    Write-Host "$_ World"
}

"Hello" | Add-Something
اما با کمک advanced functions میتوانیم چنین قابلیتی را داشته باشیم: 
Function Add-Something {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline = $true)]
        [string]$Name
    )

    Write-Host "$Name World"
}

"Hello" | Add-Something
یکی دیگر از ویژگی‌های advanced functions امکان استفاده فلگ Verbose حین فراخوانی دستورات میباشد. به عنوان مثال قطعه کد زیر را در نظر بگیرید: 
$API_KEY = "...."

Function Read-WeatherData {
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline = $true)]
        [string]$CityName
    )

    $Url = "https://api.openweathermap.org/data/2.5/forecast?q=$CityName&cnt=40&appid=$API_KEY&units=metric"
    Try {
        Write-Verbose "Reading weather data for $CityName"
        $Response = Invoke-RestMethod -Uri $Url
        $Response.list | ForEach-Object {
            Write-Verbose "Processing $($_.dt_txt)"
            [PSCustomObject]@{
                City               = $Response.city.name
                DateTime           = [DateTime]::Parse($_.dt_txt)
                Temperature        = $_.main.temp
                Humidity           = $_.main.humidity
                Pressure           = $_.main.pressure
                WindSpeed          = $_.wind.speed
                WindDirection      = $_.wind.deg
                Cloudiness         = $_.clouds.all
                Weather            = $_.weather.main
                WeatherDescription = $_.weather.description
            }
        } | Where-Object { $_.DateTime.Date -eq (Get-Date).Date }
        Write-Verbose "Done processing $CityName"
    }
    Catch {
        Write-Error $_.Exception.Message
    }
}
کاری که تابع فوق انجام میدهد، دریافت دیتای پیش‌بینی وضعیت آب‌وهوای یک شهر است. در حالت عادی فراخوانی تابع فوق پیام‌های Verbose را نمایش نمیدهد. از آنجائیکه تابع فوق یک advanced function است، میتوانیم فلگ Verbose را نیز وارد کنیم. با اینکار به صورت صریح گفته‌ایم که پیام‌های از نوع Verbose را نیز نمایش دهد: 
Read-WeatherData -CityName "London" -Verbose
هر چند این مقدار را همانطور که در قسمت‌های قبلی عنوان شد میتوانیم تغییر دهیم که دیگر مجبور نباشیم با فراخوانی هر تابع، این فلگ را نیز ارسال کنیم. بیشتر دستورات native نیز قابلیت نمایش پیام‌های Verbose را با ارسال همین فلگ در اختیارمان قرار میدهند. بنابراین بهتر است برای امکان مشاهده جزئیات بیشتر حین فراخوانی توابع‌مان از Write-Verbose استفاده کنیم. در ادامه اجزای دیگر توابع را بررسی خواهیم کرد (بیشتر این اجزا درون یک script block نیز قابل استفاده هستند)

کنترل کامل بر روی ورودی‌های توابع
بر روی ورودی‌های یک تابع میتوانیم کنترل نسبتاً کاملی داشتیم باشیم. PowerShell یک مجموعه وسیع از قابلیت‌ها را برای هندل کردن پارامترها و همچنین اعتبارسنجی ورودی‌ها ارائه میدهد. به عنوان مثال میتوانیم یک پارامتر را mandatory کنیم یا اینکه امکان positional binding و غیره را تعیین کنیم. اتریبیوت Parameter در واقع یک وهله از System.Management.Automation.ParameterAttribute میباشد. میتوانید با نوشتن دستور زیر لیستی از خواصی را که میتوانید همراه با این اتریبیوت تعیین کنید، مشاهده کنید: 
PS /> [Parameter]::new()

ExperimentName                  :
ExperimentAction                : None
Position                        : -2147483648
ParameterSetName                : __AllParameterSets
Mandatory                       : False
ValueFromPipeline               : False
ValueFromPipelineByPropertyName : False
ValueFromRemainingArguments     : False
HelpMessage                     :
HelpMessageBaseName             :
HelpMessageResourceId           :
DontShow                        : False
TypeId                          : System.Management.Automation.ParameterAttribute
در ادامه یک مثال از نحوه هندل کردن ورودی‌های یک تابع را بررسی خواهیم کرد. تابع زیر یک لیست از URLها را از کاربر دریافت کرده و یک health check توسط دستور Test-Connection انجام میدهد. در کد زیر پارامتر Websites را با تعدادی اتریبیوت مزین کرده‌ایم. توسط اتریبیوت Parameter تعیین کرده‌ایم که ورودی الزامی است و همچنین مقدار آن میتواند از pipeline نیز دریافت شود. در ادامه توسط ValidatePattern یک عبارت باقاعده را برای بررسی صحیح بودن URL دریافتی نوشته‌ایم. از آنجائیکه ورودی از نوع آرایه‌ایی از string تعریف شده است، این تست برای هر آیتم از آرایه بررسی خواهد شد. برای پارامتر دوم یعنی Count نیز رنج مقداری را که کاربر وارد میکند، حداقل ۳ و حداکثر ۳ انتخاب کرده‌ایم: 
Function Ping-Website {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidatePattern('^www\..*')]
        [string[]]$Websites,
        [ValidateRange(1, 3)]
        [int]$Count = 3
    )
    $Results = @()
    $Websites | ForEach-Object {
        $Website = $_
        $Result = Test-Connection -ComputerName $Website -Count $Count -Quiet
        $ResultText = $Result ? 'Success' : 'Failed'
        $Results += @{
            Website = $Website
            Result  = $ResultText
        }
        Write-Verbose "The result of pinging $Website is $ResultText"
    }
    $Results | ForEach-Object { 
        $_ | Select-Object @{ Name = "Website"; Expression = { $_.Website }; }, @{ Name = "Result"; Expression = { $_.Result }; }, @{ Name = "Number Of Attempts"; Expression = { $Count }; } 
    }
}
یکی دیگر از اعتبارسنجی‌هایی که میتوانیم برای پارامترهای یک تابع انتخاب کنیم، ValidateScript است. توسط این اتریبیوت میتوانیم یک منطق سفارشی برای اعتبارسنجی مقادیر پارامترها بنویسیم. به عنوان مثال تابع فوق را به گونه‌ایی تغییر خواهیم داد که لیست وب‌سایت‌ها را از طریق یک فایل JSON دریافت کند. میخواهیم قبل از دریافت فایل مطمئن شویم که فایل، به صورت فیزیکی روی دیسک وجود دارد، در غیراینصورت باید یک خطا را به کاربر نمایش دهیم: 
Function Ping-Website {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [ValidateScript({
                If (-Not ($_ | Test-Path) ) {
                    Throw "File or folder does not exist" 
                }
                If (-Not ($_ | Test-Path -PathType Leaf) ) {
                    Throw "The Path argument must be a file. Folder paths are not allowed."
                }
                If ($_ -NotMatch "(\.json)$") {
                    throw "The file specified in the path argument must be either of type json"
                }
                Return $true
            })]
        [Alias("src", "source", "file")]
        [System.IO.FileInfo]$Path,
        [int]$Count = 1
    )
    $Results = [System.Collections.ArrayList]@()
    $Urls = Get-Content -Path $Path | ConvertFrom-Json
    $Urls | ForEach-Object -Parallel {
        $Website = $_.url
        $Result = Test-Connection -ComputerName $Website -Count $using:Count -Quiet
        $ResultText = $Result ? 'Success' : 'Failed'
        $Item = @{
            Website = $Website
            Result  = $ResultText
        }
        $null = ($using:Results).Add($Item)
    }
    
    $Results | ForEach-Object -Parallel { 
        $_ | Select-Object @{ Name = "Website"; Expression = { $_.Website }; }, @{ Name = "Result"; Expression = { $_.Result }; }, @{ Name = "Number Of Attempts"; Expression = { $using:Count }; } 
    }
}
تابع Ping-Website را جهت بررسی فیچر جدیدی که همراه با دستور ForEach-Object استفاده میشود، تغییر داده‌ایم تا به صورت Parallel عمل کند؛ این قابلیت از نسخه ۷ به بعد به PowerShell اضافه شده است. از آنجائیکه این قابلیت باعث میشود script block مربوط به ForEach-Object درون یک context دیگر با نام runspace اجرا شود. در نتیجه برای دسترسی به متغیرهای بیرون از script block نیاز خواهیم داشت از یک متغیر خودکار تحت‌عنوان using قبل از نام متغیر و بعد از علامت $ استفاده کنیم. همچنین آرایه مثال قبل را نیز به ArrayList تغییر داده‌ایم. زیرا در حالت قبلی امکان تغییر سایز یک آرایه با سایز ثابت را نخواهیم داشت. نکته دیگری که در مورد کد فوق میتوان به آن توجه کرد، نال کردن خروجی متد Add مربوط به آرایه‌ی Results است. همانطور که در قسمت قبل توضیح دادیم، از این تکنیک برای suppress کردن خروجی استفاده میکنیم و چون در اینجا خروجی متد Add یک عدد میباشد، با تکنیک فوق، خروجی را دیگر درون کنسول مشاهده نخواهیم کرد. توسط اتریبیوت Alias نیز نام‌های دیگری را که میتوان برای پارامتر Path حین فراخوانی تابع استفاده کرد، تعیین کرده‌ایم. لیست کامل اتریبیوت‌هایی را که میتوان برای پارامترهای یک تابع تعیین کرد، میتوانید در مستندات PowerShell ببینید. 
نکته: اگر تابع فوق را همراه با فلگ Verbose فراخوانی کنیم، لاگ‌های موردنظر را درون کنسول مشاهده نخواهیم کرد؛ زیرا همانطور که اشاره شد script block درون یک context جدا اجرا میشود و باید متغیرهای خودکار مربوط به Output را مجدداً مقداردهی کنیم:
Function Ping-Website {
    [CmdletBinding()]
    Param(
        # As before
    )
    # As before
    $Urls | ForEach-Object -Parallel {
        $DebugPreference = $using:DebugPreference 
        $VerbosePreference = $using:VerbosePreference 
        $InformationPreference = $using:InformationPreference 
        
        # As before
    }
    
    # As before
}

قابلیت تعریف بلاک‌ها/توابع، به صورت تودرتو  
درون توابع و script block امکان نوشتن بلاک‌های تودرتو را نیز داریم:
$scriptBlock = {
    $logOutput = {
        param($message)
        Write-Host $message
    }

    [int]$someVariable = 10
    $doSomeWork = {
        & $logOutput -message "Some variable value: $someVariable"
    }
    $someVariable = 20

    & $doSomeWork
}
خروجی بلاک فوق  Some variable value: 20 خواهد بود؛ زیرا قبل از فراخوانی doSomeWork مقدار متغیر عددی someVariable را به ۲۰ تغییر داده‌ایم. برای script blocks این امکان را داریم که دقیقاً در همان جایی که بلاک را تعریف میکنیم، یک snapshot تهیه کنیم. در اینحالت خروجی، مقدار Some variable value: 10 خواهد شد: 
$scriptBlock = {
    $logOutput = {
        param($message)
        Write-Host $message
    }

    [int]$someVariable = 10
    $doSomeWork = {
        & $logOutput -message "Some variable value: $someVariable"
    }.GetNewClosure()
    $someVariable = 20

    & $doSomeWork
}
یکسری بلاک‌های ویژه نیز درون توابع و script blockها میتوانیم بنویسیم که اصطلاحاً به name blocks معروف هستند:
begin
process
end
dynamicparam
درون یک تابع اگر هیچکدام از بلاک‌های فوق استفاده نشود، به صورت پیش‌فرض بدنه تابع، درون بلاک end قرار خواهد گرفت. بلاک begin قبل از شروع pipeline اجرا میشود. process به ازای هر آیتم pipe شده اجرا خواهد شد. end نیز در پایان اجرا میشود. به عنوان مثال تابع زیر را در نظر بگیرید:
function Show-Pipeline {
    begin { 
        Write-Host "Pipeline start" 
    }
    process { 
        Write-Host  "Pipeline process $_" 
    }
    end { 
        Write-Host  "Pipeline end $_" 
    }
}
در ادامه یکسری آیتم را به ورودی این تابع pipe خواهیم کرد:
PS /> 1..2 | Show-Pipeline                                   
Pipeline start 
Pipeline process 1
Pipeline process 2
Pipeline end 2
همانطور که مشاهده میکنید، به ازای هر آیتم pipe شده، یکبار بلاک process اجرا شده است. همچنین برای دسترسی به مقدار آیتم pipe شده نیز از متغیر خودکار _$ استفاده کرده‌ایم (PSItem$ نیز به همین متغیر اشاره دارد).

با توجه به توضیحات named blockهای فوق، اکنون اگر بخواهیم نسخه اول تابع Ping-Website را با pipe کردن یک آرایه فراخوانی کنیم، خروجی که در کنسول نمایش داده خواهد شد، تنها آیتم آخر از آرایه خواهد بود:
PS /> "www.google.com", "www.yahoo.com" | Ping-Website                 

Website       Result  Number Of Attempts
-------       ------  ------------------
www.yahoo.com Success                  3
دلیل آن نیز این است که به صورت صریح کدها را درون بلاک process ننوشته بودیم. همانطور که عنوان شد، در حالت پیش‌فرض، بدنه توابع درون بلاک end قرار خواهند گرفت و تنها یکبار اجرا خواهند شد. بنابراین:
Function Ping-Website {
    [CmdletBinding()]
    Param(
        # As before
    )
    process {
        # As before
    }
}
اینبار اگر تابع را مجدداً فراخوانی کنیم، خروجی مطلوب را نمایش خواهد داد:
PS /> "www.google.com", "www.yahoo.com" | Ping-Website

Website        Result  Number Of Attempts
-------        ------  ------------------
www.google.com Success                  3
www.yahoo.com  Success                  3

بلاک dynamicparam
از این بلاک برای تعریف پارامترهای داینامیک که به صورت on the fly نیاز هست ایجاد شوند، استفاده میشود. برای درک بهتر آن فرض کنید میخواهیم تابعی را بنویسیم که امکان خواندن یک فایل CSV را به ما میدهد. تا اینجای کار توسط Import-CSV به یک خط دستور قابل انجام است. اما فرض کنید میخواهیم به کاربر این امکان را بدهیم که یک ستون موردنظر از فایل را مشاهده کند. همچنین میخواهیم یک اعتبارسنجی هم روی نام ستونی که کاربر قرار است وارد کند نیز داشته باشیم. به عنوان مثال یک فایل CSV با ستون‌های name, lname, age داریم و کاربر میخواهد تنها ستون اول یک name را واکشی کند:
PS /> Read-Csv ./users.csv -Columns name
برای اینکار میتوانیم با کمک dynamic param یک پارامتر را در زمان اجرا ایجاده کرده و مقادیری را که کاربر برای ستون‌ها مجاز است وارد کند، براساس هدر فایل CSV تنظیم کنیم:
using namespace System.Management.Automation
Function Read-Csv {
    Param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$Path
    )
    DynamicParam {
        $firstLine = Get-Content $Path | Select-Object -First 1
        [String[]]$headers = $firstLine -split ', '
        $parameters = [RuntimeDefinedParameterDictionary]::new()
        $parameter = [RuntimeDefinedParameter]::new(
            'Columns', [String[]], [Attribute[]]@(
                [Parameter]@{ Mandatory = $false; Position = 1 }
                [ValidateSet]::new($headers)
            )
        )
        $parameters.Add($parameter.Name, $parameter) 
        Return $parameters
    }
    Begin {
        $csvContent = Import-Csv $Path
        If ($PSBoundParameters.ContainsKey('Columns')) {
            $columns = $PSBoundParameters['Columns']
            $csvContent | Select-Object -Property $columns
        }
        Else {
            $csvContent
        }
    }
}
درون کنسول PowerShell هم یک IntelliSense برای مقادیر مجاز نمایش داده خواهد شد:

مطالب
بررسی تغییرات Blazor 8x - قسمت چهاردهم - امکان استفاده از کامپوننت‌های Blazor در برنامه‌های ASP.NET Core 8x
ASP.NET Core 8x به همراه یک IResult جدید به‌نام RazorComponentResult است که توسط آن می‌توان در Endpoint‌های Minimal-API و همچنین اکشن متدهای MVC، از کامپوننت‌های Blazor، خروجی گرفت. این خروجی نه فقط static یا به عبارتی SSR، بلکه حتی می‌تواند تعاملی هم باشد. در این مطلب، جزئیات فعالسازی و استفاده از این IResult جدید را در یک برنامه‌ی Minimal-API بررسی می‌کنیم.


ایجاد یک برنامه‌ی Minimal-API جدید در دات نت 8

پروژه‌ای را که در اینجا پیگیری می‌کنیم، بر اساس قالب استاندارد تولید شده‌ی توسط دستور dotnet new webapi تکمیل می‌شود.


ایجاد یک صفحه‌ی Blazor 8x به همراه مسیریابی و دریافت پارامتر

در ادامه قصد داریم که یک کامپوننت جدید را به نام SsrTest.razor در پوشه‌ی جدید Components\Tests ایجاد کرده و برای آن مسیریابی از نوع page@ هم تعریف کنیم. یعنی نه‌فقط قصد داریم آن‌را توسط RazorComponentResult رندر کنیم، بلکه می‌خواهیم اگر آدرس آن‌را در مرورگر هم وارد کردیم، قابل دسترسی باشد.
به همین جهت یک پوشه‌ی جدید را به نام Components در ریشه‌ی پروژه‌ی Web API جاری ایجاد می‌کنیم، با این محتوا:
برای ایده گرفتن از محتوای مورد نیاز، به «معرفی قالب‌های جدید شروع پروژه‌های Blazor در دات نت 8» قسمت دوم این سری مراجعه کرده و برای مثال قالب ساده‌ترین حالت ممکن را توسط دستور زیر تولید می‌کنیم (در یک پروژه‌ی مجزا، خارج از پروژه‌ی جاری):
dotnet new blazor --interactivity None
پس از اینکار، محتویات پوشه‌ی Components آن‌را مستقیما داخل پوشه‌ی پروژه‌ی Minimal-API جاری کپی می‌کنیم. یعنی در نهایت در این پروژه‌ی جدید Web API، به فایل‌های زیر می‌رسیم:
- فایل Imports.razor_ ساده شده برای سهولت کار با فضاهای نام در کامپوننت‌های Blazor (فضاهای نامی را که در آن وجود ندارند و مرتبط با پروژه‌ی دوم هستند، حذف می‌کنیم).
- فایل App.razor، برای تشکیل نقطه‌ی آغازین برنامه‌ی Blazor.
- فایل Routes.razor برای معرفی مسیریابی صفحات Blazor تعریف شده.
- پوشه‌ی Layout برای معرفی فایل MainLayout.razor که در Routes.razor استفاده شده‌است.

و ... یک فایل آزمایشی جدید به نام Components\Tests\SsrTest.razor با محتوای زیر:
@page "/ssr-page/{Data:int}"

<PageTitle>An SSR component</PageTitle>

<h1>An SSR component rendered by a Minimal-API!</h1>

<div>
    Data: @Data
</div>

@code {

    [Parameter]
    public int Data { get; set; }

}
این فایل، می‌تواند پارامتر Data را از طریق فراخوانی مستقیم آدرس فرضی http://localhost:5227/ssr-page/2 دریافت کند و یا ... از طریق خروجی جدید RazorComponentResult که توسط یک Endpoint سفارشی ارائه می‌شود:




تغییرات مورد نیاز در فایل Program.cs برنامه‌ی Web-API برای فعالسازی رندر سمت سرور Blazor

در ادامه کل تغییرات مورد نیاز جهت اجرای این برنامه را مشاهده می‌کنید:
var builder = WebApplication.CreateBuilder(args);

// ...

builder.Services.AddRazorComponents();

// ...

// http://localhost:5227/ssr-component?data=2
// or it can be called directly http://localhost:5227/ssr-page/2
app.MapGet("/ssr-component",
           (int data = 1) =>
           {
               var parameters = new Dictionary<string, object?>
                                {
                                    { nameof(SsrTest.Data), data },
                                };
               return new RazorComponentResult<SsrTest>(parameters);
           });

app.UseStaticFiles();
app.UseAntiforgery();

app.MapRazorComponents<App>();
app.Run();

// ...
توضیحات:
- همین اندازه تغییر در جهت فعالسازی رندر سمت سرور کامپوننت‌های Blazor در یک برنامه‌ی ASP.NET Core کفایت می‌کند. یعنی اضافه شدن:
AddRazorComponents ،UseAntiforgery و MapRazorComponents
- در اینجا نحوه‌ی ارسال پارامترها را به یک RazorComponentResult نیز مشاهده می‌کنید.
- در حالت فراخوانی از طریق مسیر endpoint (یعنی فراخوانی مسیر http://localhost:5227/ssr-component در مثال فوق)، خود کامپوننت فراخوانی شده، بدون layout تعریف شده‌ی در فایل App.razor، رندر می‌شود. علت اینجا است که layout برنامه به همراه کامپوننت Router و RouteView آن فعال می‌شود که این دو هم مختص به صفحات دارای مسیریابی Blazor هستند و برای رندر کامپوننت‌های خالص آن بکار گرفته نمی‌شوند. خروجی RazorComponentResult تنها یک static SSR خالص است؛ مگر اینکه فایل blazor.web.js را نیز بارگذاری کند.

یک نکته: اگر در حالت رندر توسط RazorComponentResult، علاقمند به استفاده‌ی از layout هستید، می‌توان از کامپوننت LayoutView داخل یک کامپوننت فرضی به صورت زیر استفاده کرد؛ اما این مورد هم شامل اطلاعات فایل App.razor نمی‌شود:
<LayoutView Layout="@typeof(MainLayout)">
    <PageTitle>Home</PageTitle>

    <h2>Welcome to your new app.</h2>
</LayoutView>


سؤال: آیا در این حالت کامپوننت‌های تعاملی هم کار می‌کنند؟

پاسخ: بله. فقط برای ایده گرفتن، یک نمونه پروژه‌ی تعاملی Blazor 8x را در ابتدا ایجاد کنید و قسمت‌های اضافی AddRazorComponents و MapRazorComponents آن‌را در اینجا کپی کنید؛ یعنی برای مثال جهت فعالسازی کامپوننت‌های تعاملی Blazor Server، به این دو تغییر زیر نیاز است:
// ...

builder.Services.AddRazorComponents()
       .AddInteractiveServerComponents();

// ...

app.MapRazorComponents<App>().AddInteractiveServerRenderMode();

// ...
همچنین باید دقت داشت که امکانات تعاملی، به دلیل وجود و دسترسی به یک سطر ذیل که در فایل Components\App.razor واقع شده، اجرایی می‌شوند:
<script src="_framework/blazor.web.js"></script>
و همانطور که عنوان شد، اگر از روش new RazorComponentResult استفاده می‌شود، باید این سطر را به صورت دستی اضافه‌کرد؛ چون به همراه رندر layout تعریف شده‌ی در فایل App.razor نیست. برای مثال فرض کنید کامپوننت معروف Counter را به صورت زیر داریم که حالت رندر آن به InteractiveServer تنظیم شده‌است:
@rendermode InteractiveServer

<h1>Counter</h1>

<p role="status">Current count: @_currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int _currentCount;

    private void IncrementCount()
    {
        _currentCount++;
    }

}
در این حالت پس از تعریف endpoint زیر، خروجی آن فقط یک صفحه‌ی استاتیک SSR خواهد بود و دکمه‌ی Click me آن کار نمی‌کند:
// http://localhost:5227/server-interactive-component
app.MapGet("/server-interactive-component", () => new RazorComponentResult<Counter>());
علت اینجا است که اگر به سورس HTML رندر شده مراجعه کنیم، خبری از درج اسکریپت blazor.web.js در انتهای آن نیست. به همین جهت برای مثال فایل جدید CounterInteractive.razor را به صورت زیر اضافه می‌‌کنیم که ساختار آن شبیه به فایل App.razor است:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Interactive server component</title>
    <base href="/"/>
</head>
<body>
   <h1>Interactive server component</h1>

   <Counter/>

  <script src="_framework/blazor.web.js"></script>
</body>
</html>
و هدف اصلی از آن، تشکیل یک قالب و درج اسکریپت blazor.web.js در انتهای آن است.
سپس تعریف endpoint متناظر را به صورت زیر تغییر می‌دهیم:
// http://localhost:5227/server-interactive-component
app.MapGet("/server-interactive-component", () => new RazorComponentResult<CounterInteractive>());
اینبار به علت بارگذاری فایل blazor.web.js، امکانات تعاملی کامپوننت Counter فعال شده و قابل استفاده می‌شوند.


سؤال: آیا می‌توان این خروجی static SSR کامپوننت‌های بلیزر را در سرویس‌های یک برنامه ASP.NET Core هم دریافت کرد؟

منظور این است که آیا می‌توان از یک کامپوننت Blazor، به همراه تمام پیشرفت‌های Razor در آن که در Viewهای MVC قابل دسترسی نیستند، به‌شکل یک رشته‌ی خالص، خروجی گرفت و برای مثال از آن به‌عنوان قالب پویای محتوای ایمیل‌ها استفاده کرد؟
پاسخ: بله! زیر ساخت RazorComponentResult که از سرویس HtmlRenderer استفاده می‌کند، بدون نیاز به برپایی یک endpoint هم قابل دسترسی است:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;

namespace WebApi8x.Services;

public class BlazorStaticRendererService
{
    private readonly HtmlRenderer _htmlRenderer;

    public BlazorStaticRendererService(HtmlRenderer htmlRenderer) => _htmlRenderer = htmlRenderer;

    public Task<string> StaticRenderComponentAsync<T>() where T : IComponent
        => RenderComponentAsync<T>(ParameterView.Empty);

    public Task<string> StaticRenderComponentAsync<T>(Dictionary<string, object?> dictionary) where T : IComponent
        => RenderComponentAsync<T>(ParameterView.FromDictionary(dictionary));

    private Task<string> RenderComponentAsync<T>(ParameterView parameters) where T : IComponent =>
        _htmlRenderer.Dispatcher.InvokeAsync(async () =>
                                             {
                                                 var output = await _htmlRenderer.RenderComponentAsync<T>(parameters);
                                                 return output.ToHtmlString();
                                             });
}
برای کار با آن، ابتدا باید سرویس فوق را به صورت زیر ثبت و معرفی کرد:
builder.Services.AddScoped<HtmlRenderer>();
builder.Services.AddScoped<BlazorStaticRendererService>();
و سپس یک نمونه مثال فرضی نحوه‌ی تزریق و فراخوانی سرویس BlazorStaticRendererService به صورت زیر است که در آن روش ارسال پارامترها هم بررسی شده‌است:
app.MapGet("/static-renderer-service-test",
           async (BlazorStaticRendererService renderer, int data = 1) =>
           {
               var parameters = new Dictionary<string, object?>
                                {
                                    { nameof(SsrTest.Data), data },
                                };
               var html = await renderer.StaticRenderComponentAsync<SsrTest>(parameters);
               return Results.Content(html, "text/html");
           });

کدهای کامل این مطلب را می‌توانید از اینجا دریافت کنید: WebApi8x.zip
مطالب
روش استفاده‌ی از jQuery در برنامه‌های AngularJS 2.0
استفاده‌ی گسترده‌ی از jQuery به همراه برنامه‌های AngularJS 2.0 ایده‌ی خوبی نیست؛ زیرا jQuery و کدهای آن، ارتباط تنگاتنگی را بین DOM و JavaScript برقرار می‌کنند که برخلاف رویه‌ی فریم‌ورک‌های MVC است. اما با این حال استفاده‌ی از افزونه‌های بی‌شمار jQuery در برنامه‌های AngularJS 2.0، جهت غنا بخشیدن به ظاهر و همچنین کاربردپذیری برنامه، یکی از استفاده‌های پذیرفته شده‌ی آن است.
روش استفاده‌ی از jQuery نیز در حالت کلی همانند مطلب «استفاده از کتابخانه‌های ثالث جاوا اسکریپتی در برنامه‌های AngularJS 2.0» است؛ اما به همراه چند نکته‌ی اضافه‌تر مانند محل فراخوانی و دسترسی به DOM، در کدهای یک کامپوننت.


هدف: استفاده از کتابخانه‌ی Chosen

می‌خواهیم جهت غنی‌تر کردن ظاهر یک دراپ داون در برنامه‌های AngularJS 2.0، از یک افزونه‌ی بسیار معروف jQuery به نام Chosen استفاده کنیم.


تامین پیشنیازهای اولیه

می‌توان فایل‌های این کتابخانه را مستقیما از GitHub دریافت و به پروژه اضافه کرد. اما بهتر است این‌کار را توسط bower مدیریت کنیم. این کتابخانه هنوز دارای بسته‌ی رسمی npm نیست (و بسته‌ی chosen-npm که در مخزن npm وجود دارد، توسط این تیم ایجاد نشده‌است). اما همانطور که در مستندات آن نیز آمده‌است، توسط دستور ذیل نصب می‌شود:
bower install chosen
برای مدیریت این مساله در ویژوال استودیو، به منوی project و گزینه‌ی add new item مراجعه کرده و bower را جستجو کنید:


در اینجا نام پیش فرض bower.json را پذیرفته و سپس محتوای فایل ایجاد شده را به نحو ذیل تغییر دهید:
{
    "name": "asp-net-mvc5x-angular2x",
    "version": "1.0.0",
    "authors": [
        "DNT"
    ],
    "license": "MIT",
    "ignore": [
        "node_modules",
        "bower_components"
    ],
    "dependencies": {
        "chosen": "1.4.2"
    },
    "devDependencies": {
    }
}
ساختار این فایل هم بسیار شبیه‌است به ساختار package.json مربوط به npm. در اینجا کتابخانه‌ی chosen در قسمت لیست وابستگی‌ها اضافه شده‌است. با ذخیره کردن این فایل، کار دریافت خودکار بسته‌های تعریف شده، آغاز می‌شود و یا کلیک راست بر روی فایل bower.json دارای گزینه‌ی restore packages نیز هست.
پس از دریافت خودکار chosen، بسته‌ی آن‌را در مسیر bower_components\chosen واقع در ریشه‌ی پروژه می‌توانید مشاهده کنید.


استفاده از jQuery و chosen به صورت untyped

ساده‌ترین و متداول‌ترین روش استفاده از jQuery و افزونه‌های آن شامل موارد زیر هستند:
الف) تعریف مداخل مرتبط با آن‌ها در فایل index.html
<script src="~/node_modules/jquery/dist/jquery.min.js"></script>
<script src="~/node_modules/bootstrap/dist/js/bootstrap.min.js"></script>

<script src="~/bower_components/chosen/chosen.jquery.min.js"></script>
<link href="~/bower_components/chosen/chosen.min.css" rel="stylesheet" type="text/css" />
که در اینجا jQuery و بوت استرپ و chosen به همراه فایل css آن اضافه شده‌اند.
ب) تعریف jQuery به صورت untyped
declare var jQuery: any;
همینقدر که این یک سطر را به ابتدای ماژول خود اضافه کنید، امکان دسترسی به تمام امکانات جی‌کوئری را خواهید یافت. البته بدون intellisense و بررسی نوع‌های پارامترها؛ چون نوع آن، any یا همان حالت پیش فرض جاوا اسکریپت تعریف شده‌است.


محل صحیح فراخوانی متدهای مرتبط با jQuery

در تصویر ذیل، چرخه‌ی حیات یک کامپوننت را مشاهده می‌کنید که با تعدادی از آن‌ها پیشتر آشنا شده‌‌ایم:

روش متداول استفاده از jQuery، فراخوانی آن پس از رخداد document ready است. در اینجا معادل این رخداد، hook ویژ‌ه‌ای به نام ngAfterViewInit است. بنابراین تمام فراخوانی‌های jQuery را باید در این متد انجام داد.
همچنین جی‌کوئری نیاز دارد تا بداند هم اکنون قرار است با چه المانی کار کنیم و کامپوننت بارگذاری شده کدام است. برای این منظور، یکی از سرویس‌های توکار AngularJS 2.0 را به نام ElementRef، به سازنده‌ی کلاس تزریق می‌کنیم. توسط خاصیت this._el.nativeElement آن می‌توان به المان ریشه‌ی کامپوننت جاری دسترسی یافت.
constructor(private _el: ElementRef) {
}


تهیه‌ی کامپوننت نمایش یک دراپ داون مزین شده با chosen

در ادامه قصد داریم نکاتی را که تاکنون مرور کردیم، به صورت یک مثال پیاده سازی کنیم. به همین جهت فایل جدید using-jquery-addons.component.ts را به پروژه اضافه کنید به همراه فایل قالب آن به نام using-jquery-addons.component.html؛ با این محتوا:
الف) کامپوننت using-jquery-addons.component.ts
import { Component, ElementRef, AfterViewInit } from "@angular/core";

declare var jQuery: any; // untyped

@Component({
    templateUrl: "app/using-jquery-addons/using-jquery-addons.component.html"
})
export class UsingJQueryAddonsComponent implements AfterViewInit {

    dropDownItems = ["First", "Second", "Third"];
    selectedValue = "Second";

    constructor(private _el: ElementRef) {
    }

    ngAfterViewInit() {
        jQuery(this._el.nativeElement)
            .find("select")
            .chosen()
            .on("change", (e, args) => {
                this.selectedValue = args.selected;
            });
    }
}

ب) قالب using-jquery-addons.component.html
<select>
    <option *ngFor="let item of dropDownItems"
            [value]="item"
            [selected]="item == selectedValue">
        {{item}} option
    </option>
</select>

<h4> {{selectedValue}}</h4>
با این خروجی


توضیحات

کلاس UsingJQueryAddonsComponent، اینترفیس AfterViewInit را پیاده سازی کرده‌است؛ تا توسط متد ngAfterViewInit بتوانیم با عناصر DOM کار کنیم. هرچند در کل اینکار باید صرفا محدود شود به مواردی مانند مثال جاری و در حد آغاز یک افزونه‌ی jQuery و اگر قرار است تغییراتی دیگری صورت گیرند بهتر است از همان روش binding توکار AngularJS 2.0 استفاده کرد.
در سازنده‌ی کلاس، سرویس ElementRef تزریق شده‌است تا توسط خاصیت this._el.nativeElement آن بتوان به المان ریشه‌ی کامپوننت جاری دسترسی یافت. به همین جهت است که پس از آن از متد find، برای یافتن دراپ داون استفاده شده‌است و سپس chosen به نحو متداولی به آن اعمال گردیده‌است.
در اینجا هر زمانیکه یکی از آیتم‌های دراپ داون انتخاب شوند، مقدار آن به خاصیت selectedValue انتساب داده شده و این انتساب سبب فعال سازی binding و نمایش مقدار آن در ذیل دراپ داون می‌گردد.
در قالب این کامپوننت هم با استفاده از ngFor، عناصر دراپ داون از آرایه‌ی dropDownItems تعریف شده در کلاس کامپوننت، تامین می‌شوند. متغیر محلی item تعریف شده‌ی در اینجا، در محدودی همین المان قابل دسترسی است. برای مثال از آن جهت تنظیم دومین آیتم لیست به صورت انتخاب شده، در حین اولین بار نمایش view استفاده شده‌است.


استفاده از jQuery و chosen به صورت typed

کتابخانه‌ی jQuery در مخزن کد https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/jquery دارای فایل d.ts. خاص خود است. برای نصب آن می‌توان از روش ذیل استفاده کرد:
npm install -g typings
typings install jquery --save --ambient
پس از افزودن فایل typings این کتابخانه، در ابتدای فایل main.ts یک سطر ذیل را اضافه کنید:
 /// <reference path="../typings/browser/ambient/jquery/index.d.ts" />
اینکار سبب خواهد شد تا intellisense درون ویژوال استودیو بتواند مداخل متناظر را یافته و راهنمای مناسبی را ارائه دهد.

در ادامه به فایل using-jquery-addons.component.ts مراجعه کرده و تغییر ذیل را اعمال کنید:
// declare var jQuery: any; // untyped
declare var jQuery: JQueryStatic; // typed
فایل d.ts. اضافه شده، امکان استفاده‌ی از نوع جدید JQueryStatic را مهیا می‌کند. پس از آن در کدهای تعریف شده، chosen به رنگ قرمز در می‌آید، چون در تعاریف نوع دار jQuery، این افزونه وجود خارجی ندارد. برای رفع این مشکل، فایل typings/browser/ambient/jquery/index.d.ts را گشوده و سپس به انتهای آن، تعریف chosen را به صورت دستی اضافه کنید:
interface JQuery {
   //...
   chosen(options?:any):JQuery;
}
declare module "jquery" {
   export = $;
}
به این ترتیب این متد به عنوان جزئی از متدهای زنجیروار jQuery، شناسایی شده و قابل استفاده خواهد شد.


کدهای کامل این پروژه را از اینجا می‌توانید دریافت کنید.