Skip to content

3. کلاینت Angular JS

3.1. مراجع برای فریم‌ورک Angular JS

در ابتدای این سند دو مرجع برای فریم‌ورک Angular JS ارائه شده است. ما آنها را در اینجا تکرار می‌کنیم:

  • [ref1]: کتاب «Pro AngularJS» نوشتهٔ آدام فریمن و منتشرشده توسط Apress. این یک کتاب عالی است. کد منبع مثال‌های این کتاب به‌صورت رایگان در URL [http://www.apress.com/downloadable/download/sample/sample_id/1527/] در دسترس است؛
  • [ref2]: مستندات رسمی Angular JS [https://docs.angularjs.org/guide];

Angular JS شایسته کتابی تماماً مختص خود است. کتاب آدام فریمن بیش از ۶۰۰ صفحه دارد و حتی یک صفحه از آن نیز بیهوده نیست. ما یک برنامه Angular را توصیف خواهیم کرد و در جریان این توصیف، اصول این فریم‌ورک را مورد بحث قرار می‌دهیم. با این حال، خود را تنها به توضیحاتی محدود خواهیم کرد که برای درک راه‌حل پیشنهادی ضروری هستند. Angular یک فریم‌ورک فوق‌العاده غنی از امکانات است و راه‌های زیادی برای رسیدن به یک نتیجه وجود دارد. این موضوع می‌تواند چالش‌برانگیز باشد، زیرا وقتی تازه‌کار هستید، نمی‌دانید راه‌حلی که استفاده می‌کنید بهتر است یا بدتر از راه‌حل دیگر. این موضوع در مورد راه‌حل ارائه‌شده در اینجا نیز صدق می‌کند. این راه‌حل می‌تواند به روش‌های مختلفی و شاید مطابق با بهترین شیوه‌ها نوشته شود.

3.2. معماری کلاینت Angular

معماری سمت کلاینت Angular شبیه معماری یک برنامه وب سنتی MVC است، با چند تفاوت. برای مثال، یک برنامه وب Spring MVC معماری زیر را دارد:

پردازش یک درخواست کلاینت به شرح زیر انجام می‌شود:

  1. درخواست URL درخواستی به شکل http://machine:port/contexte/Action/param1/param2/....?p1=v1&p2=v2&... هستند. [Dispatcher Servlet] کلاسی از Spring است که URL ورودی را مدیریت می‌کند. این کلاس، URL را به اکشنی که باید آن را پردازش کند، مسیریابی می‌کند. این اکشن‌ها، متدهایی از کلاس‌های خاصی به نام [Contrôleurs] هستند. 'C' در MVC در این مورد، رشته [Dispatcher Servlet, Contrôleur, Action] است. اگر هیچ اقدامی برای رسیدگی به URL ورودی پیکربندی نشده باشد، سرولت [Dispatcher Servlet] پاسخ خواهد داد که URL درخواستی یافت نشد (خطای 404 NOT FOUND);
  1. پردازش
  • عمل انتخاب‌شده می‌تواند از پارامترهای parami که توسط سرولت [Dispatcher Servlet] به آن ارسال شده است، استفاده کند. این پارامترها ممکن است از چندین منبع بیایند:
    • از مسیر [/param1/param2/...] از URL،
    • پارامترهای [p1=v1&p2=v2] از URL,
    • پارامترهای ارسال‌شده توسط مرورگر در درخواست آن؛
  • هنگام پردازش درخواست کاربر، ممکن است اقدام به لایه‌های [metier] و [2b] نیاز داشته باشد. پس از پردازش درخواست مشتری، ممکن است پاسخ‌های مختلفی ایجاد شود. یک مثال کلاسیک این است:
    • یک صفحهٔ خطا اگر درخواست نتوانست به‌درستی پردازش شود
    • در غیر این صورت، یک صفحه تأیید
  • اقدام دستور نمایش یک نمای خاص ([3]) را صادر می‌کند. این نما داده‌هایی را که به عنوان مدل نما شناخته می‌شوند، نمایش می‌دهد. این همان M در MVC است. اقدام این مدل M [2c] را ایجاد کرده و دستور نمایش یک نما V [3] را صادر می‌کند؛
  1. پاسخ – نمای انتخاب‌شده V از مدل M که توسط اکشن ساخته شده است برای راه‌اندازی بخش‌های پویا از پاسخی که باید به کلاینت ارسال کند HTML استفاده می‌کند، و سپس این پاسخ را ارسال می‌کند.

معماری کلاینت Angular ما مشابه خواهد بود، هرچند با اصطلاحات کمی متفاوت. در ابتدا، برنامه‌های Angular معمولاً برنامه‌های وب تک‌صفحه‌ای (APU) یا برنامه‌های تک‌صفحه (SPA) هستند:

Image

  • کاربر URL اولیهٔ برنامه را به شکل زیر درخواست می‌کند: http://machine:port/contexte. مرورگر برای بازیابی سند درخواستی از یک سرور وب پرس‌وجو خواهد کرد. این یک صفحه است که با CSS طراحی شده و توسط JavaScript پویا شده است؛
  • سپس کاربر با نماهای ارائه‌شده تعامل خواهد کرد. می‌توان انواع مختلفی از تعامل را متمایز کرد:
    • آن‌هایی که نیازی به تعامل با منابع خارجی ندارند، مانند پنهان یا نمایش دادن عناصر در نما. این موارد توسط جاوااسکریپت تعبیه‌شده مدیریت می‌شوند؛
    • آنهایی که به داده‌ها از یک سرویس وب راه دور نیاز دارند. این داده‌ها از طریق یک فراخوانی AJAX (جاوااسکریپت و XML غیرهمزمان)، یک مدل ساخته می‌شود و یک نما نمایش داده می‌شود؛
    • آنهایی که به ویوی دیگری غیر از ویوی اولیه نیاز دارند. این درخواست از طریق یک فراخوانی آژاکس (Ajax) به سروری که صفحه اولیه را ارائه کرده است، ارسال می‌شود. سپس فرآیند فوق مجدداً تکرار خواهد شد. صفحه حاصل در مرورگر کش (cache) می‌شود. در درخواست بعدی، این صفحه از سرور راه دور HTML بارگیری نخواهد شد؛

در نهایت، مرورگر تنها یک درخواست HTTP انجام می‌دهد—درخواستی که صفحه اولیه را بازیابی می‌کند. درخواست‌های بعدی HTTP، به سرور صفحه HTML یا سرویس‌های وب از راه دور، توسط جاوااسکریپتی که در صفحات تعبیه شده است، انجام می‌شوند.

اکنون معماری برنامه را در داخل مرورگر تشریح خواهیم کرد. سرور HTML را که صفحات HTML برنامه را ارائه می‌دهد، نادیده می‌گیریم. برای اهداف این توضیح، می‌توانیم فرض کنیم که همه آن‌ها در حافظه پنهان مرورگر موجود هستند.

ابتدا، باید این معماری را در بستر خود قرار دهیم:

  • در [1]، ما در یک مرورگر هستیم؛
  • در [2]، کاربر با نماهای نمایش‌داده‌شده توسط مرورگر تعامل می‌کند؛
  • در [3]، داده‌ها از شبکه، اغلب از سرویس‌های وب، بازیابی می‌شوند؛

کاربر با نماها تعامل می‌کند: فرم‌ها را پر کرده و ارسال می‌کند. بیایید این فرایند را با استفاده از نمای V1 که در بالا نشان داده شده است توضیح دهیم. فرض می‌کنیم این نمای اولیهٔ برنامه است. این نما به شرح زیر به دست آمده است:

  • کاربر نمای اولیهٔ URL برنامه را در قالب http://machine:port/contexte درخواست می‌کند؛
  • مرورگر سند مرتبط با این URL را درخواست کرد. این کنترلر، صفحه HTML / CSS / JS را از نمای V1 دریافت کرد؛
  • سپس جاوااسکریپت تعبیه‌شده در صفحه کنترل را به کنترلر C1 [5] واگذار کرد؛
  • که مدل M1 [8] [9] را برای نما V1 ساخت. ایجاد این مدل ممکن است مستلزم استفاده از سرویس‌های داخلی [6] و پرس‌وجو از سرویس‌های خارجی [7] بوده باشد؛

کاربر اکنون یک نما V1 را در مقابل خود دارد. بیایید تصور کنیم این یک فرم است. او آن را پر می‌کند و سپس ارسال می‌کند:

  • در [4]، کاربر فرم را ارسال می‌کند؛
  • در [5]، این رویداد توسط یکی از متدهای کنترل‌کننده C1 مدیریت خواهد شد؛

اگر این رویداد به چیزی بیش از یک تغییر ساده در نمای V1 (پنهان/نمایش دادن فیلدها) منجر نشود، کنترل‌کننده C1 مدل M1 نما V1 را اصلاح کرده و سپس نما V1 را مجدداً نمایش می‌دهد. برای انجام این کار، ممکن است به یکی از سرویس‌های لایه [services] یا [6] نیاز باشد.

اگر رویداد به داده‌های خارجی نیاز داشته باشد:

  • در [6]، کنترل‌کننده C1 از لایه [DAO] درخواست بازیابی آن را خواهد کرد؛
  • در [7]، لایه یک یا چند فراخوانی AJAX را برای بازیابی آن انجام خواهد داد؛
  • در [8] و [9]، مدل M1 اصلاح شده و نمای V1 نمایش داده می‌شود؛

اگر رویداد باعث تغییر نما شود، در هر دو مورد بالا، به جای نمایش نمای V1، کنترل‌کننده C1 یک URL [10] جدید درخواست خواهد کرد. این یک URL داخلی در مرورگر است. این بلافاصله منجر به تماس HTTP با سرور صفحه HTML نمی‌شود. این تغییر در URL توسط یک روتر که به گونه‌ای پیکربندی شده است که هر URL داخلی با یک نما V و کنترل‌کننده آن C مطابقت دارد، مدیریت می‌شود. سپس روتر نمای جدید Vn را نمایش می‌دهد. قبل از نمایش صفحه، کنترلر آن Cn کنترل را در دست می‌گیرد، مدل Mn را می‌سازد و سپس نمای Vn [11] را نمایش می‌دهد. اگر صفحه HTML برای نمای Vn در مرورگر کش نشده باشد، از سرور صفحه HTML درخواست خواهد شد.

لایه [Présentation] این معماری مشابه معماری JSF (Java Server Faces) است:

  • ویوی V با ویوی نوع Facelet از JSF مطابقت دارد؛
  • کنترل‌کننده C با بین JSF، یک کلاس جاوا که هم مدل M نما V و هم رسیدگی‌کننده‌های رویداد آن را در خود جای داده است، مطابقت دارد؛

لایه [Services] با لایه‌های [Services] که به آن‌ها عادت کرده‌ایم متفاوت است. در توسعه وب سمت سرور، معماری لایه‌ای زیر بیشترین کاربرد را دارد:

در نمودار بالا، لایه [web] تنها از طریق لایه [métier] با لایه [DAO] ارتباط برقرار می‌کند. هیچ چیز مانع از تزریق یک مرجع به لایه [DAO] به داخل لایه [web]، که این ارتباط را ممکن می‌سازد، نمی‌شود. اما ما تصمیم می‌گیریم این کار را انجام ندهیم.

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

  • در [1]، لایه [présentation] می‌تواند مستقیماً با هر سرویسی ارتباط برقرار کند؛
  • در [2]، سرویس‌ها از یکدیگر آگاه هستند. یک سرویس می‌تواند از یک یا چند سرویس دیگر استفاده کند.

3.3. نماهای کلاینت Angular

نمایش‌های کلاینت Angular قبلاً در بخش 1.3.3 ارائه شده بودند. برای خوانایی بیشتر این فصل جدید، آن‌ها را مجدداً در اینجا آورده‌ایم. نمای اول به شرح زیر است:

  • در [6]، صفحه اصلی برنامه. این یک برنامه رزرو وقت ملاقات برای پزشکان است؛
  • در [7]، یک کادر تیک که به کاربر اجازه می‌دهد انتخاب کند که آیا در حالت [debug] باشد یا خیر. حالت دوم با وجود فریم [8] مشخص می‌شود که قالب نمای فعلی را نمایش می‌دهد؛
  • در [9]، یک زمان انتظار مصنوعی به میلی‌ثانیه. مقدار پیش‌فرض آن 0 (بدون انتظار) است. اگر N مقدار این زمان انتظار باشد، هر اقدام کاربر پس از یک زمان انتظار به مدت N میلی‌ثانیه اجرا خواهد شد. این به شما امکان می‌دهد تا مدیریت انتظار پیاده‌سازی‌شده توسط برنامه را مشاهده کنید؛
  • در [10]، URL سرور Spring 4. پیرو مورد قبلی، این [http://localhost:8080] است؛
  • در [11] و [12]، نام کاربری و رمز عبور فردی که مایل به استفاده از برنامه است. دو کاربر وجود دارد: admin/admin (نام کاربری/رمز عبور) با نقش (ADMIN) و user/user با نقش (USER). تنها نقش ADMIN مجاز به استفاده از برنامه است. نقش USER صرفاً برای نمایش پاسخ سرور در این مورد استفاده گنجانده شده است؛
  • در [13]، دکمه‌ای که برای ورود به سرور استفاده می‌شود؛
  • در [14]، زبان برنامه. دو گزینه وجود دارد: فرانسوی (پیش‌فرض) و انگلیسی.
  • در [1]، شما وارد می‌شوید؛
  • پس از ورود به سیستم، می‌توانید پزشکی را که مایل به رزرو وقت ملاقات با او هستید [2] و تاریخ قرار ملاقات [3] را انتخاب کنید؛
  • می‌توانید دفترچه یادداشت پزشک منتخب را برای روز انتخاب‌شده از طریق [4] مشاهده کنید؛
  • پس از دسترسی به دفترچه یادداشت پزشک، می‌توانید یک نوبت رزرو کنید [5];
  • در [6]، بیمار را برای قرار ملاقات انتخاب کرده و این انتخاب را در [7] تأیید می‌کنید؛

پس از تأیید قرار ملاقات، شما به طور خودکار به تقویم بازمی‌گردید که در آن قرار ملاقات جدید اکنون فهرست شده است. این قرار ملاقات را می‌توان در تاریخ بعدی از طریق [7] حذف کرد.

ویژگی‌های اصلی توضیح داده شده‌اند. آن‌ها ساده هستند. مواردی که توضیح داده نشده‌اند، عملکردهای ناوبری برای بازگشت به نمای قبلی هستند. بیایید با تنظیمات زبان به پایان برسیم:

  • در [1]، شما از فرانسوی به انگلیسی تغییر می‌دهید؛

2

Image

  • به [2]، نما به انگلیسی، از جمله تقویم، تغییر می‌کند؛

3.4. پیکربندی پروژه Angular

ما کلاینت Angular خود را گام به گام خواهیم ساخت. ما از WebStorm استفاده می‌کنیم.

بیایید یک پوشه خالی به نام [rdvmedecins-angular-v1] ایجاد کنیم و سپس آن را با WebStorm باز کنیم:

  • در [1]، یک پوشه باز می‌کنیم؛
  • در [2]، پوشه‌ای را که ایجاد کرده‌ایم انتخاب کنید؛
  • در [3]، یک پروژه خالی WebStorm دریافت می‌کنیم؛
  • در [4]، پروژه با استفاده از گزینه [File / Settings] پیکربندی می‌شود؛
  • در [5] و [6]، ویژگی [Spelling] که کنترل بررسی املایی را بر عهده دارد، پیکربندی شده است. به‌طور پیش‌فرض، این ویژگی فعال است. از آنجا که نرم‌افزار دانلودشده به زبان انگلیسی است، توضیحات فرانسوی ما در برنامه‌ها به‌عنوان خطاهای املایی احتمالی زیرخط‌دار خواهند شد. بنابراین ما این قابلیت بررسی املایی ([7]) را غیرفعال می‌کنیم؛
  • در [8]، یک فایل جدید ایجاد می‌کنیم؛
  • در [9]، ما انتخاب می‌کنیم که فایل [package.json] را ایجاد کنیم، که برنامه را با استفاده از نحو JSON توصیف می‌کند؛
  • در [10]، فایل تولیدشده همان‌طور که در [11] نشان داده شده است، اصلاح می‌شود؛
  • در [12]، این فایل هم در [package.json] و هم در [bower.json] ذخیره می‌شود؛
  • در [13]، پروژه دوباره پیکربندی می‌شود؛
  • در [14]، ویژگی [Javascript / Bower] را پیکربندی کنید، که به ما امکان می‌دهد کتابخانه‌های جاوااسکریپتی را که نیاز داریم، اعلام کنیم؛
  • در [15]، فایل [bower.json] را که همین حالا ایجاد کرده‌ایم مشخص کنید؛
  • در [16]، بیایید یک کتابخانهٔ جاوااسکریپت اضافه کنیم؛
  • در [17]، تمام کتابخانه‌های جاوااسکریپت قابل دانلود نمایش داده می‌شوند؛
  • در [18]، می‌توانیم فیلتری را برای محدود کردن لیست در [17] وارد کنیم. در اینجا، مشخص می‌کنیم که کتابخانه [Angular JS] را می‌خواهیم؛
  • در [19]، جزئیات کتابخانه نمایش داده می‌شود. در اینجا می‌توانیم ببینیم که نسخه ۱.۲.۱۸ Angular در حال دانلود است؛
  • در [20]، در حال دانلود است؛
  • در [21]، می‌توانیم ببینیم که دانلود شده است؛
  • در [22]، می‌توانیم نسخه دانلود شده را ببینیم. این در واقع نسخه 1.2.19 است؛
  • در [23] می‌توانید آخرین نسخهٔ موجود را مشاهده کنید؛
  • در [24]، با دنبال کردن همان رویه قبلی، کتابخانه‌های زیر دانلود می‌شوند:
angular-base64
برای رمزگذاری رشته "user:password" در Base64؛
angular-i18n
برای بین‌المللی‌سازی تقویم
angular-route
برای مسیریابی درخواست‌های داخلی URL برنامه به کنترل‌کننده و ویوی صحیح؛
angular-translate
بین‌المللی‌سازی ویوها را امکان‌پذیر می‌سازد. این پروژه مستقل از Angular است. در اینجا از دو زبان فرانسوی و انگلیسی استفاده خواهد شد؛
angular-ui-bootstrap-bower
کامپوننت‌های بصری سازگار با بوت‌استرپ را فراهم می‌کند. ما در اینجا از تقویم آن استفاده خواهیم کرد؛
bootstrap
چارچوب بوت‌استرپ CSS. این برای ساخت نماها استفاده خواهد شد؛
footable
یک کامپوننت بصری از نوع «جدول» را فراهم می‌کند. این کامپوننت «واکنش‌گرا» است به این معنی که می‌تواند خود را با اندازه صفحه نمایش تطبیق دهد؛
bootstrap-select
یک کامپوننت «لیست کشویی» ارائه می‌دهد؛
  • در [25]، کتابخانه‌های دانلود شده در پوشه [bower_components] نصب شده‌اند؛
  • در [26] می‌توانیم ببینیم که کتابخانه JQuery دانلود شده است. این به این دلیل است که Bootstrap از آن استفاده می‌کند. سیستم نصب وابستگی‌های جاوااسکریپت پروژه مشابه سیستمی است که Maven در دنیای جاوا از آن استفاده می‌کند: اگر خود یک کتابخانه دانلود شده وابستگی‌هایی داشته باشد، این وابستگی‌ها به طور خودکار دانلود می‌شوند؛

فایل [bower.json] تغییر کرده است:

{
  "name": "rdvmedecins-angular",
  "version": "0.0.1",
  "dependencies": {
    "angular": "~1.2.18",
    "angular-base64": "~2.0.2",
    "angular-route": "~1.2.18",
    "angular-translate": "~2.2.0",
    "bootstrap": "~3.1.1",
    "footable": "~2.0.1",
    "angular-ui-bootstrap-bower": "~0.11.0",
    "bootstrap-select": "~1.5.2"
  }
}

تمام وابستگی‌های دانلودشده در فایل ثبت شده‌اند.

3.5. صفحه اصلی کلاینت Angular

ما در حال ایجاد نسخه اولیه صفحه اصلی کلاینت Angular هستیم:

  • به نام [1] و [2]، ما فایل‌هایی به نام HTML، [app-01]، [3] و [4] ایجاد می‌کنیم؛

فایل [app-01.html] در حال حاضر صفحهٔ اصلی ما خواهد بود. ما آن را پیکربندی خواهیم کرد تا فایل‌های CSS و JS را که برنامه به آن‌ها نیاز دارد، وارد کند:


<!DOCTYPE html>
<html>
<head>
  <title>RdvMedecins</title>
  <!-- META -->
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Angular client for RdvMedecins">
  <meta name="author" content="Serge Tahé">
  <!-- CSS -->
  <link href="bower_components/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet" />
  <link href="bower_components/bootstrap/dist/css/bootstrap-theme.min.css" rel="stylesheet"/>
  <link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
  <link href="bower_components/footable/css/footable.core.min.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
  <h1>Rdvmedecins - v1</h1>
</div>
<!--هسته Bootstrap JavaScript ================================================== -->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<script type="text/javascript" src="bower_components/footable/dist/footable.min.js"></script>
<!-- AngularJS -->
<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<script type="text/javascript" src="bower_components/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bower_components/angular-translate/angular-translate.min.js"></script>
<script type="text/javascript" src="bower_components/angular-base64/angular-base64.min.js"></script>
</body>
</html>
  • خطوط ۱۱–۱۲: فایل‌های CSS برای بوت‌استرپ؛
  • خط ۱۳: فایل CSS برای کامپوننت [boostrap-select
  • خط ۱۴: فایل CSS برای کامپوننت [footable];
  • خطوط ۲۱–۲۴: فایل‌های JS برای کامپوننت‌های Bootstrap؛
  • خط ۲۱: کامپوننت‌های بوت‌استرپ توسط JQuery پشتیبانی می‌شوند؛
  • خط ۲۲: فایل بوت‌استرپ JS;
  • خط ۲۳: فایل JS برای کامپوننت [boostrap-select];
  • خط ۲۴: فایل JS برای کامپوننت [footable];
  • خطوط ۲۶–۳۰: فایل‌های JS برای Angular و پروژه‌های مرتبط با آن؛
  • خط ۲۶: فایل Angular با نام JS. اگر از این کتابخانه استفاده شود، باید پس از JQuery بارگذاری شود؛
  • خط ۲۷: فایل JS از پروژه [angular-ui-bootstrap
  • خط ۲۸: فایل JS از روتر [angular-route];
  • خط ۲۹: فایل JS از ماژول بین‌المللی‌سازی برنامه Angular؛
  • خط ۳۰: فایل JS از ماژول [angular-base64];

می‌توان اعتبار فایل [app-01.html] را بررسی کرد:

  • در [1]، بازرسی کد درخواست شده است؛
  • در [2]، نتیجه زمانی که همه چیز صحیح است؛

این بازرسی سیستماتیک کد پیش از اجرا توصیه می‌شود. در اینجا، این بررسی امکان تشخیص هرگونه خطای ارجاع در فایل‌های CSS و JS را فراهم می‌کند. اگر مسیری نادرست باشد، بازرس کد آن را علامت‌گذاری خواهد کرد.

  • در [3]، صفحه می‌تواند از طریق یک عیب‌یاب در مرورگر بارگذاری شود. نتیجه زیر در مرورگر نمایش داده می‌شود:
  • در [4]، صفحه [app-01.html] توسط یک سرور داخلی WebStorm که در اینجا روی پورت 63342 در حال اجرا است، ارائه شد؛
  • در [5]، کنسول عیب‌یاب (debugger console) قرار دارد. اگر خطایی رخ داده بود، در اینجا نمایش داده می‌شد. همچنین اینجاست که خروجی صفحه تولید شده توسط دستور جاوااسکریپت [console.log(expression)] نمایش داده می‌شود. ما از این ویژگی به طور گسترده استفاده خواهیم کرد؛

حالت اشکال‌زدایی به شما امکان می‌دهد صفحه را در WebStorm تغییر دهید و نتایج این تغییرات را بدون نیاز به بارگذاری مجدد صفحه در مرورگر مشاهده کنید. بنابراین، اگر خط ۳ زیر را اضافه کنیم:


<div class="container">
  <h1>Rdvmedecins - v1</h1>
  <h2>Version 1</h2>
</div>

و به مرورگر بازگردیم، می‌بینیم که صفحه تغییر کرده است:

 

3.6. مقدمه‌ای بر بوت‌استرپ

اکنون برخی از ویژگی‌های بوت‌استرپ که در این برنامه استفاده شده‌اند را تشریح خواهیم کرد. من تنها دانش محدودی از این فریم‌ورک دارم که با کپی و پیست کدهای موجود در اینترنت به دست آورده‌ام. من نقش کلاس‌های CSS را که فکر می‌کنم متوجه شده‌ام، توضیح خواهم داد. از اظهار نظر در مورد بقیه خودداری می‌کنم.

3.6.1. مثال ۱

در Angular، عملیات‌هایی که اطلاعات را از منابع خارجی دریافت می‌کنند، غیرهمزمان (asynchronous) هستند. این بدان معناست که عملیات آغاز می‌شود و کنترل بلافاصله به ویو بازگردانده می‌شود تا کاربر بتواند به تعامل با آن ادامه دهد. هنگامی که عملیات کامل می‌شود، اپلیکیشن از طریق یک رویداد (event) مطلع می‌گردد. این رویداد توسط تابع JS مدیریت می‌شود که سپس می‌تواند نمای فعلی را به‌روزرسانی یا تغییر دهد. اگر احتمال می‌رود عملیات زمان زیادی ببرد، ارائه گزینه لغو آن به کاربر مفید است. ما این گزینه را به صورت سیستماتیک ارائه خواهیم کرد. برای این کار، از یک بنر Bootstrap استفاده خواهیم کرد:

Image

برای دستیابی به این نتیجه، [app-01.html] را در [app-02.html] کپی کرده و خطوط زیر را اصلاح می‌کنیم:


<div class="container">
  <h1>Rdvmedecins - v1</h1>
  <div class="alert alert-warning">
    <h1>Opération en cours. Veuillez patienter...
      <button class="btn btn-primary pull-right">Annuler</button>
      <img src="assets/images/waiting.gif" alt=""/>
    </h1>
  </div>
</div>
  • خط ۱: کلاس CSS [container] یک ناحیه نمایش را در داخل مرورگر تعریف می‌کند؛
  • خط ۳: کلاس CSS [alert] یک ناحیه رنگی را نمایش می‌دهد. کلاس [alert-warning] از یک رنگ از پیش تعریف‌شده استفاده می‌کند؛
  • خط ۵: کلاس [btn] یک دکمه را استایل می‌دهد. کلاس [btn-primary] رنگ مشخصی به آن اختصاص می‌دهد. کلاس [pull-right] آن را در سمت راست بنر هشدار قرار می‌دهد؛
  • خط ۶: یک تصویر بارگذاری متحرک؛

3.6.2. مثال ۲

نمای‌های مختلف برنامه یک عنوان مشترک خواهند داشت:

Image

برای دستیابی به این هدف، [app-01.html] را در [app-03.html] کپی می‌کنیم و خطوط زیر را اصلاح می‌کنیم:


<div class="container">
  <h1>Rdvmedecins - v1</h1>
  <!-- بوت‌استرپ جامبوترون -->
  <div class="jumbotron">
    <div class="row">
      <div class="col-md-2">
        <img src="assets/images/caduceus.jpg" alt="RvMedecins"/>
      </div>
      <div class="col-md-10">
        <h1>Les Médecins associés</h1>
      </div>
    </div>
  </div>
</div>
  • منطقه رنگی با استفاده از کلاس [jumbotron] در خط ۴ ایجاد می‌شود؛
  • خط ۵: کلاس [row] یک سطر با ۱۲ ستون را تعریف می‌کند؛
  • خط ۶: کلاس [col-md-2] یک ناحیهٔ دو ستونی را در داخل ردیف تعریف می‌کند؛
  • خط ۷: یک تصویر در این دو ستون قرار می‌گیرد؛
  • خطوط ۹–۱۱: متن در ۱۰ ستون باقی‌مانده قرار می‌گیرد؛

3.6.3. مثال ۳

نماها دارای یک نوار کنترل بالایی خواهند بود. این نوار شامل گزینه‌های کنترلی، پیوندها یا دکمه‌ها خواهد بود. همچنین شامل عناصر فرم خواهد بود. برای مثال:

برای دستیابی به این نتیجه، [app-01.html] را در [app-04.html] کپی می‌کنیم و خطوط زیر را اصلاح می‌کنیم:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
      <div class="navbar-header">
        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" href="#">RdvMedecins</a>
      </div>
       <div class="navbar-collapse collapse">
        <form class="navbar-form navbar-right">
          <!-- حالت اشکال‌زدایی -->
          <label style="width: 100px">
            <input type="checkbox">
            <span style="color: white">Debug</span>
          </label>
          <!-- فرم ورود -->
          <div class="form-group">
            <input type="text" class="form-control" placeholder="Temps d'attente"
                   style="width: 150px"/>
            <input type="text" class="form-control" placeholder="URL du service web"
                   style="width: 200px"/>
            <input type="text" class="form-control" placeholder="Login"
                   style="width: 100px"/>
            <input type="password" class="form-control" placeholder="Mot de passe"
                   style="width: 100px"/>
          </div>
          <button class="btn btn-success">
            Connexion
          </button>
        </form>
      </div>
          <button class="btn btn-success">
            Connexion
          </button>
        </form>
      </div>
    </div>
  </div>
</div>
  • خط ۴: کلاس [navbar] نوار ناوبری را استایل می‌کند. کلاس [navbar-inverse] به آن پس‌زمینه مشکی می‌دهد. کلاس [navbar-fixed-top] تضمین می‌کند که وقتی صفحه نمایش‌داده‌شده توسط مرورگر اسکرول می‌شود، نوار ناوبری در بالای صفحه باقی بماند؛
  • خطوط ۶–۱۴: ناحیه [1] را تعریف می‌کنند. این معمولاً مجموعه‌ای از کلاس‌ها است که من آن‌ها را درک نمی‌کنم. من این کامپوننت را همان‌طور که هست استفاده می‌کنم؛
  • خط ۱۵: یک ناحیه «واکنش‌گرا» از نوار فرمان را تعریف می‌کند. در یک گوشی هوشمند، این ناحیه در یک بخش منو ناپدید می‌شود؛
  • خط ۱۶: کلاس [navbar-form] یک فرم را در نوار فرمان استایل می‌کند. کلاس [navbar-right] آن را در سمت راست فرم قرار می‌دهد؛
  • خطوط ۲۳–۳۲: چهار فیلد ورودی فرم در خط ۱۷، [3]. آنها در یک کلاس [form-group] قرار دارند که عناصر یک فرم را استایل می‌کند، و هر یک از آنها دارای کلاس [form-control] هستند؛
  • خط ۳۳: کلاس [btn] که پیش از این با آن مواجه شده‌ایم، با کلاس [btn-success] که رنگ سبز را به آن می‌بخشد، تقویت شده است؛

3.6.4. مثال ۴

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

Image

برای دستیابی به این نتیجه، ما [app-01.html] را در [app-05.html] کپی می‌کنیم و خطوط زیر را به نوار کنترل اضافه می‌کنیم:


          <button class="btn btn-success">
            Connexion
          </button>
          <!-- زبان‌ها -->
          <div class="btn-group">
            <button type="button" class="btn btn-danger">
              Langues
            </button>
            <button type="button" class="btn btn-danger dropdown-toggle" data-toggle="dropdown">
              <span class="caret"></span>
              <span class="sr-only">Toggle Dropdown</span>
            </button>
            <ul class="dropdown-menu" role="menu">
              <li>
                <a href="">Français</a>
              </li>
              <li>
                <a href="">English</a>
              </li>
            </ul>
          </div>
</form>

خطوط اضافه شده، خطوط ۴ تا ۲۱ هستند.

  • خط ۵: کلاس [btn-group] یک گروه از دکمه‌ها را استایل می‌دهد. دو مورد از این دکمه‌ها در خطوط ۶ و ۹ وجود دارند؛
  • خطوط ۶–۸: دکمهٔ اول برچسب فهرست کشویی را تعریف می‌کند. کلاس [btn-danger] به آن رنگ قرمز می‌دهد؛
  • خطوط ۹–۱۲: دکمه دوم، دکمه فهرست کشویی است. این دکمه در کنار دکمه اول قرار گرفته و این تصور را ایجاد می‌کند که یک مؤلفه واحد است؛
  • خط ۱۰: پیکان رو به پایین را نمایش می‌دهد که نشان می‌دهد این دکمه یک لیست کشویی است؛
  • خط ۱۱: برای خواناهای صفحه؛
  • خطوط ۱۳–۲۰: موارد موجود در فهرست کشویی، موارد یک فهرست غیر مرتب هستند؛

3.6.5. مثال ۵

برای ارسال فرم یا پیمایش، کاربر در نوار کنترل گزینه‌ها یا دکمه‌هایی خواهد داشت که در زیر نشان داده شده است:

گزینه‌های منو در [1] تنظیم شده‌اند. برای این کار، ما [app-01.html] را به [app-06.html] کپی کرده و خطوط زیر را اضافه می‌کنیم:


<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
      <div class="navbar-header">
...
      </div>
      <!-- گزینه‌های منو -->
      <div class="collapse navbar-collapse">
        <ul class="nav navbar-nav">
          <li class="active">
            <a href="">
              <span>Home</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span>Agenda</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span>Valider</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span>Annuler</span>
            </a>
          </li>
        </ul>
        <!-- دکمه‌های سمت راست -->
        <form class="navbar-form navbar-right" role="form">
...
        </form>
      </div>
    </div>
  </div>
</div>
  • گزینه‌های منو توسط خطوط ۸ تا ۲۹ تولید می‌شوند. این‌ها، بار دیگر، عناصری از یک لیست هستند. کلاس [active] متن را زیرخط‌دار می‌کند و بدین ترتیب نشان می‌دهد که گزینه قابل کلیک است.

3.6.6. مثال ۶

ما پزشکان و مشتریان را در لیست‌های کشویی همان‌طور که در زیر نشان داده شده است نمایش خواهیم داد:

 

لیست کشویی مورد استفاده یک کامپوننت بومی بوت‌استرپ نیست. این کامپوننت [bootstrap-select] (http://silviomoreto.github.io/bootstrap-select/) است. برای دستیابی به این نتیجه، [app-01.html] را در [app-07.html] کپی کرده و خطوط زیر را اضافه می‌کنیم:


<!DOCTYPE html>
<html>
<head>
...
<link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>

</head>
<body>
<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <h2><label for="medecins">Médecins</label></h2>
  <select id="medecins" data-style="btn btn-primary" class="selectpicker">
    <option value="1">Mme Marie PELISSIER</option>
    <option value="1">Mr Jacques BROMARD</option>
    <option value="1">Mr Philippe JANDOT</option>
    <option value="1">Mme Justine JACQUEMOT</option>
  </select>
</div>
<!--هسته Bootstrap JavaScript ================================================== -->
...
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<!-- اسکریپت محلی -->
<script>
  $('.selectpicker').selectpicker();
</script>
</body>
</html>
  • خط ۵: شیوه‌نامه از [bootstrap-select] باید وارد شود؛
  • خط ۱۳: ویژگی [data-style] توسط [bootstrap-select] استفاده می‌شود. این ویژگی برای استایل‌دهی به لیست کشویی به کار می‌رود. در اینجا، ما آن را به عنوان یک دکمه آبی [btn-primary] استایل می‌کنیم؛
  • خط ۱۳: ویژگی [class] در خط ۲۳ استفاده می‌شود. این ویژگی می‌تواند هر چیزی باشد؛
  • خطوط ۱۴–۱۷: عناصر لیست کشویی. این‌ها تگ‌های استاندارد HTML هستند؛
  • خط ۲۲: JS باید از [bootstrap-select] وارد شود؛
  • خطوط ۲۴–۲۶: یک اسکریپت JS که پس از اتمام بارگذاری صفحه اجرا می‌شود؛
  • خط ۲۵: یک دستور JQuery. متد [selectpicker] (selectpicker()) بر روی تمام عناصری که کلاس [selectpicker] را دارند اعمال می‌شود ($('.selectpicker')). تنها یک عنصر از این نوع وجود دارد: تگ <select> در خط ۱۳. متد [selectpicker] از فایل JS که در خط ۲۲ به آن ارجاع شده است، فراخوانی می‌شود؛

3.6.7. مثال ۷

برای نمایش دفترچه یادداشت پزشک، از یک جدول «واکنش‌گرا» که توسط کتابخانه JS [footable] ارائه شده است، استفاده خواهیم کرد:

  • در [1]: جدول با نمایش عادی؛
  • در [2]: جدول هنگام تغییر اندازه پنجره مرورگر. ستون [Action] به‌طور خودکار به خط بعدی می‌پیچد. این به عنوان یک مؤلفه «واکنش‌گرا» یا به‌سادگی تطبیقی شناخته می‌شود.

ما [app-01.html] را در [app-08.html] کپی کرده و خطوط زیر را اضافه می‌کنیم:


...
<link href="bower_components/footable/css/footable.core.min.css" rel="stylesheet"/>
<link href="assets/css/rdvmedecins.css" rel="stylesheet"/>
...
<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <div class="row alert alert-warning">
    <div class="col-md-6">
      <table id="creneaux" class="table">
        <thead>
        <tr>
          <th data-toggle="true">
            <span>Créneau horaire</span>
          </th>
          <th>
            <span>Client</span>
          </th>
          <th data-hide="phone">
            <span>Action</span>
          </th>
        </thead>
        <tbody>
        <tr>
          <td>
            <span class='status-metro status-active'>
              9h00-9h20
            </span>
          </td>
          <td>
            <span></span>
          </td>
          <td>
            <a href="" class="status-metro status-active">
              Réserver
            </a>
          </td>
        </tr>
        <tr>
          <td>
            <span class='status-metro status-suspended'>
              9h20-9h40
            </span>
          </td>
          <td>
            <span>Mme Paule MARTIN</span>
          </td>
          <td>
            <a href="" class="status-metro status-suspended">
              Supprimer
            </a>
          </td>
        </tr>
        </tbody>
      </table>
    </div>
  </div>
</div>
...
<script src="bower_components/footable/dist/footable.min.js" type="text/javascript"></script>
  • خطوط ۲ و ۶۰ از قبل در [app-01.html] موجود هستند. این‌ها فایل‌های CSS و JS هستند که توسط کتابخانه [footable] ارائه شده‌اند؛
  • خط ۳ به فایل زیر، CSS، اشاره دارد:

@CHARSET "UTF-8";

#th slots {
    text-align: center;
}

#td slots {
    text-align: center;
    font-weight: bold;
}

.status-metro {
  display: inline-block;
  padding: 2px 5px;
  color:#fff;
}

.status-metro.status-active {
  background: #43c83c;
}

.status-metro.status-suspended {
  background: #fa3031;
}

استایل‌های [status-*] از مثالی از نحوه استفاده از جدول [footable] که در وب‌سایت کتابخانه یافت می‌شود، گرفته شده‌اند.

  • خط ۸: جدول را در یک ردیف واحد [row] و یک کادر رنگی [alert alert-warning] قرار می‌دهد؛
  • خط ۹: جدول عرض ۶ ستون را خواهد داشت [col-md-6];
  • خط ۱۰: جدول HTML با استفاده از بوت‌استرپ [class='table'] قالب‌بندی می‌شود؛
  • خط ۱۳: ویژگی [data-toggle] ستونی را مشخص می‌کند که حاوی نماد [+/-] است، که سطر را باز/بسته می‌کند؛
  • خط ۱۹: ویژگی [data-hide='phone'] مشخص می‌کند که ستون در صورتی که اندازه صفحه نمایش برابر با اندازه صفحه نمایش تلفن همراه باشد، پنهان شود. مقدار «tablet» نیز قابل استفاده است؛

3.6.8. مثال ۸

برای کمک به کاربر، ما ابزارنُکته‌هایی (tooltips) را در اطراف اجزای اصلی نماها ایجاد خواهیم کرد:

برای دستیابی به این هدف، [app-01.html] را در [app-09.html] کپی کرده و خطوط زیر را اضافه می‌کنیم:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body>
<div class="container">
  <h1>Rdvmedecins - v1</h1>
  <div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
      <div class="navbar-header">
        <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
          <span class="sr-only">Toggle navigation</span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
          <span class="icon-bar"></span>
        </button>
        <a class="navbar-brand" href="#">RdvMedecins</a>
      </div>
      <!-- گزینه‌های منو -->
      <div class="collapse navbar-collapse">
        <ul class="nav navbar-nav">
          <li class="active">
            <a href="">
              <span tooltip="Retourne à la page d'accueil" tooltip-placement="bottom">Home</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span tooltip="Affiche l'agenda" tooltip-placement="top">Agenda</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span tooltip="Valide le rendez-vous" tooltip-placement="right">Valider</span>
            </a>
          </li>
          <li class="active">
            <a href="">
              <span tooltip="Annule l'opération en cours" tooltip-placement="left">Annuler</span>
            </a>
          </li>
        </ul>
      </div>
    </div>
  </div>
</div>
<!--هسته Bootstrap JavaScript ================================================== -->
<...
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<!-- اسکریپت محلی -->
<script>
  // --------------------- ماژول Angular
  angular.module("rdvmedecins", ['ui.bootstrap']);
</script>
</body>
</html>

نکات راهنما توسط کتابخانه [angular-ui-bootstrap] ارائه می‌شوند که خود بر کتابخانه [angular] متکی است. خط ۵۰ کتابخانه [angular-ui-bootstrap] را وارد می‌کند. برای پیاده‌سازی کامپوننت‌های کتابخانه [angular-ui-bootstrap]، باید یک ماژول Angular ایجاد کنیم. این کار در خطوط ۵۲–۵۵ انجام می‌شود. این خطوط یک ماژول Angular به نام [rdvmedecins] (پارامتر اول) را تعریف می‌کنند. یک ماژول Angular می‌تواند از ماژول‌های دیگر Angular استفاده کند. این‌ها به عنوان وابستگی‌های ماژول شناخته می‌شوند. این وابستگی‌ها به صورت یک آرایه به عنوان پارامتر دوم تابع [angular.module] ارائه می‌شوند. در اینجا، ماژول با نام [ui.bootstrap] توسط کتابخانه [angular-ui-bootstrap] ارائه می‌شود. این ماژول است که ابزارنکش‌ها را فراهم می‌کند.

خط ۵۴ یک ماژول Angular را تعریف می‌کند. به طور پیش‌فرض، این هیچ تأثیری بر صفحه ندارد. ما با پیوند دادن صفحه به یک ماژول Angular مشخص می‌کنیم که این صفحه باید توسط Angular مدیریت شود. این کار در خط ۲ انجام می‌شود. ویژگی [ng-app='rdvmedecins'] صفحه را به ماژول ایجاد شده در خط ۵۴ پیوند می‌دهد. سپس صفحه توسط Angular تحلیل می‌شود. ویژگی‌های [tooltip] توسط ماژول [ui.bootstrap] شناسایی و پردازش خواهند شد.

سینتکس ابزارک (tooltip) به شرح زیر است:


 <span tooltip="Retourne à la page d'accueil" tooltip-placement="bottom">Home</span>

در بالا، یک ابزارک (tooltip) به متن [Home]: اضافه می‌شود:

  • [tooltip]: متن ابزارک را تعریف می‌کند؛
  • [tooltip-placement]: موقعیت آن را (پایین، بالا، چپ، راست) تعریف می‌کند؛

Angular JS امکان افزودن تگ‌ها یا ویژگی‌های جدید به موارد موجود در زبان HTML را فراهم می‌کند. این گسترش زبان HTML با استفاده از دستورهای Angular انجام می‌شود. در اینجا، ویژگی‌های [tooltip] و [tooltip-placement] ویژگی‌هایی هستند که توسط [angular-ui-bootstrap] ایجاد شده‌اند.

3.6.9. مثال ۹

برای کمک به کاربر در انتخاب تاریخ قرار ملاقات، یک تقویم در اختیار او قرار می‌دهیم:

Image

مانند tooltipها، این تقویم توسط کتابخانه [angular-ui-bootstrap] ارائه می‌شود. برای دستیابی به این نتیجه، ما [app-01.html] را در [app-10.html] کپی کرده و خطوط زیر را اضافه می‌کنیم:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
  ...
<body>
<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <div>
    <pre>Date <em>{{jour | date:'fullDate'}}</em></pre>
    <div class="row">
      <div class="col-md-2">
        <h4>Calendrier</h4>

        <div style="display:inline-block; min-height:290px;">
          <datepicker ng-model="jour" show-weeks="true" class="well"></datepicker>
        </div>

        </div>
      </div>
    </div>
  </div>
</div>
...
<!-- اسکریپت محلی -->
<script>
  // --------------------- ماژول Angular
  angular.module("rdvmedecins", ['ui.bootstrap'])
</script>

</body>
</html>

همانند قبل، صفحه با یک ماژول Angular (خطوط ۲ و ۲۸) مرتبط است. تقویم توسط تگ <datepicker> در خط ۱۶ تعریف شده است که توسط کتابخانه [angular-ui-bootstrap] ارائه می‌شود:

  • [show-weeks='true']: برای نمایش شماره‌های هفته؛
  • [class='well']: برای احاطه کردن تقویم با یک ناحیه خاکستری با گوشه‌های گرد؛
  • [ng-model='jour']: ویژگی‌های [ng-*] ویژگی‌های Angular هستند. ویژگی [ng-model] به داده‌هایی اشاره دارد که در قالب نما قرار خواهند گرفت. وقتی کاربر روی یک تاریخ کلیک می‌کند، آن تاریخ در متغیر مدل [jour] قرار می‌گیرد. این متغیر در خط ۱۰ استفاده می‌شود. سینتکس {{expression}} به یک عبارت متشکل از عناصر مدل اجازه می‌دهد تا ارزیابی شود. در اینجا، {{day}} مقدار متغیر مدل [jour] را نمایش می‌دهد. یکی از ویژگی‌های کلیدی Angular این است که ویو در پاسخ به تغییرات متغیر [jour] به‌طور خودکار به‌روزرسانی می‌شود. بنابراین، هنگامی که کاربر تاریخ‌ها را تغییر می‌دهد، این تغییرات فوراً در خط ۱۰ نمایش داده می‌شوند. به‌طور کلی، این فرآیند به شرح زیر عمل می‌کند:
    • یک نما V با یک مدل M مرتبط است؛
    • Angular مدل M را زیر نظر دارد و هرگاه تغییری در مدل M رخ دهد، نما V را به‌طور خودکار به‌روز می‌کند؛

سینتکس {{day|date}} یک فیلتر نامیده می‌شود. آنچه نمایش داده می‌شود، مقدار [jour] نیست، بلکه مقداری از [jour] است که توسط فیلتری به نام [date] فیلتر شده است. این فیلتر در Angular از پیش تعریف شده است. از آن برای قالب‌بندی تاریخ‌ها استفاده می‌شود. این عبارت پارامترهایی را برای مشخص کردن قالب دلخواه می‌پذیرد. بنابراین، عبارت {{day | date:'fullDate'}} نشان می‌دهد که ما قالب کامل تاریخ را می‌خواهیم، که در این مورد [Friday, June 20, 2014] است، زیرا تقویم به طور پیش‌فرض روی انگلیسی تنظیم شده است. به‌زودی به بحث در مورد بین‌المللی‌سازی آن خواهیم پرداخت.

3.6.10. نتیجه‌گیری

ما عناصر فریم‌ورک CSS Bootstrap را که استفاده خواهیم کرد، معرفی کردیم. این‌ها کامپوننت‌های پسیو (passive) بودند: رویدادهایشان پردازش نمی‌شدند. در نتیجه، کلیک روی دکمه‌ها یا لینک‌ها هیچ کاری انجام نمی‌داد. این رویدادها در جاوااسکریپت مدیریت خواهند شد. امکان استفاده از این زبان بدون کمک فریم‌ورک‌ها وجود دارد، اما همانند سمت سرور، برخی فریم‌ورک‌ها در سمت کلاینت ضروری هستند. این مورد در مورد فریم‌ورک Angular JS صدق می‌کند که رویکرد جدیدی را برای توسعه برنامه‌های جاوااسکریپت که توسط مرورگر اجرا می‌شوند، معرفی می‌کند. اکنون آن را معرفی خواهیم کرد.

3.7. مقدمه‌ای بر Angular JS

اکنون برخی از ویژگی‌های فریم‌ورک Angular JS مورد استفاده در این برنامه را تشریح خواهیم کرد. ما قبلاً با چند مورد از آن‌ها آشنا شده‌ایم:

  • یک صفحه HTML توسط Angular JS پشتیبانی می‌شود اگر یک ماژول به آن متصل باشد:

<html ng-app="rdvmedecins">
  • Angular به شما امکان می‌دهد از طریق دستورالعمل‌ها (directives) تگ‌ها و ویژگی‌های جدید HTML ایجاد کنید:
attributs : ng-app, ng-model, tooltip-placement, tooltip
balises : datepicker
  • Angular به شما امکان می‌دهد فیلترها را ایجاد کنید:
{{jour|date:'fullDate'}}
  • یک نما V مدل M را نمایش می‌دهد. Angular مدل M را زیر نظر دارد و هرگاه در مدل M تغییری رخ دهد، به‌طور خودکار نما V را به‌روزرسانی می‌کند. مقدار یک متغیر در مدل M در نما V با استفاده از:
{{variable}}

ما با بررسی عمیق‌تر پیاده‌سازی الگوی طراحی مدل–نما–کنترل‌کننده در Angular شروع خواهیم کرد. بیایید روابط بین آن‌ها را از منظر معماری به یاد بیاوریم:

  • ویو V1 مدلی را که توسط کنترلر C1 ساخته شده است، نمایش می‌دهد. این مورد اخیر نه تنها مدل M1 بلکه دست‌پردازنده‌های رویداد برای نما V1 را نیز در خود دارد. ما در چرخه‌های ۵، ۸ و ۹ هستیم:
    • [5]: یک رویداد در نمای V1 رخ می‌دهد. این رویداد توسط کنترل‌کننده C1 مدیریت می‌شود؛
    • که وظیفه [6-7] را انجام می‌دهد و سپس مدل M1 [8] را می‌سازد؛
    • [9]: نما V1 مدل جدید M1 را نمایش می‌دهد. همان‌طور که گفته شد، این مرحله نهایی به‌طور خودکار انجام می‌شود. برخلاف سایر چارچوب‌ها، هیچ «پش» (push) صریحی وجود ندارد (C1 قالب M1 را به V1 می‌رساند) یا «پُل» (pull) صریحی (ویوی V1 مدل M1 را از C1 فراخوانی می‌کند). یک پُش ضمنی وجود دارد که توسعه‌دهنده آن را نمی‌بیند؛
    • سپس چرخهٔ ۵، ۸، ۹ از سر گرفته می‌شود؛

3.7.1. مثال ۱: قالب Angular MVC

بیایید به مثال تقویم بازگردیم. ما دستورالعملی را که آن را تولید می‌کند دیده‌ایم:


          <datepicker ng-model="jour" show-weeks="true" class="well"></datepicker>

این دستورالعمل علاوه بر ویژگی‌های نشان‌داده‌شده در بالا، از ویژگی [min-date] نیز پشتیبانی می‌کند که تاریخ earliest قابل انتخاب در تقویم را تعیین می‌کند. این برای ما مفید خواهد بود. وقتی کاربر تاریخ قرار ملاقات را انتخاب می‌کند، باید برابر یا بعد از تاریخ فعلی باشد. بنابراین خواهیم نوشت:


<datepicker ng-model="jour" ... min-date="dateMin"></datepicker>

که در آن [dateMin] یک متغیر در قالب صفحه خواهد بود که روی تاریخ فعلی تنظیم شده است. این کار صفحه زیر را تولید می‌کند:

  • در [1]، تاریخ ۱۹ ژوئن ۲۰۱۴ است. نشانگر نشان می‌دهد که ۱۹ ژوئن قابل انتخاب است؛
  • در [2]، نشانگر نشان می‌دهد که ۱۸ ژوئن قابل انتخاب نیست؛

ما [app-10.html] را در [app-11.html] کپی کرده و تغییرات زیر را اعمال می‌کنیم:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <div>
    <pre>Date <em>{{jour | date:'fullDate' }}</em></pre>
    <div class="row">
      <div class="col-md-2">
        <h4>Calendrier</h4>

        <div style="display:inline-block; min-height:290px;">
          <datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
        </div>
      </div>
    </div>
  </div>
</div>
<!-- هستهٔ Bootstrap JavaScript ================================================== -->
...
<!-- اسکریپت محلی -->
<script>
  // --------------------- ماژول Angular
  angular.module("rdvmedecins", ['ui.bootstrap']);
  // کنترل‌کننده
  angular.module("rdvmedecins")
    .controller('rdvMedecinsCtrl', ['$scope',
      function ($scope) {
        // تاریخ حداقل
        $scope.minDate = new Date();
      }]);

</script>

</body>
</html>

ابتدا اسکریپت محلی را در خطوط 26–37 بررسی کنیم:

  • خط ۲۸: ایجاد ماژول [rdvmedecins] که به ماژول [ui.bootstrap] که تقویم را فراهم می‌کند، وابسته است؛
  • خطوط ۳۰–۳۵: ایجاد یک کنترلر. این کنترلر قالب صفحه ما را در خود نگه می‌دارد. در اینجا هیچ رویدادپرداز (event handler) وجود نخواهد داشت؛
  • خطوط ۳۰–۳۱: کنترل‌کننده [rdvMedecinsCtrl] متعلق به ماژول [rdvmedecins] است. شما می‌توانید هر تعداد کنترل‌کننده که بخواهید به یک ماژول اضافه کنید. در برنامه ما، خواهیم داشت:
    • یک ماژول مدیریت برنامه؛
    • یک کنترل‌کننده برای هر نما؛
  • پارامتر دوم تابع [controller] یک آرایه به شکل ['O1', 'O2', ..., 'On', function(O1, O2, ..., On)] است. پارامتر نهایی، تابعی است که کنترلر را پیاده‌سازی می‌کند. پارامترهای آن اشیایی هستند که Angular JS در اختیار تابع قرار می‌دهد.

بیایید به معماری یک برنامه Angular بازگردیم:

در مثال بالا، کنترلر C1 شامل تمام رویدادپردازها برای نما V1 و همچنین مدل نما M1 است. رویدادپردازها ممکن است برای انجام وظایف خود به یک یا چند سرویس [6] نیاز داشته باشند. همه این‌ها به عنوان پارامتر به سازنده کنترلر پاس داده می‌شوند:

['S1', 'S2', ..., 'Sn', function(S1, S2, ..., Sn)]

خدمات Si تک‌نمونه هستند. Angular برای هر یک یک نمونه واحد ایجاد می‌کند. آن‌ها با نام Si شناسایی می‌شوند. چرا در جدول بالا دو بار ظاهر می‌شوند؟ در محیط تولید، اسکریپت‌های JS کوچک‌سازی (minify) می‌شوند. در طول این فرآیند کوچک‌سازی، جدول بالا به شکل زیر درمی‌آید:

['S1', 'S2', ..., 'Sn', function(a1, a2, ..., an)]

پارامترها نام‌های خود را از دست می‌دهند. با این حال، این‌ها نام سرویس‌ها هستند. بنابراین حفظ این نام‌ها اهمیت دارد. به همین دلیل آن‌ها به صورت رشته‌ای به عنوان پارامترهای پیش از تابع ارسال می‌شوند. رشته‌ها در فرایند کوچک‌سازی تغییر نمی‌کنند. وقتی Angular کنترلر را با استفاده از آرایه جدید می‌سازد، a1 را با S1، a2 را با S2 و غیره جایگزین می‌کند. بنابراین ترتیب پارامترها مهم است. این ترتیب باید با ترتیب سرویس‌هایی که قبل از تعریف تابع قرار دارند مطابقت داشته باشد.

بیایید به تعریف کنترلر [rdvMedecinsCtrl] بازگردیم:


  // کنترلر
  angular.module("rdvmedecins")
    .controller('rdvMedecinsCtrl', ['$scope',
      function ($scope) {
        //حداقل تاریخ
        $scope.minDate = new Date();
}]);
  • خطوط ۳–۴: تنها ابجکتی که به کنترلر تزریق می‌شود، ابجکت $scope است. این یک ابجکت از پیش تعریف‌شده است که مدل M را برای ویوهایی که با کنترلر مرتبط هستند، نشان می‌دهد. برای غنی‌سازی مدل یک ویو، کافی است فیلدهایی را به ابجکت $scope اضافه کنید؛
  • که همین کار در خط ۶ انجام می‌شود. فیلد [minDate] با تاریخ جاری به‌عنوان مقدار آن ایجاد می‌شود؛

ویوی V از این مدل M به شرح زیر استفاده می‌کند:


<body ng-controller="rdvMedecinsCtrl">
<div class="container">
 ...
        <div style="display:inline-block; min-height:290px;">
          <datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
        </div>
...
</div>
...
  • خط ۱: بدنه صفحه از طریق ویژگی [ng-controller] به کنترل‌کننده [rdvMedecinsCtrl] مرتبط می‌شود. این بدان معناست که همه چیز درون تگ <body> از کنترلر [rdvMedecinsCtrl] برای مدیریت رویدادهای خود و بازیابی مدل M خود استفاده خواهد کرد. یک صفحه HTML ممکن است به چندین کنترلر وابسته باشد، چه تو در تو باشند یا نباشند:
<div id='div1' ng-controller='c1'>
    ...
    <div id='div11' ng-controller='c11'>
    ...
    </div>
    ...
    <div id='div12' ng-controller='c12'>
    ...
    </div>
</div>

بالا:

  • محتوای [div1] (خطوط 1–10) قالب M1 را که توسط کنترل‌کننده c1 مدیریت می‌شود، نمایش می‌دهد. تگ‌های این ناحیه ممکن است به دستگیرکننده‌های رویداد از کنترل‌کننده c1 ارجاع دهند؛
  • محتوای [div11] (خطوط ۳–۴) قالب M11 را که توسط کنترل‌کننده c11 مدیریت می‌شود، و همچنین قالب M1 را نمایش می‌دهد. میراث قالب وجود دارد. تگ‌های این ناحیه ممکن است به دستگیرکننده‌های رویداد از هر دو کنترل‌کننده c11 و c1 ارجاع دهند. آنها نباید به قالب M12 از کنترل‌کننده c12 یا دستگیرکننده‌های رویداد آن ارجاع دهند. در واقع، کنترل‌کننده c12 در خطوط ۳–۵ شناسایی نمی‌شود؛
  • خطوط ۷–۹: می‌توانیم استدلالی مشابه آنچه قبلاً استفاده شد را به کار ببریم؛

بیایید به کد تقویم بازگردیم:


<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>

ویژگی [min-date] با مقدار [minDate] از مدل مقداردهی اولیه می‌شود. به‌طور ضمنی، [$scope.minDate]. فیلد همیشه در شیء $scope جستجو می‌شود.

3.7.2. مثال ۲: بومی‌سازی تاریخ‌ها

در حال حاضر، این تقویم برای ما کاربرد چندانی ندارد زیرا تقویم بریتانیا است. می‌توان آن را بومی‌سازی کرد:

  • در [1]، ما یک تقویم فرانسوی داریم؛
  • با [2]، آن را به انگلیسی تغییر می‌دهیم؛
  • با [3]، تقویم انگلیسی؛

ما صفحه [app-11.html] را در [app-12.html] کپی می‌کنیم، سپس دومی را به شرح زیر اصلاح می‌کنیم:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
  ...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <pre>Date <em>{{jour | date:'fullDate' }}</em></pre>
  <div class="row">
    <!-- تقویم-->
    <div class="col-md-4">
      <h4>Calendrier</h4>

      <div style="display:inline-block; min-height:290px;">
        <datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
      </div>
    </div>
    <!-- زبان‌ها -->
    <div class="col-md-2">
      <div class="btn-group" dropdown is-open="isopen">
        <button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
          Langues<span class="caret"></span>
        </button>
        <ul class="dropdown-menu" role="menu">
          <li><a href="" ng-click="setLang('fr')">Français</a></li>
          <li><a href="" ng-click="setLang('en')">English</a></li>
        </ul>
      </div>
    </div>
  </div>
</div>
...
<script type="text/javascript" src="rdvmedecins.js"></script>
</body>
</html>

تغییرات اندکی وجود دارد. ما صرفاً خطوط 21–31 را برای فهرست کشویی زبان اضافه کرده‌ایم. برای اولین بار، در خطوط 27–28 با یک رویدادپرداز مواجه می‌شویم:

  • خط ۲۷: ویژگی [ng-click] یک ویژگی Angular است که مشخص می‌کند هنگام کلیک بر روی عنصری که این ویژگی را دارد، کدام رویدادپرداز باید اجرا شود. در اینجا، تابع [$scope.setLang('fr')] اجرا خواهد شد. این تابع تقویم را روی زبان فرانسوی تنظیم می‌کند؛
  • خط ۲۸: در اینجا، تقویم را روی حالت انگلیسی تنظیم می‌کنیم؛
  • خط ۳۵: از آنجایی که جاوااسکریپت کنترلر نسبتاً حجیم است، آن را در فایلی به نام [rdvmedecins.js] قرار می‌دهیم؛

Angular با استفاده از ماژولی به نام [ngLocale]، محلی‌سازی ویوها را مدیریت می‌کند. بنابراین تعریف ماژول ما [rdvmedecins] به شرح زیر خواهد بود:


  // --------------------- ماژول Angular
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale']);

در خط ۲، مهم است که وابستگی‌ها را فراموش نکنید، زیرا پیام‌های خطای Angular گاهی اوقات مبهم هستند. بنابراین، نادیده گرفتن یک وابستگی به ویژه دشوار برای تشخیص است. در اینجا، ما یک وابستگی جدید به ماژول [ngLocale] داریم.

به‌طور پیش‌فرض، Angular تنها به محلی‌سازی تاریخ‌ها، اعداد و غیره که دارای گونه‌های محلی هستند می‌پردازد. این فریم‌ورک به بین‌المللی‌سازی متن نمی‌پردازد. برای این کار، از کتابخانه [angular-translate] استفاده خواهیم کرد. محلی‌سازی توسط کتابخانه [angular-i18n] انجام می‌شود. این کتابخانه به تعداد واریانت‌های تاریخ، اعداد و غیره، فایل دارد.

  

برای تقویم فرانسوی، از فایل [angular-locale_fr-fr.js] و برای تقویم انگلیسی، از فایل [angular-locale_en-us.js] استفاده خواهیم کرد. بیایید برای مثال نگاهی به محتویات فایل [angular-locale_fr-fr.js] بیندازیم:


''use strict';
angular.module("ngLocale", [], ["$provide", function($provide) {
var PLURAL_CATEGORY = {ZERO: "zero", ONE: "one", TWO: "two", FEW: "few", MANY: "many", OTHER: "other"};
$provide.value("$locale", {
  "DATETIME_FORMATS": {
    "AMPMS": [
      "AM",
      "PM"
    ],
    "DAY": [
      "dimanche",
      "lundi",
      "mardi",
      "mercredi",
      "jeudi",
      "vendredi",
      "samedi"
    ],
    "MONTH": [
      "janvier",
      "f\u00e9vrier",
      "mars",
      "avril",
      "mai",
      "juin",
      "juillet",
      "ao\u00fbt",
      "septembre",
      "octobre",
      "novembre",
      "d\u00e9cembre"
    ],
    "SHORTDAY": [
      "dim.",
      "lun.",
      "mar.",
      "mer.",
      "jeu.",
      "ven.",
      "sam."
    ],
    "SHORTMONTH": [
      "janv.",
      "f\u00e9vr.",
      "mars",
      "avr.",
      "mai",
      "juin",
      "juil.",
      "ao\u00fbt",
      "sept.",
      "oct.",
      "nov.",
      "d\u00e9c."
    ],
    "fullDate": "EEEE d MMMM y",
    "longDate": "d MMMM y",
    "medium": "d MMM y HH:mm:ss",
    "mediumDate": "d MMM y",
    "mediumTime": "HH:mm:ss",
    "short": "dd/MM/yy HH:mm",
    "shortDate": "dd/MM/yy",
    "shortTime": "HH:mm"
  },
  "NUMBER_FORMATS": {
    "CURRENCY_SYM": "\u20ac",
    "DECIMAL_SEP": ",",
    "GROUP_SEP": "\u00a0",
    "PATTERNS": [
      {
        "gSize": 3,
        "lgSize": 3,
        "macFrac": 0,
        "maxFrac": 3,
        "minFrac": 0,
        "minInt": 1,
        "negPre": "-",
        "negSuf": "",
        "posPre": "",
        "posSuf": ""
      },
      {
        "gSize": 3,
        "lgSize": 3,
        "macFrac": 0,
        "maxFrac": 2,
        "minFrac": 2,
        "minInt": 1,
        "negPre": "(",
        "negSuf": "\u00a0\u00a4)",
        "posPre": "",
        "posSuf": "\u00a0\u00a4"
      }
    ]
  },
  "id": "fr-fr",
  "pluralCat": function (n) {  if (n >= 0 && n <= 2 && n != 2) {   return PLURAL_CATEGORY.ONE;  }  return PLURAL_CATEGORY.OTHER;}
});
}]);

در اینجا می‌توانیم عناصر مورد نیاز برای ایجاد یک تقویم فرانسوی را ببینیم:

  • خطوط ۱۰–۱۸: جدول روزهای هفته؛
  • خطوط ۱۹–۳۲: جدول ماه‌های سال؛
  • خطوط ۳۳–۴۱: جدول روزهای هفته به صورت مخفف؛
  • خطوط ۴۲–۵۵: جدول ماه‌های سال به صورت مخفف؛
  • خطوط ۵۶–۶۳: قالب‌های تاریخ و زمان. خط ۶۲ قالب «dd/mm/yy» مورد استفاده برای تاریخ‌های فرانسوی را نشان می‌دهد؛
  • خطوط ۶۵–۹۵: اطلاعاتی در مورد قالب‌بندی اعداد. این بخش در اینجا مرتبط نیست؛
  • خط ۹۶: شناسه‌ی «fr-fr» برای لوکال فایل (fr-fr: فرانسوی (فرانسه)، fr-ca: فرانسوی (کانادا) و غیره)

در فایل [angular-locale_en-us.js]، دقیقاً همان موضوع را داریم، اما این بار برای نسخهٔ انگلیسی USA (en-us).

کد بالا چندان خوانا نیست. با بررسی دقیق‌تر می‌بینیم که تمام این کد متغیر [$locale] را در خط ۴ تعریف می‌کند. با تغییر مقدار این متغیر است که به بین‌المللی‌سازی تاریخ‌ها، اعداد، ارز و غیره دست می‌یابیم. جالب اینجاست که Angular اجازه نمی‌دهد مقدار متغیر [$locale] در زمان اجرا تغییر کند. این متغیر با وارد کردن فایل برای لوکال مورد نظر، یک‌بار برای همیشه تعریف می‌شود:


<script type="text/javascript" src="bower_components/angular-i18n/angular-locale_fr-fr.js"></script>

وارد کردن تمام فایل‌ها برای لوکال‌های مورد نظر فایده‌ای ندارد، زیرا هر فایل، همانطور که دیدیم، تنها یک کار انجام می‌دهد: تعریف متغیر [$locale]. آخرین فایل وارد شده اولویت دارد و در آن صورت هیچ راهی برای تغییر لوکال وجود ندارد.

در حین جستجو در وب برای یافتن راه‌حلی برای این مشکل، نتوانستم چیزی پیدا کنم. من یک راه‌حل را اینجا پیشنهاد می‌کنم: [https://github.com/stahe/angular-ui-bootstrap-datepicker-with-locale-updated-on-the-fly]. ایده این است که لوکیشن‌های مختلفی را که نیاز داریم در یک دیکشنری قرار دهیم. این همان جایی است که وقتی نیاز به تغییرشان داشته باشیم، از آنجا بازیابی‌شان خواهیم کرد. کد جاوااسکریپت برای [rdvmedecins.js] ساختار زیر را دارد:

 

اگر تعاریف locale را که ۲۰۰ خط (خطوط ۱۵ تا ۲۱۵ در بالا) را اشغال کرده‌اند، حذف کنیم، کد ساده می‌شود:

  • خط ۶: ماژول [rdvmedecins] و وابستگی‌های آن را تعریف می‌کند؛
  • خطوط ۸–۱۰: کنترل‌کننده صفحه [rdvMedecinsCtrl] را تعریف می‌کند؛
  • خط ۹: سازندهٔ کنترلر دو پارامتر می‌گیرد:
    • $scope: برای ایجاد قالب نما؛
    • $locale: متغیری که مدیریت بومی‌سازی تقویم را بر عهده دارد. این متغیر است که هنگام تغییر زبان‌ها باید تغییر داده شود؛
  • خط ۱۳: متغیر [minDate] در قالب با تاریخ امروز مقداردهی اولیه می‌شود؛
  • خط ۱۵: دیکشنری [locales] را تعریف می‌کند. توجه داشته باشید که ما [$scope.locales] را ننوشته‌ایم. متغیر [locales] در واقع بخشی از قالب ارائه شده به ویو نیست؛
  • خطوط ۱۵–۲۱۵: یک دیکشنری {'fr':locale-fr-fr, 'en':locale-en-us} را تعریف می‌کنند. مقادیر [locale-fr-fr] و [locale-en-us] به ترتیب از فایل‌های JS، [angular-locale_fr-fr.js] و [angular-locale_en-us.js] گرفته شده‌اند. سخت‌ترین بخش، اطمینان از این است که با این تعداد زیاد پرانتز در این فرهنگ لغت اشتباه نکنید...
  • خط ۲۱۷: متغیر $locale با locales['fr']، یعنی نسخه فرانسوی لوکال، مقداردهی اولیه می‌شود. شما نمی‌توانید صرفاً [$locale=locales['fr']] را بنویسید، که آدرس locales['fr'] را به $locale اختصاص می‌دهد. شما باید یک کپی مقادیر انجام دهید. این کار را می‌توان با استفاده از تابع از پیش تعریف‌شده [angular.copy] انجام داد؛
  • خط ۲۱۹: متغیر [jour] در قالب با تاریخ امروز مقداردهی اولیه می‌شود. این بدان معناست که تقویم با این تاریخ نمایش داده خواهد شد؛
  • خطوط ۲۲۳–۲۳۰: رویدادپرداز را تعریف کنید که هنگام تغییر زبان فراخوانی می‌شود. به نحویات توجه کنید:
$scope.nom_fonction=function(param1, param2, ...){...}

برای تعریف یک رویدادپرداز به نام [nom_fonction]، که پارامترهای [param1, param2, ...] را می‌پذیرد؛

بیایید کد HTML را از لیست کشویی به یاد بیاوریم:


    <!--زبان‌ها -->
    <div class="col-md-2">
      <div class="btn-group" dropdown is-open="isopen">
        <button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
          Langues<span class="caret"></span>
        </button>
        <ul class="dropdown-menu" role="menu">
          <li><a href="" ng-click="setLang('fr')">Français</a></li>
          <li><a href="" ng-click="setLang('en')">English</a></li>
        </ul>
      </div>
</div>
  • خط ۸: انتخاب زبان فرانسوی باعث فراخوانی [setLang('fr')] می‌شود؛
  • خط ۹: انتخاب انگلیسی باعث فراخوانی [setLang('en')] می‌شود؛
  • خط ۳: ویژگی [is-open] یک مقدار بولی است که کنترل می‌کند فهرست کشویی باز (true) یا بسته (false) باشد. این ویژگی با متغیر [isopen] از مدل نما (view model) مقداردهی اولیه می‌شود؛

بیایید به کد مربوط به [rdvmedecins.js] بازگردیم:

  • خط ۲۲۵: ما مقدار متغیر [$locale] را به مقدار مناسب از فرهنگ لغت [locales] تنظیم می‌کنیم؛
  • خط ۲۲۷: ما بیان کرده‌ایم که وقتی مدل M یک نما V تغییر می‌کند، نما V به‌طور خودکار با مدل جدید تازه می‌شود. در خط ۲۲۵، ما مقدار متغیر [$locale] را تغییر دادیم که بخشی از مدل M نمایش‌داده‌شده توسط نما V نیست. ما باید راهی پیدا کنیم تا این مدل M را تغییر دهیم تا تقویم بازطراحی شده و از لوکال جدید آن استفاده کند. در اینجا، متغیر [jour] را در مدل تقویم تغییر می‌دهیم. ما آن را با یک نشانگر جدید (new) که به تاریخی یکسان با تاریخ نمایش‌داده‌شده فعلی اشاره می‌کند، مقداردهی اولیه می‌کنیم. [$scope.jour.getTime()] تعداد میلی‌ثانیه‌های سپری‌شده بین ۱ ژانویه ۱۹۷۰ و تاریخ نمایش‌داده‌شده توسط تقویم است. با استفاده از این عدد، یک تاریخ جدید بازسازی می‌کنیم. ما البته به همان تاریخ خواهیم رسید و تقویم در همان تاریخی که نمایش می‌داد باقی خواهد ماند. با این حال، مقدار [$scope.jour] – که در واقع یک نشانگر است – تغییر خواهد کرد و تقویم دوباره بارگذاری می‌شود؛
  • خط ۲۲۹: ما مقدار متغیر [isopen] را در قالب روی false تنظیم می‌کنیم. این متغیر یکی از ویژگی‌های لیست کشویی را کنترل می‌کند:

<div class="btn-group" dropdown is-open="isopen">
    <button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
          Langues<span class="caret"></span>
    </button>
...
</div>

همان‌طور که در خط ۱ بالا نشان داده شده است، ویژگی [is-open] به false تغییر خواهد کرد که باعث بسته شدن لیست کشویی می‌شود.

3.7.3. مثال ۳: بومی‌سازی متن

بیایید به بومی‌سازی تقویم بازگردیم:

در [3] می‌بینیم که تقویم به زبان انگلیسی است، اما متن در [Calendrier, Langues] نیست. به‌طور پیش‌فرض، Angular ابزاری برای بومی‌سازی پیام‌ها ارائه نمی‌دهد. در اینجا از کتابخانه [angular-translate] (https://github.com/angular-translate/angular-translate) استفاده خواهیم کرد.

ما مثال زیر را توسعه خواهیم داد:

  • در [1]، نمای فرانسوی؛
  • به [2]، نمای انگلیسی؛

بیایید به پیکربندی مورد نیاز برای بین‌المللی‌سازی نگاهی بیندازیم. اسکریپت [rdvmedecins.js] به شرح زیر اصلاح شده است:


  // --------------------- ماژول Angular
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale', 'pascalprecht.translate']);
//پیکربندی i18n
angular.module("rdvmedecins")
  .config(['$translateProvider', function ($translateProvider) {
    // پیام‌های فرانسوی
    $translateProvider.translations("fr", {
      'msg_header': 'مطب پزشکی<br/>پزشکان همکار',
      'msg_langues': 'زبان‌ها',
      'msg_agenda': 'دفترچه خاطرات {{title}} {{first name}} {{surname}} <br/> در {{day}}',
      'msg_calendrier': 'تقویم',
      'msg_jour': 'روز انتخاب‌شده: ',
      'msg_meteo': "امروز باران می‌بارد..."
    });
    // پیام‌های انگلیسی
    $translateProvider.translations("en", {
      'msg_header': 'پزشکان مرتبط',
      'msg_langues': 'زبان‌ها',
      'msg_agenda': "یادداشت‌های روزانهٔ {{title}} {{first name}} {{surname}}<br/> در {{day}}"،
      'msg_calendrier': 'تقویم',
      'msg_jour': 'روز انتخاب‌شده: ',
      'msg_meteo': 'امروز، باران خواهد بارید...'
    });
    //زبان پیش‌فرض
    $translateProvider.preferredLanguage("fr");
}]);
  • خط ۲: اولین تغییر افزودن یک وابستگی جدید است. بین‌المللی‌سازی برنامه به ماژول Angular با شناسه [pascalprecht.translate] نیاز دارد؛
  • خطوط ۵–۲۶: این خطوط تابع [config] از ماژول [rdvmedecins] را تعریف می‌کنند. هنگامی که یک برنامه Angular راه‌اندازی می‌شود، فریم‌ورک تمام سرویس‌های مورد نیاز برنامه را، هم سرویس‌های از پیش تعریف‌شده Angular و هم آن‌هایی که توسط کاربر تعریف شده‌اند، نمونه‌سازی می‌کند. در این مرحله، ما هیچ سرویسی را تعریف نکرده‌ایم. تابع [config] در ماژول یک برنامه، قبل از نمونه‌سازی هرگونه سرویس اجرا می‌شود. از آن می‌توان برای تعریف اطلاعات پیکربندی برای سرویس‌هایی که در ادامه نمونه‌سازی می‌شوند، استفاده کرد. در اینجا، از تابع [config] برای تعریف پیام‌های بین‌المللی‌سازی‌شده برنامه استفاده خواهد شد؛
  • خط ۵: پارامتر تابع [config] یک آرایه ['O1', 'O2', ..., 'On', function(O1, O2, ..., On)] است که در آن Oi یک شیء شناخته‌شده ارائه شده توسط Angular است. در اینجا، شیء [$translateProvider] توسط ماژول [pascalprecht.translate] فراهم می‌شود. [function] تابعی است که برای پیکربندی برنامه اجرا می‌شود؛
  • خطوط ۷–۱۴: تابع [$translateProvider.translations] دو پارامتر می‌گیرد:
    • پارامتر اول یک کلید زبان است. می‌توانید آن را به هر چیزی که می‌خواهید تنظیم کنید. در اینجا، ما برای ترجمه‌های فرانسوی «fr» (خط ۷) و برای ترجمه‌های انگلیسی «en» (خط ۱۶) را تنظیم کرده‌ایم،
    • دومی لیست ترجمه‌ها به شکل یک دیکشنری است {'key1':'msg1', 'key2':'msg2', ...};
  • خطوط ۷–۱۴: پیام‌های فرانسوی؛
  • خطوط ۱۶–۲۳: پیام‌های انگلیسی؛
  • خط ۲۵: متد [preferredLanguage] زبان پیش‌فرض را تنظیم می‌کند. پارامتر آن یکی از آرگومان‌هایی است که به عنوان اولین پارامتر تابع [$translateProvider.translations] استفاده می‌شود، بنابراین در اینجا این مقدار یا 'fr' (خط ۷) یا 'en' (خط ۱۶) است؛
  • توجه کنید که سه نوع پیام وجود دارد:
    • پیام‌هایی بدون پارامتر یا عناصر HTML (خطوط 9، 11، 12، …)،
    • پیام‌هایی با عناصر HTML (خطوط ۸، ۱۰، …)،
    • پیام‌های دارای پارامتر (خطوط ۱۰، ۱۹)؛

اکنون [app-11.html] را در [app-12.html] کپی کرده و تغییرات زیر را اعمال می‌کنیم:


<div class="container">
  <!-- یک متن اول شامل عناصر HTML -->
  <h3 class="alert alert-info" translate="{{'msg_header'}}"></h3>
  <!-- یک متن دوم با پارامترها -->
  <h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
  <!-- متن سوم ترجمه‌شده توسط کنترلر -->
  <h3 class="alert alert-danger">{{msg2}}</h3>

  <pre>{{'msg_jour'|translate}}<em>{{jour | date:'fullDate' }}</em></pre>
  <div class="row">
    <!-- تقویم-->
    <div class="col-md-4">
      <h4>{{'msg_calendrier'|translate}}</h4>

      <div style="display:inline-block; min-height:290px;">
        <datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"></datepicker>
      </div>
    </div>
    <!-- زبان‌ها -->
    <div class="col-md-2">
      <div class="btn-group" dropdown is-open="isopen">
        <button type="button" class="btn btn-primary dropdown-toggle" style="margin-top: 30px">
          {{'msg_langues'|translate}}<span class="caret"></span>
        </button>
        <ul class="dropdown-menu" role="menu">
          <li><a href="" ng-click="setLang('fr')">Français</a></li>
          <li><a href="" ng-click="setLang('en')">English</a></li>
        </ul>
      </div>
    </div>
  </div>
</div>
  • ترجمه‌ها در خطوط ۳، ۵، ۹، ۱۳ و ۲۳ انجام می‌شوند؛
  • سه نحوی قابل تشخیص هستند:
    • سینتکس [translate={{'msg_key'}}] (خط ۳)، که در آن [msg_key] یکی از کلیدهای یک فرهنگ لغت ترجمه است. این سینتکس برای پیام‌هایی با یا بدون عناصر HTML مناسب است اما برای پیام‌هایی که دارای پارامتر هستند، مناسب نیست؛
    • سینتکس [translate={{'msg_key'}} translate-values={{dictionnaire]}}] (خط ۵)، که برای پیام‌هایی با یا بدون عناصر HTML و با پارامترها مناسب است؛
    • سینتکس [{{'msg_key'|translate}}] (خطوط 9، 13، 23) برای پیام‌هایی بدون پارامتر و بدون عناصر HTML مناسب است؛

بیایید به پیام‌های مختلف در این نما نگاهی بیندازیم:

ligne
français
anglais
3
مطب پزشکی<br/>پزشکان همکار
پزشکان همکار
13
تقویم
تقویم
23
زبان‌ها
زبان‌ها
9
روز انتخاب‌شده:
روز انتخاب‌شده:

اکنون به خط ۵ نگاه کنیم:


<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>

توجه کنید که [msg.text] و [msg.model] در گیومهٔ تکی قرار نگرفته‌اند. این‌ها رشته‌های کاراکتری نیستند بلکه عناصر قالب هستند:

  • msg.text: کلید پیام پارامتریک مورد استفاده را تعریف می‌کند؛
  • msg.model: فرهنگ لغت ارائه‌دهنده مقادیر پارامتر است؛

نام فیلدها [text, model] می‌تواند هر چیزی باشد. در کنترلر نما [rdvMedecinsCtrl]، شیء [msg] به شرح زیر تعریف شده است:

Image

  • خط ۲۴۵: تعریف شیء [msg]؛
  • خط ۲۴۵: فیلد [text] دارای مقدار کلید [msg_agenda] است که با دو مقدار مرتبط است:
    • یادداشت‌های روزانهٔ {{title}} {{first name}} {{surname}}<br/>در {{day}} در فرهنگ لغت انگلیسی؛
    • دفترچه خاطرات {{title}} {{first name}} {{surname}}<br/> در {{day}} در فرهنگ لغت انگلیسی؛

بنابراین پیام نمایش داده شده دارای چهار پارامتر است: [titre, prenom, nom, jour];

  • خط 245: فیلد [model] یک فرهنگ لغت است که برای این چهار پارامتر مقداری را تعیین می‌کند. مشکلی با پارامتر [jour] وجود دارد. ما می‌خواهیم نام کامل روز را نمایش دهیم. این نام بسته به اینکه روز به فرانسوی یا انگلیسی باشد، متفاوت است. بنابراین از فیلتر [date] استفاده می‌کنیم که قبلاً در ویو به شکل {{ day | date:'fullDate'}} به کار رفته است. هر فیلتری را می‌توان در کد جاوااسکریپت به شکل $filter('filter')(value, options) استفاده کرد، که در آن $filter یک شیء از پیش تعریف‌شده در Angular است و 'filter' نام فیلتر است؛
  • خطوط ۳۳–۳۴: شیء از پیش تعریف‌شده $filter به‌عنوان پارامتر به کنترلر ارسال می‌شود، که امکان استفاده از آن را در خط ۲۴۵ فراهم می‌کند؛

بیایید به یک خط دیگر در نمای نمایش‌داده‌شده بازگردیم:


  <!-- متن سوم ترجمه‌شده توسط کنترلر -->
<h3 class="alert alert-danger">{{msg2}}</h3>

تمام ترجمه‌های قبلی در ویو با استفاده از ویژگی‌های ماژول [pascalprecht.translate] انجام شده‌اند. می‌توان این ترجمه را در سمت سرور نیز انجام داد. این کاری است که در اینجا انجام شده است. کنترلر (خط 247 در تصویر بالا) شامل کد زیر است:


$scope.msg2 = $filter('translate')('msg_meteo');

از همان نحوی که برای فیلتر «تاریخ» استفاده می‌شود، استفاده می‌گردد، زیرا «translate» نیز یک فیلتر است. در اینجا، ما درخواستی برای پیام کلیدی «msg_meteo» داریم.

بیایید بررسی کنیم که تغییرات زبان چگونه انجام می‌شود. ما دیده‌ایم که تابع پیکربندی [config] برای ماژول [rdvmedecins]، زبان فرانسوی را به عنوان زبان پیش‌فرض تعیین کرده است (خط ۹ در زیر):


//پیکربندی i18n
angular.module("rdvmedecins")
  .config(['$translateProvider', function ($translateProvider) {
    // پیام‌های فرانسوی
    $translateProvider.translations("fr", {...});
    // پیام‌های انگلیسی
    $translateProvider.translations("en", {...});
    // زبان پیش‌فرض
    $translateProvider.preferredLanguage("fr");
}]);

همچنین باید توجه داشت که لوکال پیش‌فرض نیز فرانسوی بود. در инициализация کنترلر [rdvmedecins]، موارد زیر نوشته شده بود:


// محلی را روی فرانسوی تنظیم کنید
angular.copy(locales['fr'], $locale);
  • خط ۲: [locales] یک فرهنگ لغت است که ما ایجاد کرده‌ایم؛

هیچ ارتباطی بین بین‌المللی‌سازی پیام‌های ارائه‌شده توسط ماژول [pascalprecht.translate] و محلی‌سازی تاریخ که ما پیاده‌سازی کرده‌ایم وجود ندارد. مورد دوم از متغیری به نام $locale استفاده می‌کند که توسط ماژول [pascalprecht.translate] به کار گرفته نمی‌شود. این دو فرایند به‌طور مستقل از یکدیگر عمل می‌کنند.

اکنون زمان آن است که ببینیم وقتی کاربر زبان را تغییر می‌دهد چه اتفاقی می‌افتد:

Image

  • خط ۲۵۱: هنگامی که زبان تغییر می‌کند، تابع [setLang] با یکی از دو پارامتر ['fr','en'] فراخوانی می‌شود؛
  • خطوط ۲۵۲–۲۵۷: این موارد قبلاً توضیح داده شده‌اند – آنها متغیر تقویم [$locale] را تغییر می‌دهند. این کار تأثیری بر زبان ترجمه‌ها ندارد؛
  • خط ۲۵۹: زبان ترجمه تغییر می‌کند. از شیء [$translate] که توسط ماژول [pascalprecht.translate] فراهم شده است، استفاده می‌شود. برای این کار، باید آن را به کنترلر تزریق کرد:

// کنترل‌کننده
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', '$locale', '$translate', '$filter',
function ($scope, $locale, $translate, $filter) {

در خطوط ۳ و ۴ بالا، شیء $translate تزریق می‌شود؛

  • پارامتر 'lang' تابع [$translate.use(lang)] باید روی یکی از کلیدهای استفاده شده در پیکربندی به عنوان اولین پارامتر تابع [$translateProvider.translations] تنظیم شود، یعنی یا 'fr' یا 'en'. در واقع همینطور است؛
  • خط ۲۶۱: مقدار `msg2` مجدداً محاسبه می‌شود. چرا؟ در ویو، پس از تغییر زبان انجام‌شده در خط ۲۵۹، تمام ویژگی‌های موجود `[translate]` مجدداً ارزیابی خواهند شد. این امر برای عبارت {{msg2}} صدق نمی‌کند، زیرا این عبارت فاقد این ویژگی است. بنابراین، مقدار جدید آن در کنترلر محاسبه می‌شود. این کار باید پس از تغییر زبان در خط 259 انجام شود تا از زبان جدید برای محاسبه [msg2] استفاده شود؛

اگر همین‌جا متوقف شویم، دو ناهنجاری مشاهده می‌کنیم:

  1. در [1]، روز به زبان فرانسوی باقی می‌ماند در حالی که بقیه نما به زبان انگلیسی است؛
  2. در [2] و [3]، تاریخ انتخاب‌شده ۲۴ ژوئن است، در حالی که در [1]، تاریخ همچنان روی ۲۰ ژوئن تنظیم باقی می‌ماند؛

بیایید قبل از جستجوی راه‌حل‌ها این موضوع را توضیح دهیم. پیام [1] در کنترلر با استفاده از کد زیر تولید می‌شود:


      $scope.msg = {'text': 'msg_agenda', 'model': {'titre': 'Mme', 'prenom': 'Laure', 'nom': 'PELISSIER', 'jour': $filter('date')($scope.jour, 'fullDate')}};

و با استفاده از کد زیر در ویو نمایش داده می‌شود:


  <h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>

ناهنجاری [1] (روز به زبان فرانسوی باقی می‌ماند در حالی که بقیهٔ نما به زبان انگلیسی است) به نظر می‌رسد نشان دهد که در حالی که ویژگی [translate] در طول تغییر زبان مجدداً ارزیابی می‌شود، ویژگی [translate-values] ارزیابی نشد. بنابراین می‌توانیم این ارزیابی را در کنترلر اجباری کنیم:


      // ------------------- پردازشگر رویداد
      // تغییر زبان
      $scope.setLang = function (lang) {
...
        // به‌روزرسانی msg2
        $scope.msg2 = $filter('translate')('msg_meteo');
        // و تاریخ پیام
        $scope.msg.model.jour = $filter('date')($scope.jour, 'fullDate');
};

هر بار که زبان تغییر می‌کند، خط ۸ بالا روز نمایش‌داده‌شده را مجدداً ارزیابی می‌کند. این در عمل مشکل اول را حل می‌کند اما مشکل دوم را نه (روز نمایش‌داده‌شده در پیام وقتی روز دیگری از تقویم انتخاب می‌شود تغییر نمی‌کند). دلیل این رفتار به شرح زیر است. پیام در ویو با استفاده از کد زیر نمایش داده می‌شود:


<h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>

ویوی نمایش‌داده‌شده V تنها در صورتی تغییر می‌کند که قالب آن M تغییر کند. با این حال، در این مورد، انتخاب یک روز جدید در تقویم، رویدادی را فعال می‌کند که پردازش نمی‌شود، به این معنی که قالب [msg] تغییر نمی‌کند و در نتیجه ویو نیز تغییر نمی‌کند. ما تعریف تقویم را در داخل ویو به شرح زیر به‌روزرسانی می‌کنیم:


<datepicker ng-model="jour" show-weeks="true" class="well" min-date="minDate"
ng-click="calendarClick()"></datepicker>

در بالا، مشخص می‌کنیم که کلیک روی تقویم باید توسط تابع [$scope.calendarClick] پردازش شود. این به شرح زیر است:

Image

  • خط ۲۶۷: رویدادپرداز برای کلیک‌ها روی تقویم؛
  • خط ۲۶۹: روز نمایش‌داده‌شده از طریق پیام [msg] به‌روزرسانی می‌شود؛

3.7.4. مثال ۴: یک سرویس پیکربندی

بیایید به معماری یک برنامه Angular JS بازگردیم:

در اینجا، ما بر مفهوم سرویس تمرکز خواهیم کرد. این یک مفهوم نسبتاً گسترده است. در حالی که لایه [DAO] فوق به وضوح یک سرویس است، هر شیء Angular می‌تواند به یک سرویس تبدیل شود:

  • یک سرویس از سینتکس مشخصی پیروی می‌کند. آن یک نام دارد و Angular آن را با همان نام شناسایی می‌کند؛
  • یک سرویس می‌تواند توسط Angular به کنترلرها و سرویس‌های دیگر تزریق شود؛

برخی از سرویس‌هایی که قصد داریم در ماژول [rdvmedecins] پیکربندی کنیم، نیاز به پیکربندی دارند. از آنجایی که یک سرویس می‌تواند در سرویس دیگری تزریق شود، وسوسه می‌شود که پیکربندی را در داخل سرویسی که آن را [config] می‌نامیم، انجام دهیم و آن را به سرویس‌ها و کنترلرهایی که نیاز به پیکربندی دارند، تزریق کنیم. اکنون این فرآیند را شرح خواهیم داد.

ما [app-13.html] را به عنوان [app-14.html] کپی کرده و تغییرات زیر را اعمال می‌کنیم:


<div class="container">
  <!-- در حال بررسی پیام در انتظار -->
  <label>
    <input type="checkbox" ng-model="waiting.visible">
    <span>Voir le message d'attente</span>
  </label>

  <!-- پیام در انتظار -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    <h1>{{ waiting.text | translate}}
      <button class="btn btn-primary pull-right" ng-click="waiting.cancel()">
            {{'msg_cancel'|translate}}</button>
      <img src="assets/images/waiting.gif" alt=""/>
    </h1>
  </div>
...
</div>
...
<script type="text/javascript" src="rdvmedecins-02.js"></script>
  • خطوط ۳–۶: یک تیک‌باکس که کنترل می‌کند آیا پیام انتظار در خطوط ۹–۱۵ نمایش داده شود یا خیر. مقدار این تیک‌باکس در متغیر [waiting.visible] در مدل M از نمای V ذخیره می‌شود. این مقدار در صورتی که باکس تیک خورده باشد برابر true و در غیر این صورت برابر false است. این مکانیزم در هر دو جهت عمل می‌کند. اگر متغیر [waiting.visible] را روی true تنظیم کنیم، چک‌باکس تیک‌خورده خواهد شد. یک ارتباط دوطرفه بین نما V و مدل آن M وجود دارد؛
  • خطوط ۹–۱۵: یک پیام انتظار با دکمه‌ای برای لغو انتظار (خط ۱۱);
  • خط ۹: پیام تنها زمانی قابل مشاهده است که متغیر [waiting.visible] روی true تنظیم شده باشد. بنابراین وقتی در خط ۴ تیک را می‌زنیم:
    • مقدار true به متغیر [waiting.visible] اختصاص داده می‌شود (ng-model، خط ۴);
    • از آنجایی که تغییری در مدل M ایجاد شده است، ویو V به طور خودکار مجدداً ارزیابی می‌شود. پیام انتظار سپس نمایش داده خواهد شد (ng-show، خط 9);
    • دلیل این امر نیز زمانی که تیک کادر در خط ۴ برداشته می‌شود، یکسان است: پیام بارگذاری مخفی می‌شود؛
  • خط ۱۰: پیام انتظار ترجمه می‌شود (فیلتر translate);
  • خط ۱۱: وقتی دکمه کلیک می‌شود، متد [waiting.cancel()] اجرا می‌شود (ویژگی ng-click);
  • خط ۱۲: برچسب دکمه ترجمه می‌شود؛
  • خط ۱۹: کد جاوا اسکریپت برنامه در فایل‌های جدید JS و [rdvmedecins-02] قرار داده می‌شود تا کدی که قبلاً نوشته شده و اکنون نیاز به سازماندهی مجدد دارد، از دست نرود؛

این منجر به نمای زیر می‌شود:

  • در [1]، تیک انتخابی نیست؛
  • به [2]، کادر تیک خورده است؛

اسکریپت [rdvmedecins-02] سازماندهی مجدد اسکریپت [rdvmedecins] است:

Image

  • خط ۶: ماژول [rdvmedecins] برنامه؛
  • خطوط ۹–۱۰: تابع پیکربندی برنامه؛
  • خطوط ۳۸–۳۹: سرویس [config
  • خطوط ۲۸۳–۲۸۴: کنترل‌کننده [rdvMedecinsCtrl

قبلاً، ما دیکشنری **locales={&#x27;fr&#x27;:..., &#x27;en&#x27;: ...}** را در کنترلر تعریف کرده بودیم که ۲۰۰ خط طول داشت. این دیکشنری به وضوح یک عنصر پیکربندی است، بنابراین آن را در خطوط ۳۸–۳۹ به سرویس [config] منتقل می‌کنیم. این سرویس به صورت زیر تعریف شده است:

Image

  • خطوط ۳۸–۳۹: یک سرویس با استفاده از تابع [factory] از شیء [angular.module] ایجاد می‌شود. سینتکس این تابع مانند موارد قبلی است: `factory('nom_service', ['O1','O2', ...., 'On', function (O1, O2, ..., On){...}])`, که در آن Oi نام اشیایی هستند که توسط Angular شناخته می‌شوند (پیش‌تعریف‌شده یا ایجاد شده توسط توسعه‌دهنده) و Angular آن‌ها را به عنوان پارامتر در تابع `factory` تزریق می‌کند. از آنجایی که این تابع در اینجا پارامتری ندارد، از یک سینتکس کوتاه‌تر استفاده شده است که آن نیز پذیرفته می‌شود: `factory('nom_service', function (){...})]`;
  • خط ۴۰: تابع [factory] باید سرویس را از طریق ابجکتی که بازمی‌گرداند، پیاده‌سازی کند. این ابجکت است که خود سرویس را تشکیل می‌دهد. به همین دلیل است که به این تابع، فکتوری (کارخانه ساخت ابجکت) گفته می‌شود؛

به طور کلی، کد یک سرویس به شکل زیر است:


Angular.module('nom_module')
  .factory('nom_service',['O1','O2', ...., 'On', function (O1, O2, ..., On){
     // آماده‌سازی سرویس
    ...
     // بازگرداندن شیء پیاده‌سازی‌کننده سرویس
    return {
         // fields
        ...
         // متدها
        ...
        }
});
  • خط ۶: یک شیء JS بازگردانده می‌شود که ممکن است حاوی هم فیلدها و هم متدها باشد. این متدها هستند که سرویس را فراهم می‌کنند؛

در اینجا، سرویس [config] تنها فیلدها را تعریف می‌کند و هیچ متدی ندارد. ما همه چیزهایی را که می‌توان در برنامه پیکربندی کرد، در اینجا قرار می‌دهیم:

  • خطوط ۴۲–۴۷: کلیدهای پیام‌هایی که باید ترجمه شوند؛
  • خطوط ۵۹–۶۲: URL برای برنامه؛
  • خطوط ۶۴–۶۹: URL برای سرویس وب راه دور؛
  • خط ۷۱: فراخوانی HTTP به یک سرویس وب که پاسخ نمی‌دهد؛ این ممکن است زمان زیادی ببرد. در اینجا، حداکثر زمان انتظار برای پاسخ از سرویس وب روی ۱ ثانیه تنظیم شده است. پس از گذشت این زمان، فراخوانی HTTP با شکست مواجه شده و یک استثنای JS پرتاب می‌شود؛
  • خط ۷۳: قبل از هر فراخوانی به سرور، یک وقفه شبیه‌سازی می‌شود که مدت آن در اینجا به میلی‌ثانیه تنظیم شده است. زمان انتظار 0 به معنای عدم وجود انتظار است. برنامه به گونه‌ای طراحی خواهد شد که کاربر بتواند عملی را که آغاز کرده است لغو کند. برای اینکه یک عملیات قابل لغو باشد، باید حداقل چند ثانیه طول بکشد. ما از این انتظار مصنوعی برای شبیه‌سازی عملیات‌های طولانی‌مدت استفاده خواهیم کرد؛
  • خط ۷۵: در حالت [debug=true]، اطلاعات اضافی در نمای فعلی نمایش داده می‌شود. به طور پیش‌فرض، این حالت فعال است. در تولید، این فیلد را روی false تنظیم خواهیم کرد؛
  • خطوط ۷۷–۲۷۸: فرهنگ لغت برای دو منطقه «fr» و «en». این قبلاً در کنترل‌کننده [rdvMedecinsCtrl] بود؛

با این سرویس، کنترل‌کننده [rdvMedecinsCtrl] به شرح زیر تغییر می‌کند:

Image

  • سطور ۲۸۴–۲۸۵: سرویس [config] به کنترلر تزریق می‌شود؛
  • خط ۲۹۰: دیکشنری [locales] اکنون در سرویس [config] یافت می‌شود و دیگر در کنترلر نیست؛
  • خط ۲۹۴: شیء [waiting] که نمایش پیام در حال انتظار را کنترل می‌کند. کلید پیام انتظار در سرویس [config] (میدان متنی) یافت می‌شود. به‌طور پیش‌فرض، پیام انتظار مخفی است (میدان قابل مشاهده). مقدار میدان «cancel» نام تابع در خط ۳۱۶ است. بنابراین این میدان یک متد یا تابع است؛
  • خط ۳۱۶: تابع [cancel] خصوصی است (ما $scope.cancel=function(){} را ننوشته‌ایم). بیایید به کد دکمه لغو بازگردیم:

<button class="btn btn-primary pull-right" ng-click="waiting.cancel()">

وقتی کاربر روی دکمه لغو کلیک می‌کند، متد [$scope.waiting.cancel()] فراخوانی می‌شود. در نهایت، تابع خصوصی cancel در خط ۳۱۶ اجرا می‌شود. این تابع به سادگی با مقدار دادن false به متغیر مدل [waiting.visible] (خط ۳۱۸)، پیام بارگذاری را مخفی می‌کند؛

3.7.5. مثال ۵: برنامه‌نویسی غیرهمزمان

اکنون سرویس جدیدی را معرفی می‌کنیم که مفهومی جدید به نام برنامه‌نویسی غیرهمزمان را ارائه می‌دهد.

برنامه ما سه سرویس خواهد داشت:

  • [config]: سرویس پیکربندی که همین حالا معرفی کردیم؛
  • [utils]: سرویس روش‌های ابزار. ما دو مورد از آن‌ها را معرفی خواهیم کرد؛
  • [dao]: سرویس دسترسی به سرویس وب رزرو قرار ملاقات. به زودی به آن خواهیم پرداخت؛

ما قصد داریم برنامه زیر را بنویسیم:

  • هدف نمایش بنر [2] برای مدت زمانی است که توسط [1] تعیین می‌شود. این انتظار را می‌توان توسط [3] لغو کرد.

ما [app-01.html] را در [app-15.html] کپی کرده و کد را به شرح زیر تغییر می‌دهیم:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
  <title>RdvMedecins</title>
  ...
</head>
<body ng-controller="rdvMedecinsCtrl">
<div class="container">

  <!-- پیام در انتظار -->
  <div class="alert alert-warning" ng-show="waiting.visible" ng-cloak="">
    <h1>{{ waiting.text | translate}}
      <button class="btn btn-primary pull-right" ng-click="waiting.cancel()">{{'msg_cancel'|translate}}</button>
      <img src="assets/images/waiting.gif" alt=""/>
    </h1>
  </div>

  <!-- فرم -->
  <div class="alert alert-info" ng-hide="waiting.visible">
    <div class="form-group">
      <label for="waitingTime">{{waitingTimeText | translate}}</label>
      <input type="text" id="waitingTime" ng-model="waiting.time"/>
    </div>
    <button class="btn btn-primary" ng-click="execute()">Exécuter</button>
  </div>
</div>
..
<script type="text/javascript" src="rdvmedecins-03.js"></script>
</body>
</html>
  • خط ۱۱: ویژگی [ng-cloak] مانع از نمایش فیلد تا زمانی می‌شود که عبارات Angular آن ارزیابی نشده‌اند. این کار از نمایش گذرای فیلد قبل از ارزیابی ویژگی [ng-show] که در واقع باعث پنهان شدن آن می‌شود، جلوگیری می‌کند؛
  • خط ۲۲: ورودی کاربر (زمان انتظار) در مدل [waiting.time] (ویژگی ng-model) ذخیره خواهد شد؛
  • خط ۲۸: صفحه از یک اسکریپت جدید، [rdvmedecins-03]، استفاده می‌کند؛

اسکریپت [rdvmedecins-03] به شرح زیر است:

Image

  • خط ۶: ماژول Angular که برنامه را مدیریت می‌کند؛
  • خط ۱۰: تابع [config] که برای بین‌المللی‌سازی پیام‌ها استفاده می‌شود؛
  • خط ۴۱: سرویس [config] که توضیح داده‌ایم؛
  • خط ۲۸۶: سرویس [utils] که قصد داریم بسازیم؛
  • خط ۳۱۵: کنترل‌کننده [rdvmedecinsCtrl] که قصد داریم بسازیم؛

ما یک کلید پیام جدید به تابع [config] (خطوط 6، 11) اضافه می‌کنیم:


angular.module("rdvmedecins")
  .config(['$translateProvider', function ($translateProvider) {
    //پیام‌های فرانسوی
    $translateProvider.translations("fr", {
...
      'msg_waiting_time_text': "زمان انتظار: "
    });
    // پیام‌های انگلیسی
    $translateProvider.translations("en", {
...
      'msg_waiting_time_text': "زمان انتظار:"
    });
    //زبان پیش‌فرض
    $translateProvider.preferredLanguage("fr");
}]);

ما یک خط جدید (خط ۶) به سرویس [config] برای این کلید پیام اضافه می‌کنیم:


angular.module("rdvmedecins")
  .factory('config', function () {
    return {
      // پیام‌ها برای بین‌المللی‌سازی
      ...
waitingTimeText: 'msg_waiting_time_text',

سرویس [utils] شامل دو متد است (خطوط ۴، ۱۲):


angular.module("rdvmedecins")
  .factory('utils', ['config', '$timeout', '$q', function (config, $timeout, $q) {
    // نمایش نمایش JSON یک شیء
    function debug(message, data) {
      if (config.debug) {
        var text = data ? message + " : " + angular.toJson(data) : message;
        console.log(text);
      }
    }

    // در حال انتظار
    function waitForSomeTime(milliseconds) {
      // انتظار ناهمزمان به میلی‌ثانیه
      var task = $q.defer();
      $timeout(function () {
        task.resolve();
      }, milliseconds);
      // وظیفه بازگردانده می‌شود
      return task;
    };

    // مثال سرویس
    return {
      debug: debug,
      waitForSomeTime: waitForSomeTime
    }
}]);
  • خط ۲: سرویس [utils] (پارامتر اول) فراخوانی می‌شود. این سرویس به سه سرویس وابسته است: دو سرویس از پیش تعریف‌شده Angular، $timeout و $q، و سرویس config. سرویس [$timeout] امکان اجرای یک تابع پس از گذشت مدت زمان مشخصی را فراهم می‌کند. سرویس [$q] امکان ایجاد وظایف ناهمزمان را فراهم می‌کند؛
  • خط ۴: یک تابع محلی [debug];
  • خط ۱۲: یک تابع محلی [waitForSomeTime];
  • خطوط ۲۳–۲۶: نمونه سرویس [utils]. این یک شیء است که دو متد را در دسترس قرار می‌دهد، متدهای موجود در خطوط ۴ و ۱۲. توجه داشته باشید که فیلدهای این شیء می‌توانند هر نامی داشته باشند. برای یکنواختی، به آنها نام متدهایی که به آنها ارجاع می‌دهند داده شده است؛
  • خطوط ۴–۹: متد [debug] پیامی [message] را به کنسول می‌نویسد و در صورت لزوم، نمایش JSON یک شیء [data] را. این امکان را فراهم می‌کند که اشیاء با هر پیچیدگی‌ای نمایش داده شوند؛
  • خطوط ۱۲–۲۰: متد [waitForSomeTime] یک وظیفه ناهمزمان ایجاد می‌کند که به مدت [milliseconds] میلی‌ثانیه طول می‌کشد؛
  • خط 14: ایجاد یک وظیفه با استفاده از شیء از پیش تعریف‌شده [$q] (https://docs.angularjs.org/api/ng/service/$q). در زیر API برای وظیفه‌ای که در مستندات Angular با نام [deferred] ذکر شده است، آورده شده است:

Image

  • یک وظیفه ناهمزمان [task] توسط دستور [$q.defer()] ایجاد می‌شود؛
  • این کار با استفاده از یکی از دو روش زیر انجام می‌شود:
    • [task.resolve(value)]: که وظیفه را با موفقیت به پایان می‌رساند و مقدار [value] را به کسانی که منتظر تکمیل وظیفه هستند، بازمی‌گرداند؛
    • [task.reject(value)]: که وظیفه را با شکست به پایان می‌رساند و مقدار [value] را به کسانی که در انتظار تکمیل وظیفه هستند بازمی‌گرداند؛

وظیفه [task] می‌تواند به‌روزرسانی‌های دوره‌ای را برای کسانی که در انتظار اتمام آن هستند فراهم کند:

    • [task.notify(value)]: مقدار [value] را به کسانی که در انتظار اتمام وظیفه هستند ارسال می‌کند. وظیفه به اجرا ادامه می‌دهد؛

کسانی که می‌خواهند منتظر پایان وظیفه بمانند از فیلد [promise] وظیفه استفاده می‌کنند:

var promise=[task].promise ;

شیء [promise] دارای API زیر است (http://www.frangular.com/2012/12/api-promise-angularjs.html):

Image

برای رسیدگی به نتایج موفقیت‌آمیز و ناموفق وظایف، می‌نویسیم:

1
2
3
var promise=[task].promise;
promise.then(successCallback, errorCallBack);
promise['finally'](finallyCallback);
  • خط ۱: وعده (promise) وظیفه را بازیابی کنید؛
  • خط ۲: تعریف توابعی که در صورت موفقیت یا شکست اجرا شوند. نیازی به مشخص کردن تابع شکست نیست. تابع [successCallback] تنها در پایان وظیفه [task] اجرا می‌شود، به شرطی که [task.resolve()] با موفقیت انجام شده باشد. تابع [errorCallBack] تنها زمانی اجرا می‌شود که وظیفه [task] شکست خورده باشد ([task.reject()]).
  • خط ۳: تابع را طوری تعریف می‌کنیم که پس از اجرای یکی از دو تابع قبلی اجرا شود. در اینجا، کد مشترک هر دو تابع، [successCallback, errorCallBack]، را قرار می‌دهیم.

بیایید به کد تابع [waitForSomeTime] بازگردیم:


    // انتظار
    function waitForSomeTime(milliseconds) {
      // انتظار ناهمزمان به میلی‌ثانیه
      var task = $q.defer();
      $timeout(function () {
        task.resolve();
      }, milliseconds);
      // وظیفه بازگردانده می‌شود
      return task;
};
  • خط ۴: یک وظیفه ایجاد می‌شود؛
  • خطوط ۵–۷: شیء [$timeout] برای تعریف یک تابع (پارامتر اول) استفاده می‌شود که پس از یک تأخیر معین بیان‌شده به میلی‌ثانیه (پارامتر دوم) اجرا می‌شود. در اینجا، پارامتر دوم تابع [$timeout]، پارامتر method است (خط ۱)؛
  • خط ۶: پس از سپری شدن تأخیر مشخص‌شده توسط [milliseconds]، وظیفه با موفقیت تکمیل می‌شود؛
  • خط ۹: وظیفه [task] بازگردانده می‌شود. باید توجه داشت که خط ۹ بلافاصله پس از تعریف شیء [$timeout] اجرا می‌شود. ما منتظر به پایان رسیدن زمان‌بندی [milliseconds] نمی‌مانیم. بنابراین کد در خطوط ۲ تا ۱۰ در دو زمان مختلف اجرا می‌شود:
    • اولاً، هنگامی که شیء [$timeout] تعریف می‌شود؛
    • بار دوم زمانی که تأخیر [milliseconds] سپری شده است؛

این یک تابع ناهمزمان است: نتیجهٔ آن در زمانی دیرتر از زمان اجرای آن به دست می‌آید.

کد کنترلر که از سرویس [config] استفاده می‌کند به شرح زیر است:


// کنترل‌کننده
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', '$filter',
    function ($scope, utils, config, $filter) {
      // ------------------- inicializه‌سازی مدل
      // انتظار پیام
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
      $scope.waitingTimeText = config.waitingTimeText;
      // وظیفه در حال انتظار
      var task;
      // گزارش‌ها
      utils.debug("libellé temps d'attente", $filter('translate')($scope.waitingTimeText));
      utils.debug("locales['fr']=", config.locales['fr']);

      // اجرای اقدام
      $scope.execute = function () {
        // لاگ
        utils.debug('début', new Date());
        //نمایش پیام در حال انتظار
        $scope.waiting.visible = true;
        // انتظار شبیه‌سازی‌شده
        task = utils.waitForSomeTime($scope.waiting.time);
        //پایان انتظار
        task.promise.then(function () {
          // موفقیت
          utils.debug('fin', new Date());
        }, function () {
          // نابودی
          utils.debug('Opération annulée')
        });
        task.promise['finally'](function () {
          //پایان انتظار در همه موارد
          $scope.waiting.visible = false;
        });

      };

      //لغو صف
      function cancel() {
        // وظیفه انجام شد
        task.reject();
      }
    }]);
  • خط ۳: کنترل‌کننده از سرویس [config] استفاده می‌کند؛
  • خط ۷: فیلد [time] به شیء [$scope.waiting] اضافه شده است. به شیء [$scope.waiting.time] مقدار تایم‌اوت تعریف‌شده توسط کاربر اختصاص داده شده است؛
  • خط ۸: کلید مربوط به پیام انتظار نمایش‌داده‌شده توسط ویو در قالب [$scope.waitingTimeText] قرار می‌گیرد. به طور کلی، هر چیزی که توسط یک ویو V نمایش داده می‌شود باید در شیء [$scope] قرار گیرد؛
  • خط ۱۰: یک متغیر محلی. این متغیر در دسترس نمای V قرار نمی‌گیرد؛
  • خطوط ۱۲–۱۳: استفاده از متد [debug] از سرویس [config]. نتیجه زیر در کنسول نمایش داده می‌شود:
libellé temps d'attente : "Temps d'attente : "
locales['fr']= : {"DATETIME_FORMATS":{"AMPMS":["AM","PM"],"DAY":["dimanche","lundi","mardi","mercredi","jeudi","vendredi","samedi"],"MONTH":["janvier","février","mars","avril","mai","juin","juillet","août","septembre","octobre","novembre","décembre"],"SHORTDAY":["dim.","lun.","mar.","mer.","jeu.","ven.","sam."],"SHORTMONTH":["janv.","févr.","mars","avr.","mai","juin","juil.","août","sept.","oct.","nov.","déc."],"fullDate":"EEEE d MMMM y","longDate":"d MMMM y","medium":"d MMM y HH:mm:ss","mediumDate":"d MMM y","mediumTime":"HH:mm:ss","short":"dd/MM/yy HH:mm","shortDate":"dd/MM/yy","shortTime":"HH:mm"},"NUMBER_FORMATS":{"CURRENCY_SYM":"","DECIMAL_SEP":",","GROUP_SEP":" ","PATTERNS":[{"gSize":3,"lgSize":3,"macFrac":0,"maxFrac":3,"minFrac":0,"minInt":1,"negPre":"-","negSuf":"","posPre":"","posSuf":""},{"gSize":3,"lgSize":3,"macFrac":0,"maxFrac":2,"minFrac":2,"minInt":1,"negPre":"(","negSuf":" ¤)","posPre":"","posSuf":" ¤"}]},"id":"fr-fr"}

در خط ۲، نشانه JSON برای شی locales['fr'] به دست می‌آید.

  • خط ۱۶: متدی که هنگام کلیک کاربر روی دکمه [Executer] اجرا می‌شود؛
  • خط ۱۸: زمان شروع اجرای متد را نمایش می‌دهد؛
  • خط ۲۲: وظیفه [waitForSomeTime] راه‌اندازی می‌شود. ما منتظر اتمام آن نمی‌مانیم. اجرا با خط بعدی، خط ۲۴، ادامه می‌یابد؛
  • خطوط ۲۴–۳۰: توابعی که باید هنگام تکمیل موفقیت‌آمیز وظیفه (خط ۲۶) و در صورت بروز خطا (خط ۲۹) اجرا شوند، تعریف شده‌اند؛
  • خط ۲۶: زمان پایان اجرای متد را نمایش می‌دهد؛
  • خط ۲۹: نمایش می‌دهد که عملیات لغو شده است. این تنها زمانی فعال می‌شود که کاربر دکمه [Annuler] را کلیک کند. دستور در خط ۴۱ سپس وظیفه غیرهمزمان را با یک کد خطا خاتمه می‌دهد؛
  • خطوط ۳۱–۳۴: تابعِ قابل اجرا پس از اجرای یکی از دو تابع قبلی تعریف شده است؛

درک توالی اجرای این کد مهم است. اگر کاربر تأخیر ۳ ثانیه‌ای را تنظیم کند و منتظر بماند:

  • هنگامی که کاربر دکمه [Exécuter] را کلیک می‌کند، تابع [$scope.execute] اجرا می‌شود. خطوط ۱۶ تا ۳۴ بدون انتظار برای ۳ ثانیه اجرا می‌شوند. در پایان این اجرا، نما V با مدل M همگام‌سازی می‌شود. پیام انتظار نمایش داده می‌شود (ng-show=$scope.waiting.visible=true، خط ۲۰) و فرم پنهان می‌شود (ng-hide=$scope.waiting.visible=true، خط ۲۰)؛
  • از این نقطه به بعد، کاربر می‌تواند دوباره با نما تعامل داشته باشد. به طور خاص، آنها می‌توانند روی دکمه [Annuler] کلیک کنند؛
  • اگر این کار را انجام ندهند، پس از ۳ ثانیه، تابع [$timeout] (به خطوط ۵–۷ زیر مراجعه کنید) اجرا می‌شود:

    // در حال انتظار
    function waitForSomeTime(milliseconds) {
      // انتظار ناهمزمان برای میلی‌ثانیه میلی‌ثانیه
      var task = $q.defer();
      $timeout(function () {
        task.resolve();
      }, milliseconds);
      // وظیفه بازگردانده می‌شود
      return task;
};
  • بنابراین، پس از ۳ ثانیه، کد اجرا می‌شود. این کد وظیفه [task] را با کد موفقیت (حل‌شده) خاتمه می‌دهد. این امر اجرای تمام کدهایی را که منتظر این اتمام بودند (خط ۴ زیر) تحریک می‌کند:

        // انتظار شبیه‌سازی‌شده
        task = utils.waitForSomeTime($scope.waiting.time);
        // پایان انتظار
        task.promise.then(function () {
          // موفقیت
          utils.debug('fin', new Date());
        }, function () {
          // شکست
          utils.debug('Opération annulée')
        });
        task.promise['finally'](function () {
          //پایان انتظار در همه موارد
          $scope.waiting.visible = false;
        });

  • بنابراین، خط ۶ بالا (تکمیل موفقیت‌آمیز) اجرا خواهد شد. سپس، خطوط ۱۱ تا ۱۴ اجرا می‌شوند. پس از اجرای این کد، کنترل به نمای V بازمی‌گردد که سپس با مدل M خود همگام‌سازی می‌شود. پیام بارگذاری پنهان می‌شود (ng-show=$scope.waiting.visible=false، خط ۱۳) و فرم نمایش داده می‌شود (ng-hide=$scope.waiting.visible=false، خط ۱۳);

نمایش‌های صفحه سپس به شرح زیر است:

début : "2014-06-23T15:05:58.480Z"
fin : "2014-06-23T15:06:01.481Z"

همان‌طور که در بالا مشاهده می‌شود، بین شروع و پایان انتظار تأخیری ۳ ثانیه‌ای (06:01–05:58) وجود دارد. اگر از طرف دیگر، کاربر قبل از اتمام ۳ ثانیه انتظار را لغو کند، موارد زیر نمایش داده می‌شود:

début : "2014-06-23T15:08:09.564Z"
Opération annulée

در نهایت، مهم است که درک کنیم در هر لحظه تنها یک نخ اجرایی وجود دارد که به نام نخ UI (رابط کاربری) شناخته می‌شود. تکمیل یک وظیفه ناهمزمان با یک رویداد سیگنال داده می‌شود، درست مانند کلیک روی یک دکمه. این رویداد بلافاصله پردازش نمی‌شود. این رویداد در صف رویدادهای در انتظار اجرا قرار می‌گیرد. وقتی نوبتش می‌رسد، پردازش می‌شود. این پردازش از نخ UI استفاده می‌کند و بنابراین در این مدت، رابط کاربری منجمد می‌شود. این رابط به ورودی کاربر پاسخ نمی‌دهد. به همین دلیل، مهم است که رویداد به سرعت پردازش شود. از آنجایی که هر رویداد توسط نخ UI پردازش می‌شود، هرگز نیازی به حل مسائل همگام‌سازی بین نخ‌های در حال اجرا به طور همزمان نیست. در هر لحظه، تنها نخ UI در حال اجرا است.

3.7.6. مثال ۶: سرویس‌های HTTP

اکنون سرویس [dao] را معرفی می‌کنیم که با سرور وب ارتباط برقرار می‌کند:

3.7.6.1. دید V

ما قصد داریم یک فرم برای درخواست فهرست پزشکان بنویسیم:

Image

ما [app-01.html] را در [app-16.html] کپی می‌کنیم، که سپس آن را به شرح زیر اصلاح می‌کنیم:


<div class="container" ng-cloak="">
  <h1>Rdvmedecins - v1</h1>

  <!-- پیام انتظار -->
  <div class="alert alert-warning" ng-show="waiting.visible" ng-cloak="">
    <h1>{{ waiting.text | translate}}
      <button class="btn btn-primary pull-right" ng-click="waiting.cancel()">{{'msg_cancel'|translate}}</button>
      <img src="assets/images/waiting.gif" alt=""/>
    </h1>
  </div>

  <!-- درخواست -->
  <div class="alert alert-info" ng-hide="waiting.visible">
    <div class="form-group">
      <label for="waitingTime">{{waitingTimeText | translate}}</label>
      <input type="text" id="waitingTime" ng-model="waiting.time"/>
    </div>
    <div class="form-group">
      <label for="urlServer">{{urlServerLabel | translate}}</label>
      <input type="text" id="urlServer" ng-model="server.url"/>
    </div>
    <div class="form-group">
      <label for="login">{{loginLabel | translate}}</label>
      <input type="text" id="login" ng-model="server.login"/>
    </div>
    <div class="form-group">
      <label for="password">{{passwordLabel | translate}}</label>
      <input type="password" id="password" ng-model="server.password"/>
    </div>
    <button class="btn btn-primary" ng-click="execute()">{{medecins.title|translate:medecins.model}}</button>
  </div>

  <!-- فهرست پزشکان -->
  <div class="alert alert-success" ng-show="medecins.show">
    {{medecins.title|translate:medecins.model}}
    <ul>
      <li ng-repeat="medecin in medecins.data">{{medecin.titre}}{{medecin.prenom}} {{medecin.nom}}</li>
    </ul>
  </div>

  <!--فهرست خطاها -->
  <div class="alert alert-danger" ng-show="errors.show">
    {{errors.title|translate:errors.model}}
    <ul>
      <li ng-repeat="message in errors.messages">{{message|translate}}</li>
    </ul>
  </div>

</div>
...
<script type="text/javascript" src="rdvmedecins-04.js"></script>
  • خطوط ۱۳–۳۱: فرم را پیاده‌سازی کنید. این فرم زمانی که پیام بارگذاری نمایش داده می‌شود (ng-hide="waiting.visible") قابل مشاهده نیست. توجه داشته باشید که چهار فیلد ورودی در (ویژگی‌های ng-model) [waiting.time (ligne 16), server.url (ligne 20), server.login (ligne 24), server.password (ligne 28)] ذخیره می‌شوند؛
  • خطوط ۳۴–۳۹: نمایش لیست پزشکان. این لیست همیشه قابل مشاهده نیست (ng-show="medecins.show").
  • خط ۳۵: جایگزینی برای دستورات <div ... translate="{{medecins.title}}" translate-values="{{medecins.model}}"> که قبلاً با آن مواجه شده است؛
  • خط ۳۶: یک لیست غیر مرتب؛
  • خط ۳۷: فهرست پزشکان در قالب [medecins.data] قرار دارد. دستور Angular با شناسه [ng-repeat] به شما امکان می‌دهد تا روی یک فهرست حلقه‌زنی کنید. دستور ng-repeat="medecin in medecins.data" به سیستم دستور می‌دهد که تگ را برای هر مورد در لیست [medecins.data] تکرار کند. مورد فعلی در لیست با [medecin] مشخص می‌شود؛
  • خط ۳۷: برای هر ، عنوان، نام و نام خانوادگی پزشک فعلی، که توسط متغیر [medecin] مشخص شده است، نوشته می‌شوند؛
  • خطوط ۴۲–۴۷: نمایش فهرست خطاها. این فهرست همیشه قابل مشاهده نیست (ng-show="errors.show"). این نمایش از همان الگوی نمایش فهرست پزشکان پیروی می‌کند. به‌طور کلی، برای نمایش فهرست اشیاء از دستور Angular با نام [ng-repeat] استفاده می‌شود؛
  • خط ۵۱: کد جاوااسکریپت اکنون در فایل [rdvmedecins-04] قرار دارد

3.7.6.2. کنترل‌کننده C و مدل M

کد جاوااسکریپت به شرح زیر تغییر می‌کند:

Image

  • خطوط ۶–۹: ماژول [rdvmedecins] وابستگی خود را به ماژول [base64] که توسط کتابخانه [angular-base64] ارائه می‌شود، اعلام می‌کند، که یکی از وابستگی‌های پروژه است. این ماژول برای رمزگذاری رشته [login:password]، که برای احراز هویت به سرویس وب ارسال می‌شود، با استفاده از Base64 به کار می‌رود؛
  • خطوط ۱۲–۱۳: تابع инициализация حاوی پیام‌های بین‌المللی‌سازی ما. پیام‌های جدیدی ظاهر شده‌اند. ما به جزئیات بیشتر در مورد این موارد نمی‌پردازیم؛
  • خطوط ۶۹–۷۰: سرویس [config] که برنامه ما را پیکربندی می‌کند. کلیدهای پیام جدید به آن اضافه شده‌اند. ما دیگر به آنها نخواهیم پرداخت؛
  • خطوط ۳۱۸–۳۱۹: سرویس [utils] که حاوی متدهای کاربردی است. موارد جدیدی به آن اضافه شده است. ما آنها را مورد بحث قرار خواهیم داد؛
  • خطوط ۳۸۵–۳۸۶: سرویس [dao] که مسئول ارتباط با سرویس وب است. این همان سرویسی است که ما بر روی آن تمرکز خواهیم کرد؛
  • خطوط ۴۶۷–۴۶۸: کنترل‌کننده C برای نمای V که همین‌اکنون در مورد آن بحث کردیم. اکنون به آن می‌پردازیم، زیرا این کنترل‌کننده به‌عنوان هماهنگ‌کننده عمل کرده و به درخواست‌های کاربر پاسخ می‌دهد؛

3.7.6.3. کنترل‌کننده C

کد کنترل‌کننده به شرح زیر است:


angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
    function ($scope, utils, config, dao, $translate) {
      // ------------------- آغازین‌سازی قالب
      // قالب
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
      $scope.waitingTimeText = config.waitingTimeText;
      $scope.server = {url: undefined, login: undefined, password: undefined};
      $scope.medecins = {title: config.listMedecins, show: false, model: {}};
      $scope.errors = {show: false, model: {}};
      $scope.urlServerLabel = config.urlServerLabel;
      $scope.loginLabel = config.loginLabel;
      $scope.passwordLabel = config.passwordLabel;

      // وظیفهٔ ناهمزمان
      var task;

      // اجرای اقدام
      $scope.execute = function () {
        // به‌روزرسانی UI
        $scope.waiting.visible = true;
        $scope.medecins.show = false;
        $scope.errors.show = false;
        // انتظار شبیه‌سازی‌شده
        task = utils.waitForSomeTime($scope.waiting.time);
        var promise = task.promise;
        // انتظار
        promise = promise.then(function () {
          // درخواست لیست پزشکان؛
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
          return task.promise;
        });
        // در حال تجزیه و تحلیل نتیجه تماس قبلی
        promise.then(function (result) {
          // result={err: 0, data: [med1, med2, ...]}
          // result={err: n, messages: [msg1, msg2, ...]}
          if (result.err == 0) {
            //داده‌های به‌دست‌آمده در مدل وارد می‌شوند
            $scope.medecins.data = result.data;
            // UI به‌روزرسانی شد
            $scope.medecins.show = true;
            $scope.waiting.visible = false;
          } else {
            //خطاهایی هنگام بازیابی فهرست پزشکان رخ داد
            $scope.errors = { title: config.getMedecinsErrors, messages: utils.getErrors(result), show: true, model: {}};
            // UI به‌روزرسانی شد
            $scope.waiting.visible = false;
          }
        });
      };

      //لغو در حال انتظار
      function cancel() {
        // وظیفه در حال انجام است
        task.reject();
        //در حال به‌روزرسانی UI
        $scope.waiting.visible = false;
        $scope.medecins.show = false;
        $scope.errors.show = false;
      }

    }
  ])
;
  • خط ۲: کنترل‌کننده وابستگی جدیدی دارد، یعنی به سرویس [dao]؛
  • خطوط ۶–۱۳: مدل M نما V برای اولین بار که نما نمایش داده می‌شود، مقداردهی اولیه می‌شود؛
  • خط ۸: [$scope.server] برای بازیابی سه مورد از چهار بخش اطلاعات از فرم V استفاده خواهد شد و بخش چهارم در [$scope.waiting.time] (خط ۶) ذخیره می‌شود؛
  • خط ۹: [$scope.medecins] اطلاعات لازم برای نمایش فهرست پزشکان را جمع‌آوری خواهد کرد:

  <!-- فهرست پزشکان -->
  <div class="alert alert-success"  ng-show="medecins.show">
    {{medecins.title|translate:medecins.model}}
    <ul>
      <li ng-repeat="medecin in medecins.data">{{medecin.titre}}{{medecin.prenom}} {{medecin.nom}}</li>
    </ul>
</div>

ویژگی [medecins.title] عنوان بنر خواهد بود. این ویژگی در سرویس [config] تعریف شده است. ویژگی [medecins.show] کنترل می‌کند که بنر نمایش داده شود یا خیر (ویژگی ng-show="medecins.show"). ویژگی [medecins.model] یک دیکشنری خالی است و به همین صورت باقی خواهد ماند. این ویژگی صرفاً برای نشان دادن نحوه استفاده از واریانت ترجمه مورد استفاده در خط ۳ به کار رفته است. ویژگی [medecins.data] که قرار است حاوی لیست پزشکان (خط ۵) باشد، هنوز تعریف نشده است.

  • خط ۱۰: [$scope.errors] اطلاعات لازم برای نمایش لیست خطاها را جمع‌آوری خواهد کرد:

  <!-- فهرست خطاها -->
  <div class="alert alert-danger"  ng-show="errors.show">
    {{errors.title|translate:errors.model}}
    <ul>
      <li ng-repeat="message in errors.messages">{{message|translate}}</li>
    </ul>
</div>

ویژگی [errors.title] عنوان بنر خواهد بود. این ویژگی در سرویس [config] تعریف شده است. ویژگی [errors.show] کنترل می‌کند که آیا بنر نمایش داده شود یا خیر (ویژگی ng-show="errors.show"). ویژگی [errors.model] یک دیکشنری خالی است و به همین صورت باقی خواهد ماند. این ویژگی صرفاً برای نشان دادن کاربرد واریانت ترجمه‌ای است که در خط ۳ استفاده شده است. ویژگی [errors.messages] که هنوز تعریف نشده است، حاوی لیست پیام‌های خطایی خواهد بود که باید نمایش داده شوند (خط ۵).

  • خط ۱۶: وظیفه غیرهم‌زمان. کنترل‌کننده دو وظیفه غیرهم‌زمان را پشت سر هم اجرا خواهد کرد. ارجاعات به این وظایف متوالی در متغیر [task] ذخیره خواهند شد. این کار امکان لغو آنها را فراهم می‌کند (خط ۵۵)؛
  • خط ۱۹: متدی که هنگام کلیک کاربر روی دکمه [Liste des médecins] اجرا می‌شود:

    <button class="btn btn-primary" ng-click="execute()">Liste des médecins</button>
  • خطوط ۲۱–۲۳: رابط کاربری به‌روزرسانی می‌شود: پیام «لطفاً منتظر بمانید» نمایش داده می‌شود و سایر موارد پنهان می‌شوند؛
  • خط ۲۵: وظیفه انتظار ناهمزمان ایجاد می‌شود. سیگنالی (تکمیل وظیفه) پس از اتمام زمانی که کاربر در فرم وارد کرده است، دریافت خواهد شد؛
  • خط ۲۶: وعده (promise) مربوط به وظیفه غیرهم‌زمان بازیابی می‌شود. برنامه‌ای که وظیفه را راه‌اندازی می‌کند با این وعده کار می‌کند. با این حال، برای اینکه بتوان آن را لغو کرد (خط ۵۵)، به ارجاع خودِ وظیفه نیاز است؛
  • خطوط ۲۸–۳۲: کاری را که پس از اتمام انتظار باید انجام شود، تعریف می‌کنیم؛
  • خط ۳۰: متد [dao.getData] برای راه‌اندازی یک وظیفه غیرهمزمان جدید استفاده می‌شود. ما اطلاعات مورد نیاز آن را به آن پاس می‌کنیم:
    • ریشه URL سرویس وب [$scope.server.url]، برای مثال [http://localhost:8080];
    • شناسه ورود [$scope.server.login] برای احراز هویت، برای مثال [admin];
    • رمز عبور [$scope.server.password] برای ورود، برای مثال [admin];
    • متد URL که سرویس درخواستی [config.urlSvrMedecins] را انجام می‌دهد، در این مورد [/getAllMedecins]. در مجموع، URL کامل، [http://localhost:8080/getAllMedecins] خواهد بود؛

روش [dao.getData] نتیجه‌ای را بازمی‌گرداند که می‌تواند دو شکل ممکن داشته باشد:

  • (ادامه)
    • {err: 0, data: [med1, med2, ...]} که در آن [medi] یک شیء نماینده یک پزشک (عنوان، نام، نام خانوادگی) است،
    • {err: n, messages: [msg1, msg2, ...]} که در آن [msgi] یک پیام خطا است و n برابر صفر نیست؛
  • خط ۳۱: وعدهٔ وظیفه بازگردانده می‌شود. در اینجا نکته‌ای برای درک وجود دارد. ما دو وعده داریم:
    • promise.then(): یک وعدهٔ اول، [promise1] را بازمی‌گرداند؛
    • return task.promise: یک وعده دوم، [promise2] را بازمی‌گرداند؛
    • در نهایت، `promise=promise.then(...; return task.promise)` یک زنجیره از دو `promise`، `[promise2.promise1]` است. [promise1] تنها پس از حل شدن وعده [promise2]، یعنی پس از اتمام وظیفه [dao.getData]، ارزیابی خواهد شد. وعده [promise1] به هیچ‌یک از وظایف غیرهمزمان وابسته نیست. بنابراین فوراً برآورده خواهد شد؛
  • خطوط ۳۴–۵۰: از توضیحات قبلی برمی‌آید که این خطوط تنها پس از اتمام وظیفه [dao.getData] اجرا خواهند شد. پارامتر [result] که در خط ۳۴ به تابع ارسال شده است، توسط متد [dao.getData] ساخته شده و توسط عملیات [task.resolve(result)] به کد فراخوانی‌کننده ارسال می‌شود، که در آن [result] به شکل زیر است:
    • {err: 0, data: [med1, med2, ...]} که در آن [medi] یک شیء نمایانگر یک پزشک (عنوان، نام، نام خانوادگی) است،
    • {err: n, messages: [msg1, msg2, ...]} که در آن [msgi] یک پیام خطا است و n با 0 برابر نیست؛
  • خط ۳۷: کد خطا [result.err] را بررسی کنید؛
  • خطوط ۳۸–۴۲: اگر خطایی وجود نداشته باشد (result.err == 0)، آنگاه فهرست پزشکان را بازیابی کرده و نمایش دهید؛
  • خطوط ۴۴–۴۷: از سوی دیگر، اگر خطایی وجود داشته باشد (result.err ≠ 0)، آنگاه فهرست پیام‌های خطا را بازیابی کرده و نمایش دهید؛
  • خطوط ۵۳–۵۶: پیام انتظار همراه با دکمه لغو تا زمانی که هر دو عملیات غیرهمزمان به پایان برسند، قابل مشاهده باقی می‌ماند. بیایید ببینیم بسته به زمانی که لغو رخ می‌دهد چه اتفاقی می‌افتد:
    • اولاً، مهم است که درک کنیم خطوط ۱۹ تا ۵۰ یکجا اجرا می‌شوند. بنابراین، تنها یک وظیفه ناهمزمان، یعنی وظیفه خط ۲۵، آغاز شده است؛
    • پس از این اجرای اولیه، نمای V به‌روزرسانی می‌شود و بنابراین بنر انتظار و دکمه لغو آن قابل مشاهده هستند. اگر کاربر قبل از اتمام وظیفه در خط ۲۵، انتظار را لغو کند، متد در خط ۵۳ اجرا می‌شود و وظیفه با خطا لغو می‌گردد (خط ۵۵)؛
    • خطوط ۵۶–۵۹: رابط کاربری به‌روزرسانی می‌شود: فرم مجدداً نمایش داده می‌شود و سایر موارد پنهان می‌شوند،
    • سپس کاربر به نمای V بازگردانده می‌شود و مرورگر رویداد بعدی را پردازش می‌کند. از آنجایی که وظیفه تکمیل شده است، پرامیس مربوط به آن وظیفه حل می‌شود، که این امر یک رویداد را فعال می‌کند. سپس این رویداد مدیریت می‌شود؛
    • سپس خطوط ۲۸–۳۲ اجرا می‌شوند. هیچ تابعی برای حالت خطا تعریف نشده است، بنابراین هیچ کدی اجرا نمی‌شود. یک پرامیس جدید به دست می‌آید، همان پرامیسی که هنوز توسط [promise.then] بازگردانده می‌شود و هنوز حل‌نشده است،
    • از آنجایی که رویداد مدیریت شده است، کنترل به نمای V بازمی‌گردد و مرورگر رویداد بعدی را مدیریت خواهد کرد. از آنجا که [promise] در خط ۲۸ مدیریت شده است، آنی که در خط ۳۴ قرار دارد حل خواهد شد، که این امر یک رویداد جدید را فعال می‌کند. سپس این رویداد مدیریت می‌شود؛
    • سپس خطوط ۳۴ تا ۴۹ به ترتیب اجرا خواهند شد، زیرا وعده‌ای (promise) که در خط ۳۴ استفاده شده است، برآورده شده است. بار دیگر، از آنجایی که هیچ تابعی برای حالت خطا تعریف نشده است، هیچ کدی اجرا نمی‌شود،
    • و بدین ترتیب به خط ۵۰ می‌رسیم. دیگر هیچ وظیفه‌ای در انتظار نیست و نمای جدید V نمایش داده می‌شود؛
    • حال فرض کنید که لغو در حالی رخ می‌دهد که دومین وظیفه غیرهمزمان [dao.getData] در حال اجرا است. استدلال قبلی بار دیگر اعمال می‌شود. پایان وظیفه، اجرای خطوط 34 تا 50 را با یک خطای وظیفه (task failure) تحریک خواهد کرد. به‌زودی خواهیم دید که متد [dao.getData] از طریق HTTP یک فراخوانی ناهمزمان به سرویس وب انجام می‌دهد. این فراخوانی لغو نخواهد شد، اما نتیجه‌ی آن مورد استفاده قرار نخواهد گرفت.

درک این رفت و برگشت مداوم بین نمایش نمای V و رسیدگی به رویدادهای مرورگر مهم است. رویدادها توسط کاربر (مانند یک کلیک) یا توسط عملیات سیستمی مانند تکمیل یک عملیات غیرهمزمان، فعال می‌شوند. حالت بیکاری مرورگر، رندر شدن نمای V است. این حالت با وقوع یک رویداد که سپس پردازش می‌شود، از حالت بیکاری خارج می‌شود. به محض پردازش رویداد، مرورگر به حالت بیکاری خود بازمی‌گردد. سپس اگر رویداد پردازش‌شده مدل M را تغییر داده باشد، نمای V به‌روزرسانی می‌شود. مرورگر توسط رویداد بعدی از حالت بیکاری خارج می‌شود.

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

یک نکته دیگر نیز برای توضیح وجود دارد. برای نمایش پیام‌های خطا، می‌نویسیم:


$scope.errors = { title: config.getMedecinsErrors, messages: utils.getErrors(result), show: true, model: {}};

فهرست پیام‌ها توسط متد [utils.getErrors] که در سرویس [utils] تعریف شده است، ارائه می‌شود. این متد به شرح زیر است:


// تحلیل خطاها در پاسخ سرور JSON
    function getErrors(data) {
      // data {err:n, messages:[]}, err!=0
      //خطاها
      var errors = [];
      // کد خطا
      var err = data.err;
      switch (err) {
        case 2 :
          // مجاز نیست
          errors.push('not_authorized');
          break;
        case 3 :
          // ممنوع
          errors.push('forbidden');
          break;
        case 4 :
          // خطای محلی
          errors.push('not_http_error');
          break;
        case 6 :
          // سند یافت نشد
          errors.push('not_found');
          break;
        default :
          // سایر موارد
          errors = data.messages;
          break;

      }
      // اگر پیامی وجود نداشته باشد، یک پیام نمایش دهید
      if (! errors || errors.length == 0) {
        errors=['error_unknown'];
      }
      // بازگرداندن فهرست خطاها
      return errors;
    }
  • خطوط ۲–۳: پارامتر دریافتی [data] یک شیء با دو ویژگی است:
    • [err]: یک کد خطا؛
    • [messages]: فهرستی از پیام‌ها؛
  • خط ۵: ما قصد داریم یک آرایه از پیام‌های خطا بسازیم. این پیام‌ها بومی‌سازی شده‌اند. به همین دلیل، خود پیام‌ها در آرایه قرار نمی‌گیرند، بلکه کلیدهای بین‌المللی‌سازی آن‌ها قرار می‌گیرند، به جز در خط ۲۷. در این مورد، از ویژگی [messages] پارامتر [data] استفاده می‌شود. این پیام‌ها پیام‌های واقعی هستند و نه کلیدهای پیام. با این حال، ویو V آن‌ها را به عنوان کلیدهای پیام در نظر می‌گیرد، که در نتیجه پیدا نخواهند شد. در این حالت، ماژول [translate] کلید پیامی را که نتوانسته پیدا کند نمایش می‌دهد – که در این مورد، یک پیام واقعی است. این نتیجه مطلوب است؛
  • خطوط ۳۲–۳۴: رسیدگی به حالتی که [data.messages] در خط ۲۷ برابر null باشد. این مورد در سرویس وب به همین شکل نوشته شده رخ می‌دهد. این سناریو باید اجتناب می‌شد.

3.7.6.4. سرویس [dao]

سرویس [dao] مبادلات را با سرویس وب HTTP / JSON مدیریت می‌کند. کد آن به شرح زیر است:


angular.module("rdvmedecins")
  .factory('dao', ['$http', '$q', 'config', '$base64', 'utils',
    function ($http, $q, config, $base64, utils) {

      // گزارش‌ها
      utils.debug("[dao] init");

      // ----------------------------------متدهای خصوصی
      // بازیابی داده‌ها از سرویس وب
      function getData(serverUrl, username, password, urlAction, info) {
        // عملیات غیرهمزمان
        var task = $q.defer();
        // URL درخواست HTTP
        var url = serverUrl + urlAction;
        // احراز هویت پایه
        var basic = "Basic " + $base64.encode(username + ":" + password);
        // پاسخ
        var réponse;
        // تمام درخواست‌های HTTP باید احراز هویت شوند
        var headers = $http.defaults.headers.common;
        headers.Authorization = basic;
        //درخواست HTTP ارسال می‌شود
        var promise;
        if (info) {
          promise = $http.post(url, info, {timeout: config.timeout});
        } else {
          promise = $http.get(url, {timeout: config.timeout});
        }
        promise.then(success, failure);
        // ما خودِ وظیفه را بازمی‌گردانیم تا بتوان آن را لغو کرد
        return task;

        // موفقیت
        function success(response) {
          // response.data={status:0, data:[med1, med2, ...]} یا {status:x, data:[msg1, msg2, ...]
          utils.debug("[dao] getData[" + urlAction + "] success réponse", response);
          // پاسخ
          var payLoad = response.data;
          réponse = payLoad.status == 0 ? {err: 0, data: payLoad.data} : {err: 1, messages: payLoad.data};
          //پاسخ بازگردانده می‌شود
          task.resolve(réponse);
        }

        // نابودی
        function failure(response) {
          utils.debug("[dao] getData[" + urlAction + "] error réponse", response);
          // تحلیل وضعیت
          var status = response.status;
          var error;
          switch (status) {
            case 401 :
              // غیرمجاز
              error = 2;
              break;
            case 403:
              // ممنوع
              error = 3;
              break;
            case 404:
              // یافت نشد
              error = 6;
              break;
            case 0:
              // خطای محلی
              error = 4;
              break;
            default:
              // چیز دیگری
              error = 5;
          }
          // پاسخ در حال بازگشت
          task.resolve({err: error, messages: [response.statusText]});
        }
      }

      // --------------------- نمونه سرویس [dao]
      return {
        getData: getData
      }
}]);
  • خطوط ۷۷–۷۹: این سرویس تنها یک فیلد دارد: متد [getData] که برای بازیابی اطلاعات از سرویس وب / JSON استفاده می‌شود؛
  • خط ۲: وابستگی‌ای به نام [$http] ظاهر می‌شود که قبلاً با آن مواجه نشده بودیم. این یک سرویس از پیش تعریف‌شده در Angular است که ارتباط (HTTP) با یک موجودیت راه دور را ممکن می‌سازد؛
  • خط ۶: یک لاگ برای نمایش اینکه کد در کدام مرحله از چرخه عمر برنامه اجرا می‌شود؛
  • خط ۱۰: متد [getData] پنج پارامتر می‌گیرد:
    • [serverUrl]: URL ریشه سرویس وب (http://localhost:8080);
    • [urlAction]: URL سرویس خاص درخواست‌شده (/getAllMedecins);
    • [username]: نام کاربری کاربر؛
    • [password]: رمز عبور آنها؛
    • [info]: ابجکتی حاوی اطلاعات اضافی هنگامی که URL برای سرویس خاص درخواستی از طریق عملیات POST درخواست می‌شود. در مورد URL (/getAllMedecins)، این پارامتر ارسال نشده است. بنابراین [undefined] است؛
  • خط ۱۲: یک وظیفه ناهمزمان ایجاد می‌شود؛
  • خط ۱۴: URL سرویس درخواستی را تکمیل می‌کند (http://localhost:8080/getAllMedecins);
  • خط 16: احراز هویت با ارسال هدر زیر انجام می‌شود: HTTP:
Authorization:Basic code

که در آن [code] رشته رمزگذاری‌شده با Base64 از [username:password] است؛

خط ۱۶ بخش [Basic code] از هدر HTTP را می‌سازد؛

  • خط ۱۸: پاسخ سرویس وب؛
  • خط ۲۰: سربرگ‌های HTTP که به‌طور پیش‌فرض توسط Angular در یک درخواست HTTP ارسال می‌شوند، در شیء [$http.defaults.headers.common] تعریف شده‌اند. سربرگ [Authorization:Basic code] گنجانده نشده است؛
  • خط ۲۱: این به سربرگ‌های HTTP برای ارسال سیستماتیک اضافه می‌شود. در سمت چپ تخصیص، سربرگ [Authorization] که باید مقداردهی اولیه شود قرار دارد و در سمت راست، مقدار سربرگ که در این مورد، مقداری است که در خط ۱۶ تعریف شده است. بنابراین اگر بنویسیم:
headers.Authorization = 'x';

Angular سربرگ HTTP را ارسال خواهد کرد:

Authorization : x
  • خط ۲۳: متدهای سرویس [$httppromiseها را برمی‌گردانند. این‌ها در متغیر [promise] ذخیره خواهند شد؛
  • خط ۲۷: زیرا در اینجا پارامتر [info] مقدار [undefined] را دارد، خط ۲۷ اجرا می‌شود. URL (http://localhost:8080/getAllMedecins) با استفاده از GET درخواست می‌شود. برای جلوگیری از انتظار طولانی، یک تایم‌اوت حداکثر برای دریافت پاسخ سرور تعیین می‌شود. به طور پیش‌فرض، این تایم‌اوت یک ثانیه است؛
  • خط ۲۹: ما دو متد را تعریف می‌کنیم که وقتی وعده محقق شد اجرا شوند:
    • [success]: تعریف‌شده در خط ۳۴، متدی است که هنگام حل شدن وعده پس از موفقیت یک وظیفه اجرا می‌شود؛
    • [failure]: تعریف‌شده در خط ۴۵، متدی است که هنگام حل شدن پرامیس پس از شکست وظیفه اجرا می‌شود؛
    • هر دو متد (یا بهتر است بگوییم، توابع) در داخل تابع [getData] تعریف شده‌اند. این کار در جاوا اسکریپت امکان‌پذیر است. متغیرهای تعریف‌شده در [getData] در داخل هر دو تابع داخلی [success, failure] قابل دسترسی هستند؛
  • خط ۳۱: وظیفه‌ای که در خط ۱۲ ایجاد شده بازگردانده می‌شود. مهم است که کد فراخوانی‌کننده را در اینجا مد نظر داشته باشیم:

        promise = promise.then(function () {
          // درخواست فهرست پزشکان؛
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
          return task.promise;
});

در خط ۳ بالا، یک وظیفه واقعاً بازیابی می‌شود.

  • خط ۳۴: تابع [success] بعداً، پس از اتمام موفقیت‌آمیز فراخوانی HTTP، اجرا می‌شود. این مفهوم موفقیت به خط اول یک پاسخ از HTTP مرتبط است. این پاسخ به شکل زیر است:
HTTP/1.1 code texte

این کد یک عدد سه‌رقمی است که نشان می‌دهد تماس با موفقیت انجام شده یا خیر. به طور کلی، کدهای 2xx و 3xx کدهای موفقیت و سایر کدها کدهای خطا هستند. متن یک پیام توضیحی مختصر است. در اینجا دو پاسخ ممکن آورده شده است، یکی برای موفقیت و دیگری برای خطا:

HTTP/1.1 200 OK
HTTP/1.1 404 Not Found
  • خط ۳۶: پاسخ سرور روی کنسول نمایش داده می‌شود. در صورت وقوع خطای [404 Not Found]، خروجی چیزی شبیه به این است:

[dao] getData[/getAllMedecins] error réponse : {"data":"...","status":404,"config":{...},"statusText":"Not Found"}

در این پاسخ، ما فقط از فیلدهای [data]، [status] و [statusText] استفاده خواهیم کرد.

  • خط ۳۸: ما فیلد [data] را از پاسخ بازیابی می‌کنیم. این فیلد یکی از اشکال زیر را خواهد داشت:
    • {status: 0, data: [med1, med2, ...]} که در آن [medi] یک شیء نمایانگر یک پزشک (عنوان، نام، نام خانوادگی) است،
    • {status: n, data: [msg1, msg2, ...]} که در آن [msgi] یک پیام خطا است و n برابر با 0 نیست؛

Image

  • خط ۳۹: پاسخ {0,data} یا {n,messages} ساخته می‌شود. پاسخ اول حاوی پزشکان در فیلد [data] است. دومی نشان‌دهنده خطایی است که در سمت سرور رخ داده است. سرور این مورد را مدیریت کرده و یک کد خطا در [err] و فهرستی از پیام‌های خطا در [data] تولید کرده است. در هر دو حالت، یک کد وضعیت ۲۰۰ (HTTP) بازگردانده می‌شود که نشان می‌دهد درخواست (HTTP) به طور کامل پردازش شده است. به همین دلیل است که هر دو حالت در داخل یک تابع واحد ([success]) مدیریت می‌شوند؛
  • خط ۴۱: کار کامل شده است ([task.resolve]) و یکی از دو پاسخ بازگردانده می‌شود:
    • {err: 0, data: [med1, med2, ...]} که در آن [medi] یک شیء نماینده یک پزشک (عنوان، نام، نام خانوادگی) است،
    • {err: n, messages: [msg1, msg2, ...]} که در آن [msgi] یک پیام خطا است و n برابر صفر نیست؛

این کد باید با نحوه بازیابی این پاسخ در کد فراخوانی کنترل‌کننده مرتبط باشد:


        // تحلیل نتیجه تماس قبلی
        promise.then(function (result) {
          // result={err: 0, data: [med1, med2, ...]}
          //result={err: n, messages: [msg1, msg2, ...]}
          ...
          }

پاسخ از [task.resolve(réponse)] در متغیر [result] بالا ذخیره می‌شود.

  • خط ۴۵: تابع [failure] زمانی فراخوانی می‌شود که وظیفه غیرهمزمان با شکست مواجه شود. دو سناریوی ممکن وجود دارد:
    • سرور این خطا را با بازگرداندن یک کد وضعیت که نه 2xx و نه 3xx است، اعلام می‌کند،
    • Angular تماس HTTP را لغو می‌کند. در این حالت، هیچ تماسی برقرار نمی‌شود. یک خطای Angular رخ می‌دهد اما هیچ کد خطای HTTP توسط سرور بازگردانده نمی‌شود. این حالت، برای مثال، زمانی رخ می‌دهد که یک URL نامعتبر ارائه شود که قابل فراخوانی نباشد؛
  • خط ۴۶: پاسخ روی کنسول نمایش داده می‌شود؛
  • خط ۴۸: یادآوری می‌کنیم که پاسخ سرور به شکل زیر است:

{"data":"...","status":404,"config":{...},"statusText":"Not Found"}

در خط ۴۸، ویژگی [status] که در بالا ذکر شد را بازیابی می‌کنیم؛

  • خطوط ۵۰–۷۰: با استفاده از کد خطای HTTP، یک کد خطای جدید تولید می‌کنیم تا ماهیت متد HTTP را از کدهای فراخوانی‌کننده پنهان کنیم. می‌توان تأیید کرد که در کنترلری که از این متد استفاده می‌کند، هیچ چیزی دال بر وجود فراخوانی HTTP در داخل متد وجود ندارد؛
    • خط ۵۱: خطای [401] مربوط به احراز هویت ناموفق است (برای مثال، رمز عبور نادرست)،
    • خط ۵۵: خطای [403] مربوط به یک فراخوانی غیرمجاز است. کاربر به درستی احراز هویت شده اما حقوق کافی برای درخواست URL را که درخواست کرده است، ندارد. این خطا برای کاربر [user / user] رخ می‌دهد. این کاربر در پایگاه داده وجود دارد اما اجازه استفاده از برنامه را ندارد. تنها کاربر [admin / admin] این اجازه را دارد؛
    • خط ۵۹: خطای [404] مربوط به یافت نشدن URL است. این خطا ممکن است چندین علت داشته باشد:
      • کاربر در URL سرویس دچار اشتباه تایپی شده است؛
      • سرویس وب راه‌اندازی نشده است؛
      • سرویس وب به اندازه کافی سریع پاسخ نداد (زمان‌انتظار پیش‌فرض یک ثانیه)؛
    • خط ۶۳: کد خطا HTTP 0 وجود ندارد. این موردی است که Angular تماس درخواستی HTTP را برقرار نکرده است زیرا URL وارد شده توسط کاربر نامعتبر است و قابل تماس نیست. ما بعداً با موارد دیگری مواجه خواهیم شد که در آن Angular از اجرای فراخوانی درخواستی HTTP جلوگیری می‌شود؛
  • خط ۷۲: وظیفه با موفقیت به پایان رسید (task.resolve) با بازگرداندن پاسخی از نوع {err, messages}، که آرایه [messages] تنها شامل پیام [response.statusText] است. اگر Angular تماس درخواستی HTTP را انجام ندهد، یک رشته خالی بازگردانده خواهد شد؛

اکنون که هم نمای کلی و هم نمای دقیقی از برنامه داریم، می‌توانیم آزمایش را آغاز کنیم.

3.7.6.5. آزمون برنامه – ۱

بیایید با ورودی‌های معتبر شروع کنیم:

Image

  • برای [1]، عدد 0 را وارد می‌کنیم تا از هرگونه تأخیر جلوگیری شود؛
  • برای [2]، با وجود صحیح بودن ورودی‌ها، یک پیام خطا دریافت می‌کنیم. ما پیام‌های خطای مختلف را نمایش نداده‌ایم. پیامی که برای [2] نمایش داده می‌شود، یک پیام کلی مرتبط با خطای 0 است که با یک استثنای Angular مطابقت دارد. Angular با مشکلی مواجه شد که مانع تماس با HTTP شد. در چنین مواردی، باید گزارش‌های کنسول جاوااسکریپت را بررسی کنید. دو روش برای انجام این کار وجود دارد:
    • [F12] را در مرورگر کروم وارد کنید؛
    • از کنسول WebStorm استفاده کنید؛

در کنسول WebStorm، پیام‌های مختلفی از جمله این پیام را مشاهده می‌کنیم:

XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. هیچ هدر 'Access-Control-Allow-Origin' روی منبع درخواست‌شده وجود ندارد. بنابراین منشأ 'http://localhost:63342' مجاز به دسترسی نیست.
[dao] getData[/getAllMedecins] error réponse : {"data":"","status":0,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllMedecins","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":""}
  • خط ۱: Angular یک خطا گزارش می‌کند، که بعداً به آن باز خواهیم گشت؛
  • خط ۲: لاگ متد [dao.getData]. در اینجا جزئیات جالبی وجود دارد:
    • [status] برابر با 0 است، که نشان می‌دهد هیچ فراخوانی برای HTTP وجود نداشته است. در نتیجه، [statusText] خالی است،
    • [url] معادل [http://localhost:8080/getAllMedecins] است که صحیح است؛
    • سربرگ HTTP برای احراز هویت [Authorization":"Basic YWRtaW46YWRtaW4=] نیز صحیح است؛

پس چرا کار نکرد؟ عبارت کلیدی در لاگ‌ها [No 'Access-Control-Allow-Origin' header is present] است. برای درک آن، توضیح مفصل لازم است. بیایید با بررسی معماری کلی برنامهٔ کلاینت/سرور شروع کنیم:

Image

  • صفحات HTML / CSS / JS در برنامه Angular از سرور [1] می‌آیند؛
  • در [2]، سرویس [dao] درخواستی به سرور دیگری، یعنی سرور [2]، ارسال می‌کند. با این حال، این درخواست توسط مرورگری که برنامه Angular را اجرا می‌کند مسدود می‌شود، زیرا یک آسیب‌پذیری امنیتی محسوب می‌شود. این برنامه فقط می‌تواند از سروری که از آن آمده است، یعنی سرور [1]، درخواست کند؛

در واقع، این درست نیست که بگوییم مرورگر مانع از ارسال درخواست توسط اپلیکیشن Angular به سرور [2] می‌شود. در عمل، این مرورگر به آن سرور درخواست می‌دهد تا بپرسد آیا به کلاینتی که از دامنهٔ خود آن سرور نیست اجازه می‌دهد برای آن سرور درخواست ارسال کند یا خیر. این تکنیک اشتراک‌گذاری با نام اشتراک‌گذاری منابع فرامنشأ (CORS) شناخته می‌شود. سرور [2] با ارسال سربرگ‌های خاص، رضایت خود را اعلام می‌کند. این به این دلیل است که در این مورد، سرور ما [2] این هدرها را ارسال نکرده بود که مرورگر از انجام تماس HTTP مورد درخواست برنامه خودداری کرد.

حال بیایید جزئیات را بررسی کنیم. بیایید ترافیک شبکه‌ای را که در طول تماس HTTP رخ داد، بررسی کنیم. برای این کار، در مرورگر کروم، کلید [F12] را فشار می‌دهیم تا ابزارهای توسعه‌دهنده باز شود و سپس زبانه [Network] را برای مشاهده ترافیک شبکه انتخاب می‌کنیم:

  • در [1]، زبانه [network] را انتخاب می‌کنیم؛
  • در [2]، فهرست پزشکان را درخواست می‌کنیم؛

در زبانه [network] اطلاعات زیر را دریافت می‌کنیم:

  • در [1]، اطلاعاتی که به سرور ارسال می‌شود؛
  • در [2]، پاسخ سرور؛

می‌توانیم در [1] ببینیم که مرورگر درخواستی HTTP [OPTIONS] را برای URL ارسال کرده است. [OPTIONS] یکی از دستورات ممکن HTTP است، در کنار دستورات شناخته‌شده‌تر [GET] و [POST]. این دستور برای درخواست اطلاعات از یک سرور، به‌ویژه در مورد گزینه‌های HTTP که پشتیبانی می‌کند، استفاده می‌شود؛ از این رو نام آن گرفته شده است. سرور با [2] پاسخ می‌دهد. برای نشان دادن اینکه درخواست‌ها را از کلاینت‌های خارج از دامنه‌اش می‌پذیرد، باید یک هدر خاص به نام [Access-Control-Allow-Origin] را بازگرداند. و به همین دلیل که این هدر را بازنمی‌گرداند، Angular فراخوانی درخواستی HTTP را اجرا نکرد و خطا را بازگرداند:

XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. هیچ هدر 'Access-Control-Allow-Origin' روی منبع درخواست‌شده وجود ندارد. بنابراین منشأ 'http://localhost:63342' اجازه دسترسی ندارد.

بنابراین باید سرور خود را اصلاح کنیم تا هدر مورد انتظار HTTP را ارسال کند.

3.7.6.6. ویرایش وب‌سرور / JSON

به اِکلِپس بازمی‌گردیم. برای حفظ پیشرفت خود، نسخهٔ جاری وب‌سرور / JSON [rdvmedecins-webapi-v2] را در [rdvmedecins-webapi-v3] [1] کپی می‌کنیم:

ما یک تغییر اولیه در [ApplicationModel] ایجاد می‌کنیم که یکی از عناصر پیکربندی سرویس وب است:


package rdvmedecins.web.models;

...

@Component
public class ApplicationModel implements IMetier {

    //لایه [métier]
    @Autowired
    private IMetier métier;

    //داده‌ها از لایه [métier]
    private List<Medecin> médecins;
    private List<Client> clients;
    private List<String> messages;
    //داده‌های پیکربندی
    private boolean CORSneeded = true;

...

    public boolean isCORSneeded() {
        return CORSneeded;
    }

}
  • خط ۱۷: ما یک متغیر بولی ایجاد می‌کنیم که نشان می‌دهد آیا کلاینت‌های خارج از دامنه سرور پذیرفته می‌شوند یا خیر؛
  • خطوط 21–23: متد دسترسی به این اطلاعات؛

سپس یک کنترلر جدید Spring به نام MVC [3] ایجاد می‌کنیم:

کلاس [RdvMedecinsCorsController] به شرح زیر است:


package rdvmedecins.web.controllers;

import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import rdvmedecins.web.models.ApplicationModel;

@Controller
public class RdvMedecinsCorsController {

    @Autowired
    private ApplicationModel application;

    // گزینه‌های ارسال به کلاینت
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // تنظیم هدر CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
        }

    }

    // فهرست پزشکان
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
    public void getAllMedecins(HttpServletResponse response) {
        sendOptions(response);
    }
}
  • خطوط ۲۸–۳۱: یک کنترلر برای URL [/getAllMedecins] زمانی که با استفاده از دستور HTTP [OPTIONS] درخواست می‌شود، تعریف می‌کند؛
  • خط ۲۹: متد [getAllMedecins] شیء [HttpServletResponse] را به‌عنوان پارامتر می‌پذیرد، که به کلاینتی که درخواست را ارسال کرده است ارسال خواهد شد. این شیء توسط Spring تزریق می‌شود؛
  • خط ۳۰: پردازش درخواست به متد خصوصی در خطوط ۱۹–۲۵ واگذار می‌شود؛
  • خطوط ۱۵–۱۶: شیء [ApplicationModel] تزریق می‌شود؛
  • خطوط ۲۰–۲۳: اگر سرور برای پذیرش کلاینت‌ها از خارج دامنه‌اش پیکربندی شده باشد، آنگاه هدر HTTP ارسال می‌شود:

Access-Control-Allow-Origin: *

که به این معنی است که سرور کلاینت‌ها را از هر دامنه‌ای (*). می‌پذیرد.

اکنون برای آزمایش‌های بیشتر آماده‌ایم. نسخه جدید سرویس وب را راه‌اندازی می‌کنیم و متوجه می‌شویم که مشکل همچنان حل نشده باقی مانده است. هیچ چیز تغییر نکرده است. اگر یک دستور خروجی کنسول به خط ۳۰ بالا اضافه کنیم، هرگز نمایش داده نمی‌شود، که نشان می‌دهد متد [getAllMedecins] در خط ۲۹ هرگز فراخوانی نمی‌شود.

پس از بررسی‌های بیشتر، متوجه شدیم که متد MVC در Spring، دستورات HTTP و [OPTIONS] را با استفاده از پردازش پیش‌فرض خود مدیریت می‌کند. در نتیجه، همیشه Spring پاسخ می‌دهد و هرگز متد [getAllMedecins] در خط ۲۹ فراخوانی نمی‌شود. این رفتار پیش‌فرض Spring MVC را می‌توان تغییر داد. ما یک کلاس پیکربندی جدید برای راه‌اندازی رفتار جدید معرفی می‌کنیم:

  

کلاس پیکربندی جدید [WebConfig] به شرح زیر است:


package rdvmedecins.web.config;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    // پیکربندی DispatcherServlet برای سربرگ‌ها CORS
    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet();
        servlet.setDispatchOptionsRequest(true);
        return servlet;
    }
}
  • خط ۸: این کلاس یک کلاس پیکربندی Spring است. این کلاس، بین‌هایی را اعلام می‌کند که در کانکست Spring قرار خواهند گرفت؛
  • خط ۱۲: بین [dispatcherServlet] برای تعریف سرولتی که درخواست‌های کلاینت را مدیریت می‌کند، استفاده می‌شود. نوع آن [DispatcherServlet] است. این سرولت معمولاً به‌صورت پیش‌فرض ایجاد می‌شود. اگر آن را خودمان ایجاد کنیم، می‌توانیم آن را پیکربندی کنیم؛
  • خط ۱۴: ما یک نمونه از نوع [DispatcherServlet] ایجاد می‌کنیم؛
  • خط ۱۵: ما به سرولت دستور می‌دهیم که دستورات HTTP و [OPTIONS] را به برنامه فوروارد کند؛
  • خط 16: سرولتی را که به این صورت پیکربندی شده است بازمی‌گردانیم؛

اکنون باید کلاس [AppConfig] را اصلاح کنیم:


package rdvmedecins.web.config;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;

import rdvmedecins.config.DomainAndPersistenceConfig;

@EnableAutoConfiguration
@ComponentScan(basePackages = { "rdvmedecins.web" })
@Import({ DomainAndPersistenceConfig.class, SecurityConfig.class, WebConfig.class })
public class AppConfig {

}
  • خط ۱۱: کلاس پیکربندی جدید [WebConfig] وارد می‌شود؛

3.7.6.7. آزمایش برنامه – ۲

ما نسخه جدید سرویس وب /JSON را راه‌اندازی می‌کنیم و با استفاده از کلاینت Angular خود سعی در بازیابی فهرست پزشکان داریم. ترافیک شبکه را در زبانه [Network] بررسی می‌کنیم:

  • در [1]، می‌توانیم ببینیم که هدر HTTP [Access-Control-Allow-Origin: *] اکنون در پاسخ سرور موجود است. و با این حال هنوز کار نمی‌کند. ما لاگ‌های کنسول را در [2] بررسی می‌کنیم. در آنجا به ورودی لاگ زیر می‌رسیم:
XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. فیلد هدر درخواست «Authorisation» توسط «Access-Control-Allow-Headers» مجاز نیست

می‌توانیم ببینیم که مرورگر منتظر یک هدر جدید است، HTTP [Access-Control-Allow-Headers]، که به آن می‌گوید ما مجاز هستیم هدر احراز هویت را برای آن ارسال کنیم:

Authorization:Basic code

این می‌تواند یک نشانه خوب باشد. ممکن است Angular قصد داشته باشد فرمان HTTP GET را ارسال کند. با این حال، از آنجایی که این فرمان با یک هدر احراز هویت همراه است، در حال بررسی است که آیا سرور آن را می‌پذیرد یا خیر.

ما وب‌سرور خود را / JSON برای ارسال این هدر اصلاح می‌کنیم. کلاس [RdvMedecinsCorsController] به شرح زیر به‌روزرسانی می‌شود:


    // ارسال گزینه‌ها به کلاینت
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // تنظیم هدر CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
            // مجاز کردن هدر [Authorization]
            response.addHeader("Access-Control-Allow-Headers", "Authorization");            
}
  • خطوط ۶–۷ هدرِ missing را اضافه می‌کنند.

ما سرور را مجدداً راه‌اندازی می‌کنیم و با استفاده از کلاینت Angular مجدداً لیست پزشکان را درخواست می‌کنیم:

 

این بار، کار می‌کند. لاگ‌های کنسول پاسخ دریافت‌شده توسط متد [dao.getData] را نشان می‌دهند:


[dao] getData[/getAllMedecins] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mme","nom":"PELISSIER","prenom":"Marie"},{"id":2,"version":1,"titre":"Mr","nom":"BROMARD","prenom":"Jacques"},{"id":3,"version":1,"titre":"Mr","nom":"JANDOT","prenom":"Philippe"},{"id":4,"version":1,"titre":"Melle","nom":"JACQUEMOT","prenom":"Justine"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllMedecins","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}

می‌توانیم ببینیم که:

  • سرور کد خطای [status=200] را با پیام [statusText=OK] برگرداند. به همین دلیل ما در تابع [success] هستیم؛
  • سرور یک شی [data] را با دو فیلد بازگردانده است:
    • [status]: (با کد خطای HTTP [status] اشتباه گرفته نشود). در اینجا، [status=0] نشان می‌دهد که URL و [/getAllMedecins] بدون خطا پردازش شده‌اند؛
    • [data]: که شامل لیست JSON پزشکان است؛

حال به چند مورد جالب دیگر می‌پردازیم:

در شناسه‌های [login, password] خطایی وجود دارد:

ما با شناسه [user / user] وارد می‌شویم، که به برنامه دسترسی ندارد (فقط [admin] دسترسی دارد):

این بار، خطا دیگر [Erreur d'authentification] نیست بلکه [Accès refusé] است.

3.7.7. مثال ۷: فهرست مشتریان

ما از برنامه قبلی دوباره استفاده خواهیم کرد، این بار برای نمایش لیست مشتریان در یک لیست کشویی از نوع [Bootstrap select] (به بخش 3.6.6 مراجعه کنید).

3.7.7.1. نما V

نمای اولیه به شرح زیر خواهد بود:

 

برای به‌دست آوردن نما V، کد [app-16.html] را در [app-17.html] کپی کرده و آن را به شرح زیر اصلاح می‌کنیم:


<div class="container" >
  <h1>Rdvmedecins - v1</h1>

  <!-- پیام در انتظار -->
  <div class="alert alert-warning" ng-show="waiting.visible" >
...
  </div>

  <!--درخواست -->
  <div class="alert alert-info" ng-hide="waiting.visible" >
...
    <button class="btn btn-primary" ng-click="execute()">{{clients.title|translate}}</button>
  </div>

  <!-- فهرست مشتریان -->
  <div class="row" style="margin-top: 20px" ng-show="clients.show">
    <div class="col-md-3">
      <h2 translate="{{clients.title}}"></h2>
      <select data-style="btn-primary" class="selectpicker">
        <option ng-repeat="client in clients.data" value="{{client.id}}">
          {{client.titre}} {{client.prenom}} {{client.nom}}
        </option>
      </select>
    </div>
  </div>

  <!-- فهرست خطاها -->
  <div class="alert alert-danger"  ng-show="errors.show">
   ...
  </div>

</div>
....
<script type="text/javascript" src="rdvmedecins-05.js"></script>
  • خطوط ۵–۷: بنر بارگذاری بدون تغییر باقی می‌ماند؛
  • خطوط ۱۰–۱۳: فرم به جز برچسب دکمه (خط ۱۲) بدون تغییر باقی می‌ماند؛
  • خطوط ۲۸–۳۰: بنر خطا بدون تغییر باقی می‌ماند؛
  • رده‌های ۱۶–۲۵: فهرست مشتریان در یک لیست کشویی نمایش داده می‌شود که با کامپوننت [Bootstrap-selectpicker] استایل شده است (ویژگی‌های data-style و class، ردیف ۱۹);
  • خط ۲۰: دستور [ng-repeat] برای تولید گزینه‌های مختلف در لیست کشویی استفاده می‌شود. توجه داشته باشید که برچسب یک گزینه از نوع [Mme Julienne Tatou] و مقدار گزینه از نوع [100] است، که در آن 100 شناسه مشتری در حال نمایش است؛
  • خط ۳۴: کد جاوا اسکریپت به یک فایل جدید به نام [rdvmedecins-05] منتقل می‌شود؛

3.7.7.2. کنترل‌کننده C و مدل M

کد جاوااسکریپت در فایل [rdvmedecins-05] از فایل [rdvmedecins-04] کپی شده است:

Image

تقریباً هیچ چیز تغییر نکرده است، به جز در کنترلر که اکنون برای بازگرداندن لیست مشتریان تطبیق داده شده است:


angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
    function ($scope, utils, config, dao, $translate) {
      // ------------------- inicializه‌سازی قالب
      // قالب
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
      $scope.waitingTimeText = config.waitingTimeText;
      $scope.server = {url: undefined, login: undefined, password: undefined};
      $scope.clients = {title: config.listClients, show: false, model: {}};
      $scope.errors = {show: false, model: {}};
      $scope.urlServerLabel = config.urlServerLabel;
      $scope.loginLabel = config.loginLabel;
      $scope.passwordLabel = config.passwordLabel;

      // وظیفهٔ ناهمزمان
      var task;

      // اجرای اقدام
      $scope.execute = function () {
        // به‌روزرسانی UI
        $scope.waiting.visible = true;
        $scope.clients.show = false;
        $scope.errors.show = false;
        //صف شبیه‌سازی‌شده
        task = utils.waitForSomeTime($scope.waiting.time);
        var promise = task.promise;
        //در حال انتظار
        promise = promise.then(function () {
          //درخواست فهرست مشتریان؛
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
          return task.promise;
        });
        // در حال تجزیه و تحلیل نتیجه تماس قبلی
        promise.then(function (result) {
          // result={err: 0, data: [client1, client2, ...]}
          //result={err: n, messages: [msg1, msg2, ...]}
          if (result.err == 0) {
            //داده‌های به‌دست‌آمده در مدل درج می‌شوند
            $scope.clients.data = result.data;
            // ما UI را به‌روزرسانی می‌کنیم
            $scope.clients.show = true;
            $scope.waiting.visible = false;
            // لیست کشویی استایل می‌شود
            $('.selectpicker').selectpicker();
          } else {
            //خطاهایی هنگام بازیابی فهرست مشتریان رخ داد
            $scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
            //به‌روزرسانی UI
            $scope.waiting.visible = false;
          }
        });
      };

      //لغو در حال انتظار
      function cancel() {
        // تکمیل وظیفه
        task.reject();
        //در حال به‌روزرسانی UI
        $scope.waiting.visible = false;
        $scope.clients.show = false;
        $scope.errors.show = false;
      }
    }
  ])
;
  • تغییرات بسیار کمی در کنترلر ایجاد شده است. این کنترلر قبلاً فهرستی از پزشکان را ارائه می‌داد. اکنون فهرستی از مشتریان را ارائه می‌دهد؛
  • خط ۹: [$scope.clients] اکنون قالب بنر مشتری در نمای V خواهد بود؛
  • خط ۳۰: اکنون از URL [/getAllClients] استفاده می‌شود؛
  • خطوط ۳۵–۳۶: دو فرم پاسخ بازگردانده شده توسط متد [dao.getData]. اکنون به جای پزشکان، مشتریان را داریم؛
  • خط ۴۴: دستوری نسبتاً نادر در کد Angular. ما مستقیماً DOM (مدل شیء مستند) را دستکاری می‌کنیم. در اینجا می‌خواهیم روش [selectpicker] را اعمال کنیم (بخشی از [bootstrap-select.min.js]) را بر روی عناصر DOM که دارای کلاس [selectpicker] [$('.selectpicker')] هستند اعمال کنیم. تنها یک عنصر از این دست وجود دارد: لیست کشویی:

      <select data-style="btn-primary" class="selectpicker" select-enable="">
....
      </select>

در بخش 3.6.6 نشان داده شد که این لیست کشویی به صورت زیر قالب‌بندی شده است:

همان‌طور که برای پزشکان انجام شد، اکنون باید سرویس وب را نیز اصلاح کنیم.

3.7.7.3. اصلاح سرویس وب – ۱

  

کلاس [RdvMedecinsController] با یک متد جدید گسترش یافته است:


package rdvmedecins.web.controllers;

...

@Controller
public class RdvMedecinsCorsController {

    @Autowired
    private ApplicationModel application;

    // ارسال گزینه‌ها به کلاینت
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // تنظیم هدر CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
            // سربرگ را مجاز کنید [Authorization]
            response.addHeader("Access-Control-Allow-Headers", "Authorization");
        }

    }

    // فهرست پزشکان
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
    public void getAllMedecins(HttpServletResponse response) {
        sendOptions(response);
    }

    // فهرست مشتریان
    @RequestMapping(value = "/getAllClients", method = RequestMethod.OPTIONS)
    public void getAllClients(HttpServletResponse response) {
        sendOptions(response);
    }
}
  • خطوط ۲۹–۳۲: متد [getAllClients] درخواست‌های HTTP و [OPTIONS] را که توسط مرورگر ارسال می‌شوند، مدیریت خواهد کرد؛

3.7.7.4. آزمون برنامه – ۱

اکنون آمادهٔ آزمایش برنامه هستیم. وب‌سرور را راه‌اندازی کرده و سپس مقادیر معتبر را در فرم Angular وارد می‌کنیم. پاسخ زیر را دریافت می‌کنیم:

Image

این پیام خطا زمانی نمایش داده می‌شود که Angular نتوانسته است پرس‌وجوی درخواستی HTTP را اجرا کند. بنابراین باید علت را در لاگ‌های کنسول بررسی کرد. پیام زیر در آنجا یافت می‌شود:

XMLHttpRequest cannot load http://localhost:8080/getAllClients. هیچ هدر 'Access-Control-Allow-Origin' روی منبع درخواست‌شده وجود ندارد. بنابراین منشأ 'http://localhost:63342' مجاز به دسترسی نیست.

مشکلاتی که فکر می‌کردیم حل شده است. اکنون به ترافیک شبکه‌ای که رخ داده است نگاه می‌کنیم:

Image

می‌توانیم ببینیم که عملیات [getAllClients]، با استفاده از متدهای HTTP و [OPTIONS]،با موفقیت تکمیل شد، اما عملیات [getAllClients]، با استفاده از روش‌های HTTP و [GET]، لغو شد. پاسخ به درخواست [OPTIONS] به شرح زیر بود:

Image

سربرگ‌های HTTP از CORS موجود هستند. اکنون بیایید تبادلات HTTP را در طول GET بررسی کنیم:

Image

درخواست HTTP به نظر صحیح است. به‌ویژه می‌توانیم هدر احراز هویت را ببینیم.

علاوه بر پیام خطای قبلی، پیام زیر در لاگ‌های کنسول ظاهر می‌شود:


[dao] getData[/getAllClients] error réponse : {"data":"","status":0,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":""}

این لاگی است که متد [dao.getData] به طور سیستماتیک پس از دریافت پاسخ به درخواست خود HTTP تولید می‌کند. دو نکته قابل توجه وجود دارد:

  • [status=0]: این بدان معناست که Angular درخواست HTTP را لغو کرده است؛
  • [method=GET]: و این درخواست GET است که لغو شده است؛

با در نظر گرفتن پیام اول، این بدان معناست که برای درخواست GET نیز، Angular منتظر سربرگ‌ها برای CORS است. با این حال، در حال حاضر، سرویس وب ما آنها را فقط برای درخواست‌های HTTP و [OPTIONS] ارسال می‌کند. بسیار عجیب است که این خطا اکنون رخ می‌دهد و نه برای لیست پزشکان. من توضیحی ندارم.

بنابراین باید بار دیگر سرویس وب را اصلاح کنیم.

3.7.7.5. اصلاح سرویس وب – ۲

  

متدهای [GET] و [POST] در کلاس [RdvMedecinsController] مدیریت می‌شوند. ما باید این کلاس را اصلاح کنیم تا این متدها سربرگ‌های CORS را ارسال کنند. این کار را به شرح زیر انجام می‌دهیم:


@RestController
public class RdvMedecinsController {

    @Autowired
    private ApplicationModel application;

    @Autowired
    private RdvMedecinsCorsController rdvMedecinsCorsController;

...

    // فهرست مشتریان
    @RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
    public Reponse getAllClients(HttpServletResponse response) {
        // سربرگ‌ها CORS
        rdvMedecinsCorsController.getAllClients(response);
        //وضعیت برنامه
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // فهرست مشتریان
        try {
            return new Reponse(0, application.getAllClients());
        } catch (Exception e) {
            return new Reponse(1, Static.getErreursForException(e));
        }
    }
...
  • خط ۸: ما می‌خواهیم کدی را که در کنترلر [RdvMedecinsCorsController] قرار داده‌ایم، مجدداً استفاده کنیم. بنابراین آن را اینجا وارد می‌کنیم؛
  • خط ۱۴: متدی که درخواست [GET /getAllClients] را مدیریت می‌کند. ما دو تغییر انجام می‌دهیم:
    • خط ۱۴: ما شیء [HttpServletResponse] را به پارامترهای متد تزریق می‌کنیم،
    • خط ۱۶: ما از متدهای کلاس [RdvMedecinsCorsController] برای پر کردن این شیء با سربرگ‌های CORS استفاده می‌کنیم؛

3.7.7.6. تست برنامه – ۲

نسخه جدید سرویس وب را راه‌اندازی می‌کنیم و دوباره لیست مشتریان را درخواست می‌کنیم. پاسخ زیر را دریافت می‌کنیم:

  • در [1]، ما در واقع یک پاسخ داریم، اما آن خالی است ([2]);
  • برای [3]: ارتباط شبکه به درستی برقرار شد؛

در لاگ‌های کنسول، متد [dao.getData] پاسخی را که دریافت کرده بود نمایش داد:


[dao] getData[/getAllClients] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"} 

بنابراین متد واقعاً لیست کلاینت‌ها را دریافت کرده است. پس از بررسی کد، اکنون به دستور زیر مشکوک هستیم که به طور کامل آن را درک نمی‌کنیم:


// استایل‌بندی لیست کشویی
$('.selectpicker').selectpicker();

ما خط ۲ را غیرفعال کرده و دوباره امتحان می‌کنیم. سپس پاسخ زیر را دریافت می‌کنیم:

بنابراین ما مشکل را شناسایی کرده‌ایم. این اجرای متد [selectpicker] روی لیست کشویی است که باعث این مشکل می‌شود. وقتی به کد منبع صفحه‌ای که خطا دارد نگاه می‌کنیم، موارد زیر را می‌بینیم:

  • کشف می‌کنیم که در [1]، لیست کشویی واقعاً با موارد خود وجود دارد اما در [style='display:none'] نمایش داده نمی‌شود [1];
  • در [2]، دکمه از [bootstrap select] نمایش داده می‌شود. موارد لیست کشویی باید در لیست <ul role='menu'> ظاهر شوند. آنها در آنجا نیستند، بنابراین لیست خالی است. به نظر می‌رسد زمانی که متد [selectpicker] روی لیست کشویی اعمال شد، محتوای آن در آن زمان خالی بود؛

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


// استایل‌بندی لیست کشویی
$('.selectpicker').selectpicker();

با کد زیر جایگزین کنید:


            // استایل‌دهی لیست کشویی
            $timeout(function(){
              $('.selectpicker').selectpicker();
});

استایل [bootstrap-select] از طریق تابع [$timeout] اعمال می‌شود. ما قبلاً با این تابع آشنا شده‌ایم که امکان اجرای یک تابع پس از یک تأخیر مشخص را فراهم می‌کند. در اینجا، عدم وجود تأخیر معادل تأخیر صفر است. خطوط قبلی یک رویداد را در صف رویدادهای مرورگر قرار می‌دهند. هنگامی که پردازش رویداد فعلی (کلیک روی دکمه [Liste des clients]) در شرف تکمیل شدن است، نمای V نمایش داده خواهد شد. بلافاصله پس از آن، مرورگر صف رویدادهای خود را بررسی می‌کند. از آنجا که این رویداد تأخیر صفر دارد، رویداد [$timeout] در بالای لیست قرار گرفته و پردازش می‌شود. سپس استایل [bootstrap-select] بر روی یک لیست کشویی پرشده اعمال می‌شود. بیایید نتیجه را ببینیم:

اگر دوباره به کد منبع صفحه نمایش داده شده نگاه کنیم، موارد زیر را مشاهده می‌کنیم:

دکمه [bootstrap-select] که قبلاً خالی بود، اکنون حاوی فهرست مشتریان است.

3.7.7.7. با استفاده از یک دستور

در کنترلر C برای نمای V، کد زیر را یافتیم:


            // استایل‌بندی لیست کشویی
            $('.selectpicker').selectpicker();

ما در حال دستکاری یک شیء DOM هستیم. بسیاری از توسعه‌دهندگان Angular مخالف دستکاری DOM در کد یک کنترلر هستند. برای آن‌ها، این کار باید در یک directive انجام شود. یک دستور Angular را می‌توان به‌عنوان توسعه‌ای بر زبان HTML در نظر گرفت. این امکان را فراهم می‌کند که عناصر یا ویژگی‌های جدید HTML ایجاد شوند. بیایید به اولین مثال نگاه کنیم:

ما فایل JS [selectEnable] زیر را ایجاد می‌کنیم:


angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout) {
  return {
    link: function (scope, element, attrs) {
      $timeout(function () {
        var selectpicker = $('.selectpicker');
        selectpicker.selectpicker();
      });
    }
  };
}]);
  • این دستورالعمل از نحو کنترلر که اکنون با آن آشنا هستیم پیروی می‌کند:

angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout)

این دستور به ماژول [rvmedecins] تعلق دارد. این یک تابع است که دو پارامتر را می‌پذیرد:

  • (ادامه)
    • اولین مورد نام دستور [selectEnable] است؛
    • دومین یک آرایه ['obj1','obj2',..., function(obj1, obj2,...)] است، که در آن [obj] اشیایی هستند که باید به تابع تزریق شوند. در اینجا، تنها شیء تزریقی، شیء از پیش تعریف شده [$timeout] است؛
  • تابع [directive] شیئی را بازمی‌گرداند که ممکن است ویژگی‌های مختلفی داشته باشد. در اینجا، تنها ویژگی، ویژگی [link] (خط ۳) است. مقدار آن در اینجا تابعی است که سه پارامتر می‌گیرد:
    • دامنه: قالب نمایی که دستورالعمل در آن استفاده می‌شود؛
    • element: عنصر در نما، هدف دستورالعمل؛
    • attrs: ویژگی‌های این عنصر؛

بیایید یک مثال بزنیم. دستور [selectEnable] می‌تواند در زمینه زیر استفاده شود:

<div select-enable="data"></div>

در مثال بالا، ویژگی [select-enable] دستور [selectEnable] را به عنصر از نوع HTML اعمال می‌کند. دستور [doSomething] را می‌توان با افزودن صفت [do-something] به هر عنصر HTML اعمال کرد. به تفاوت املایی بین نام دستور و صفت مربوطه توجه کنید. املای آن از [camelCase] به [camel-case] تغییر می‌کند.

دستور [selectEnable] می‌تواند به شکل زیر نیز استفاده شود:

<select-enable attr1='val1' attr2='val2' ...>...</select-enable>

در اینجا، دستور [doSomething] به صورت یک تگ اعمال می‌شود: HTML <do-something>.

بیایید به کد بازگردیم

<div select-enable="data"></div>

و سه پارامتر تابع [link] دستور، [scope, element, attrs]:

  • scope: قالب نمایی است که در آن قرار دارد؛
  • element: خود است؛
  • attrs: آرایهٔ ویژگی‌های است. این ویژگی‌ها می‌توانند برای ارسال اطلاعات به دستور directive استفاده شوند. در مثال بالا، برای دریافت اطلاعات [dataattrs['selectEnable'] را می‌نویسیم. تغییر در نشانه‌گذاری به [selectEnable] برای اشاره به ویژگی [select-enable] را توجه کنید؛

بیایید به کد دستورالعمل بازگردیم:


angular.module("rdvmedecins").directive('selectEnable', ['$timeout', function ($timeout) {
  return {
    link: function (scope, element, attrs) {
      $timeout(function () {
        $('.selectpicker').selectpicker();
      });
    }
  };
}]);
  • خطوط 14–16: در اینجا کدهایی را می‌بینیم که قبلاً در کنترلر قرار داده بودیم. این کدها زمانی اجرا می‌شوند که دستور [select-enable] (به شکل یک عنصر یا یک ویژگی) هنگام نمایش نمای V مواجه شود.

برای پیاده‌سازی این دستور، فایل [app-17.html] را به [app-17B.html] کپی کرده و آن را به شرح زیر تغییر می‌دهیم:


      <select data-style="btn-primary" class="selectpicker" select-enable="">
        <option ng-repeat="client in clients.data" value="{{client.id}}">
          {{client.titre}} {{client.prenom}} {{client.nom}}
        </option>
</select>
  • خط ۱: دستور [selectEnable] را بر عنصر HTML [select] اعمال می‌کنیم. از آنجایی که اطلاعاتی برای ارسال به دستور وجود ندارد، به سادگی [select-enable=""] را می‌نویسیم؛

ما همچنین با کپی کردن فایل JS [rdvmedecins-05.js] به [rdvmedecins-05B.js]، کنترلر را اصلاح می‌کنیم و در فایل دستور JS به فایل جدید [app-17B.html] ارجاع می‌دهیم. مهم است که این نکته آخر را نادیده نگیریم. اگر فایل دستور وجود نداشته باشد، [selectEnable.js]; 002520ZQX و فایل دستور [selectEnable.js].002520ZQX و فایل دستورالعمل [selectEnable.js]. مهم است که این نکتهٔ آخر را نادیده نگیرید. اگر فایل دستورالعمل وجود نداشته باشد، ویژگی [select-enable=""] پردازش نخواهد شد، اما Angular هیچ خطایی گزارش نخواهد کرد.


<script type="text/javascript" src="rdvmedecins-05B.js"></script>
<script type="text/javascript" src="selectEnable.js"></script>

در فایل JS [rdvmedecins-05B.js]، خطوط زیر را از کنترلر حذف می‌کنیم:


            // سبک‌دهی لیست کشویی
            $timeout(function(){
              $('.selectpicker').selectpicker();
});

زیرا این عملیات اکنون توسط دیکتیو انجام می‌شود.

3.7.7.8. آزمون برنامه – ۳

هنگام آزمایش برنامه جدید [app-17B.html]، نتیجه زیر به دست می‌آید:

  • در [1]، یک لیست خالی بازگردانده می‌شود.

لاگ‌های کنسول موارد زیر را نمایش می‌دهند:

1
2
3
[dao] init
directive selectEnable
[dao] getData[/getAllClients] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}
  • خط ۱: inicialization سرویس [dao]؛
  • خط ۲: در نمایش اولیه نما V، دستور [selectEnable] اجرا می‌شود؛
  • خط ۳: این خط زمانی ظاهر می‌شود که کاربر روی دکمه [Liste des clients] کلیک می‌کند. می‌بینیم که دستور [selectEnable] بار دوم اجرا نمی‌شود. در نهایت، این دستور زمانی اجرا شد که لیست مشتریان خالی بود، بنابراین با یک لیست کشویی خالی مواجه می‌شویم؛

به عبارت دیگر، عمل:


$('.selectpicker').selectpicker();

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


      <select data-style="btn-primary" class="selectpicker" select-enable="" ng-if="clients.data">
        <option ng-repeat="client in clients.data" value="{{client.id}}">
          {{client.titre}} {{client.prenom}} {{client.nom}}
        </option>
</select>

خط ۱: تگ <select> تنها در صورتی تولید می‌شود که [clients.data] وجود داشته باشد. این امر زمانی که نما V برای اولین بار نمایش داده می‌شود، صادق نیست. بنابراین، تگ <select> تولید نخواهد شد و دستور [selectEnable] ارزیابی نخواهد شد. هنگامی که کاربر روی دکمه [Liste des clients] کلیک می‌کند، [clients.data] در قالب M مقدار جدیدی به خود می‌گیرد. از آنجایی که قالب M تغییر کرده است، تگ <select> در این نقطه مجدداً ارزیابی و تولید خواهد شد. دستور [selectEnable] نیز بنابراین ارزیابی خواهد شد. وقتی این دستور ارزیابی می‌شود، خطوط ۲ تا ۴ تگ <select> هنوز ارزیابی نشده‌اند. بنابراین ما یک لیست خالی از مشتریان داریم. اگر دستور [selectEnable] را به شکل زیر بنویسیم:


angular.module("rdvmedecins").directive('selectEnable', ['$timeout', 'utils', function ($timeout, utils) {
  return {
    link: function (scope, element, attrs) {
      utils.debug("directive selectEnable");
      $('.selectpicker').selectpicker();
    }
  }
}]);

خط ۵ با یک لیست خالی اجرا خواهد شد و یک لیست کشویی خالی نمایش داده می‌شود. بنابراین باید بنویسید:


angular.module("rdvmedecins").directive('selectEnable', ['$timeout', 'utils', function ($timeout, utils) {
  return {
    link: function (scope, element, attrs) {
      utils.debug("directive selectEnable");
      $timeout(function () {
        $('.selectpicker').selectpicker();
      })
    }
  }
}]);

برای دستیابی به نتیجه مورد انتظار. به دلیل وجود [$timeout] در خط ۵، خط ۶ تنها پس از ارزیابی کامل نمای V اجرا خواهد شد، یعنی در زمانی که تگ <select> شامل تمام عناصر خود باشد.

3.7.8. مثال ۸: دفترچه یادداشت پزشک

اکنون برنامه‌ای را ارائه می‌کنیم که دفترچه یادداشت پزشک را نمایش می‌دهد.

3.7.8.1. ویوی برنامه V

ما فرم زیر را ارائه خواهیم داد:

  • در [1]، دفتر یادداشت قرار ملاقات خانم PELISSIER [2]، در تاریخ ۲۵ ژوئن ۲۰۱۴ [3] را درخواست می‌کنیم؛

نتیجهٔ زیر به‌دست آمد: [4]:

ما این دو دیدگاه را به‌طور جداگانه بررسی خواهیم کرد.

3.7.8.2. فرم

ما فایل [app-17.html] را به‌عنوان [app-18.html] کپی می‌کنیم، سپس کد را به شرح زیر تغییر می‌دهیم:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <!-- پیام در حال انتظار -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    ...
  </div>

  <!-- درخواست -->
  <div class="alert alert-info" ng-hide="waiting.visible">
    <div class="row" style="margin-bottom: 20px">
      <div class="col-md-3">
        <h2 translate="{{medecins.title}}"></h2>
        <select data-style="btn-primary" class="selectpicker">
          <option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
            {{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
          </option>
        </select>
      </div>
      <div class="col-md-3">
        <h2 translate="{{calendar.title}}"></h2>
        <div style="display:inline-block; min-height:290px;">
          <datepicker ng-model="calendar.jour" min-date="calendar.minDate" show-weeks="true"
                      class="well well-sm"></datepicker>
        </div>
      </div>
    </div>
    <button class="btn btn-primary" ng-click="execute()">{{agenda.title|translate}}</button>
  </div>

  <!-- فهرست خطاها -->
  <div class="alert alert-danger" ng-show="errors.show">
...
  </div>

  <!-- تقویم -->
  <div id="agenda" ng-show="agenda.show">
...
  </div>
</div>
...
<script type="text/javascript" src="rdvmedecins-06.js"></script>
  • خطوط ۵–۷: پیام در حال انتظار بدون تغییر باقی می‌ماند؛
  • خطوط ۱۲–۱۹: فهرست پزشکان از نوع [bootstrap select
  • خطوط ۲۰–۲۶: تقویم از [ui-bootstrap]، که قبلاً ارائه شده است. توجه داشته باشید که روز انتخاب‌شده در قالب [calendar.jour] (ویژگی ng-model) قرار می‌گیرد؛
  • خط ۲۸: دکمه‌ای که تقویم را درخواست می‌کند؛
  • خطوط ۳۲–۳۴: لیست خطاها بدون تغییر باقی می‌ماند؛
  • خطوط ۳۷–۳۹: تقویم، که بعداً به آن خواهیم پرداخت؛
  • خط ۴۲: کد JS با کپی کردن از فایل [rdvmedecins-05.js] به فایل [rdvmedecins-06.js] منتقل می‌شود؛

3.7.8.3. کنترلر C

کد برنامه JS به شرح زیر تغییر می‌یابد:

Image

تنها سرویس [utils] و کنترل‌کننده [rdvMedecinsCtrl] تحت تأثیر این تغییرات قرار خواهند گرفت.

کنترل‌کننده [rdvMedecinsCtrl] به شرح زیر تغییر می‌یابد:


// کنترل‌کننده
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
    function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
      // ------------------- آغازین‌سازی قالب
      // قالب
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
      $scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
      $scope.errors = {show: false, model: {}};
      $scope.medecins = {
        data: [
          {id: 1, version: 1, titre: "Mme", nom: "PELISSIER", prenom: "Marie"},
          {id: 2, version: 1, titre: "Mr", nom: "BROMARD", prenom: "Jacques"},
          {id: 3, version: 1, titre: "Mr", nom: "JANDOT", prenom: "Philippe"},
          {id: 4, version: 1, titre: "Melle", nom: "JACQUEMOT", prenom: "Justine"}
        ],
        title: config.listMedecins};
      $scope.agenda = {title: config.getAgendaTitle, data: undefined, show: false};
      $scope.calendar = {title: config.getCalendarTitle, minDate: new Date(), jour: new Date()};
      // استایل‌دهی لیست کشویی
      $timeout(function () {
        $('.selectpicker').selectpicker();
      });
      // محلی فرانسوی برای تقویم
      angular.copy(config.locales['fr'], $locale);
 ...
    }
  ])
;
  • خط ۷: یک تایم‌اوت ۳ ثانیه‌ای قبل از تماس با HTTP تنظیم می‌شود؛
  • خط ۸: عناصر مورد نیاز برای اتصال HTTP به‌صورت سخت‌کد شده‌اند؛
  • خطوط ۱۰–۱۷: فهرست پزشکان به‌صورت کد سخت درج شده است؛
  • خط ۱۸: قالب [agenda] نمایش تقویم را در نما پیکربندی می‌کند؛
  • خط ۱۹: قالب [calendar] نمایش تقویم را در نما پیکربندی می‌کند. یک تاریخ حداقل [minDate] روی امروز تنظیم شده است، و تاریخ فعلی نیز روی امروز تنظیم شده است؛
  • خطوط ۲۱–۲۳: لیست کشویی با استفاده از روشی که قبلاً دیده شد، استایل‌دهی شده است؛
  • خط ۲۵: زبان برنامه روی «fr» تنظیم می‌شود. به‌طور پیش‌فرض روی «en» تنظیم شده است؛

متدی که هنگام درخواست تقویم اجرا می‌شود به شرح زیر است:


//اجرای یک اقدام
      $scope.execute = function () {
        // جزئیات فرم
        var idMedecin = $('.selectpicker').selectpicker('val');

        // اعتبارسنجی
        utils.debug("[homeCtrl] idMedecin", idMedecin);
        utils.debug("[homeCtrl] jour", $scope.calendar.jour);

        // تنظیم تاریخ به فرمت yyyy-MM-dd
        var formattedJour = $filter('date')($scope.calendar.jour, 'yyyy-MM-dd');
        // به‌روزرسانی نما
        $scope.waiting.visible = true;
        $scope.errors.show = false;
        $scope.agenda.show = false;
...
      };
  • خط ۴: ما ویژگی [value] را برای پزشک انتخاب‌شده بازیابی می‌کنیم. در اینجا، مجدداً از متد [selectpicker] که از فایل [bootstrap-select.min.js] آمده است، استفاده می‌کنیم. مهم است که فرمت گزینه‌های لیست کشویی را به خاطر بسپاریم:

          <option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
            {{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}

بنابراین، مقدار (ویژگی value) این گزینه، شناسهٔ پزشک [id] است.

  • خط ۱۱: ما روز انتخاب‌شده توسط کاربر را به فرمت [aaaa-mm-jj] تبدیل می‌کنیم که فرمت تاریخ مورد انتظار وب‌سرور است؛
  • خطوط ۱۳–۱۵: پس از اتمام متد [execute]، بنر بارگذاری نمایش داده می‌شود و سایر موارد پنهان می‌گردند؛

کد به شرح زیر ادامه می‌یابد:


// انتظار شبیه‌سازی‌شده
        var task = utils.waitForSomeTime($scope.waiting.time);
        // درخواست دفترچه یادداشت پزشک
        var promise = task.promise.then(function () {
          //مسیر به پزشک شیفت URL
          var path = config.urlSvrAgenda + "/" + idMedecin + "/" + formattedJour;
          //درخواست دفترچه یادداشت
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
          // بازگرداندن وعدهٔ اتمام وظیفه
          return task.promise;
        });
        // تحلیل نتیجه فراخوانی سرویس [dao]
        promise.then(function (result) {
          //پایان انتظار
          $scope.waiting.visible = false;
          // خطا؟
          if (result.err == 0) {
            // قالب تقویم در حال آماده‌سازی است
            $scope.agenda.data = result.data;
            $scope.agenda.show = true;
            //قالب‌بندی نمایش جدول زمانی
            angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
              creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
            });
            // ایجاد یک رویداد برای قالب‌بندی جدول پس از نمایش نما
            $timeout(function () {
              $("#creneaux").footable();
            });
          } else {
            //خطاهایی هنگام بازیابی تقویم رخ داد
            $scope.errors = {
              title: config.getAgendaErrors,
              messages: utils.getErrors(result),
              show: true
            };
}
  • خط ۲: وظیفهٔ انتظار غیرهمزمان ۳ ثانیه‌ای؛
  • خطوط ۵–۱۰: کدی که پس از اتمام این انتظار اجرا خواهد شد؛
  • خط ۶: URL با پرس‌وجو از [/getAgendaMedecinJour/1/2014-06-25] ساخته می‌شود؛
  • خط ۸: URL پرس‌وجو می‌شود. یک وظیفه ناهمزمان آغاز می‌شود؛
  • خط ۱۰: وعده (promise) برای این وظیفه ناهمزمان حل (resolved) می‌شود؛
  • خطوط ۱۴–۳۸: کدی که پس از بازگشت پاسخ HTTP اجرا خواهد شد؛
  • خط ۱۳: [result] پاسخی است که توسط متد [dao.getData] ارسال می‌شود. مهم است که فرمت پاسخ وب‌سرور را به خاطر بسپاریم:

پارامتر [result.data] در خط ۱۹، ویژگی [data] [1] ذکر شده در بالا است. این ویژگی به نوبه خود شامل ویژگی [creneauxMedecin] [2] نشان داده شده در بالا است. این یک آرایه از بازه‌های زمانی است که هر کدام شامل دو مورد اطلاعات زیر هستند:

  • [rv]: فرم JSON یک قرار ملاقات، یا [null] اگر برای آن بازه زمانی قرار ملاقاتی رزرو نشده باشد؛
  • [hDeb, mDeb, hFin, mFin]: جزئیات زمانی برای بازه زمانی؛

بیایید به کد کنترل‌کننده بازگردیم:

  • خط ۱۵: انتظار به پایان رسید؛
  • خط ۱۹: قالب [$scope.agenda] که نمایش تقویم را کنترل می‌کند، پر می‌شود؛
  • خط ۲۰: تقویم نمایش داده می‌شود؛
  • خطوط ۲۲–۲۴: ما روی هر یک از عناصر C در آرایه [creneauxMedecin] که همین حالا در مورد آن صحبت کردیم، تکرار می‌کنیم؛
  • خط ۲۳: هر عنصر C دارای یک ویژگی [creneau] است که جایگاه زمانی است. این ویژگی با یک ویژگی [text] تقویت می‌شود که نمایش متنی جایگاه زمانی در قالب [10h20:10h40] خواهد بود؛
  • خطوط ۲۶–۲۸: ما جدول HTML را که برای نمایش شکاف‌های تقویم استفاده می‌شود، «واکنش‌گرا» می‌کنیم. ما این مفهوم را در بخش ۳.۶.۷ بررسی کردیم؛
 
  • خط ۲۷: برای واکنش‌گرا کردن جدول، باید متد [footable] روی آن اعمال شود. ما در اینجا با همان دشواری‌ای مواجه می‌شویم که در مورد کامپوننت [bootstrap-select] با آن روبرو بودیم. اگر به سادگی خط ۱۷ را کپی کنیم، می‌بینیم که جدول واکنش‌گرا نیست. این مشکل به همان روش با استفاده از تابع [$timeout] (خط ۲۶) حل می‌شود؛
  • خطوط ۳۱–۳۴: حالتی که فراخوانی HTTP با شکست مواجه شده است. پیام‌های خطا سپس نمایش داده می‌شوند؛

3.7.8.4. نمایش تقویم

اکنون به کد تقویم در فایل [app-18.html] بازمی‌گردیم. این کد به شرح زیر است:


<!-- تقویم -->
  <div id="agenda" ng-show="agenda.show">
    <!-- موردی از یک پزشک که هیچ نوبت مشاوره در دسترس ندارد -->
    <h4 class="alert alert-danger" ng-if="agenda.data.creneauxMedecin.length==0"
        translate="agenda_medecinsanscreneaux"></h4>
    <!--دفترچه یادداشت پزشک -->
    <div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
      <div class="tab-pane active col-md-6">
        <table creneaux-table id="creneaux" class="table">
          <thead>
          <tr>
            <th data-toggle="true">
              <span translate="agenda_creneauhoraire"></span>
            </th>
            <th>
              <span translate="agenda_client">Client</span>
            </th>
            <th data-hide="phone">
              <span translate="agenda_action">Action</span>
            </th>
          </tr>
          </thead>
          <tbody>
          <tr ng-repeat="creneauMedecin in agenda.data.creneauxMedecin">
            <td>
            <span
              ng-class="! creneauMedecin.rv ? 'status-metro status-active' : 'status-metro status-suspended'">
              {{creneauMedecin.creneau.text}}
            </span>
            </td>
            <td>
              <span>{{creneauMedecin.rv.client.titre}} {{creneauMedecin.rv.client.prenom}} {{creneauMedecin.rv.client.nom}}</span>
            </td>
            <td>
              <a href="" ng-if="!creneauMedecin.rv" translate="agenda_reserver" class="status-metro status-active">
              </a>
              <a href="" ng-if="creneauMedecin.rv" translate="agenda_supprimer" class="status-metro status-suspended">
              </a>
            </td>
          </tr>
          </tbody>
        </table>
      </div>
    </div>
</div>
  • خطوط ۴–۵: به یاد داشته باشید که [agenda.data] تقویم است و [agenda.data.creneauxMedecin] آرایه‌ای از اشیاء از نوع [creneauMedecin] است. هر عنصر از این نوع دارای یک ویژگی [creneauMedecin.creneau] است که یک بازه زمانی است. هر بازه زمانی دو عنصر دارد که برای ما جالب توجه است:
    • [creneauMedecin.creneau.rv]، که پتانسیل RV (rv!=null) مرتبط با بازه زمانی است؛
    • [creneauMedecin.creneau.text]، که متن [début:fin] برای بازه زمانی است؛
  • خط ۴: اگر پزشک هیچ بازه زمانی نداشته باشد، یک پیام ویژه نمایش می‌دهد. این امر بعید است، اما از آنجایی که پایگاه داده ما ناقص است، این سناریو اتفاق می‌افتد. تولید یا عدم تولید پیام HTML توسط دستور [ng-if] کنترل می‌شود؛

Image

دستور [ng-if] با دستور [ng-show, ng-hide] متفاوت است. دستور دوم صرفاً یک فیلد موجود در سند را پنهان می‌کند. اگر [ng-if='false'] باشد، آنگاه فیلد از سند حذف می‌شود. ما از آن در اینجا برای اهداف نمایشی استفاده کرده‌ایم؛

  • خط ۹: ویژگی [id='creneaux'] مهم است. این ویژگی است که در دستور استفاده می‌شود:

$("#creneaux").footable();
  • خطوط ۱۰–۲۲: نمایش سربرگ‌های جدول [1];
  • خطوط 23–45: نمایش محتویات جدول [2];
  • خط ۲۴: آرایه [agenda.data.creneauxMedecin] را به‌صورت بازگشتی پردازش می‌کند؛
  • خطوط ۲۶–۲۹: متن [3] نوشته می‌شود. دستور [ng-class] استفاده می‌شود که ویژگی [class] عنصر را تولید می‌کند. در اینجا، اگر مقدار برابر با [creneauMedecin.rv==null] باشد، این بدان معناست که اسلات در دسترس است و متن پس‌زمینه سبز رنگ داده می‌شود. در غیر این صورت، پس‌زمینه آن قرمز است؛
  • خط ۳۲: ما نام مشتری را می‌نویسیم که برای او RV [4] رزرو شده است. اگر این مقدار [rv==null] باشد، این اطلاعات وجود ندارد، اما Angular این مورد را به درستی مدیریت می‌کند و خطایی گزارش نمی‌کند؛
  • خطوط ۳۴–۳۹: نمایش یکی از دو دکمه، [Réserver] یا [Supprimer]. وجود یا عدم وجود یک قرار ملاقات تعیین می‌کند که کدام یک از این دو دکمه انتخاب شود؛

3.7.8.5. اصلاح وب‌سرور

مانند مثال‌های قبلی، وب‌سرور باید اصلاح شود تا URL و [/getAgendaMedecinJour] سربرگ‌های CORS را ارسال کنند:

  

در کلاس [RdvMedecinsCorsController]، یک متد جدید اضافه کنید:


    //دفترچه یادداشت پزشک
    @RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.OPTIONS)
    public void getAgendaMedecinJour(HttpServletResponse response) {
        sendOptions(response);
}

این متد سربرگ‌های CORS را برای درخواست‌های HTTP و [OPTIONS] ارسال خواهد کرد. ما باید همین کار را برای درخواست‌های HTTP و [GET] در کلاس [RdvMedecinsController] انجام دهیم:


@RequestMapping(value = "/getAgendaMedecinJour/{idMedecin}/{jour}", method = RequestMethod.GET)
    public Reponse getAgendaMedecinJour(@PathVariable("idMedecin") long idMedecin, @PathVariable("jour") String jour, HttpServletResponse response) {
        // سربرگ‌ها CORS
        rdvMedecinsCorsController.getAgendaMedecinJour(response);
...
}

3.7.8.6. استفاده از دستورها

همان‌طور که قبلاً انجام دادیم، پردازش DOM را به دستورالعمل‌ها منتقل می‌کنیم. ما دو نمونه از پردازش DOM داریم:

  • هنگامی که نما برای اولین بار نمایش داده می‌شود:

      // سبک‌دهی لیست کشویی
      $timeout(function () {
        $('.selectpicker').selectpicker();
});
  • هنگام نمایش تقویم:

            //ایجاد رویدادی برای استایل‌بندی جدول پس از نمایش نما
            $timeout(function () {
              $("#creneaux").footable();
});

برای مورد اول، از دستور [selectEnable] که قبلاً ارائه شده است، استفاده خواهیم کرد. برای مورد دوم، دستور [footable] را در فایل JS [footable.js] زیر ایجاد می‌کنیم:


angular.module("rdvmedecins").directive('footable', ['$timeout', 'utils', function ($timeout, utils) {
  return {
    link: function (scope, element, attrs) {
      utils.debug("directive footable");
      $timeout(function () {
        $("#creneaux").footable();
      })
    }
  }
}]);

بنابراین از همان تکنیک دستور [selectEnable] استفاده می‌کنیم.

کد HTML [app-18.html] در [app-18B.html] کپی شده است. سپس به شرح زیر اصلاح می‌شود:


        <select data-style="btn-primary" class="selectpicker" select-enable="">
          <option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
            {{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
          </option>
</select>
  • خط ۱: دستور [selectEnable] (از طریق ویژگی [select-enable]) برای تگ <select> پزشکان اعمال می‌شود؛

    <div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
      <div class="tab-pane active col-md-6">
        <table id="creneaux" class="table" footable="">
          <thead>
<tr>
  • خط ۳: دستور [footable] (از طریق ویژگی [footable]) به جدول HTML در دفترچه یادداشت اعمال می‌شود؛

<script type="text/javascript" src="rdvmedecins-06B.js"></script>
<!-- دستورالعمل‌ها -->
<script type="text/javascript" src="selectEnable.js"></script>
<script type="text/javascript" src="footable.js"></script>
  • خطوط ۳–۴: فایل‌های JS برای هر دو دستورالعمل ارجاع داده شده‌اند؛
  • خط ۱: کد JS از [app-18B.html]، همان کد JS از [app-18.html] است که در فایل [rdvmedecins-06B.js] تکرار شده است؛

فایل [rdvmedecins-06B.js] به جز دو مورد جزئی با فایل [rdvmedecins-06.js] یکسان است. خطوط مربوط به DOM حذف شده‌اند:


      // سبک‌دهی لیست کشویی
      $timeout(function () {
        $('.selectpicker').selectpicker();
});

            //ایجاد یک رویداد برای استایل‌بندی جدول پس از نمایش نما
            $timeout(function () {
              $("#creneaux").footable();
});

در نتیجه، اجرای برنامه [app-18B.html] نتایج یکسانی با اجرای [app-18.html] دارد.

3.7.9. مثال ۹: ایجاد و لغو رزروها

اکنون برنامه‌ای را ارائه می‌کنیم که به شما امکان ایجاد و لغو رزروها را می‌دهد.

3.7.9.1. نما V برنامه

ما فرم زیر را ارائه خواهیم داد:

  • در [1]، می‌توانید یک رزرو انجام دهید. رزرو برای یک مشتری تصادفی انجام خواهد شد؛
  • در [2]، شما قادر خواهید بود رزروهایی را که انجام داده‌ایم حذف کنید؛

ما فایل [app-18.html] را در [app-19.html] کپی می‌کنیم، سپس کد را به شرح زیر تغییر می‌دهیم:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <!-- پیام انتظار -->
  <div class="alert alert-warning" ng-show="waiting.visible">
  ...
  </div>

  <!-- فهرست خطاها -->
  <div class="alert alert-danger" ng-show="errors.show">
...
  </div>

  <!--دفترچه یادداشت -->
  <div id="agenda" ng-show="agenda.show">
..
    <!-- دفترچه یادداشت پزشک -->
    <div class="row tab-content alert alert-warning" ng-if="agenda.data.creneauxMedecin.length!=0">
      <div class="tab-pane active col-md-6">
        <table id="creneaux" class="table" footable="">
...
          <tbody>
          <tr ng-repeat="creneauMedecin in agenda.data.creneauxMedecin">
...
            <td>
              <a href="" ng-if="!creneauMedecin.rv" translate="agenda_reserver" class="status-metro status-active"  ng-click="reserver(creneauMedecin.creneau.id)">
              </a>
              <a href="" ng-if="creneauMedecin.rv" translate="agenda_supprimer" class="status-metro status-suspended" ng-click="supprimer(creneauMedecin.rv.id)">
              </a>
            </td>
          </tr>
          </tbody>
        </table>
      </div>
    </div>
  </div>
</div>
....
<script type="text/javascript" src="rdvmedecins-07.js"></script>
<script type="text/javascript" src="footable.js"></script>
  • خطوط ۵–۷: پیام انتظار همان نسخه قبلی است؛
  • خطوط ۱۰–۱۲: پیام خطا همانند نسخه قبلی است؛
  • خطوط ۱۵–۳۶: تقویم همانند نسخه قبلی است، با دو تفاوت جزئی:
    • خط ۲۶: کلیک روی دکمه [réserver] (ویژگی ng-click) توسط متد [reserver] مدل M در نمای V پردازش می‌شود. شماره اسلات رزرو به آن ارسال می‌شود؛
    • خط 26: کلیک روی دکمه [supprimer] توسط متد [reserver] از مدل M در نمای V مدیریت می‌شود. شماره قرار ملاقاتی که باید حذف شود به آن ارسال می‌شود؛
  • خط ۳۹: کد JS که مدیریت برنامه را بر عهده دارد، در فایل [rdvmedecins-07.js] قرار دارد؛
  • خط ۴۰: کد JS از دستور [footable] که در خط ۲۰ اعمال شده است؛

3.7.9.2. کنترل‌کننده C

کد JS از [rdvmedecins-07.js] ابتدا با کپی کردن از فایل [rdvmedecins-06.js] به دست می‌آید. سپس اصلاح می‌شود. بلوک‌های بزرگ معمول کد همچنان موجود هستند. اصلاحات عمدتاً در کنترلر انجام می‌شوند:

Image

ما کنترل‌کننده C را برای نمای V در چند مرحله شرح خواهیم داد.

3.7.9.3. ابتدایی‌سازی کنترلر C

کد مربوط به инициалиزه‌سازی کنترلر به شرح زیر است:


angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
    function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
      // ------------------- inicializه‌سازی قالب
      // قالب
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
      $scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
      $scope.errors = {show: false, model: {}};
      $scope.medecins = {
        data: [
          {id: 1, version: 1, titre: "Mme", nom: "PELISSIER", prenom: "Marie"},
          {id: 2, version: 1, titre: "Mr", nom: "BROMARD", prenom: "Jacques"},
          {id: 3, version: 1, titre: "Mr", nom: "JANDOT", prenom: "Philippe"},
          {id: 4, version: 1, titre: "Melle", nom: "JACQUEMOT", prenom: "Justine"}
        ],
        title: config.listMedecins
      };
      var médecin = $scope.medecins.data[0];
      var clients = [
        {id: 1, version: 1, titre: "Mr", nom: "MARTIN", prenom: "Jules"},
        {id: 2, version: 1, titre: "Mme", nom: "GERMAN", prenom: "Christine"},
        {id: 3, version: 1, titre: "Mr", nom: "JACQUARD", prenom: "Maurice"},
        {id: 4, version: 1, titre: "Melle", nom: "BISTROU", prenom: "Brigitte"}
      ];
      // قالب تاریخ فرانسوی
      angular.copy(config.locales['fr'], $locale);
      var today = new Date();
      var formattedDay = $filter('date')(today, 'yyyy-MM-dd');
      var fullDay = $filter('date')(today, 'fullDate');
      $scope.agenda = {title: config.agendaTitle, data: undefined, show: false, model: {titre: médecin.titre, prenom: médecin.prenom, nom: médecin.nom, jour: fullDay}};


      // ---------------------------------------------------------------- تقویم اولیه
      // وظیفهٔ ناهمزمان جهانی
      var task;
      // درخواست تقویم
      getAgenda();

      // ------------------------------------------------------------------ رزرو
      $scope.reserver = function (creneauId) {
....
      };

      // ------------------------------------------------------------ حذف RV
      $scope.supprimer = function (idRv) {
...
      };

      // بازیابی تقویم
      function getAgenda() {
 ...
      }

      // لغو در انتظار
      function cancel() {
...
      }
} ]);
  • خط ۶: پیکربندی پیام انتظار. به‌طور پیش‌فرض، سیستم ۳ ثانیه قبل از فراخوانی HTTP منتظر می‌ماند؛
  • خط ۷: اطلاعات مورد نیاز برای فراخوانی‌های HTTP؛
  • خط ۸: پیکربندی پیام خطا؛
  • خطوط ۹–۱۷: پزشکان از پیش تعیین‌شده؛
  • خط ۱۸: یک پزشک خصوصی. نوبت‌های ملاقات او رزرو خواهد شد؛
  • رده‌های ۱۹–۲۴: مشتریان عادی؛
  • خط ۲۶: می‌خواهیم تاریخ‌های فرانسوی را مدیریت کنیم؛
  • خط ۲۷: قرار ملاقات‌ها برای تاریخ امروز رزرو خواهند شد؛
  • خط ۲۸: سرویس رزرو آنلاین انتظار دارد تاریخ‌ها به فرمت «yyyy-mm-dd» باشند؛
  • خط ۲۹: تاریخ امروز در قالب [jeudi 26 juin 2014];
  • خط ۳۰: پیکربندی تقویم. ویژگی [model] پارامترهای پیام بین‌المللی‌سازی‌شده‌ای را که باید نمایش داده شود، حمل می‌کند:

        agenda_title: "Agenda de {{titre}} {{prenom}} {{nom}} le {{jour}}"
  • خط ۳۵: متغیر سراسری [task] در هر لحظه، نمایانگر وظیفه غیرهمزمان (asynchronous) در حال اجرا است؛
  • خط ۳۷: دفترچه خاطرات اولیه درخواست می‌شود؛

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

Image

3.7.9.4. بازیابی دفترچه خاطرات

تقویم با استفاده از روش زیر بازیابی می‌شود: [getAgenda]:


      //بازیابی تقویم
      function getAgenda() {
        //مسیر سرویس URL
        var path = config.urlSvrAgenda + "/" + médecin.id + "/" + formattedDay;
        // درخواست دفترچه یادداشت
        task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
        // پیام انتظار
        $scope.waiting.visible = true;
        // تحلیل نتیجه تماس با سرویس [dao]
        task.promise.then(function (result) {
          //پایان انتظار
          $scope.waiting.visible = false;
          // خطا؟
          if (result.err == 0) {
            // آماده‌سازی قالب تقویم
            $scope.agenda.data = result.data;
            $scope.agenda.show = true;
            //قالب‌بندی نمایش جدول زمانی
            angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
              creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
            });
          } else {
            //خطاهایی هنگام بازیابی تقویم رخ داد
            $scope.errors = {title: config.getAgendaErrors, messages: utils.getErrors(result), show: true};
          }
        });
}

این کد مشابه کدی است که در برنامه قبلی بررسی شد. دو تغییر وجود دارد:

  • قبل از فراخوانی HTTP هیچ وقفه شبیه‌سازی‌شده‌ای وجود ندارد؛
  • خط ۴: از doctor که در حین راه‌اندازی کنترل‌کننده ایجاد شده است، به همراه روز قالب‌بندی‌شده‌ای که ساخته شده بود، استفاده می‌شود؛

این کد به یک تابع استخراج شده است، زیرا توابع [reserver] و [supprimer] نیز از آن استفاده می‌کنند.

3.7.9.5. رزرو یک بازه زمانی

لطفاً توجه داشته باشید که مشتریان به‌صورت تصادفی انتخاب می‌شوند.

کد رزرو به شرح زیر است:


$scope.reserver = function (creneauId) {
        utils.debug("réservation du créneau", creneauId);
        //ایجاد یک RV با یک مشتری تصادفی در بازه زمانی شناسایی‌شده توسط [id]
        var idClient = clients[Math.floor(Math.random() * clients.length)].id;
        utils.debug("réservation du créneau pour le client", idClient);
        // انتظار شبیه‌سازی‌شده
        $scope.waiting.visible = true;
        var task = utils.waitForSomeTime($scope.waiting.time);
        // فاصله زمانی اضافه می‌شود
        var promise = task.promise.then(function () {
          //مسیر سرویس URL
          var path = config.urlSvrResaAdd;
          //داده‌هایی که باید به سرویس ارسال شوند
          var post = {jour: formattedDay, idCreneau: creneauId, idClient: idClient};
          // راه‌اندازی وظیفه ناهمزمان
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
          // بازگرداندن وعدهٔ اتمام وظیفه
          return task.promise;
        });

        // تحلیل نتیجه وظیفه
        promise = promise.then(function (result) {
          if (result.err != 0) {
            //خطاهایی در حین اعتبارسنجی RV رخ داد
            $scope.errors = {title: config.postResaErrors, messages: utils.getErrors(result, $filter), show: true};
          } else {
            // درخواست برنامه جدید
            getAgenda();
          }
        });

      };
  • خط ۱: لطفاً توجه داشته باشید که پارامتر برای تابع [reserver]، شماره اسلات (ویژگی id) است؛
  • خط ۴: یک مشتری به صورت تصادفی از لیست مشتریان که در کد راه‌اندازی به صورت ثابت کدگذاری شده‌اند، انتخاب می‌شود. شناسه‌ی آنها، [id]، حفظ می‌شود؛
  • خطوط ۷–۸: انتظار ۳ ثانیه‌ای؛
  • خطوط ۱۱–۱۸: این خطوط تنها پس از گذشت ۳ ثانیه اجرا می‌شوند؛
  • خط ۱۲: URL از سرویس رزرو [/ajouterRv]. این URL با نمونه‌های قبلی که تاکنون با آن‌ها مواجه شده‌ایم متفاوت است. در سرویس وب به صورت زیر تعریف شده است:

    @RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post, HttpServletResponse response) {
  • (ادامه)
    • خط ۱: URL هیچ پارامتری ندارد و با یک POST درخواست می‌شود؛
    • خط ۲: پارامترها به صورت یک شیء JSON ارسال می‌شوند. این شیء به پارامتر [post] (@RequestBody) دی‌سریالایز خواهد شد؛

ما مثالی از این را در POST (بخش 2.12.2) دیده‌ایم:

  • در [0]، URL از سرویس وب؛
  • به [1]، روش POST استفاده می‌شود؛
  • در [2]، متن JSON اطلاعات ارسال‌شده به سرویس وب به شکل {day, idClient, idCreneau} است؛
  • در [3]، کلاینت به سرویس وب اطلاع می‌دهد که در حال ارسال اطلاعات JSON برای آن است؛

بیایید به کد JS در تابع [reserver] بازگردیم:

  • خط ۱۴: مقداری که باید ارسال شود به شکل یک شیء JS ایجاد می‌شود. Angular هنگام ارسال آن را به JSON سریالیزه خواهد کرد؛
  • خط 16: فراخوانی HTTP انجام می‌شود. مقدار قابل ارسال، پارامتر آخر تابع [dao.getData] است. وقتی این پارامتر وجود داشته باشد، تابع [dao.getData] به جای GET، یک POST بازمی‌گرداند (به کد در بخش 3.7.6.4 مراجعه کنید);
  • خط ۱۸: وعده (promise) از فراخوانی HTTP بازگردانده می‌شود؛
  • خطوط ۲۳–۲۹: تنها پس از بازگشت پاسخ فراخوانی HTTP اجرا می‌شوند؛
  • خط ۲۳: پارامتر [result] شکل [err,data] یا [err,messages] را دارد، که در آن [err] یک کد خطا است؛
  • خطوط ۲۳–۲۶: اگر خطایی رخ داده باشد، پیام خطا نمایش داده می‌شود؛
  • خط ۲۸: اگر رزرو موفق بوده باشد، تقویم جدید دوباره نمایش داده می‌شود؛

3.7.9.6. اصلاح سرور

  

در کلاس [RdvMedecinsCorsController]، متد زیر را اضافه می‌کنیم:


    //ارسال گزینه‌ها به کلاینت
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // سربرگ روی CORS تنظیم شده است
            response.addHeader("Access-Control-Allow-Origin", "*");
            // سربرگ مجاز است [authorization]
            response.addHeader("Access-Control-Allow-Headers", "authorization");
        }

    @RequestMapping(value = "/ajouterRv", method = RequestMethod.OPTIONS)
    public void ajouterRv(HttpServletResponse response) {
        sendOptions(response);
}

این افزوده در خطوط ۱۰ تا ۱۳ انجام می‌شود. سربرگ‌ها در خطوط 2–8 به URL و [/ajouterRv] (خط 10) و به متد HTTP و [OPTIONS] (خط 10) ارسال خواهند شد.

آیا کلاس [RdvMedecinsController] به شرح زیر اصلاح شده است:


    @RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse ajouterRv(@RequestBody PostAjouterRv post, HttpServletResponse response) {
        // سربرگ‌ها CORS
        rdvMedecinsCorsController.ajouterRv(response);
...

برای متد [POST] (خط 1) و متدهای URL و [/ajouterRv] (خط 1)، متدی که ما همین حالا در [RdvMedecinsCorsController] اضافه کرده‌ایم فراخوانی می‌شود (خط ۴)، و بدین ترتیب همان سربرگ‌های مربوط به متدهای HTTP و [OPTIONS] را بازمی‌گرداند.

3.7.9.7. Tests

بیایید یک تست اولیه انجام دهیم که در آن هر اسلات قدیمی را رزرو می‌کنیم:

 

همان‌طور که همیشه در چنین مواردی، باید لاگ‌های کنسول را بررسی کنیم:


[dao] getData[/ajouterRv] error réponse : {"data":"","status":0,"config":{"method":"POST","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/ajouterRv","data":{"jour":"2014-06-30","idCreneau":1,"idClient":4},"headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4=","Content-Type":"application/json;charset=utf-8"}},"statusText":""}

متد [dao.getData] با [status=0] شکست خورد، که به این معنی است که Angular درخواست را لغو کرده است. علت خطا در لاگ‌ها نمایش داده شده است:

XMLHttpRequest cannot load http://localhost:8080/ajouterRv. فیلد هدر Request با نام Content-Type توسط Access-Control-Allow-Headers مجاز نیست.

اگر به ترافیک شبکه نگاه کنیم، موارد زیر را مشاهده می‌کنیم:

  • در [1] و [2]: تنها یک درخواست وجود داشت، HTTP؛ درخواست [OPTIONS
  • در [3]، کلاینت Angular دو مجوز درخواست می‌کند:
    • مجوز ارسال سربرگ‌های HTTP و [accept, authorization, content-type
    • مجوز ارسال یک فرمان POST;
  • در [4]: سرور، هدر [authorization] را مجاز می‌کند. به یاد داشته باشید که در سمت سرور، خود ما این مجوز را ارسال می‌کنیم؛

توسعه جدید این است که برای یک عملیات POST، کلاینت Angular درخواست مجوزهای بیشتری از سرور می‌کند. بنابراین باید سرور را طوری اصلاح کنیم که آن‌ها را اعطا کند:

  

در کلاس [RdvMedecinsCorsController]، ما متد خصوصی را که سربرگ‌های HTTP را برای دستورات OPTIONS ارسال می‌کند، اصلاح می‌کنیم، GET و POST:


    // ارسال گزینه‌ها به کلاینت
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            //تنظیم هدر CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
            // سرور برخی هدرها را مجاز می‌داند
            response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
            // POST مجاز است
            response.addHeader("Access-Control-Allow-Methods", "POST");
        }
}
  • خط ۷: مجوز برای سربرگ‌های HTTP و [accept, content-type] اضافه شده است؛
  • خط ۹: مجوز برای متد POST اضافه شده است؛

ما پس از راه‌اندازی مجدد سرور، تست را دوباره اجرا خواهیم کرد:

 

این بار رزرو با موفقیت انجام شد.

3.7.9.8. حذف یک قرار ملاقات

کد عملکرد برای [supprimer] به شرح زیر است:


$scope.supprimer = function (idRv) {
        utils.debug("suppression rv n°", idRv);
        // انتظار شبیه‌سازی‌شده
        $scope.waiting.visible = true;
        task = utils.waitForSomeTime($scope.waiting.time);
        // اسلات اضافه شده است
        var promise = task.promise.then(function () {
          //مسیر سرویس URL
          var path = config.urlSvrResaRemove;
          //داده‌ها باید به سرویس ارسال شوند
          var post = {idRv: idRv};
          // راه‌اندازی وظیفه ناهمزمان
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
          // بازگرداندن وعدهٔ اتمام وظیفه
          return task.promise;
        });

        // تحلیل نتیجه وظیفه
        promise = promise.then(function (result) {
          if (result.err != 0) {
            //خطاهایی هنگام حذف rv رخ داد
            $scope.errors = {title: config.postRemoveErrors, messages: utils.getErrors(result, $filter), show: true};
            // UI به‌روزرسانی می‌شود
            $scope.waiting.visible = false;
          } else {
            //درخواست برنامه زمان‌بندی جدید
            getAgenda();
          }
        });
      };
  • خط ۱: مهم است به خاطر داشته باشید که پارامتر تابع، شماره قرار ملاقاتی است که باید حذف شود. این کد بسیار شبیه به کدی است که برای رزروها استفاده می‌شود. ما فقط به تفاوت‌ها اشاره خواهیم کرد؛
  • خط ۹: URL مربوط به سرویس در اینجا به [/supprimerRV] تغییر یافته است و در اینجا نیز از طریق POST به آن دسترسی پیدا می‌شود:

    @RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse response) {

پارامتر ارسال‌شده مجدداً در قالب JSON منتقل می‌شود. در بخش 2.12.17، ماهیت POST تولیدشده به‌صورت دستی را نشان دادیم:

  • در [1]، URL از سرویس وب؛
  • برای [2]، از متد POST استفاده می‌شود؛
  • در [3]، متن JSON اطلاعات ارسال‌شده به سرویس وب در قالب {idRv} است؛
  • در [4]، کلاینت به سرویس وب اطلاع می‌دهد که در حال ارسال اطلاعات JSON برای آن است؛

بیایید به کد JS در تابع [supprimer] بازگردیم:

  • خط ۱۱: شیء POST ایجاد می‌شود. Angular به‌طور خودکار آن را به‌عنوان JSON سریالیزه می‌کند؛

بقیه کد مشابه کد رزرو است.

3.7.9.9. تغییرات سمت سرور

در سمت سرور، تغییرات زیر را اعمال می‌کنیم:

  

در کلاس [RdvMedecinsCorsController]، متد زیر را اضافه می‌کنیم:


    // ارسال گزینه‌ها به کلاینت
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            //تنظیم هدر CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
            // مجوزدهی برخی سربرگ‌ها
            response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
            // مجوزدهی POST
            response.addHeader("Access-Control-Allow-Methods", "POST");
        }
    }
...
    @RequestMapping(value = "/supprimerRv", method = RequestMethod.OPTIONS)
    public void supprimerRv(HttpServletResponse response) {
        sendOptions(response);
}

این افزوده در خطوط ۱۳–۱۶ انجام شده است. سربرگ‌ها در خطوط ۲–۱۰ به URL و [/supprimerRv] (خط ۱۳) و متد HTTP و [OPTIONS] (خط ۱۳) ارسال خواهند شد.

کلاس [RdvMedecinsController] به شرح زیر اصلاح شده است:


    @RequestMapping(value = "/supprimerRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
    public Reponse supprimerRv(@RequestBody PostSupprimerRv post, HttpServletResponse response) {
        // سربرگ‌ها CORS
        rdvMedecinsCorsController.supprimerRv(response);
...

برای متد [POST] (خط 1) و متدهای URL و [/supprimerRv] (خط 1)، متدی که ما همین حالا در [RdvMedecinsCorsController] اضافه کرده‌ایم فراخوانی می‌شود (خط ۴)، و بدین ترتیب همان سربرگ‌های مربوط به متدهای HTTP و [OPTIONS] را بازمی‌گرداند.

3.7.10. مثال ۱۰: ایجاد و لغو رزروها – ۲

اکنون همان برنامه قبلی را ارائه می‌دهیم، اما به جای رزرو برای یک مشتری تصادفی، مشتری از یک لیست کشویی انتخاب می‌شود.

3.7.10.1. نما V برنامه

فرم زیر را ارائه خواهیم داد:

مشتریان در [1] انتخاب خواهند شد.

کد مشابه برنامه قبلی است، بنابراین تنها تفاوت‌های اصلی را برجسته می‌کنیم.

ما فایل [app-19.html] را به‌عنوان [app-20.html] کپی می‌کنیم، سپس کد مربوط به لیست کشویی مشتری [1] را ایجاد می‌کنیم:


<!-- فهرست مشتریان -->
  <div class="alert alert-info">
    <h3>{{agenda.title|translate:agenda.model}}</h3>

    <div class="row" ng-show="clients.show">
      <div class="col-md-3">
        <h2 translate="{{clients.title}}"></h2>
        <select data-style="btn-primary" class="selectpicker" select-enable="" ng-if="clients.data">
          <option ng-repeat="client in clients.data" value="{{client.id}}">
            {{client.titre}} {{client.prenom}} {{client.nom}}
          </option>
        </select>
      </div>
    </div>
  </div>
  • خطوط ۸–۱۲: لیست کشویی با استفاده از کامپوننت [bootstrap-select] پیاده‌سازی خواهد شد؛
  • خط ۱: دستور [selectEnable] از طریق ویژگی [select-enable] اعمال می‌شود؛
  • خط ۱: تگ <select> تنها در صورتی تولید می‌شود که [clients.data] وجود داشته باشد (# null, undefined). این نکته مهم است و در بخش 3.7.7.8 توضیح داده شده است؛

علاوه بر این، ما فایل‌های جدید JS را وارد می‌کنیم:


<script type="text/javascript" src="rdvmedecins-08.js"></script>
<!-- دستورالعمل‌ها -->
<script type="text/javascript" src="selectEnable.js"></script>
<script type="text/javascript" src="footable.js"></script>
  • خط ۱: فایل [rdvmedecins-08.js] با کپی کردن فایل [rdvmedecins-0.js] ایجاد می‌شود؛
  • خطوط ۳–۴: فایل‌های مربوط به هر دو دستور وارد می‌شوند؛

3.7.10.2. کنترل‌کنندهٔ C

کد کنترلر C به شرح زیر تغییر می‌کند:


//کنترل‌کننده
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
    function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
      // ------------------- آغازین‌سازی قالب
...
      // مشتریان
      $scope.clients = {title: config.listClients, show: false, model: {}};

      //------------------------------------------- инициализация نما
      // وظیفهٔ جهانی غیرهم‌زمان
      var task;
      // بازیابی مشتریان سپس تقویم
      getClients().then(function () {
        getAgenda();
      });
...

      // اجرای اقدام
      function getClients() {
....
      };
} ]);
  • خط ۸: شیء [$scope.clients] لیست کشویی مشتری را در نمای V پیکربندی می‌کند؛
  • خطوط ۱۴–۱۶: به‌صورت غیرهم‌زمان، ابتدا فهرست مشتریان درخواست می‌شود؛ پس از دریافت آن، دفترچه یادداشت امروز خانم PELISSIER درخواست می‌شود. سینتکسی که در اینجا استفاده شده تنها به این دلیل کار می‌کند که تابع [getClients] یک promise بازمی‌گرداند؛

متد [getClients] فهرست مشتریان را بازیابی می‌کند:


function getClients() {
        // به‌روزرسانی UI
        $scope.waiting.visible = true;
        $scope.clients.show = false;
        $scope.errors.show = false;
        // بازیابی فهرست مشتریان؛
        task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
        var promise = task.promise;
        // تحلیل نتیجه تماس قبلی
        promise = promise.then(function (result) {
          // result={err: 0, data: [client1, client2, ...]}
          //result={err: n, messages: [msg1, msg2, ...]}
          if (result.err == 0) {
            //داده‌های به‌دست‌آمده در مدل وارد می‌شوند
            $scope.clients.data = result.data;
            // ما UI را به‌روزرسانی می‌کنیم
            $scope.clients.show = true;
            $scope.waiting.visible = false;
          } else {
            //خطاهایی هنگام بازیابی فهرست مشتریان رخ داد
            $scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
            // UI به‌روزرسانی شد
            $scope.waiting.visible = false;
          }
        });
        // انجام وعده
        return promise;
      };

این کدی است که قبلاً با آن مواجه شده و در مورد آن بحث کرده‌ایم. نکته مهم قابل توجه، خط ۳۱ است:

  • خط ۲۷: وعده‌ای که از خط ۱۰ بازگشته است، یعنی آخرین وعده‌ای که در کد به دست آمده، بازگردانده می‌شود. این وعده تنها پس از بازگشت پاسخ تابع HTTP به دست می‌آید؛

متد [reserver] کمی تغییر می‌کند:


      $scope.reserver = function (creneauId) {
        utils.debug("réservation du créneau", creneauId);
        //در حال ایجاد یک RV برای مشتری انتخاب‌شده
        var idClient = $(".selectpicker").selectpicker('val');
        ...
        });
  • خط ۴: دیگر برای یک مشتری تصادفی رزرو نمی‌کنیم، بلکه برای مشتری انتخاب‌شده از لیست مشتریان رزرو می‌کنیم.

3.7.11. مثال ۱۱: یک دستور [selectEnable2]

این مثال بار دیگر به دستورالعمل‌ها می‌پردازد.

3.7.11.1. نما V

برنامه نمای زیر را نمایش می‌دهد:

 

3.7.11.2. کد HTML برای نما

کد HTML برای نما [app-21.html] به شرح زیر است:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <!-- پیام در حال انتظار -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    ...
  </div>

  <!-- فهرست خطاها -->
  <div class="alert alert-danger" ng-show="errors.show">
   ...
  </div>

  <!-- فهرست مشتریان -->
  <div class="alert alert-info">
    <div class="row" ng-show="clients.show">
      <div class="col-md-4">
        <h2 translate="{{clients.title}}"></h2>
        <select data-style="btn-primary" id="selectpickerClients" select-enable2="" ng-if="clients.data">
          <option ng-repeat="client in clients.data" value="{{client.id}}">
            {{client.titre}} {{client.prenom}} {{client.nom}}
          </option>
        </select>
      </div>
    </div>
  </div>

  <!-- فهرست پزشکان -->
  <div class="alert alert-info">
    <div class="row" ng-show="medecins.show">
      <div class="col-md-4">
        <h2 translate="{{medecins.title}}"></h2>
        <select data-style="btn-primary" id="selectpickerMedecins" select-enable2="" ng-if="medecins.data">
          <option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
            {{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
          </option>
        </select>
      </div>
    </div>
  </div>
</div>
...
<script type="text/javascript" src="rdvmedecins-09.js"></script>
<!-- دستورالعمل‌ها -->
<script type="text/javascript" src="selectEnable2.js"></script>
  • خطوط ۱۹–۲۳: لیست کشویی مشتری؛
  • خط ۱۹: دستور [selectEnable2] (ویژگی [select-enable2]) اعمال می‌شود؛
  • خط ۱۹: تنها در صورتی که [clients.data] خالی نباشد؛
  • خط ۱۹: لیست کشویی توسط ویژگی [id="selectpickerClients"] شناسایی می‌شود؛
  • خطوط ۳۳–۳۷: فهرست کشویی پزشکان؛
  • خط ۳۳: دستور [selectEnable2] (ویژگی [select-enable2]) اعمال می‌شود؛
  • خط ۳۳: تنها در صورتی که [medecins.data] خالی نباشد؛
  • خط ۳۳: لیست کشویی توسط ویژگی [id="selectpickerMedecins"] شناسایی می‌شود؛
  • خط ۴۳: یک فایل جدید JS از [rdvmedecins-09.js] وارد می‌شود؛
  • خط ۴۵: فایل JS از دستور جدید وارد می‌شود؛

3.7.11.3. دستور [selectEnable2]

کد دستور [selectEnable2] به شرح زیر است:


angular.module("rdvmedecins").directive('selectEnable2', ['$timeout', 'utils', function ($timeout, utils) {
  return {
    link: function (scope, element, attrs) {
      utils.debug("directive selectEnable2 attrs", attrs);
      $timeout(function () {
        $('#' + attrs['id']).selectpicker();
      })
    }
  }
}]);
  • خط ۴: مقدار پارامتر [attrs] نمایش داده می‌شود تا نشان دهد کد چگونه کار می‌کند. خواهیم دید که attrs['id']='selectpickerClients' برای لیست مشتریان است؛
  • خط ۶: برای یافتن یک عنصر از [id='x'] درون DOM، [$('#x')] را می‌نویسیم. بنابراین، برای یافتن فهرست مشتریان باید [$('#selectpickerClients')] را بنویسیم. این کار با استفاده از سینتکس [$('#' + attrs['id'])] انجام می‌شود؛

دستور [selectEnable2] بنابراین از اطلاعاتی که توسط یکی از ویژگی‌های عنصر HTML که بر روی آن اعمال می‌شود، استفاده می‌کند.

3.7.11.4. کنترل‌کننده C

کنترل‌کننده C در فایل JS [rdvmedecins-09.js] قرار دارد و ساختار زیر را دارد:


// کنترل‌کننده
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
    function ($scope, utils, config, dao) {
      // ------------------- inicializه‌سازی قالب
      // پیام بارگذاری
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
      // اطلاعات ورود
      $scope.server = {url: 'http://'localhost:8080', login: 'admin', password: 'admin'};
      // خطاها
      $scope.errors = {show: false, model: {}};
      // پزشکان
      $scope.medecins = {title: config.listMedecins, show: false, model: {}};
      // کلاینت‌ها
      $scope.clients = {title: config.listClients, show: false, model: {}};

      // وظیفهٔ همگانی غیرهم‌زمان
      var task;
      // ---------------------------------------------------- inicializه‌سازی نما
      // به‌روزرسانی UI
      $scope.waiting.visible = true;
      $scope.clients.show = false;
      $scope.medecins.show = false;
      $scope.errors.show = false;
      // بازیابی مشتریان، سپس پزشکان
      getClients().then(function () {
        getMedecins();
      });

      //فهرست مشتریان
      function getClients() {
        ...
      }

      // فهرست پزشکان
      function getMedecins() {
...
      }

      //لغو در انتظار
      function cancel() {
...
      }
    } ]);
  • خطوط ۲۶–۲۸: ابتدا از مراجعین پرسش می‌شود، سپس از پزشکان؛

3.7.11.5. آزمایش

این نسخه جدید را آزمایش کنید.

3.7.12. مثال ۱۲: یک دستور [list]

ما از همان مثال قبلی استفاده می‌کنیم، اما می‌خواهیم با استفاده از یک دستور، کد HTML را ساده‌تر کنیم. در حال حاضر، کد زیر را داریم: HTML:


<!-- فهرست مشتریان -->
  <div class="alert alert-info">
    <div class="row" ng-show="clients.show">
      <div class="col-md-4">
        <h2 translate="{{clients.title}}"></h2>
        <select data-style="btn-primary" id="selectpickerClients" select-enable2="" ng-if="clients.data">
          <option ng-repeat="client in clients.data" value="{{client.id}}">
            {{client.titre}} {{client.prenom}} {{client.nom}}
          </option>
        </select>
      </div>
    </div>
  </div>
  <!-- فهرست پزشکان -->
  <div class="alert alert-info">
    <div class="row" ng-show="medecins.show">
      <div class="col-md-4">
        <h2 translate="{{medecins.title}}"></h2>
        <select data-style="btn-primary" id="selectpickerMedecins" select-enable2="" ng-if="medecins.data">
          <option ng-repeat="medecin in medecins.data" value="{{medecin.id}}">
            {{medecin.titre}} {{medecin.prenom}} {{medecin.nom}}
          </option>
        </select>
      </div>
    </div>
  </div>

خطوط ۱۴ تا ۲۶ دقیقاً مشابه خطوط ۱ تا ۱۳ هستند. این خطوط به جای مشتریان، برای پزشکان اعمال می‌شوند. ما می‌خواهیم بتوانیم به صورت زیر بنویسیم:


  <!-- فهرست مشتریان -->
  <list model="clients" ng-if="clients.show"></list>
  <!-- فهرست پزشکان -->
<list model="medecins" ng-if="medecins.show"></list>

این کد شامل یک دستور جدید به نام [list] است که اکنون قصد داریم آن را ایجاد کنیم.

3.7.12.1. دستور [list]

دستور [list] در فایل JS [list.js] قرار می‌گیرد. کد آن به شرح زیر است:


angular.module("rdvmedecins")
  .directive("list", ['utils', '$timeout', function (utils, $timeout) {
    // نمونه دستور بازگشتی
    return {
      // عنصر HTML
      restrict: "E",
      // قطعه URL
      templateUrl: "list.html",
      // دامنه منحصربه‌فرد برای هر نمونه از دستور
      scope: true,
      // تابع متصل به سند
      link: function (scope, element, attrs) {
        utils.debug("directive list attrs", attrs);
        scope.model = scope[attrs['model']];
        utils.debug("directive list model", scope.model);
        $timeout(function () {
          $('#' + scope.model.id).selectpicker();
        })
      }
    }
}]);
  • خط ۲: یک دستور به نام 'list' را تعریف می‌کند؛
  • خط ۶: صفت [restrict] مشخص می‌کند که دستور چگونه باید استفاده شود. [restrict: "E"] به این معنی است که دستور [list] می‌تواند به عنوان عنصر HTML استفاده شود <list ...>...</list>. [restrict: "A"] به این معنی است که دستور [list] می‌تواند به‌عنوان یک ویژگی استفاده شود، برای مثال <div ... list='...'>. [restrict: "AE"] به این معنی است که دستور [list] می‌تواند هم به‌عنوان یک ویژگی و هم به‌عنوان یک عنصر استفاده شود؛
  • خط ۸: صفت [templateUrl] نام قطعه HTML را که هنگام برخورد با تگ باید استفاده شود، مشخص می‌کند. این قطعه بدنه‌ی تگ را تشکیل خواهد داد؛
  • خط ۱۰: ویژگی [scope] دامنهٔ قالب دستور را تعیین می‌کند. [scope: true] به این معنی است که دو عنصر از نوع <list> هر کدام قالب خود را خواهند داشت. به طور پیش‌فرض (در صورتی که دامنهٔ آن‌ها مشخص نشده باشد)، آن‌ها قالب‌های خود را با یکدیگر به اشتراک می‌گذارند؛
  • خط ۱۲: تابع [link]، که قبلاً چندین بار از آن استفاده کرده‌ایم؛

برای درک کد بالا، باید در نظر داشته باشیم که دستور چگونه استفاده خواهد شد:


  <!-- فهرست مشتریان -->
  <list model="clients" ng-if="clients.show"></list>
  <!-- فهرست پزشکان -->
<list model="medecins" ng-if="medecins.show"></list>

دستور [list] به‌عنوان عنصر <list> در HTML استفاده می‌شود. این عنصر دو ویژگی دارد:

  • [model]: که مقدار آن عنصرِ مدل M از نما V خواهد بود که دستور [list] در آن قرار دارد. این عنصر مدل دستور را پر خواهد کرد؛
  • [ng-if]: که تضمین می‌کند کد دستور HTML در صورتی که چیزی برای نمایش وجود نداشته باشد، تولید نشود؛

بیایید به کد تابع [link] در دستورالعمل بازگردیم:


link: function (scope, element, attrs) {
        utils.debug("directive list attrs", attrs);
        scope.model = scope[attrs['model']];
        utils.debug("directive list model", scope.model);
        $timeout(function () {
          $('#' + scope.model.id).selectpicker();
        })
      }

بیایید این کد، JS، را با کد HTML که از دستور استفاده می‌کند، لینک کنیم:


  <list model="clients" ng-if="clients.show"></list>
  • خط ۳: attrs['model'] در اینجا مقدار 'clients' را دارد؛
  • خط ۳: scope[attrs['model']] مقدار scope['clients'] را دارد و بنابراین نمایانگر [$scope.clients] است، یعنی فیلد [clients] در مدل نما. این فیلد دارای مقدار {id: '...', data: [client1, client2, ...], show: ..., title: '...'}; خواهد بود.
  • خط ۳: یک فیلد [model] به مدل دستورالعمل (directive) اضافه می‌شود. این دستورالعمل مدل را از ویویی که در آن قرار دارد به ارث برده است. بنابراین، برای جلوگیری از تداخل با هر فیلد [model] که ممکن است ویو نیز داشته باشد، لازم است از تداخل جلوگیری شود. در این مورد، هیچ تداخلی وجود نخواهد داشت؛
  • خط ۴: ما [scope.model] را نمایش می‌دهیم تا کد را بهتر درک کنیم؛
  • خطوط ۵–۷: ما کدی را می‌بینیم که قبلاً با آن مواجه شده‌ایم. تفاوت این است که id کامپوننت قبلاً در داخل یک ویژگی attrs['id'] قرار داشت. در اینجا، این ویژگی در داخل [scope.model.id] قرار خواهد گرفت؛

اکنون بیایید به کدی که توسط دستور HTML تولید شده است نگاه کنیم. به دلیل ویژگی [templateUrl: "list.html"] دستور، باید آن را در فایل [list.html] جستجو کنیم:


<!-- فهرست مشتریان یا پزشکان -->
<div class="alert alert-info" ng-show="model.show">
  <div class="row">
    <div class="col-md-4">
      <h2 translate="{{model.title}}"></h2>
      <select data-style="btn-primary" id="{{model.id}}" ng-if="model.data">
        <option ng-repeat="element in model.data" value="{{element.id}}">
          {{element.titre}} {{element.prenom}} {{element.nom}}
        </option>
      </select>
    </div>
  </div>
</div>
  • اولین نکته‌ای که هنگام خواندن این کد باید به خاطر داشته باشید این است که دستورالعمل یک شیء [scope.model] به شکل [{id :'...', data:[client1, client2, ...], show : ..., title :'...'}] ایجاد کرده است. این شیء [model] (scope به طور ضمنی در کد HTML وجود دارد) توسط کد HTML در دستورالعمل استفاده می‌شود؛
  • خط ۲: استفاده از [model.show] برای نمایش/پنهان کردن نمای تولید شده توسط دستور؛
  • خط ۵: استفاده از [model.title] برای تنظیم یک عنوان؛
  • خط ۶: استفاده از [model.id] برای تخصیص یک شناسه به تگ <select>. این id توسط کد JS در دستور استفاده می‌شود؛
  • خط ۶: استفاده از [model.data] برای تولید تگ <select> تنها در صورتی که داده‌ای برای نمایش وجود داشته باشد؛
  • خطوط ۷–۹: استفاده از [model.data] برای تولید موارد لیست کشویی؛

3.7.12.2. کد HTML

کد HTML برای برنامه [app-22.html] به شرح زیر است:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <!-- پیام در حال انتظار -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    ...
  </div>

  <!-- فهرست خطاها -->
  <div class="alert alert-danger" ng-show="errors.show">
    ...
  </div>

  <!-- فهرست مشتریان -->
  <list model="clients" ng-if="clients.show"></list>
  <!-- فهرست پزشکان -->
  <list model="medecins" ng-if="medecins.show"></list>
</div>
...
<script type="text/javascript" src="rdvmedecins-10.js"></script>
<!-- دستورالعمل‌ها -->
<script type="text/javascript" src="list.js"></script>
  • خط ۲۲: فراموش نکنید که کد JS را از دستورالعمل وارد کنید؛

3.7.12.3. کنترل‌کننده C

کنترلر C تقریباً بدون تغییر باقی می‌ماند:


angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
    function ($scope, utils, config, dao) {
      // ------------------- inicializه‌سازی قالب
...
      // پزشکان
      $scope.medecins = {title: config.listMedecins, show: false, id: 'medecins'};
      // مشتریان
      $scope.clients = {title: config.listClients, show: false, id: 'clients'};
...
  • خطوط ۷ و ۹: ما ویژگی [id] را به قالب‌های دکتر و مشتری اضافه می‌کنیم؛

3.7.12.4. تست‌ها

تست‌ها نتایج مشابهی با مثال قبلی تولید می‌کنند.

3.7.13. مثال ۱۳: به‌روزرسانی قالب یک دستور

ما بررسی دستورها را ادامه می‌دهیم و دوباره از مثال فهرست کشویی استفاده می‌کنیم. در اینجا می‌خواهیم رفتار دستور [list] را وقتی محتواي فهرست کشویی تغییر می‌کند بررسی کنیم.

3.7.13.1. نماهای V

نمای‌های مختلف به شرح زیر هستند:

  • در [1]، فهرست مشتریان برای اولین بار درخواست می‌شود؛
  • در [2]، فهرست مشتریان برای بار دوم درخواست می‌شود. این فهرست دوم سپس با فهرست اول، [3]، ترکیب می‌شود. این به‌روزرسانی کامپوننت [Bootstrap select] است که می‌خواهیم در این مثال بررسی کنیم.

3.7.13.2. صفحه HTML

صفحه HTML [app-23.html] با کپی کردن [app-22.html] به دست آمده و سپس به شرح زیر اصلاح می‌شود:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <!-- پیام در حال انتظار -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    ...
  </div>

  <!-- فهرست خطاها -->
  <div class="alert alert-danger" ng-show="errors.show">
    ...
  </div>

  <!-- دکمه -->
  <div class="alert alert-warning">
    <button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
  </div>

  <!-- فهرست مشتریان -->
  <list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-11.js"></script>
<!-- دستورالعمل‌ها -->
<script type="text/javascript" src="list2.js"></script>

تغییرات نسبت به برنامه قبلی به شرح زیر است:

  • خطوط ۱۵–۱۷: افزودن یک دکمه؛
  • خط ۲۰: استفاده از یک دستور جدید، [list2];
  • خط ۲۳: استفاده از یک فایل جدید، JS;
  • خط ۲۵: وارد کردن فایل JS از دستور [list2];

3.7.13.3. دستور [list2]

دستور [list2] در داخل [list2.js] به شرح زیر است:


angular.module("rdvmedecins")
  .directive("list2", ['utils', '$timeout', function (utils, $timeout) {
    // مثال دستور بازگشتی
    return {
      // عنصر HTML
      restrict: "E",
      // مقطع URL
      templateUrl: "list.html",
      // دامنه منحصربه‌فرد برای هر نمونه از دستور
      scope: true,
      // تابع متصل به سند
      link: function (scope, element, attrs) {
        utils.debug('directive list2');
        scope.model = scope[attrs['model']];
        $timeout(function () {
          $('#' + scope.model.id).selectpicker('refresh');
        })
      }
    }
}]);

تنها تفاوت با دستور [list] در خط ۱۶ است: با متد [selectpicker('refresh')]، به کامپوننت [Bootstrap-select] دستور داده می‌شود که خود را رفرش کند. ایده پشت این کار این است که هر بار کاربر لیست جدیدی از مشتریان را درخواست می‌کند، لیست کشویی به‌روزرسانی شود. این کار انجام نخواهد شد، اما این ایدهٔ اصلی است.

3.7.13.4. کنترل‌کننده C

کنترلر در فایل [rdvmedecins-11.js] قرار دارد که با کپی کردن فایل [rdvmedecins-10.js] ایجاد شده است:


      // مشتریان
      $scope.clients = {title: config.listClients, show: false, id: 'clients', data: []};
...
      // فهرست مشتریان
      $scope.getClients = function getClients() {
        //به‌روزرسانی UI
        $scope.waiting.visible = true;
        $scope.errors.show = false;
        // درخواست فهرست مشتریان؛
        task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
        var promise = task.promise;
        // تحلیل نتیجه تماس قبلی
        promise = promise.then(function (result) {
          //result={err: 0, data: [client1, client2, ...]}
          //result={err: n, messages: [msg1, msg2, ...]}
          if (result.err == 0) {
             //داده‌های بازیابی‌شده در یک مدل جدید قرار داده می‌شوند تا نما را مجبور به تازه‌سازی کنند
            $scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};
            //ما UI را به‌روزرسانی می‌کنیم
            $scope.clients.show = true;
            $scope.waiting.visible = false;
          } else {
            //خطاهایی هنگام بازیابی فهرست مشتریان رخ داد
            $scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
            // UI به‌روزرسانی شد
            $scope.waiting.visible = false;
          }
        });
}
  • خط ۱: برای فعال‌سازی الحاق آرایه‌ها در [clients.data]، این شیء با یک آرایه خالی مقداردهی اولیه می‌شود؛
  • خط ۱۸: فهرست جدید مشتریان با فهرست موجود در آرایه [clients.data] الحاق می‌شود؛

قبلاً نوشته‌ایم:

//داده‌های بازیابی‌شده در قالب درج می‌شوند
$scope.clients.data = result.data;

اکنون می‌نویسیم:

//داده‌ها در یک قالب جدید وارد می‌شوند تا نما به‌روزرسانی شود
$scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};

برای درک این کد، باید به یاد بیاورید که قالب M در نمای V در مورد دستور [list2] چگونه استفاده می‌شود:


  <!-- فهرست مشتریان -->
<list2 model="clients" ng-if="clients.show"></list2>

قالبی که توسط دستور [list2] استفاده می‌شود، [clients] است. این قالب تنها در صورتی در نمای V مجدداً ارزیابی می‌شود که [clients] در قالب M نما تغییر کند. اولین ایده‌ای که برای اصلاح به ذهن می‌رسد این است که بنویسیم:

$scope.clients.data=$scope.clients.data.concat(result.data) ;

تا این واقعیت را در نظر بگیریم که فهرست جدید مشتریان باید به فهرست‌های قبلی اضافه شود. با این کار، ما [clients.data] را تغییر می‌دهیم اما [clients] را تغییر نمی‌دهیم. من با ظرایف جاوااسکریپت آشنا نیستم، اما اگر [clients] مانند [clients.data] یک نشانگر باشد، جای تعجب نخواهد بود. اشاره‌گر [clients] وقتی اشاره‌گر [clients.data] تغییر می‌کند، تغییر نمی‌کند. دستور [list2] بنابراین مجدداً ارزیابی نمی‌شود. این دقیقاً همان چیزی است که هنگام اشکال‌زدایی برنامه (F12 در کروم) مشاهده می‌کنیم.

با نوشتن:

$scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};

ما اطمینان حاصل می‌کنیم که [$scope.clients] واقعاً مقدار جدیدی به آن اختصاص داده می‌شود. پوئنتر [$scope.clients] به یک شیء جدید اشاره می‌کند. بنابراین دستور [list2] باید مجدداً ارزیابی شود. با این حال، ما به نتیجه مورد انتظار نمی‌رسیم. بیایید اسکرین‌شات‌ها را زمانی که فهرست مشتریان را دو بار درخواست می‌کنیم، بررسی کنیم:

  • در [1]، به جای هشت عنصر، تنها چهار عنصر وجود دارد؛
  • در [2]، این چهار مورد در یک [select] قرار دارند، اما این مخفی است (style='display: none');
  • در [3]، این چهار کلاینت در ساختار دیگری به نام HTML قرار دارند، و این همان چیزی است که کاربر هنگام کلیک بر روی لیست کشویی می‌بیند؛

در نهایت، لاگ‌های کنسول موارد زیر را نشان می‌دهند:

1
2
3
4
[dao] init
[dao] getData[/getAllClients] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}
directive list2
[dao] getData[/getAllClients] success réponse : {"data":{"status":0,"data":[{"id":1,"version":1,"titre":"Mr","nom":"MARTIN","prenom":"Jules"},{"id":2,"version":1,"titre":"Mme","nom":"GERMAN","prenom":"Christine"},{"id":3,"version":1,"titre":"Mr","nom":"JACQUARD","prenom":"Jules"},{"id":4,"version":1,"titre":"Melle","nom":"BISTROU","prenom":"Brigitte"}]},"status":200,"config":{"method":"GET","transformRequest":[null],"transformResponse":[null],"timeout":1000,"url":"http://localhost:8080/getAllClients","headers":{"Accept":"application/json, text/plain, */*","Authorization":"Basic YWRtaW46YWRtaW4="}},"statusText":"OK"}
  • خط ۱: سرویس [dao] نمونه سازی می‌شود؛
  • خط ۲: سرویس [dao] یک لیست اولیه از کلاینت‌ها را بازیابی می‌کند؛
  • خط ۳: دستور [list2] اجرا می‌شود؛
  • خط ۴: سرویس [dao] یک فهرست دوم از مشتریان را بازیابی می‌کند؛

خروجی در خط ۲ از کد زیر در داخل دستور می‌آید:


      link: function (scope, element, attrs) {
        utils.debug('directive list2');
        ...
}

بیایید چرخهٔ عمر دستور [list2] را بررسی کنیم:

  • بین خطوط ۱ و ۲، این فعال نمی‌شود، هرچند نما برای اولین بار نمایش داده شده است. این به دلیل ویژگی [ng-if="clients.show"] در نما V است:

<list2 model="clients" ng-if="clients.show"></list2>
  • خط ۳: پس از بازیابی اولین لیست پزشکان، [clients.show] روی true تنظیم شده و دستور فعال می‌شود؛
  • پس از بازیابی دومین لیست مشتریان، مشاهده می‌کنیم که کد دستور [list2] فراخوانی نمی‌شود. به همین دلیل است که لیست دوم نمایش داده نمی‌شود؛

برای حل این مشکل، دستور [list2] را به شرح زیر اصلاح می‌کنیم:


angular.module("rdvmedecins")
  .directive("list2", ['utils', '$timeout', function (utils, $timeout) {
    //نمونه دستور بازگشتی
    return {
      //عنصر HTML
      restrict: "E",
      // آدرس اینترنتی قطعه
      templateUrl: "list.html",
      // دامنه منحصربه‌فرد برای هر نمونه از دستور
      scope: true,
      // تابع پیوند به سند
      link: function (scope, element, attrs) {
        //هرگاه attrs["model"] تغییر کند، قالب دستورالعمل نیز باید تغییر کند
        scope.$watch(attrs["model"], function (newValue) {
          utils.debug("directive list2 newValue", newValue);
          // قالب دستورالعمل به‌روزرسانی می‌شود
          scope.model = newValue;
          $timeout(function () {
            $('#' + scope.model.id).selectpicker('refresh');
          })
        });
      }
    }
}]);
  • خط ۱۴: تابع [scope.$watch] به ما امکان می‌دهد یک مقدار را در مدل نظارت کنیم. نحو آن [scope.$watch('var'), f] است، که در آن [var] شناسه یک متغیر در مدل است و f تابعی است که هنگام تغییر مقدار این متغیر باید اجرا شود. در اینجا می‌خواهیم متغیر [clients] را زیر نظر داشته باشیم. بنابراین باید [scope.$watch('clients')] را بنویسیم. از آنجایی که `attrs['model']='clients'` را داریم، می‌نویسیم `[scope.$watch(attrs["model"], function (newValue)] ;`
  • خط ۱۴: پارامتر دوم تابع [scope.$watch] تابعی است که باید هنگام تغییر مقدار متغیر مشاهده‌شده اجرا شود. پارامتر [newValue] مقدار جدید متغیر است؛ برای ما، این مقدار جدید متغیر [clients] در مدل است؛
  • خط 17: این مقدار جدید به فیلد [model] در مدل دستورالعمل تخصیص داده می‌شود؛

پس از اعمال این تغییر، لاگ‌ها تغییر می‌کنند:

در بالا می‌بینیم که پس از بازیابی دومین لیست مشتریان، دستور [list2] در واقع دوباره اجرا می‌شود، همانطور که توسط نتیجه [2] تأیید شده است.

3.7.14. مثال 14: دستورهای [waiting] و [errors]

بیایید به کد HTML از برنامه قبلی بازگردیم:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <!-- پیام بارگذاری -->
  <div class="alert alert-warning" ng-show="waiting.visible">
  ...
  </div>

  <!-- فهرست خطاها -->
  <div class="alert alert-danger" ng-show="errors.show">
  ...
  </div>

  <!-- دکمه -->
  <div class="alert alert-warning">
    <button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
  </div>

  <!-- فهرست مشتریان -->
  <list2 model="clients" ng-if="clients.show"></list2>
</div>
  • خطوط ۵–۷: پیام انتظار؛
  • خطوط ۱۰–۱۲: پیام خطا؛

ما تصمیم گرفته‌ایم کدهای HTML را برای این دو پیام در داخل دستورات قرار دهیم.

3.7.14.1. کد جدید HTML

کد جدید HTML [app-24.html] به شرح زیر است:


<div class="container">
  <h1>Rdvmedecins - v1</h1>

  <!-- پیام در حال بارگذاری -->
  <waiting model="waiting"></waiting>

  <!-- فهرست خطاها -->
  <errors model="errors"></errors>

  <!-- دکمه -->
  <div class="alert alert-warning">
    <button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
  </div>

  <!-- فهرست مشتریان -->
  <list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-12.js"></script>
<!-- دستورالعمل‌ها -->
<script type="text/javascript" src="list2.js"></script>
<script type="text/javascript" src="errors.js"></script>
<script type="text/javascript" src="waiting.js"></script>
  • خط ۵: دستورالعمل برای پیام انتظار؛
  • خط ۸: دستورالعمل برای پیام خطا؛
  • خط ۱۹: فایل جدید JS مرتبط با برنامه؛
  • خطوط ۲۱–۲۳: فایل‌های JS برای سه دستور؛

3.7.14.2. دستور [waiting]

کد JS برای دستور [waiting] در فایل زیر [waiting.js] قرار دارد:


angular.module("rdvmedecins")
  .directive("waiting", ['utils', function (utils) {
    // نمونه دستور بازگشتی
    return {
      // عنصر HTML
      restrict: "E",
      // مقطع URL
      templateUrl: "waiting.html",
      // دامنه منحصربه‌فرد برای هر نمونه از دستور
      scope: true,
      // تابع پیوند به سند
      link: function (scope, element, attrs) {
        //هرگاه attr["model"] تغییر کند، قالب صفحه نیز باید تغییر کند
        scope.$watch(attrs["model"], function (newValue) {
          utils.debug("[waiting] watch newValue", newValue);
          scope.model = newValue;
        });
      }
    }
  }]);

این کد از منطق مشابه دستور [list2] که قبلاً بررسی شده است، پیروی می‌کند.

در خط ۸، به فایل زیر ارجاع داده شده است: [waiting.html]:


<div class="alert alert-warning" ng-show="model.show">
  <h1>{{ model.title.text | translate:model.title.values}}
    <button class="btn btn-primary pull-right" ng-click="model.cancel()">{{'cancel'|translate}}</button>
    <img src="assets/images/waiting.gif" alt=""/>
  </h1>
</div>

در کد JS برنامه، قالب [$scope.waiting] برای این کد HTML به صورت زیر تعریف خواهد شد:


// پیام انتظار
$scope.waiting = {title: {text: config.msgWaiting, values: {}}, show: false, cancel: cancel, time: 3000};

3.7.14.3. دستورالعمل [errors]

کد JS از دستور [errors] در فایل زیر، [errors.js]، قرار دارد:


angular.module("rdvmedecins")
  .directive("errors", ['utils', function (utils) {
    // نمونه‌ای از دستور بازگشتی
    return {
      //عنصر HTML
      restrict: "E",
      // آدرس اینترنتی قطعه
      templateUrl: "errors.html",
      // دامنه منحصربه‌فرد برای هر نمونه از دستور
      scope: true,
      // تابع پیوند به سند
      link: function (scope, element, attrs) {
        //هرگاه attr["model"] تغییر کند، قالب صفحه نیز باید تغییر کند
        scope.$watch(attrs["model"], function (newValue) {
          utils.debug("[errors] watch newValue", newValue);
          scope.model = newValue;
        });
      }
    }
}]);

این کد از منطق مشابه دستور [list2] پیروی می‌کند که قبلاً بررسی کرده‌ایم.

در خط ۸، به فایل زیر ارجاع داده شده است: [errors.html]:


<div class="alert alert-danger" ng-show="model.show">
  {{model.title.text|translate:model.title.values}}
  <ul>
    <li ng-repeat="message in model.messages">{{message|translate}}</li>
  </ul>
</div>

در کد JS برنامه، قالب [$scope.errors] برای این کد HTML به صورت زیر تعریف خواهد شد:


// خطاهایی هنگام بازیابی فهرست مشتریان رخ داد
$scope.errors = { title: { text: config.getClientsErrors, values: {}}, messages: utils.getErrors(result), show: true, model: {}};

3.7.15. مثال ۱۵: ناوبری

تا کنون ما از برنامه‌های تک‌صفحه‌ای استفاده کرده‌ایم. در این مثال، به برنامه‌های چندصفحه‌ای و ناوبری بین آن‌ها خواهیم پرداخت.

3.7.15.1. نماهای V برنامه

  • در [1]، URL برای نما شماره ۱؛
  • در [2]، محتوای آن؛
  • در [3]، به صفحهٔ ۲ می‌رویم؛
  • در [4]، نمای شماره ۲؛
  • در [5]، به صفحه ۳ می‌روید؛
  • در [6]، نما شماره ۳؛
  • در [7]، به صفحهٔ ۱ می‌روید؛
  • در [8]، دوباره به نما شمارهٔ ۱ بازمی‌گردیم؛

3.7.15.2. سازماندهی کد

ما یک روش جدید برای سازماندهی کد را آغاز می‌کنیم:

  
  • نماهای برنامه در پوشه [views] قرار داده خواهند شد؛
  • ماژول برنامه در پوشه [modules] قرار داده خواهد شد؛
  • کنترل‌کننده‌های برنامه در پوشه [controllers] قرار خواهند گرفت؛

به همین ترتیب، در نسخه نهایی:

  • سرویس‌ها در پوشه [services] قرار داده خواهند شد؛
  • دستورالعمل‌ها در پوشه [directives] قرار داده خواهند شد؛

3.7.15.3. کانتینر ویوها

ویوها در پوشه [views] در کانتینر زیر، [app-25.html]، نمایش داده خواهند شد:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
  ...
</head>
<body>
    <div class="container" ng-controller="mainCtrl">
        <!--نوار ناوبری -->
        <ng-include src="'views/navbar.html'"></ng-include>

        <!--نمای فعلی -->
        <ng-view></ng-view>
    </div>

...
<!-- ماژول -->
<script type="text/javascript" src="modules/rdvmedecins-13.js"></script>
<!--کنترلرها -->
<script type="text/javascript" src="controllers/mainController.js"></script>
<script type="text/javascript" src="controllers/page1Controller.js"></script>
<script type="text/javascript" src="controllers/page2Controller.js"></script>
<script type="text/javascript" src="controllers/page3Controller.js"></script>
</body>
</html>
  • خط ۷: محتوای کانتینر توسط [mainCtrl] کنترل می‌شود؛
  • خط ۹: دستور [ng-include] برای فراخوانی یک فایل خارجی HTML، در این مورد یک نوار ناوبری، استفاده می‌شود؛
  • خط ۱۲: نماهای مختلف نمایش‌داده‌شده توسط کانتینر در داخل دستور [ng-view] رندر می‌شوند. در نهایت، ما یک کانتینر داریم که نمایش می‌دهد:
    • همیشه همان نوار ناوبری (خط 9);
    • نماهای متفاوت در خط ۱۲؛
  • خطوط 16–22: فایل‌های JS از ماژول برنامه [rdvmedecins-13.js] و کنترل‌کننده‌های آن وارد می‌شوند؛

3.7.15.4. ماژول برنامه

فایل [rdvmedecins-13.js] ماژول برنامه و مسیریابی بین ویوها را تعریف می‌کند:


// --------------------- ماژول Angular
angular.module("rdvmedecins", [ 'ngRoute' ]);

angular.module("rdvmedecins").config(["$routeProvider", function ($routeProvider) {
// ------------------------ مسیریابی
  $routeProvider.when("/page1",
    {
      templateUrl: "views/page1.html",
      controller: 'page1Ctrl'
    });
  $routeProvider.when("/page2",
    {
      templateUrl: "views/page2.html",
      controller: 'page2Ctrl'
    });
  $routeProvider.when("/page3",
    {
      templateUrl: "views/page3.html",
      controller: 'page3Ctrl'
    });
  $routeProvider.otherwise(
    {
      redirectTo: "/page1"
    });
}]);
  • خط ۱: ماژول [rdvmedecins] تعریف شده است. این ماژول به ماژول [ngRoute] که توسط کتابخانه [angular-route.min.js] ارائه می‌شود، وابسته است. این ماژول است که مسیریابی تعریف‌شده در خطوط ۶ تا ۲۴ را امکان‌پذیر می‌سازد؛
  • خط ۴: تابع [config] از ماژول [rdvmedecins] را تعریف می‌کند. توجه باید داشت که این تابع قبل از هرگونه نمونه‌سازی سرویس اجرا می‌شود. این یک تابع پیکربندی ماژول است. در اینجا، مسیریابی آن پیکربندی می‌شود. این کار با استفاده از شی [$routeProvider] که توسط ماژول [ngRoute] ارائه شده است، انجام می‌شود؛
  • خطوط ۶–۱۰: نمایی را که هنگام درخواست کاربر برای URL [/page1] نمایش داده شود، تعریف می‌کنند. این مسیریابی داخلی در خود برنامه است. URL در واقع [/rdvmedecins-angular-v1/app-21.html#/page1] است. می‌توانیم ببینیم که هنوز از URL متعلق به کانتینر [/rdvmedecins-angular-v1/app-21.html] استفاده می‌شود، اما با اطلاعات اضافی پس از کاراکتر #. این اطلاعات اضافی است که مسیریابی Angular آن را مدیریت می‌کند؛
  • خط ۸: قطعه HTML را مشخص می‌کند که باید در دستورالعمل (directive) کانتینر [ng-view] درج شود:
  • خط ۹: نام کنترلر برای این قطعه را مشخص می‌کند؛
  • خطوط ۱۱–۱۵: نمایی را که هنگام درخواست کاربر برای URL [/page2] نمایش داده می‌شود، تعریف می‌کنند؛
  • خطوط ۱۶–۲۰: نمایی را که هنگام درخواست کاربر برای URL [/page3] نمایش داده می‌شود، تعریف می‌کنند؛
  • خطوط ۲۱–۲۴: مسیریابی را تعریف می‌کنند که زمانی اعمال می‌شود که URL درخواستی یکی از سه مورد قبلی نباشد (در غیر این صورت، خط ۲۱)؛
  • خط ۲۳: به URL [/page1] و در نتیجه به ویوی تعریف‌شده در خطوط ۶–۱۰ هدایت می‌کند؛

3.7.15.5. کنترل‌کنندهٔ کانتینر نما

ما دیده‌ایم که کانتینر نما یک کنترلر را اعلام می‌کند:


<div class="container" ng-controller="mainCtrl">

کنترل‌کننده [mainCtrl] در فایل [mainController.js] تعریف شده است:


//کنترل‌کننده
angular.module("rdvmedecins")
  .controller('mainCtrl', ['$scope', '$location',
    function ($scope, $location) {

      // قالب‌های صفحه
      $scope.page1 = {};
      $scope.page2 = {};
      $scope.page3 = {};
      // قالب کلی
      var main = $scope.main = {};
      main.text = "[Modèle global]";

      // متدهای در دسترس ویو
      main.showPage1 = function () {
        $location.path("/page1");
      };
      main.showPage2 = function () {
        $location.path("/page2");
      };
      main.showPage3 = function () {
        $location.path("/page3");
      }
}]);
  • خط ۳: کنترل‌کننده [mainCtrl] به شیء [$location] که توسط ماژول مسیریابی [ngRoute] فراهم شده است، نیاز دارد. این شیء برای جابجایی بین ویوها (خطوط ۱۶، ۱۹، ۲۲) استفاده می‌شود؛

بیایید به کد کانتینر بازگردیم:


    <div class="container" ng-controller="mainCtrl">
        <!--نوار ناوبری -->
        <ng-include src="'views/navbar.html'"></ng-include>

        <!--نمای فعلی -->
        <ng-view></ng-view>
</div>
  • کنترل‌کننده [mainCtrl] مدل را برای ناحیه ۱-۷ می‌سازد؛
  • ویوی موجود در خط ۶ نیز یک کنترلر دارد. برای مثال، ویوی [page1] دارای کنترلر [page1Ctrl] است. این کنترلر مدل ناحیه نمایش داده شده در خط ۶ را می‌سازد. بنابراین در این ناحیه دو مدل وجود دارد:
    • مدلی که توسط کنترل‌کننده [mainCtrl] ساخته شده است؛
    • مدلی که توسط کنترل‌کننده [page1Ctrl] ساخته شده است؛

یک قرارداد نام‌گذاری برای قالب‌ها وجود دارد. در نمایی که در خط ۶ نشان داده شده است، قالب‌های کنترل‌کننده‌های [mainCtrl] و [pagexCtrl] هر دو قابل مشاهده هستند. اگر دو متغیر در این قالب‌ها نام یکسانی داشته باشند، یکی بر دیگری اولویت پیدا می‌کند. برای جلوگیری از این تداخل نام، ما چهار قالب با چهار نام متفاوت ایجاد می‌کنیم:

page
contrôleur
modèle
ligne du code
کانتینر
mainCtrl
دست
۱۱
صفحه ۱
page1Ctrl
صفحهٔ ۱
7
صفحه ۲
page2Ctrl
صفحه ۲
8
صفحه ۳
page3Ctrl
صفحه ۳
۹
  • خط ۱۲: یک عنصر [text] را در مدل [main] تعریف می‌کند؛

خطوط ۷–۱۱ تأثیر بسیار مشخصی دارند: آن‌ها [$scope] کنترل‌کننده [mainCtrl] را تعریف می‌کنند و در آن، چهار متغیر [main, page1, page2, page3] ایجاد می‌کنند. این چهار متغیر به‌عنوان قالب‌های مربوطه برای کانتینر و سه ویوی درون آن استفاده خواهند شد.

3.7.15.6. نوار ناوبری

نوار ناوبری در داخل کانتینر به شرح زیر تعریف شده است:


    <div class="container" ng-controller="mainCtrl">
        <!--نوار ناوبری -->
        <ng-include src="'views/navbar.html'"></ng-include>

        <!-- نماش فعلی -->
        <ng-view></ng-view>
</div>

نوار ناوبری در خط ۳ تعریف شده است. این بدان معناست که آن تنها قالب [main] را تشخیص می‌دهد. کد آن به شرح زیر است:


<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
  <div class="container">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
        <span class="sr-only">Toggle navigation</span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="#">RdvMedecins</a>
    </div>
    <div class="collapse navbar-collapse">
      <ul class="nav navbar-nav">
        <li class="active">
          <a href="">
            <span ng-click="main.showPage1()">Page 1</span>
          </a>
        </li>
        <li class="active">
          <a href="">
            <span ng-click="main.showPage2()">Page 2</span>
          </a>
        </li>
        <li class="active">
          <a href="">
            <span ng-click="main.showPage3()">Page 3</span>
          </a>
        </li>
      </ul>
    </div>
  </div>
</div>
  • در خطوط ۱۶، ۲۱ و ۲۶، متدهایی از مدل [main] استفاده می‌شوند؛
  • خط ۱۶: کلیک روی لینک [Page1] باعث اجرای متد [$scope.main.showPage1] می‌شود. این در کنترلر [mainCtrl] به صورت زیر تعریف شده است:

      // مدل جهانی
      var main = $scope.main = {};
      main.text = "[Modèle global]";

      // متدهای در دسترس نما
      main.showPage1 = function () {
        $location.path("/page1");
};
  • خط ۶: از کد بالا می‌توانیم ببینیم که متد [main.showPage1] در واقع متد [$scope.main.showPage1] است. بنابراین این متد است که اجرا خواهد شد؛
  • خط ۷: ما URL برنامه را به [/page1] تغییر می‌دهیم. بیایید به مسیریابی تعریف‌شده در ماژول اصلی بازگردیم:

  $routeProvider.when("/page1",
    {
      templateUrl: "views/page1.html",
      controller: 'page1Ctrl'
});

می‌توانیم ببینیم که قطعه [views/page1.html] در کانتینر درج خواهد شد و کنترلر آن [page1Ctrl] است.

3.7.15.7. ویو [/page1] و کنترلر آن

قطعه [views/page1.html] به شرح زیر است:


<h1>Page 1</h1>
<div class="alert alert-info">
  <ul>
    <li>Modèle global : {{main.text}}</li>
    <li>Modèle local : {{page1.text}}</li>
  </ul>
</div>

به یاد داریم که در نمایی که در کانتینر درج شده است، قالب [main] قابل مشاهده است. این همان چیزی است که می‌خواهیم در خط ۴ بررسی کنیم. علاوه بر این، کنترلر [page1Ctrl] برای قطعه [views/page1.html] یک قالب [page1] تعریف می‌کند. این همان قالبی است که در خط ۵ استفاده می‌شود.

کد کنترل‌کننده [page1Ctrl] به شرح زیر است:


angular.module("rdvmedecins")
  .controller('page1Ctrl', ['$scope',
    function ($scope) {

      //قالب صفحهٔ ۱
      var page1=$scope.page1;
      page1.text="[Modèle local dans page 1]";
}]);
  • خط ۲: [$scope] تزریق‌شده در اینجا خالی نیست. از آنجایی که کنترل‌کننده [page1Ctrl] ناحیه‌ای را کنترل می‌کند که در یک کانتینر کنترل‌شده توسط [mainCtrl] قرار گرفته است، [$scope] در خط ۲ شامل عناصر [$scope] است که توسط کنترل‌کننده [mainCtrl] تعریف شده است. درک این موضوع مهم است. [$scope] که توسط کنترلر [mainCtrl] تعریف شده است، شامل عناصر زیر است: [main, page1, page2, page3]. این بدان معناست که ما به مدل‌های تمام ویوها دسترسی داریم. این لزوماً مطلوب نیست، اما در اینجا چنین است. در نسخه نهایی کلاینت Angular، از این ویژگی برای ذخیره اطلاعاتی که نیاز به اشتراک‌گذاری بین ویوها دارند در مدل [main] استفاده خواهیم کرد. این امر مشابه مفهوم «سشن» در سمت سرور خواهد بود؛
  • خط ۶: ما مدل [page1] را از صفحه ۱ از طریق [$scope] بازیابی می‌کنیم و سپس با آن کار می‌کنیم (خط ۷). سپس نمایش زیر را مشاهده می‌کنیم:
 

ویو [/page2] و [/page3] بر پایهٔ همان قالبِ ویو [/page1] ساخته شده‌اند (به اسکرین‌شات‌های صفحهٔ ۲۴۰ مراجعه کنید).

3.7.15.8. کنترل ناوبری

اکنون می‌خواهیم ناوبری را به شرح زیر کنترل کنیم: [page1 --> page2 --> page3 --> page1]. بنابراین، اگر کاربر در صفحه ۱ ([/page1]) باشد و آدرس URL یا [/page3] را در مرورگر خود وارد کند، این ناوبری نباید پذیرفته شود و کاربر باید در صفحه ۱ باقی بماند.

برای دستیابی به این هدف، کنترل‌کننده‌های صفحه را به شرح زیر اصلاح می‌کنیم:


angular.module("rdvmedecins")
  .controller('page1Ctrl', ['$scope', '$location',
    function ($scope, $location) {
      //آیا ناوبری مجاز است؟
      var main = $scope.main;
      if (main.lastUrl && main.lastUrl != '/page3') {
        //بازگشت به آخرین URL
        $location.path(main.lastUrl);
        return;
      }
      //ذخیرهٔ URL صفحه
      main.lastUrl = '/page1';
      //قالب صفحه
      var page1 = $scope.page1;
      page1.text = "[Modèle local dans page 1]";
    }]);
  • خط ۱۲: هنگامی که یک صفحه نمایش داده می‌شود، ما URL آن را در مدل [main.lastUrl] ذخیره خواهیم کرد. در اینجا ما از مفهومی که قبلاً مورد بحث قرار گرفت استفاده می‌کنیم: استفاده از قالب [main] برای ذخیره اطلاعاتی که بین همه نماها مشترک است. در این مورد، این URL که آخرین بار به آن دسترسی پیدا شده است؛
  • کد در خطوط ۴ تا ۱۲ برای سه نما کپی و تطبیق داده شده است. در اینجا در نمای [/page1] هستیم؛
  • خط ۵: قالب [main] بازیابی می‌شود؛
  • خط ۶: اگر قالب [main.lastUrl] وجود داشته باشد و با [/page3] متفاوت باشد، آنگاه ناوبری ممنوع است (آخرین URL بازدید شده وجود دارد و /page3 نیست)؛
  • خط ۸: سپس به آخرین URL بازدید شده بازمی‌گردیم؛

بیایید آن را امتحان کنیم:

  • در [1]، ما در صفحهٔ ۱ هستیم و از صفحهٔ ۳ وارد URL می‌شویم که از [2] می‌آید؛
  • در [3]، ناوبری انجام نشد و ما به URL در صفحه ۱ بازگشتیم؛

3.7.16. نتیجه‌گیری

ما تمام موارد استفاده را که در نسخه نهایی کلاینت Angular با آن مواجه خواهیم شد، پوشش داده‌ایم. هنگامی که آن را ارائه می‌دهیم، بیشتر بر ویژگی‌های برنامه تمرکز خواهیم کرد تا بر جزئیات پیاده‌سازی آن. برای جزئیات پیاده‌سازی، ما به سادگی به مثالی که مورد استفاده مورد نظر را نشان می‌دهد، ارجاع خواهیم داد.

3.8. کلاینت نهایی Angular

3.8.1. ساختار پروژه

پروژه نهایی به این صورت است:

  • در [1]، کل پروژه. [app.html] صفحهٔ اصلی برنامه است؛
  • [2] شامل کنترلرها است؛
  • در [3]، دستورالعمل‌ها؛
  • در [4]، سرویس‌ها و ماژول Angular برنامه [main.js
  • در [5]، ویوهای مختلفی که در صفحهٔ اصلی [app.html] درج می‌شوند؛

3.8.2. وابستگی‌های پروژه

وابستگی‌های پروژه به شرح زیر است:

 

نقش این عناصر مختلف در بخش ۳.۴، صفحه ۱۳۴ توضیح داده شده است.

3.8.3. صفحه اصلی [app.html]

صفحه اصلی به شرح زیر است:


<!DOCTYPE html>
<html ng-app="rdvmedecins">
<head>
  <title>RdvMedecins</title>
  <!-- META -->
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Angular client for RdvMedecins">
  <meta name="author" content="Serge Tahé">
  <!-- CSS -->
  <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css"/>
  <link href="bower_components/bootstrap/dist/css/bootstrap-theme.min.css" rel="stylesheet"/>
  <link href="bower_components/bootstrap-select/bootstrap-select.min.css" rel="stylesheet"/>
  <link href="assets/css/rdvmedecins.css" rel="stylesheet"/>
  <link href="assets/css/footable.core.min.css" rel="stylesheet"/>
</head>
<!--کنترلر [appCtrl]، قالب [app] -->
<body ng-controller="appCtrl">
<div class="container">
 ...
</div>
<!-- Bootstrap core JavaScript ================================================== -->
<script type="text/javascript" src="bower_components/jquery/dist/jquery.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<script src="bower_components/footable/js/footable.js" type="text/javascript"></script>
<!-- AngularJS -->
<script type="text/javascript" src="bower_components/angular/angular.min.js"></script>
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<script type="text/javascript" src="bower_components/angular-route/angular-route.min.js"></script>
<script type="text/javascript" src="bower_components/angular-translate/angular-translate.min.js"></script>
<script type="text/javascript" src="bower_components/angular-base64/angular-base64.min.js"></script>
<!-- ماژول‌ها -->
<script type="text/javascript" src="modules/main.js"></script>
<!-- خدمات -->
<script type="text/javascript" src="services/config.js"></script>
<script type="text/javascript" src="services/dao.js"></script>
<script type="text/javascript" src="services/utils.js"></script>
<!-- دستورالعمل‌ها -->
<script type="text/javascript" src="directives/waiting.js"></script>
<script type="text/javascript" src="directives/errors.js"></script>
<script type="text/javascript" src="directives/footable.js"></script>
<script type="text/javascript" src="directives/debug.js"></script>
<script type="text/javascript" src="directives/list.js"></script>
<!-- کنترل‌کننده‌ها -->
<script type="text/javascript" src="controllers/appController.js"></script>
<script type="text/javascript" src="controllers/loginController.js"></script>
<script type="text/javascript" src="controllers/homeController.js"></script>
<script type="text/javascript" src="controllers/agendaController.js"></script>
<script type="text/javascript" src="controllers/resaController.js"></script>
</body>
</html>
  • خط ۱۸: توجه داشته باشید که [appCtrl] کنترل‌کننده صفحه اصلی است؛
  • خطوط ۱۹–۲۱: محتوای صفحهٔ اصلی؛

این محتوا به شرح زیر است:


<div class="container">
  <!--نوارهای ناوبری -->
  <ng-include src="'views/navbar-start.html'" ng-show="app.navbarstart.show"></ng-include>
  <ng-include src="'views/navbar-run.html'" ng-show="app.navbarrun.show"></ng-include>
  <!-- جامبوترون -->
  <ng-include src="'views/jumbotron.html'"></ng-include>
  <!-- عنوان صفحه -->
  <div class="alert alert-info" ng-show="app.titre.show" translate="{{app.titre.text}}"
       translate-values="{{app.titre.model}}"></div>
  <!-- خطاهای صفحه -->
  <errors model="app.errors" ng-show="app.errors.show"></errors>
  <!-- پیام بارگذاری -->
  <waiting model="app.waiting" ng-show="app.waiting.show"></waiting>
  <!-- نمایهٔ فعلی -->
  <ng-view></ng-view>
  <!--اشکال‌زدایی -->
  <debug model="app" ng-show="app.debug.on"></debug>
</div>

هر نمایی که نمایش داده شود، همیشه شامل عناصر زیر خواهد بود:

  • خطوط ۳–۴: یک نوار فرمان. این دو نوار در خطوط ۳ و ۴ یکدیگر را مستثنی می‌کنند؛

Image

Image

  • خط ۶: لوگوی برنامه یا متن:

Image

  • خط ۸: یک عنوان

Image

  • خط ۱۱: یک پیام خطا:

Image

  • خط ۱۳: یک پیام انتظار:

Image

  • خط ۱۷: یک پیام اشکال‌زدایی:

Image

تمام عناصر فوق توسط یک دستور [ng-show / ng-hide] کنترل می‌شوند، به این معنی که اگرچه آن‌ها وجود دارند، لزوماً قابل مشاهده نیستند.

3.8.4. نماهای برنامه

در کد صفحهٔ اصلی، داریم:


<div class="container">
  ...
  <!-- نمای فعلی -->
  <ng-view></ng-view>
  ...
</div>

خط ۴ نماهای مختلف برنامه را دریافت می‌کند. این‌ها در ماژول [main.js] تعریف شده‌اند:

Image

نقش پیکربندی مسیرهای مختلف در بخش 3.7.15.4، صفحه 242 توضیح داده شده است.

ویوی [login.html] خالی است، یعنی هیچ عنصری به موارد موجود در صفحه اصلی اضافه نمی‌کند.

ویوی [home.html] عنصر زیر را به صفحهٔ اصلی اضافه می‌کند:

Image

ویوی [agenda.html] عنصر زیر را به صفحهٔ اصلی اضافه می‌کند:

Image

ویوی [resa.html] عنصر زیر را به صفحه اصلی اضافه می‌کند:

Image

3.8.5. ویژگی‌های برنامه

ویوهای کلاینت Angular قبلاً در بخش 1.3.3، در صفحه 7 توضیح داده شده‌اند. برای اینکه دنبال کردن این فصل جدید آسان‌تر شود، آن‌ها را مجدداً در اینجا گنجانده‌ایم. اولین ویو به شرح زیر است:

  • در [6]، صفحه اصلی برنامه. این یک برنامه رزرو وقت ملاقات برای پزشکان است؛
  • در [7]، یک کادر تیک که به کاربر اجازه می‌دهد انتخاب کند که آیا در حالت [debug] باشد یا خیر. حالت دوم با وجود فریم [8] مشخص می‌شود که قالب نمای فعلی را نمایش می‌دهد؛
  • در [9]، یک زمان انتظار مصنوعی به میلی‌ثانیه. مقدار پیش‌فرض 0 (بدون انتظار) است. اگر N مقدار این زمان انتظار باشد، هر اقدام کاربر پس از یک زمان انتظار به مدت N میلی‌ثانیه اجرا خواهد شد. این به شما امکان می‌دهد تا مدیریت انتظار پیاده‌سازی‌شده توسط برنامه را مشاهده کنید؛
  • در [10]، URL سرور Spring 4. پی‌جری از نکته قبلی، این [http://localhost:8080] است؛
  • در [11] و [12]، نام کاربری و رمز عبور فردی که مایل به استفاده از برنامه است. دو کاربر وجود دارد: admin/admin (نام کاربری/رمز عبور) با نقش (ADMIN) و user/user با نقش (USER). تنها نقش ADMIN مجاز به استفاده از برنامه است. نقش USER صرفاً برای نمایش پاسخ سرور در این مورد استفاده گنجانده شده است؛
  • در [13]، دکمه‌ای که به شما امکان اتصال به سرور را می‌دهد؛
  • در [14]، زبان رابط کاربری. دو گزینه وجود دارد: فرانسوی (پیش‌فرض) و انگلیسی.
  • در [1]، شما وارد می‌شوید؛
  • پس از ورود به سیستم، می‌توانید دکتر مورد نظر خود را برای رزرو وقت ملاقات ([2]) و تاریخ وقت ملاقات ([3]) انتخاب کنید؛
  • می‌توانید دفترچه یادداشت پزشک منتخب را برای روز انتخاب‌شده مشاهده کنید؛
  • پس از دسترسی به برنامه دکتر، می‌توانید یک نوبت رزرو کنید [5];
  • در [6]، بیمار را برای قرار ملاقات انتخاب می‌کنید و این انتخاب را در [7] تأیید می‌کنید؛

پس از تأیید قرار ملاقات، شما به طور خودکار به تقویم بازمی‌گردید که در آن قرار ملاقات جدید اکنون فهرست شده است. این قرار ملاقات را می‌توان در تاریخ بعدی از طریق [7] حذف کرد.

ویژگی‌های اصلی توضیح داده شدند. آن‌ها ساده هستند. مواردی که توضیح داده نشده‌اند، عملکردهای ناوبری برای بازگشت به نمای قبلی هستند. بیایید با تنظیمات زبان به پایان برسیم:

  • در [1]، شما از فرانسوی به انگلیسی تغییر می‌دهید؛
  • در [2]، نما به انگلیسی، از جمله تقویم، تغییر می‌کند؛

3.8.6. ماژول [main.js]

ماژول [main.js] ماژول Angular را که کنترل برنامه را بر عهده دارد، تعریف می‌کند:

 
  • خط ۴: ماژول [rdvmedecins] نامیده می‌شود؛
  • خط ۵: ماژول [ngRoute] برای مسیریابی URL استفاده می‌شود؛
  • خط ۶: ماژول [translate] برای بین‌المللی‌سازی متن استفاده می‌شود؛
  • خط ۷: ماژول [base64] برای رمزگذاری رشته 'login:password' در Base64 استفاده می‌شود؛
  • خط ۸: ماژول [ngLocale] برای بین‌المللی‌سازی تقویم استفاده می‌شود؛
  • خط ۹: ماژول [ui.bootstrap] برای تقویم استفاده می‌شود؛
  • خط ۱۲: پیکربندی مسیر؛
  • خط ۴۰: بین‌المللی‌سازی پیام؛

3.8.7. کنترل‌کننده صفحه اصلی

بیایید کد HTML را از صفحه اصلی [app.html] به یاد بیاوریم:


<body ng-controller="appCtrl">
<div class="container">
...

خط ۱: کل بدنه صفحه اصلی توسط کنترل‌کننده [appCtrl] کنترل می‌شود. به دلیل موقعیت آن، این کنترل‌کننده به عنوان کنترل‌کننده عمومی و اصلی برنامه عمل می‌کند. همانطور که در بخش ۳.۷.۱۵ توضیح داده شده است، مدلی که توسط این کنترل‌کننده ساخته می‌شود برای تمام ویوهایی که در صفحه اصلی قرار می‌گیرند، ارث‌بری می‌شود.

کد آن به شرح زیر است:


angular.module("rdvmedecins")
  .controller("appCtrl", ['$scope', 'config', 'utils', '$location', '$locale',
    function ($scope, config, utils, $location, $locale) {

      //اشکال‌زدایی
      utils.debug("[app] init");

      // ----------------------------------------ابتدای صفحه
      // قالب‌ها برای صفحات #
      $scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
      $scope.login = {};
      $scope.home = {};
      $scope.agenda = {};
      $scope.resa = {};
      //قالب صفحهٔ جاری
      var app = $scope.app;
      ...

      // ---------------------------------- متدها

      //لغو وظیفهٔ جاری
      app.cancel = function () {
...
      };

      // خروج
      app.deconnecter = function () {
        ...
      };

      //این کد باید در اینجا باقی بماند زیرا به تابع قبلی [cancel] ارجاع می‌دهد
      app.waiting = {title: {text: config.msgWaitingInit, values: {}}, cancel: app.cancel, show: true};
    }])
;

خطوط ۱۰ تا ۱۴ پنج قالب مورد استفاده در برنامه را تعریف می‌کنند:

Modèle
Vue
Contrôleur
$scope.app
app.html
appCtrl
$scope.login
login.html
loginCtrl
$scope.home
home.html
homeCtrl
$scope.resa
resa.html
resaCtrl
$scope.agenda
agenda.html
agendaCtrl

مهم است که درک کنیم شیء [$scope]، که قالب کنترل‌کننده صفحه اصلی است، توسط تمام ویوها و کنترل‌کننده‌ها ارث‌بری می‌شود. بنابراین، کنترل‌کننده [loginCtrl] به عناصر [$scope.app, $scope.login, $scope.home, $scope.resa, $scope.agenda] دسترسی دارد. به عبارت دیگر، یک کنترل‌کننده به مدل‌های سایر کنترل‌کننده‌ها دسترسی دارد. برنامه‌ای که در نظر گرفته شده است با دقت از استفاده از این قابلیت اجتناب می‌کند. برای مثال، کنترل‌کننده [loginCtrl] تنها با دو مدل کار می‌کند:

  • تemplates مربوط به خود، [$scope.login
  • و قالب کنترل‌کننده والد، [$scope.app

همین امر در مورد سایر کنترل‌کننده‌ها نیز صدق می‌کند. مدل [$scope.app] به‌عنوان حافظه مشترک بین کنترل‌کننده‌های مختلف استفاده خواهد شد. وقتی کنترل‌کننده C1 نیاز به ارسال اطلاعات به کنترل‌کننده C2 داشته باشد، رویه زیر دنبال می‌شود:

در [C1]:

$scope.app.info=value ;

در [C2]:

var value=$scope.app.info ;

در هر دو مورد، $scope از کنترل‌کننده [appCtrl] به ارث برده شده و بنابراین در [C1] و [C2] یکسان است (این یک نشانگر است). شیء [$scope.app] که به‌عنوان حافظه مشترک بین کنترل‌کننده‌ها عمل می‌کند، اغلب در نظرات با نام session اشاره می‌شود، مطابق با قراردادی که در برنامه‌های وب سنتی استفاده می‌شود، جایی که HTTP نشان‌دهنده حافظه مشترک بین درخواست‌های متوالی است.

بیایید به کد کنترل‌کننده [appCtrl] بازگردیم:


      // قالب‌های صفحات #
      $scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
      $scope.login = {};
      $scope.home = {};
      $scope.agenda = {};
      $scope.resa = {};
      // قالب صفحهٔ جاری
      var app = $scope.app;
      // [app.debug] و [utils.verbose] باید همیشه همگام باشند
      app.debug = utils.verbose;
      app.debug.on = config.debug;
      //در حال حاضر هیچ عنوان صفحه‌ای وجود ندارد
      app.titre = {show: false};
      //هیچ نوار ناوبری وجود ندارد
      app.navbarrun = {show: false};
      app.navbarstart = {show: false};
      // بدون خطا
      app.errors = {show: false};
      // محلی پیش‌فرض
      angular.copy(config.locales['fr'], $locale);
      // نما فعلی
      app.view = {url: undefined, model: {}, done: false};
      // وظیفهٔ جاری
app.task = app.view.model.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};
  • خط ۸: [$scope.app] قالب صفحه اصلی خواهد بود. این همچنین حافظه مشترک بین کنترل‌کننده‌های مختلف خواهد بود. به جای نوشتن [$scope.app.champ=value] در همه‌جا، نشانگر [$scope.app] به متغیر [app] اختصاص داده می‌شود و سپس ما [app.champ=value] را می‌نویسیم. کافی است به خاطر داشته باشید که [app] قالبی است که در صفحه اصلی نمایش داده می‌شود؛
  • خط ۱۱: [app.debug.on] یک متغیر بولی است که حالت debug برنامه را کنترل می‌کند. به‌طور پیش‌فرض، روی true تنظیم شده است. مقدار آن به چک‌باکس [debug] در نوارهای ناوبری متصل است؛
  • خط ۱۵: [app.navbarrun.show] نمایش نوار ناوبری زیر را کنترل می‌کند:

Image

  • خط ۱۶: [app.navbarstart.show] نمایش نوار ناوبری زیر را کنترل می‌کند:

Image

  • خط ۱۸: [app.errors] قالب بنر خطا است؛

Image

  • خط ۲۲: [app.view] حاوی اطلاعات مربوط به نمای فعلی خواهد بود، یعنی نمایی که در حال حاضر توسط تگ [ng-view] در صفحهٔ اصلی نمایش داده می‌شود. ما اطلاعات زیر را در آنجا درج خواهیم کرد:
    • [url]: URL برای نمای فعلی، برای مثال [/agenda];
    • [model]: قالب برای نمای فعلی، برای مثال [$scope.agenda];
    • [done]: به vrai اشاره می‌کند که نمای فعلی وظیفه خود را به پایان رسانده و سیستم به نمای دیگری سوئیچ می‌کند؛

این اطلاعات برای کنترل ناوبری استفاده می‌شود.

  • خط ۲۴: یک وظیفه ناهمزمان، یک انتظار شبیه‌سازی‌شده را آغاز می‌کند. این وظیفه ناهمزمان توسط دو نشانگر، [app.view.model.task.action] و [app.task]، ارجاع داده می‌شود؛

دو متد به کنترلر [appCtrl] منتقل شده‌اند:


      //لغو وظیفهٔ جاری
      app.cancel = function () {
...
      };

      // خروج
      app.deconnecter = function () {
        ...
};
  • خط ۲: تابع [app.cancel] برای لغو وظیفه فعلی که در حال حاضر پیام انتظار برای آن نمایش داده می‌شود، استفاده می‌شود. همه ویوها این پیام را نمایش می‌دهند، بنابراین وظیفه در اینجا لغو خواهد شد؛
  • خط ۷: تابع [app.deconnecter] کاربر را به صفحه احراز هویت بازمی‌گرداند. همه نماها، به جز نمای [/login]، این گزینه را ارائه می‌دهند؛

تابع [app.deconnecter] به شرح زیر است:


      // خروج
      app.deconnecter = function () {
        //بازگشت به صفحه ورود
        $location.path(config.urlLogin);
};
  • خط ۴: بازگشت به صفحه ورود برای URL [/login];

3.8.8. مدیریت وظایف ناهمزمان

در برنامه ما، در هر لحظه تنها یک وظیفه غیرهمزمان در حال اجرا خواهد بود. امکان وجود چندین وظیفه همزمان وجود دارد. برای مثال، هنگامی که برنامه اجرا می‌شود، فهرست پزشکان را از سرویس وب درخواست می‌کند و سپس با استفاده از دو درخواست متوالی HTTP، فهرست مشتریان را درخواست می‌کند. ما می‌توانستیم با دو درخواست همزمان HTTP به همان نتیجه برسیم. Angular ابزارهای لازم برای این کار را فراهم می‌کند. در این مورد، ما این کار را انتخاب نکرده‌ایم.

وظیفه در حال اجرای فعلی با استفاده از کد زیر در کنترلر [appCtrl] لغو می‌شود:


      //لغو وظیفه فعلی
      app.cancel = function () {
        utils.debug("[app] cancel task");
        // لغو وظیفهٔ غیرهم‌زمان برای نمای فعلی
        var task = app.view.model.task;
        task.isFinished = true;
        task.action.reject();

        ...
};
  • خط ۵: وظیفه از [app.view.model.task] بازیابی می‌شود. بنابراین، همه کنترل‌کننده‌ها اطمینان حاصل می‌کنند که وظایف غیرهمزمان خود به این شیء ارجاع داده شوند؛
  • خط ۶: برای نشان دادن اینکه وظیفه کامل شده است؛
  • خط ۷: برای خاتمه دادن به وظیفه با یک خطا. این نشانه از آنچه در مثال‌های Angular مورد مطالعه استفاده شده، متفاوت است:
    • در مثال‌ها، شیء [task] یک شیء [$q.defer()] بود که می‌توانست متوقف شود؛
    • در نسخه نهایی، شیء [task] شیئی با فیلدهای [action, isFinished] است، که در آن [action] شیء [$q.defer()] است کهمی‌توان تکمیل شود، و [isFinished] یک بولین است که نشان می‌دهد عمل کامل شده است؛

بیایید چرخهٔ عمر شیء [task] را با استفاده از یک مثال بررسی کنیم. در هنگام راه‌اندازی، پس از کنترل‌کننده [appCtrl]، کنترل‌کننده [loginCtrl] کنترل را برای نمایش نما [views/login.html] به دست می‌گیرد. کد инициализаسيون آن به شرح زیر است:


      // بازیابی مدل والد
      var login = $scope.login;
      var app = $scope.app;
      // نما نمایش داده شده
app.view = {url: config.urlLogin, model: login, done: false};

در خط ۵، ما [model=login] را داریم. این بدان معناست که وقتی شیء [login] تغییر می‌کند، شیء [app.view.model] تغییر می‌کند و در نتیجه [$scope.app.view.model]. وقتی در کنترل‌کننده [loginCtrl] می‌خواهید یک وقفه شبیه‌سازی‌شده اجرا کنید، می‌نویسید:


// انتظار شبیه‌سازی‌شده
var task = login.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};

با افزودن فیلد [task] به شیء [login]، بنابراین به شیء [$scope.app.view.model] اضافه شده است. اگر کاربر صف را لغو کند، کد در [appCtrl.cancel]:


// مدل صفحهٔ جاری
var app = $scope.app;
...
var task = app.view.model.task;
task.isFinished = true;
task.action.reject();

انتظار شبیه‌سازی‌شده را با موفقیت به پایان می‌رساند (خطوط ۴–۶).

3.8.9. کنترل ناوبری

قوانین ناوبری مورد استفاده در برنامه به شرح زیر است:

URL cible
URL précédente
Navigation autorisée
/login
هر
بله
/home
/ورود
بله، اگر کنترلر [loginCtrl] اعلام کرده باشد که کار خود را به پایان رسانده است

/home
بله

/calendar
بله
/agenda
/home
بله، اگر کنترلر [homeCtrl] اعلام کرده باشد که کار خود را به پایان رسانده است

/resa
بله

/calendar
بله
/resa
/calendar
بله، اگر کنترلر [homeCtrl] اعلام کرده باشد که کار خود را به پایان رسانده است

/رِسا
بله

این با استفاده از کد زیر پیاده‌سازی شده است:

برای [agendaCtrl]:

Image

  • خطوط ۱۱–۲۰: پیاده‌سازی قاعده ناوبری؛
  • خط ۲۶: نمای جاری جدید؛

برای [resaCtrl]:

Image

  • خطوط ۱۲–۲۰: پیاده‌سازی قاعده ناوبری:
  • خط ۲۷: نمای جاری جدید؛

برای [loginCtrl]:

Image

  • در اینجا هیچ بررسی ناوبری وجود ندارد، زیرا قانون بیان می‌کند که می‌توان از هر کجا به URL [/login] ناوبری کرد. بنابراین، اگر کاربر این URL را در مرورگر خود وارد کند، صرف‌نظر از نمای فعلی، کار خواهد کرد؛
  • خط ۱۶: نمای جاری جدید؛

کد کنترل‌کننده [homeCtrl] در بخش 3.8.7 ارائه شده است.

در نهایت، برای قاعده‌ای مانند:

/agenda
/home
بله، اگر کنترل‌کننده [homeCtrl] اعلام کرده باشد که کار خود را به پایان رسانده است

در اینجا مثالی از کدی آمده است که از URL [/home] به URL [/agenda] سوئیچ می‌کند:

 

در بالا، ما در متد [afficherAgenda] از کنترلر [homeCtrl] هستیم. کاربر درخواست تقویم نوبت دکتر کرده است.

  • خط ۱۰۷: وعده برای وظیفه HTTP;
  • خط ۱۰۹: متغیر [app] با [$scope.app] مقداردهی اولیه شده است. همانطور که دیدیم، شیء دوم به عنوان قالب برای نمای [app.html] استفاده می‌شود. این قالب، [$scope.app]، همچنین برای ذخیره اطلاعاتی که باید بین ویوها به اشتراک گذاشته شود، استفاده می‌شود؛
  • خط ۱۱۱: کد خطای بازگشتی از وظیفه تحلیل می‌شود؛
  • خط ۱۱۳: نتیجه [result.data] در قالب [app] قرار می‌گیرد؛
  • خط ۱۱۶: کنترل‌کننده [homeCtrl] کار را به کنترل‌کننده [agendaCtrl] واگذار می‌کند. به کنترلر اطلاع می‌دهد که با استفاده از کد خط 115 وظیفه‌اش را به پایان رسانده است. این کد توسط کنترلر [agendaCtrl] به شرح زیر پردازش خواهد شد:

Image

  • خط ۱۱: شیء [$scope.app.view] بازیابی می‌شود؛
  • خط ۱۵: پردازش فیلد [$scope.app.view.done] که توسط [homeCtrl] مقداردهی اولیه شده است؛

3.8.10. خدمات

  

خدمات [config, utils, dao] همان‌هایی هستند که قبلاً در نمای کلی Angular توصیف شده‌اند:

  • سرویس [config] در بخش 3.7.4 معرفی شد؛
  • سرویس [utils] در بخش 3.7.5 معرفی شد؛
  • سرویس [dao] در بخش 3.7.6 معرفی شد؛

برای اطلاع، ساختار این سرویس‌ها به شرح زیر است:

سرویس [config]

  • در [1]: می‌توانیم ببینیم که این کد تقریباً ۲۵۰ خط طول دارد. بخش عمده این کد به برون‌سپاری کلیدها برای پیام‌های بین‌المللی [2] اختصاص دارد. ما از هاردکد کردن مستقیم این کلیدها در کد خودداری می‌کنیم؛

سرویس [utils]

 
  • خط ۸: ما هنوز با متغیر [verbose] مواجه نشده بودیم. این متغیر، تابع [debug] را به شرح زیر کنترل می‌کند:
 
  • خطوط ۲۲–۲۵: تابع [utils.debug] هیچ کاری انجام نمی‌دهد اگر [verbose.on] به false ارزیابی شود. این متغیر به متغیری در کنترل‌کننده [appCtrl] مرتبط است:
 
  • خط ۲۱: [app.debug] مقدار نشانگر [utils.verbose] را دریافت می‌کند. بنابراین، هر تغییری که در [app.debug] ایجاد شود، بر [utils.verbose] نیز اعمال خواهد شد؛
  • خط ۲۲: مقدار اولیه [app.debug.on] از فایل پیکربندی گرفته می‌شود. به‌طور پیش‌فرض، این مقدار true. است. این مقدار ممکن است در طول زمان تغییر کند. کاربر می‌تواند آن را از طریق نوارهای ناوبری تغییر دهد:
 
  • خط ۴۵: یک کادر تیک (نوع=checkbox) امکان تغییر مقدار [app.debug.on] (ویژگی ng-model) را فراهم می‌کند؛

سرویس [dao]

 

3.8.11. دستورالعمل‌ها

  

دستورالعمل‌های [errors, footable, list, waiting] همان‌هایی هستند که قبلاً در مقدمه Angular توصیف شده‌اند:

  • دستور [footable] در بخش 3.7.8.6 معرفی شد؛
  • دستورالعمل [list] در بخش 3.7.12 معرفی شد؛
  • دستورالعمل‌های [errors] و [waiting] در بخش 3.7.14 معرفی شدند؛

ما با دستور [debug] مواجه نشده بودیم. این دستور به شرح زیر است:

 

فایل [debug.html] که در خط ۱۱ ارجاع شده است، به شرح زیر است:

 
  • خط ۲: دستور [debug] قالب خود را در قالب JSON درون یک بنر Bootstrap (خط ۱) نمایش می‌دهد؛

این دستورالعمل فقط در صفحهٔ اصلی [app.html] استفاده می‌شود:

 
  • دستور [debug] در خط ۳۵ استفاده می‌شود. بنابراین، هنگام قرار گرفتن در حالت اشکال‌زدایی (ویژگی ng-show)، نسخه JSON از قالب [$scope.app] را نمایش می‌دهد. این کار خروجی‌ای مانند موارد زیر تولید می‌کند:

این امر برای تفسیر، نیازمند درک خوبی از کد است، اما پس از کسب این مهارت، اطلاعات فوق برای عیب‌یابی مفید واقع می‌شود. عناصر قالب نمایش‌داده‌شده [$scope.app] در اینجا برجسته شده‌اند. توجه به این نکته ضروری است که [$scope.app] حافظه‌ای است که بین کنترل‌کننده‌ها مشترک است؛

  • [waitingBeforeTask]: زمان انتظار شبیه‌سازی‌شده قبل از هر درخواست به HTTP;
  • [debug]: حالت اشکال‌زدایی – اگر این بنر نمایش داده شود، باید true باشد؛
  • [navbarrun]: یک متغیر بولی که نمایش نوار پیمایش زیر را کنترل می‌کند:

Image

  • [navbarstart]: یک مقدار بولی که نمایش نوار پیمایش زیر را کنترل می‌کند:

Image

  • [errors]: قالب برای دستور [errors
  • [view]: شامل اطلاعاتی درباره نمای در حال نمایش است؛
  • [waiting]: قالب برای دستور [waiting
  • [serverUrl, username, password]: جزئیات ورود سرویس وب؛
  • [medecins]: قالب برای دستور [list] که بر پزشکان اعمال می‌شود؛
  • [clients]: همین برای مشتریان؛
  • [menu]: کنترل‌کننده گزینه‌های منوی نمایش‌داده‌شده است. این گزینه‌ها در [navbar-run.html] تعریف شده‌اند:

Image

گزینه‌های منو در خطوط ۱۶، ۲۳، ۲۹ و ۳۶ قرار دارند.

  • [formattedJour]: روزی که در تقویم انتخاب شده است به فرمت 'yyyy-mm-dd
  • [agenda]: دفترچه یادداشت پزشک. این شامل هم نوبت‌های خالی (rv==null) و هم نوبت‌های رزروشده است. برای مورد دوم، نام مراجعی که نوبت را رزرو کرده است نمایش داده می‌شود؛
  • [selectedCreneau]: بازه زمانی انتخاب‌شده برای رزرو؛

3.8.12. کنترل‌کننده [loginCtrl]

  

کنترل‌کننده [loginCtrl] با نما [views/login.html] مرتبط است که با ترکیب با صفحه اصلی، صفحه زیر را تولید می‌کند:

Image

کنترل‌کننده [loginCtrl] به شرح زیر است:

Image

  • خط ۱۳: [login] قالب برای نمای فعلی خواهد بود؛
  • خط ۱۴: [app] حافظه مشترک بین کنترل‌کننده‌ها است؛
  • خط ۱۶: [app.view] با اطلاعات نمای فعلی پر می‌شود؛

این کد инициализация در هر کنترلر یافت می‌شود. برای کنترلر C1 از یک نما V1 با قالب M1، کد инициализация به شرح زیر خواهد بود:

1
2
3
var app=$scope.app;
var M1=$scope.M1;
app.view={url: config.urlV1, model:M1, done:false};
  • خط ۱۸: ممکن است به یاد داشته باشید که [appCtrl] یک وقفه شبیه‌سازی‌شده را با ارجاع به شی [app.task.action] آغاز کرد. ما از [promise] در این وظیفه برای منتظر ماندن تا تکمیل آن استفاده می‌کنیم؛
  • خط ۳۹: متد [login.setLang] به مدیریت تغییر زبان می‌پردازد؛
  • خط ۴۷: متد [login.authenticate] احراز هویت کاربر را مدیریت می‌کند؛

بیایید مراحل اصلی روش احراز هویت را بررسی کنیم:

Image

  • خطوط ۵۰–۵۱: [app.waiting] قالب بنر بارگذاری است؛
  • خط ۵۳: [app.errors] قالب بنر خطا است؛
  • خط ۵۵: یک انتظار شبیه‌سازی‌شده آغاز می‌شود. شیء [action, isFinished] توسط [login.task] ارجاع داده می‌شود و بنابراین، از طریق [app.view.model=login]، توسط [app.view.model.task] ارجاع داده می‌شود. توجه داشته باشید که این شرطی است که بر اساس آن می‌توان وظیفه را لغو کرد؛
  • خط ۵۷: پس از پایان انتظار شبیه‌سازی‌شده، پزشکان بارگذاری می‌شوند؛
  • خط ۶۲: پس از دریافت درخواست پزشکان، این درخواست تحلیل می‌شود. اگر پزشکان دریافت شده باشند، سیستم سپس از مشتریان درخواست می‌کند؛
  • خط ۸۳: پاسخ دریافتی تحلیل شده و نمای نهایی نمایش داده می‌شود. این کار با استفاده از کد زیر انجام می‌شود:

Image

  • خط ۸۷: متغیر بولی [task.isFinished] در موارد زیر روی true تنظیم می‌شود:
    • کاربر منتظر ماندن را لغو کرده است؛
    • درخواست پزشکان با خطا پایان یافت؛
  • خطوط ۹۱–۹۸: حالتی که مشتریان حضور داشتند؛
  • خط ۹۳: [app.clients] قالب دستور [list] است که مشتریان را در یک لیست کشویی نمایش می‌دهد؛
  • خطوط ۹۷–۹۸: ما در حال آماده‌سازی برای تغییر نما (view) (خط ۹۸) هستیم اما ابتدا نشان می‌دهیم که کنترل‌کننده (controller) کار خود را به پایان رسانده است (خط ۹۷). توجه داشته باشید که [$scope.app.view.done] برای کنترل ناوبری استفاده می‌شود؛

نکته مهم این است که پزشکان و مشتریان در مرورگر کش شده‌اند. دیگر از سرویس وب درخواست نخواهند شد.

3.8.13. کنترل‌کننده [homeCtrl]

  

کنترل‌کننده [homeCtrl] با نما [views/home.html] مرتبط است که با ترکیب با صفحه اصلی، صفحه زیر را تولید می‌کند:

Image

ساختار کنترل‌کننده [homeCtrl] به شرح زیر است:

Image

  • رده‌های ۱۲–۲۰: این کنترل ناوبری است. همه کنترل‌کننده‌ها این کنترل را دارند به جز [loginCtrl]، زیرا صفحه [/login.html] بدون هیچ شرطی قابل دسترسی است؛

Image

  • خطوط ۲۵–۲۸: این خطوط مشابه خطوط موجود در کنترل‌کننده [loginCtrl] هستند. بنابراین [home] قالب نمای مرتبط با این کنترل‌کننده است؛
  • خط ۳۳: یک ویژگی که هنوز با آن مواجه نشده‌ایم. این قالب برای نوار سربرگ نما است:

Image

  • خط ۳۶: [home.datepicker] قالب تقویم است؛
  • خط ۳۸: [app.menu] قالب منوی نوار ناوبری است. گزینه [Agenda] در اینجا گنجانده خواهد شد. این گزینه‌ای است که برای درخواست تقویم نوبت دکتر استفاده می‌شود؛

در نهایت، کنترلر دو متد دارد:

Image

نمایش تقویم قرار ملاقات (خط ۵۱) در بخش ۳.۷.۸ پوشش داده شده است.

3.8.14. کنترل‌کننده [agendaCtrl]

  

کنترل‌کننده [agendaCtrl] با نما [views/agenda.html] مرتبط است که با ترکیب با صفحه اصلی، صفحه زیر را تولید می‌کند:

Image

ساختار کنترل‌کننده [agendaCtrl] به شرح زیر است:

Image

  • خطوط ۱۰–۲۰ کنترل ناوبری را مدیریت می‌کنند؛

Image

  • خطوط 23–26: [agenda] قالب نمای مرتبط با کنترل‌کننده [agendaCtrl] خواهد بود؛
  • خطوط ۳۶–۴۴: [app.titre] قالب بنر سربرگ زیر است:

Image

  • خط ۴۶: منو شامل گزینه [Home / Accueil] خواهد بود:

Image

متدهای کنترل‌کننده به شرح زیر است:

Image

  • خط ۹۵: روش [agenda.supprimer] در بخش ۳.۷.۹ مورد بحث قرار گرفت؛

روش [agenda.home] یک روش ناوبری خالص است:

Image

روش [agenda.reserver] به شرح زیر است:

Image

  • خط ۷۳: پارامتر تابع [reserver] شماره اسلات (id) است؛
  • خطوط ۷۷–۸۶: برای بازیابی اسلات زمانی با این شناسه در نظر گرفته شده‌اند؛
  • خط ۸۲: بازه زمانی یافت‌شده در حافظه مشترک [app] ذخیره می‌شود. کنترلر [resaCtrl] که کنترل را بر عهده خواهد گرفت (خط ۹۰)، از این اطلاعات برای نمایش بنر عنوان خود استفاده خواهد کرد؛
  • خطوط ۸۹–۹۰: ناوبری به [/resa.html

3.8.15. کنترل‌کننده [resaCtrl]

  

کنترل‌کننده [resaCtrl] با نما [views/resa.html] مرتبط است که با ترکیب با صفحه اصلی، صفحه زیر را تولید می‌کند:

Image

ساختار کنترل‌کننده [resaCtrl] به شرح زیر است:

Image

  • خطوط ۱۲–۲۰: کنترل ناوبری؛

Image

  • خطوط ۲۴–۲۷: [resa] قالب نمای فعلی خواهد بود؛
  • خطوط ۳۸–۴۵: [app.titre] قالب بنر سربرگ زیر است:

Image

  • خط ۴۷: دو گزینه منو نمایش داده می‌شوند:

Image

متدهای کنترل‌کننده به شرح زیر است:

Image

روش [resa.valider] در بخش 3.7.9 مورد بحث قرار گرفت.

3.8.16. مدیریت زبان

تمام کنترل‌کننده‌ها روش زیر را ارائه می‌دهند: [setLang]:

Image

می‌توانست در کنترل‌کننده [appCtrl] گنجانده شود.