Skip to content

3. عميل Angular JS

3.1. مراجع لإطار عمل Angular JS

تم تقديم مرجعين لإطار عمل Angular JS في بداية هذا المستند. نذكرهما مرة أخرى هنا:

يستحق AngularJS كتابًا خاصًا به. يبلغ عدد صفحات كتاب آدم فريمان أكثر من 600 صفحة، ولا توجد صفحة واحدة مهدرة. سنصف تطبيق 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 إلى الإجراء الذي يجب أن يتعامل معه. هذه الإجراءات هي طرق لفئات محددة تسمى [Controllers]. الحرف C في MVC هنا هو السلسلة [Dispatcher Servlet، Controller، Action]. إذا لم يتم تكوين أي إجراء لمعالجة عنوان URL الوارد، فسيرد [Dispatcher Servlet] بأن عنوان URL المطلوب لم يتم العثور عليه (خطأ 404 NOT FOUND
  1. معالجة
  • يمكن للإجراء المحدد استخدام المعلمات التي مررها [Dispatcher Servlet] إليه. يمكن أن تأتي هذه المعلمات من عدة مصادر:
    • مسار [/param1/param2/...] لعنوان URL،
    • معلمات عنوان URL [p1=v1&p2=v2]،
    • من المعلمات التي أرسلها المتصفح مع طلبه؛
  • عند معالجة طلب المستخدم، قد يحتاج الإجراء إلى طبقة [الأعمال] [2b]. بمجرد معالجة طلب العميل، قد يؤدي ذلك إلى استجابات متنوعة. ومن الأمثلة الكلاسيكية على ذلك:
    • صفحة خطأ إذا تعذر معالجة الطلب بشكل صحيح
    • صفحة تأكيد في الحالات الأخرى
  • يُصدر الإجراء تعليمات لعرض طريقة عرض محددة [3]. ستعرض طريقة العرض هذه البيانات المعروفة باسم نموذج العرض. هذا هو الحرف M في MVC. سيقوم الإجراء بإنشاء نموذج M هذا [2c] وإصدار تعليمات لعرض طريقة عرض V [3]؛
  1. الاستجابة - تستخدم طريقة العرض V المحددة النموذج M الذي أنشأته الإجراء لتهيئة الأجزاء الديناميكية من استجابة HTML التي يجب إرسالها إلى العميل، ثم ترسل هذه الاستجابة.

ستكون بنية عميل Angular الخاص بنا مشابهة، مع اختلاف طفيف في المصطلحات. أولاً، تطبيقات Angular هي عمومًا تطبيقات ويب أحادية الصفحة (SPAs):

Image

  • يطلب المستخدم عنوان URL الأولي للتطبيق بالشكل التالي: http://machine:port/contexte. يستعلم المتصفح عن خادم الويب لاسترداد المستند المطلوب. هذه صفحة HTML مصممة باستخدام CSS وأصبحت ديناميكية بواسطة JavaScript؛
  • ثم يتفاعل المستخدم مع العروض المقدمة له. يمكننا التمييز بين أنواع مختلفة من التفاعلات:
    • تلك التي لا تتطلب أي تفاعل مع مصادر خارجية، مثل إخفاء أو إظهار عناصر العرض. يتم التعامل مع هذه العناصر بواسطة JavaScript المضمن؛
    • تلك التي تتطلب بيانات من خدمة ويب بعيدة. سيتم استرداد هذه البيانات عبر طلب AJAX (JavaScript و XML غير متزامن)، وسيتم إنشاء نموذج، وعرض عرض؛
    • تلك التي تتطلب عرضًا بخلاف العرض الأولي. سيتم طلبها عبر استدعاء AJAX إلى الخادم الذي قدم الصفحة الأولية. ثم تتكرر العملية السابقة. سيتم تخزين الصفحة الناتجة مؤقتًا في المتصفح. عند الطلب التالي، لن يتم جلبها من خادم HTML البعيد؛

في النهاية، لا يقوم المتصفح إلا بطلب HTTP واحد فقط — وهو الطلب الذي يجلب الصفحة الأولية. أما طلبات HTTP اللاحقة الموجهة إلى خادم صفحة HTML أو خدمات الويب البعيدة، فيتم إجراؤها بواسطة لغة جافا سكريبت المضمنة في الصفحات.

سنعرض الآن بنية التطبيق داخل المتصفح. وسنتجاهل خادم HTML الذي يقدم صفحات HTML الخاصة بالتطبيق. ولأغراض التوضيح، يمكننا افتراض أن جميعها موجودة داخل ذاكرة التخزين المؤقت للمتصفح.

أولاً، نحتاج إلى تحديد موقع هذه البنية:

  • في [1]، نحن في متصفح؛
  • في [2]، يتفاعل المستخدم مع العروض التي يعرضها المتصفح؛
  • في [3]، يتم استرداد البيانات من الشبكة، غالبًا من خدمات الويب؛

يتفاعل المستخدم مع العروض: يملأ النماذج ويرسلها. دعونا نوضح هذه العملية باستخدام العرض V1 أعلاه. سنفترض أن هذا هو العرض الأولي للتطبيق. تم الحصول عليه على النحو التالي:

  • يطلب المستخدم عنوان URL الأولي للتطبيق بالشكل التالي: http://machine:port/contexte؛
  • طلب المتصفح المستند المرتبط بعنوان URL هذا. وتلقى صفحة HTML/CSS/JS لعرض V1؛
  • ثم تولى JavaScript المضمن في الصفحة زمام الأمور وسلم التحكم إلى وحدة التحكم C1 [5]؛
  • قام وحدة التحكم ببناء نموذج M1 [8] [9] لعرض V1. قد يتطلب بناء هذا النموذج استخدام خدمات داخلية [6] والاستعلام عن خدمات خارجية [7]؛

أصبح لدى المستخدم الآن عرض V1 أمامه. لنفترض أنه نموذج. يقوم المستخدم بملئه ثم إرساله:

  • في [4]، يرسل المستخدم النموذج؛
  • في [5]، سيتم التعامل مع هذا الحدث بواسطة إحدى طرق وحدة التحكم C1؛

إذا أسفر الحدث عن تغيير بسيط فقط في العرض V1 (إخفاء/إظهار الحقول)، فستقوم وحدة التحكم C1 بتعديل نموذج M1 للعرض V1 ثم تعرض العرض V1 مرة أخرى. للقيام بذلك، قد تحتاج إلى إحدى الخدمات من طبقة [services] [6].

إذا تطلب الحدث بيانات خارجية:

  • في [6]، سيطلب وحدة التحكم C1 من طبقة [DAO] استردادها؛
  • في [7]، ستقوم طبقة [DAO] بإجراء مكالمة AJAX واحدة أو أكثر لاستردادها؛
  • في [8] و[9]، سيتم تعديل نموذج M1 وعرض العرض V1؛

إذا أدى الحدث إلى تغيير في العرض، في كلتا الحالتين السابقتين، بدلاً من عرض العرض V1، سيطلب وحدة التحكم C1 عنوان URL جديد [10]. هذا عنوان URL داخلي داخل المتصفح. ولا يؤدي ذلك على الفور إلى طلب HTTP إلى خادم صفحة HTML. يتم التعامل مع تغيير عنوان URL هذا بواسطة جهاز توجيه تم تكوينه بحيث يتوافق كل عنوان URL داخلي مع عرض V ووحدة التحكم C الخاصة به. ثم يعرض جهاز التوجيه العرض الجديد Vn. قبل العرض، تتولى وحدة التحكم Cn زمام الأمور، وتقوم ببناء النموذج Mn، ثم تعرض العرض Vn [11]. إذا لم يتم تخزين صفحة HTML الخاصة بالعرض Vn مؤقتًا في المتصفح، فسيتم طلبها من خادم صفحة HTML.

تشبه طبقة [العرض] في هذه البنية بنية JSF (Java Server Faces):

  • تتوافق طريقة العرض V مع طريقة العرض JSF Facelet؛
  • يتوافق وحدة التحكم C مع JSF bean، وهي فئة Java تحتوي على كل من النموذج M للعرض V ومعالجات الأحداث الخاصة به؛

تختلف طبقة [الخدمات] عن طبقات [الخدمات] التي اعتدنا عليها. في تطوير الويب من جانب الخادم، غالبًا ما يكون لدينا بنية الطبقات التالية:

في الرسم البياني أعلاه، تتواصل طبقة [الويب] مع طبقة [DAO] فقط من خلال طبقة [الأعمال]. لا شيء يمنعنا من إدخال مرجع إلى طبقة [DAO] في طبقة [الويب] لتمكين هذا التواصل. لكننا نتجنب القيام بذلك.

مع Angular، لا نقيّد أنفسنا. وبذلك تصبح البنية كما يلي:

  • في [1]، يمكن لطبقة [العرض] التواصل مباشرة مع أي خدمة؛
  • في [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]، يمكنك التبديل من الفرنسية إلى الإنجليزية؛

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]، والتي ستسمح لنا بتحديد مكتبات JavaScript التي نحتاجها؛
  • في [15]، حدد ملف [bower.json] الذي أنشأناه للتو؛
  • في [16]، أضف مكتبة JavaScript؛
  • في [17]، يتم عرض جميع مكتبات JavaScript القابلة للتنزيل؛
  • في [18]، يمكننا إدخال مصطلح لتصفية القائمة [17]. هنا، نحدد أننا نريد مكتبة [Angular JS
  • في [19]، تظهر تفاصيل المكتبة. هنا نرى أنه سيتم تنزيل الإصدار 1.2.18 من 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. سنستخدم تقويمه هنا؛
bootstrap
إطار عمل Bootstrap CSS. سيُستخدم لبناء طرق العرض؛
footable
يوفر مكونًا مرئيًا من نوع "جدول". وهو "متجاوب" بمعنى أنه يمكنه التكيف مع حجم الشاشة؛
bootstrap-select
يوفر مكون "قائمة منسدلة"؛
  • في [25]، تم تثبيت المكتبات التي تم تنزيلها في مجلد [bower_components
  • في [26]، نرى أن مكتبة jQuery قد تم تنزيلها. وذلك لأن Bootstrap يستخدمها. نظام تثبيت تبعيات JavaScript في المشروع مشابه لنظام Maven في عالم Java: إذا كانت المكتبة التي تم تنزيلها تحتوي على تبعيات خاصة بها، يتم تنزيل تلك التبعيات تلقائيًا؛

وقد تغير ملف [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é">
  <!-- on 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 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 type="text/javascript" src="bower_components/footable/dist/footable.min.js"></script>
<!-- angular js -->
<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>
  • السطران 11-12: ملفات CSS الخاصة بـ Bootstrap؛
  • السطر 13: ملف CSS لمكون [boostrap-select
  • السطر 14: ملف CSS لمكون [footable
  • السطور 21-24: ملفات JS لمكونات Bootstrap؛
  • السطر 21: مكونات Bootstrap مدعومة بواسطة jQuery؛
  • السطر 22: ملف Bootstrap JS؛
  • السطر 23: ملف JS لمكون [boostrap-select
  • السطر 24: ملف JS لمكون [footable
  • الأسطر 26–30: ملفات JS الخاصة بـ Angular والمشاريع ذات الصلة؛
  • السطر 26: ملف Angular JS. يجب تحميله بعد jQuery إذا تم استخدام تلك المكتبة؛
  • السطر 27: ملف JS لمشروع [angular-ui-bootstrap
  • السطر 28: ملف JS الخاص بموجه [angular-route
  • السطر 29: ملف JS الخاص بوحدة تدويل تطبيق Angular؛
  • السطر 30: ملف JS الخاص بوحدة [angular-base64

يمكن التحقق من صحة ملف [app-01.html]:

  • في [1]، نطلب فحص الكود؛
  • في [2]، النتيجة عندما يكون كل شيء صحيحًا؛

يُنصح بإجراء هذا الفحص المنهجي للكود قبل التنفيذ. هنا، يسمح هذا الفحص باكتشاف أي أخطاء في مراجع ملفات CSS و JS. إذا كان المسار غير صحيح، فسيقوم أداة فحص الكود بتمييزه.

  • في [3]، يمكن تحميل الصفحة في متصفح عبر أداة تصحيح الأخطاء. يتم عرض النتيجة التالية في المتصفح:
  • في [4]، تم تقديم الصفحة [app-01.html] بواسطة خادم WebStorm داخلي يعمل هنا على المنفذ 63342؛
  • في [5]، وحدة تحكم مصحح الأخطاء. إذا حدثت أي أخطاء، لكانت ظهرت هنا. هذا هو المكان الذي يتم فيه أيضًا عرض مخرجات الشاشة التي تم إنشاؤها بواسطة عبارة JavaScript [console.log(expression)]. سنستخدم هذه الميزة على نطاق واسع؛

يتيح لك وضع التصحيح تعديل الصفحة في WebStorm ورؤية نتائج تلك التغييرات في المتصفح دون الحاجة إلى إعادة تحميل الصفحة. لذا، إذا أضفنا السطر 3 أدناه:


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

وعندما نعود إلى المتصفح، نرى أن الصفحة قد تغيرت:

 

3.6. مقدمة إلى Bootstrap

سنقوم الآن بتوضيح بعض ميزات Bootstrap المستخدمة في التطبيق. لا أمتلك سوى معرفة محدودة بهذا الإطار، اكتسبتها عن طريق نسخ ولصق الأكواد الموجودة على الإنترنت. سأشرح دور فئات CSS التي أعتقد أنني أفهمها. وسأمتنع عن التعليق على البقية.

3.6.1. المثال 1

في Angular، تكون العمليات التي تجلب المعلومات من مصادر خارجية غير متزامنة. وهذا يعني أن العملية تبدأ، ثم يعود التحكم فورًا إلى العرض، مما يسمح للمستخدم بمواصلة التفاعل معه. يتم إخطار التطبيق بأن العملية قد اكتملت عبر حدث. يتم التعامل مع هذا الحدث بواسطة دالة JavaScript يمكنها بعد ذلك تحديث أو تغيير العرض الحالي. إذا كان من المحتمل أن تستغرق العملية وقتًا طويلاً، فمن المفيد منح المستخدم خيار إلغائها. سنقدم هذا الخيار بشكل منهجي. للقيام بذلك، سنستخدم لافتة 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>
  • السطر 1: تحدد فئة CSS [container] منطقة عرض داخل المتصفح؛
  • السطر 3: تعرض فئة CSS [alert] منطقة ملونة. تستخدم فئة [alert-warning] لونًا محددًا مسبقًا؛
  • السطر 5: تُصمم فئة [btn] زرًا. تمنحه فئة [btn-primary] لونًا محددًا. تضع فئة [pull-right] الزر على يمين لافتة التنبيه؛
  • السطر 6: صورة تحميل متحركة؛

3.6.2. المثال 2

ستكون للعروض المختلفة للتطبيق عنوان مشترك:

Image

لتحقيق ذلك، نقوم بنسخ [app-01.html] إلى [app-03.html] ونعدل الأسطر التالية:


<div class="container">
  <h1>Rdvmedecins - v1</h1>
  <!-- Bootstrap Jumbotron -->
  <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] في السطر 4؛
  • السطر 5: تحدد فئة [row] صفًا مكونًا من 12 عمودًا؛
  • السطر 6: تحدد فئة [col-md-2] منطقة من عمودين داخل الصف؛
  • السطر 7: يتم وضع صورة في هذين العمودين؛
  • الأسطر 9-11: يتم وضع النص في الأعمدة العشرة المتبقية؛

3.6.3. المثال 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">
          <!-- debug mode -->
          <label style="width: 100px">
            <input type="checkbox">
            <span style="color: white">Debug</span>
          </label>
          <!-- identification form -->
          <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>
  • السطر 4: تُستخدم فئة [navbar] لتصميم شريط التنقل. أما فئة [navbar-inverse] فتمنحه خلفية سوداء. وتضمن فئة [navbar-fixed-top] بقاء شريط التنقل في أعلى الشاشة عند تمرير الصفحة المعروضة في المتصفح؛
  • الأسطر 6–14: تحدد المنطقة [1]. عادةً ما تكون هذه سلسلة من الفئات التي لا أفهمها. أستخدم المكون كما هو؛
  • السطر 15: يحدد منطقة "متجاوبة" في شريط التنقل. على الهاتف الذكي، تنكمش هذه المنطقة لتصبح منطقة قائمة؛
  • السطر 16: تضم فئة [navbar-form] نموذجًا في شريط الأوامر. وتضع فئة [navbar-right] هذا النموذج على يمين النموذج؛
  • الأسطر 23–32: حقول الإدخال الأربعة للنموذج من السطر 17 [3]. وهي موجودة داخل فئة [form-group] التي تغلف عناصر النموذج، ولكل منها فئة [form-control
  • السطر 33: فئة [btn] التي رأيناها بالفعل، معززة بفئة [btn-success]، التي تمنحها لونها الأخضر؛

3.6.4. المثال 4

سيسمح لك شريط التحكم بتغيير اللغة باستخدام قائمة منسدلة:

Image

لتحقيق ذلك، نقوم بنسخ [app-01.html] إلى [app-05.html] وإضافة الأسطر التالية إلى شريط التحكم:


          <button class="btn btn-success">
            Connexion
          </button>
          <!-- languages -->
          <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>

السطور المضافة هي السطور من 4 إلى 21.

  • السطر 5: تضم فئة [btn-group] مجموعة من الأزرار. يوجد اثنان منها في السطرين 6 و9؛
  • الأسطر 6–8: يحدد الزر الأول تسمية القائمة المنسدلة. تمنحه فئة [btn-danger] لونًا أحمر؛
  • الأسطر 9-12: الزر الثاني هو زر القائمة المنسدلة. يتم وضعه بجوار الزر الأول، مما يعطي انطباعًا بوجود مكون واحد؛
  • السطر 10: يعرض السهم لأسفل للإشارة إلى أن الزر عبارة عن قائمة منسدلة؛
  • السطر 11: لقارئات الشاشة؛
  • الأسطر 13-20: العناصر الموجودة في القائمة المنسدلة هي عناصر قائمة غير مرتبة؛

3.6.5. المثال 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>
      <!-- menu options -->
      <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>
        <!-- right buttons -->
        <form class="navbar-form navbar-right" role="form">
...
        </form>
      </div>
    </div>
  </div>
</div>
  • يتم إنشاء خيارات القائمة بواسطة الأسطر 8–29. وهذه هي مرة أخرى عناصر قائمة <ul>. تجعل الفئة [active] النص تحت خط، مما يشير إلى أن الخيار قابل للنقر.

3.6.6. المثال 6

سنعرض الأطباء والعملاء في قوائم منسدلة كما هو موضح أدناه:

 

القائمة المنسدلة المستخدمة ليست مكونًا أصليًا في Bootstrap. إنها مكون [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 core JavaScript ================================================== -->
...
<script type="text/javascript" src="bower_components/bootstrap-select/bootstrap-select.min.js"></script>
<!-- local script -->
<script>
  $('.selectpicker').selectpicker();
</script>
</body>
</html>
  • السطر 5: يجب استيراد ورقة الأنماط [bootstrap-select
  • السطر 13: يتم استخدام السمة [data-style] بواسطة [bootstrap-select]. وهي تُستخدم لتصميم القائمة المنسدلة. هنا، نمنحها مظهر زر أزرق [btn-primary
  • السطر 13: تُستخدم السمة [class] في السطر 23. يمكن أن تكون أي شيء؛
  • الأسطر 14–17: عناصر القائمة المنسدلة. هذه علامات HTML قياسية؛
  • السطر 22: يجب استيراد JS [bootstrap-select
  • الأسطر 24-26: نص برمجي JavaScript يتم تنفيذه عند انتهاء تحميل الصفحة؛
  • السطر 25: عبارة jQuery. نطبق طريقة [selectpicker] (selectpicker()) على جميع العناصر التي تحتوي على فئة [selectpicker] ($('.selectpicker')). هناك عنصر واحد فقط: علامة <select> في السطر 13. تأتي طريقة [selectpicker] من ملف JS المشار إليه في السطر 22؛

3.6.7. المثال 7

لعرض جدول مواعيد الطبيب، سنستخدم جدولًا متجاوبًا مقدمًا من مكتبة [footable] JS:

  • في [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>
  • السطران 2 و 60 موجودان بالفعل في [app-01.html]. هذان هما ملفا CSS و JS المقدمان من مكتبة [footable
  • يشير السطر 3 إلى ملف CSS التالي:

@CHARSET "UTF-8";
 
#creneaux th {
    text-align: center;
}
 
#creneaux td {
    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] الموجود على موقع المكتبة.

  • السطر 8: يضع الجدول في صف [row] ومربع تنبيه ملون [alert alert-warning
  • السطر 9: سيمتد الجدول على 6 أعمدة [col-md-6
  • السطر 10: يتم تنسيق جدول HTML بواسطة Bootstrap [class='table'];
  • السطر 13: تحدد السمة [data-toggle] العمود الذي يحتوي على رمز [+/-] الذي يوسع/يطوي الصف؛
  • السطر 19: تحدد السمة [data-hide='phone'] أنه يجب إخفاء العمود إذا كان حجم الشاشة بحجم شاشة الهاتف. يمكن أيضًا استخدام القيمة 'tablet

3.6.8. المثال 8

لمساعدة المستخدم، سننشئ تلميحات حول المكونات الرئيسية للعروض:

لتحقيق ذلك، نقوم بنسخ [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>
      <!-- menu options -->
      <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 core JavaScript ================================================== -->
<...
<script type="text/javascript" src="bower_components/angular-ui-bootstrap-bower/ui-bootstrap-tpls.min.js"></script>
<!-- local script -->
<script>
  // --------------------- module Angular
  angular.module("rdvmedecins", ['ui.bootstrap']);
</script>
</body>
</html>

يتم توفير تلميحات الأدوات بواسطة مكتبة [angular-ui-bootstrap]، التي تعتمد بدورها على مكتبة [angular]. يستورد السطر 50 مكتبة [angular-ui-bootstrap]. لتنفيذ مكونات مكتبة [angular-ui-bootstrap]، نحتاج إلى إنشاء وحدة Angular. ويتم ذلك في الأسطر 52–55. تحدد هذه الأسطر وحدة Angular باسم [rdvmedecins] (المعلمة الأولى). يمكن لوحدة Angular استخدام وحدات Angular أخرى. وتسمى هذه الوحدات تبعيات الوحدة. يتم توفيرها في صفيف كمعلمة ثانية لدالة [angular.module]. هنا، يتم توفير الوحدة المسماة [ui.bootstrap] بواسطة مكتبة [angular-ui-bootstrap]. ستوفر لنا هذه الوحدة تلميحات الأدوات.

يحدد السطر 54 وحدة Angular. بشكل افتراضي، لا يؤثر هذا على الصفحة. نحدد أن الصفحة يجب أن تدار بواسطة Angular من خلال ربطها بوحدة Angular. وهذا ما يتم في السطر 2. تربط السمة [ng-app='rdvmedecins'] الصفحة بالوحدة التي تم إنشاؤها في السطر 54. ثم سيتم تحليل الصفحة بواسطة Angular. وسيتم اكتشاف سمات [tooltip] ومعالجتها بواسطة وحدة [ui.bootstrap].

صيغة نص التلميح هي كما يلي:


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

فيما سبق، أضفنا تلميحًا إلى النص [الصفحة الرئيسية]:

  • [tooltip]: يحدد نص تلميح الأداة؛
  • [tooltip-placement]: يحدد موضعه (أسفل، أعلى، يسار، يمين)؛

يتيح لك Angular JS إضافة علامات أو سمات جديدة إلى تلك الموجودة بالفعل في HTML. يتم تحقيق هذا التوسيع لـ HTML باستخدام توجيهات Angular. هنا، يتم إنشاء السمتين [tooltip] و [tooltip-placement] بواسطة [angular-ui-bootstrap].

3.6.9. مثال 9

لمساعدة المستخدم على اختيار تاريخ الموعد، سنوفر تقويمًا:

Image

كما هو الحال مع تلميحات الأدوات، يتم توفير هذا التقويم بواسطة مكتبة [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>
...
<!-- local script -->
<script>
  // --------------------- module Angular
  angular.module("rdvmedecins", ['ui.bootstrap'])
</script>
 
</body>
</html>

كما في السابق، ترتبط الصفحة بوحدة Angular (السطران 2 و28). يتم تعريف التقويم بواسطة علامة <datepicker> في السطر 16، التي توفرها مكتبة [angular-ui-bootstrap]:

  • [show-weeks='true']: لعرض أرقام الأسابيع؛
  • [class='well']: لإحاطة التقويم بمربع رمادي بزوايا مستديرة؛
  • [ng-model='day']: سمات [ng-*] هي سمات Angular. تحدد السمة [ng-model] البيانات التي سيتم وضعها في نموذج العرض. عندما ينقر المستخدم على تاريخ، سيتم وضعه في متغير [day] في النموذج. يتم استخدام هذا المتغير في السطر 10. تقوم صيغة {{expression}} بتقييم تعبير مكون من عناصر من النموذج. هنا، سيعرض {{day}} قيمة متغير [day] من النموذج. تتمثل إحدى الميزات الرئيسية لـ Angular في أن العرض يتم تحديثه تلقائيًا استجابةً للتغييرات في متغير [day]. وبالتالي، عندما يغير المستخدم التواريخ، سيتم عرض هذه التغييرات على الفور في السطر 10. بشكل عام، تعمل العملية على النحو التالي:
    • ترتبط طريقة العرض V بنموذج M؛
    • يراقب Angular النموذج M ويقوم تلقائيًا بتحديث العرض V عند حدوث تغيير في النموذج M؛

يُطلق على بناء الجملة {{day|date}} اسم مرشح. لا يتم عرض قيمة [day]، بل قيمة [day] التي تمت تصفيتها من خلال مرشح يُسمى [date]. هذا المرشح محدد مسبقًا في Angular. ويُستخدم لتنسيق التواريخ. ويقبل المعلمات التي تحدد التنسيق المطلوب. وبالتالي، يشير التعبير {{day | date:'fullDate'}} إلى أننا نريد تنسيق التاريخ الكامل، هنا [Friday, June 20, 2014]، لأن التقويم باللغة الإنجليزية بشكل افتراضي. سنناقش تدويله قريبًا.

3.6.10. خلاصة

لقد قدمنا عناصر إطار عمل Bootstrap CSS التي سنستخدمها. كانت هذه مكونات سلبية: لم يتم التعامل مع أحداثها. لذا فإن النقر على الأزرار أو الروابط لم يؤدِ إلى أي شيء. سيتم التعامل مع هذه الأحداث في JavaScript. من الممكن استخدام هذه اللغة بدون أطر عمل، ولكن كما كان الحال على جانب الخادم، فإن بعض أطر العمل ضرورية على جانب العميل. هذا هو الحال مع إطار عمل AngularJS، الذي يقدم نهجًا جديدًا لتطوير تطبيقات JavaScript التي يتم تشغيلها بواسطة متصفح. سنقدمه الآن.

3.7. مقدمة إلى Angular JS

سنقوم الآن بتوضيح بعض ميزات إطار عمل Angular JS المستخدم في التطبيق. وقد تعرفنا بالفعل على بعضها:

  • تُشغَّل صفحة HTML بواسطة Angular JS إذا كانت هناك وحدة مدمجة بها:

<html ng-app="rdvmedecins">
  • يتيح لك Angular إنشاء علامات وسمات HTML جديدة باستخدام التوجيهات:
attributs : ng-app, ng-model, tooltip-placement, tooltip
balises : datepicker
  • يتيح لك Angular إنشاء عوامل تصفية:
{{jour|date:'fullDate'}}
  • تعرض طريقة العرض V النموذج M. يراقب Angular النموذج M ويقوم تلقائيًا بتحديث طريقة العرض V كلما حدث تغيير في النموذج M. يتم عرض قيمة المتغير في النموذج M في طريقة العرض V باستخدام:
{{variable}}

سنبدأ بالتعمق في تنفيذ نمط تصميم النموذج-العرض-المحرك (Model–View–Controller) في Angular. دعونا نستعرض العلاقات بينها من منظور معماري:

  • تعرض طريقة العرض V1 النموذج M1 الذي أنشأته وحدة التحكم C1. لا تحتوي وحدة التحكم C1 على النموذج M1 فحسب، بل تحتوي أيضًا على معالجات الأحداث لطريقة العرض V1. نحن في الدورة 5 و8 و9:
    • [5]: يحدث حدث في العرض V1. يتم معالجته بواسطة وحدة التحكم C1؛
    • تقوم وحدة التحكم بمهمتها [6-7] ثم تبني النموذج M1 [8]؛
    • [9]: تعرض طريقة العرض V1 النموذج الجديد M1. كما ذكرنا، هذه الخطوة الأخيرة تتم تلقائيًا. على عكس أطر عمل MVC الأخرى، لا يوجد دفع صريح (تقوم C1 بدفع النموذج M1 إلى V1) أو سحب صريح (تقوم طريقة العرض V1 بجلب النموذج M1 من C1). هناك دفع ضمني لا يراه المطور؛
    • ثم يستأنف الدورة 5، 8، 9؛

3.7.1. المثال 1: نموذج MVC في Angular

لنعد إلى مثال التقويم. لقد رأينا التوجيه الذي يولده:


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

يدعم هذا التوجيه سمات أخرى إلى جانب تلك الموضحة أعلاه، بما في ذلك السمة [min-date]، التي تحدد أقرب تاريخ يمكن تحديده في التقويم. سيكون هذا مفيدًا لنا. عندما يحدد المستخدم تاريخ موعد، يجب أن يكون هذا التاريخ مساويًا للتاريخ الحالي أو لاحقًا له. لذلك سنكتب:


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

حيث ستكون [dateMin] متغيرًا في نموذج الصفحة بقيمة تساوي تاريخ اليوم. سيؤدي ذلك إلى ظهور الصفحة التالية:

  • في [1]، التاريخ هو 19 يونيو 2014. يشير المؤشر إلى أنه يمكن تحديد 19 يونيو؛
  • في [2]، يشير المؤشر إلى أنه لا يمكن تحديد 18 يونيو؛

نقوم بنسخ [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 core JavaScript ================================================== -->
...
<!-- local script -->
<script>
  // --------------------- module Angular
  angular.module("rdvmedecins", ['ui.bootstrap']);
  // contrôleur
  angular.module("rdvmedecins")
    .controller('rdvMedecinsCtrl', ['$scope',
      function ($scope) {
        // date minimale
        $scope.minDate = new Date();
      }]);
 
</script>
 
</body>
</html>

دعونا أولاً ندرس البرنامج النصي المحلي في الأسطر 26–37:

  • السطر 28: إنشاء وحدة [rdvmedecins] مع اعتمادها على وحدة [ui.bootstrap]، التي توفر التقويم؛
  • الأسطر 30-35: إنشاء وحدة تحكم. هذا هو ما سيحتوي على نموذج صفحتنا. لن يكون هناك معالج أحداث هنا؛
  • السطور 30–31: وحدة التحكم [rdvMedecinsCtrl] تنتمي إلى الوحدة النمطية [rdvmedecins]. يمكنك إضافة أي عدد تريده من وحدات التحكم إلى الوحدة النمطية. في تطبيقنا، سيكون لدينا:
    • وحدة نمطية لإدارة التطبيق؛
    • وحدة تحكم واحدة لكل عرض؛
  • المعلمة الثانية لوظيفة [controller] هي مصفوفة على شكل ['O1', 'O2', ..., 'On', function(O1, O2, ..., On)]. المعلمة الأخيرة هي الوظيفة التي تنفذ وحدة التحكم. معلماتها هي كائنات سيوفرها AngularJS للوظيفة.

لنعد إلى بنية تطبيق Angular:

في الأعلى، تحتوي وحدة التحكم C1 على جميع معالجات الأحداث الخاصة بعرض V1 بالإضافة إلى نموذج M1 الخاص به. قد تتطلب معالجات الأحداث خدمة واحدة أو أكثر [6] لأداء مهامها. نمرر كل هذه العناصر كمعلمات إلى دالة منشئ وحدة التحكم:

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

خدمات Si هي خدمات فردية. يقوم Angular بإنشاء مثيل واحد لكل منها. يتم تحديدها بواسطة اسم Si. لماذا تظهر مرتين في الجدول أعلاه؟ في مرحلة الإنتاج، يتم تصغير حجم نصوص JS. خلال عملية التصغير هذه، يصبح الجدول أعلاه كما يلي:

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

تفقد المعلمات أسماءها. ومع ذلك، فهذه هي أسماء الخدمات. لذلك من المهم الحفاظ على هذه الأسماء. وهذا هو السبب في تمريرها كسلاسل نصية كمعلمات تسبق الدالة. لا يتم تغيير السلاسل النصية أثناء عملية التصغير. عندما يقوم Angular بإنشاء وحدة التحكم باستخدام المصفوفة الجديدة، سيستبدل a1 بـ S1، و a2 بـ S2، وهكذا. لذلك فإن ترتيب المعلمات مهم. يجب أن يتطابق مع ترتيب الخدمات التي تسبق تعريف الدالة.

لنعد إلى تعريف وحدة التحكم [rdvMedecinsCtrl]:


  // controller
  angular.module("rdvmedecins")
    .controller('rdvMedecinsCtrl', ['$scope',
      function ($scope) {
        // minimum date
        $scope.minDate = new Date();
}]);
  • السطران 3-4: الكائن الوحيد الذي يتم إدخاله في وحدة التحكم هو كائن $scope. هذا كائن محدد مسبقًا يمثل نموذج M للعروض المرتبطة بوحدة التحكم. لإثراء نموذج العرض، ما عليك سوى إضافة حقول إلى كائن $scope؛
  • وهذا ما تم في السطر 6. نقوم بإنشاء الحقل [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>
...
  • السطر 1: يرتبط نص الصفحة بوحدة التحكم [rdvMedecinsCtrl] عبر السمة [ng-controller]. وهذا يعني أن كل ما يقع داخل العلامة <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] (السطور 3-4) النموذج M11 الذي يديره وحدة التحكم c11 بالإضافة إلى النموذج M1. هناك وراثة للنماذج. يمكن أن تشير العلامات في هذه المنطقة إلى كل من معالجات الأحداث من وحدة التحكم c11 ومعالجات الأحداث من وحدة التحكم c1. ولا يمكنها الإشارة إلى نموذج M12 من وحدة التحكم c12 أو معالجات الأحداث الخاصة به. لم يتم تعريف وحدة التحكم c12 بين الأسطر 3-5؛
  • الأسطر 7-9: يمكننا تطبيق نفس المنطق المستخدم سابقًا؛

لنعد إلى كود التقويم:


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

يتم تهيئة السمة [min-date] بقيمة [minDate] من النموذج. ضمنيًا، هذه هي [$scope.minDate]. يتم البحث عن الحقل دائمًا في كائن $scope.

3.7.2. مثال 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">
    <!-- the calendar-->
    <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>
    <!-- languages -->
    <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:

  • السطر 27: السمة [ng-click] هي سمة Angular تحدد معالج الأحداث الذي سيتم تنفيذه عند النقر على العنصر الذي يحتوي على هذه السمة. هنا، سيتم تنفيذ الدالة [$scope.setLang('fr')]. ستقوم بتعيين التقويم إلى اللغة الفرنسية؛
  • السطر 28: هنا، نضبط التقويم على اللغة الإنجليزية؛
  • السطر 35: نظرًا لأن جافا سكريبت وحدة التحكم كبير جدًا، فإننا نضعه في ملف باسم [rdvmedecins.js

تدير Angular ترجمة العرض باستخدام وحدة نمطية تسمى [ngLocale]. وبالتالي، سيكون تعريف وحدة [rdvmedecins] النمطية كما يلي:


  // --------------------- Angular module
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale']);

السطر 2: لا تنسَ التبعيات، حيث إن رسائل الخطأ في 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;}
});
}]);

فيما يلي العناصر المستخدمة لإنشاء تقويم باللغة الفرنسية:

  • الأسطر 10–18: مصفوفة أيام الأسبوع؛
  • السطور 19–32: مصفوفة أشهر السنة؛
  • الأسطر 33–41: الجدول المختصر لأيام الأسبوع؛
  • الأسطر 42–55: جدول أشهر السنة المختصرة؛
  • الأسطر 56–63: تنسيقات التاريخ والوقت. تظهر السطر 62 التنسيق 'dd/mm/yy' المستخدم للتواريخ باللغة الفرنسية؛
  • الأسطر 65–95: معلومات عن تنسيق الأرقام. هذا غير ذي صلة هنا؛
  • السطر 96: المعرف «fr-fr» لإعدادات اللغة للملف (fr-fr: الفرنسية من فرنسا، fr-ca: الفرنسية من كندا، ...)

في الملف [angular-locale_en-us.js]، لدينا نفس الشيء تمامًا، ولكن هذه المرة للغة الإنجليزية الأمريكية (en-us).

الرمز أعلاه ليس سهل القراءة. إذا قرأته بعناية، سترى أن كل هذا الرمز يحدد المتغير [$locale] في السطر 4. ومن خلال تغيير قيمة هذا المتغير، نحقق تدويل التواريخ والأرقام والعملات، إلخ. ومن الغريب أن 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]. الفكرة هي وضع الإعدادات المحلية المختلفة التي نحتاجها في قاموس. ومن هناك سنستردها عندما نحتاج إلى تغييرها. يتكون كود JavaScript في [rdvmedecins.js] من البنية التالية:

 

إذا أزلنا تعريفات الإعدادات المحلية، التي تشغل 200 سطر (السطور 15–215 أعلاه)، يصبح الكود بسيطًا:

  • السطر 6: يحدد الوحدة النمطية [rdvmedecins] وتبعياتها؛
  • الأسطر 8–10: تحدد وحدة التحكم [rdvMedecinsCtrl] للصفحة؛
  • السطر 9: يأخذ منشئ وحدة التحكم معلمتين:
    • $scope: لإنشاء قالب العرض؛
    • $locale: وهي المتغير الذي يتحكم في الإعدادات الإقليمية للتقويم. وهذا هو المتغير الذي عليك تغييره عند التبديل بين اللغات؛
  • السطر 13: يتم تهيئة متغير النموذج [minDate] بتاريخ اليوم؛
  • السطر 15: يُعرّف قاموس [locales]. لاحظ أننا لم نكتب [$scope.locales]. المتغير [locales] ليس جزءًا من النموذج المعروض على العرض؛
  • الأسطر 15–215: تحدد قاموس {'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]، على التوالي. الجزء الأصعب هو عدم ارتكاب خطأ في الأقواس العديدة الموجودة في هذا القاموس...
  • السطر 217: نقوم بتهيئة المتغير $locale بقيمة locales['fr'], أي النسخة الفرنسية من الإعدادات المحلية. لا يمكننا ببساطة كتابة [$locale=locales['fr']] لأن ذلك سيقوم بتعيين عنوان locales['fr'] إلى $locale. يجب علينا إجراء نسخ للقيمة. يمكن القيام بذلك باستخدام الدالة المحددة مسبقًا [angular.copy
  • السطر 219: يتم تهيئة المتغير [day] في النموذج بتاريخ اليوم. وهذا يضمن عرض التقويم مع تعيين التاريخ على اليوم؛
  • الأسطر 223–230: تحدد معالج الأحداث الذي يتم استدعاؤه عند تغيير اللغة. لاحظ بناء الجملة:
$scope.nom_fonction=function(param1, param2, ...){...}

لتعريف معالج الأحداث المسمى [function_name] الذي يقبل المعلمات [param1, param2, ...]؛

دعونا نراجع كود HTML الخاص بالقائمة المنسدلة:


    <!-- languages -->
    <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>
  • السطر 8: يؤدي اختيار اللغة الفرنسية إلى استدعاء [setLang('fr')]؛
  • السطر 9: يؤدي اختيار اللغة الإنجليزية إلى استدعاء [setLang('en')]؛
  • السطر 3: السمة [is-open] هي قيمة منطقية تحدد ما إذا كانت القائمة المنسدلة مفتوحة (true) أم مغلقة (false). يتم تهيئتها باستخدام المتغير [isopen] من نموذج العرض؛

لنعد إلى الكود في [rdvmedecins.js]:

  • السطر 225: نغير قيمة المتغير [$locale] إلى القيمة المناسبة من قاموس [locales
  • السطر 227: ذكرنا أنه عندما يتغير النموذج M لعرض V، يتم تحديث العرض V تلقائيًا بالنموذج الجديد. في السطر 225، قمنا بتغيير قيمة المتغير [$locale]، الذي لا يشكل جزءًا من النموذج M المعروض بواسطة العرض V. نحتاج إلى إيجاد طريقة لتحديث هذا النموذج M بحيث يتم تحديث التقويم واستخدام الإعدادات المحلية الجديدة. هنا، نغير متغير [day] في نموذج التقويم. نقوم بتهيئته بمؤشر جديد (new) يشير إلى تاريخ مطابق للتاريخ المعروض حاليًا. [$scope.day.getTime()] هو عدد المللي ثانية التي انقضت بين 1 يناير 1970 والتاريخ الذي يعرضه التقويم. باستخدام هذا الرقم، نعيد بناء تاريخ جديد. وبالطبع، سنحصل على نفس التاريخ، وسيظل التقويم في الموضع الذي كان يعرضه. لكن قيمة [$scope.day]، التي هي في الواقع مؤشر، ستكون قد تغيرت، وسيتم تحديث التقويم؛
  • السطر 229: نضبط قيمة المتغير [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>

في السطر 1 أعلاه، ستتغير السمة [is-open] إلى false، مما سيؤدي إلى إغلاق القائمة المنسدلة.

3.7.3. المثال 3: تدويل النص

دعونا نعود إلى توطين التقويم:

في [3]، نرى أن التقويم باللغة الإنجليزية، لكن نصوص [Calendar, Languages] ليست كذلك. بشكل افتراضي، لا يوفر Angular أداة لتدويل الرسائل. هنا، سنستخدم مكتبة [angular-translate] (https://github.com/angular-translate/angular-translate).

سنقوم بتطوير المثال التالي:

  • في [1]، النص باللغة الفرنسية؛
  • في [2]، العرض باللغة الإنجليزية؛

لنلقِ نظرة على التكوين المطلوب للتدويل. تم تعديل البرنامج النصي [rdvmedecins.js] على النحو التالي:


  // --------------------- Angular module
angular.module("rdvmedecins", ['ui.bootstrap', 'ngLocale', 'pascalprecht.translate']);
// configuration i18n
angular.module("rdvmedecins")
  .config(['$translateProvider', function ($translateProvider) {
    // messages français
    $translateProvider.translations("fr", {
      'msg_header': 'Cabinet Médical<br/>Les Médecins Associés',
      'msg_langues': 'Langues',
      'msg_agenda': 'Agenda de {{titre}} {{prenom}} {{nom}}<br/>le {{jour}}',
      'msg_calendrier': 'Calendrier',
      'msg_jour': 'Jour sélectionné : ',
      'msg_meteo': "Aujourd'hui, il va pleuvoir..."
    });
    // messages anglais
    $translateProvider.translations("en", {
      'msg_header': 'The Associated Doctors',
      'msg_langues': 'Languages',
      'msg_agenda': "{{titre}} {{prenom}} {{nom}}'s Diary<br/> on {{jour}}",
      'msg_calendrier': 'Calendar',
      'msg_jour': 'Selected day: ',
      'msg_meteo': 'Today, it will be raining...'
    });
    // langue par défaut
    $translateProvider.preferredLanguage("fr");
}]);
  • السطر 2: التغيير الأول هو إضافة تبعية جديدة. تتطلب تدويل التطبيق وحدة Angular [pascalprecht.translate
  • السطور 5-26: تعريف وظيفة [config] لوحدة [rdvmedecins]. عند بدء تشغيل تطبيق Angular، يقوم إطار العمل بإنشاء مثيلات لجميع الخدمات التي يتطلبها التطبيق، بما في ذلك خدمات Angular المحددة مسبقًا والخدمات المحددة من قبل المستخدم. في الوقت الحالي، لم نحدد أي خدمات. يتم تنفيذ دالة [config] الخاصة بوحدة التطبيق قبل إنشاء أي خدمة. يمكن استخدامها لتحديد معلومات التكوين للخدمات التي سيتم إنشاؤها لاحقًا. هنا، ستُستخدم دالة [config] لتحديد الرسائل المُترجمة للتطبيق؛
  • السطر 5: المعلمة الخاصة بوظيفة [config] هي مصفوفة ['O1', 'O2', ..., 'On', function(O1, O2, ..., On)] حيث Oi هو كائن معروف مقدم من Angular. هنا، يتم توفير الكائن [$translateProvider] بواسطة الوحدة النمطية [pascalprecht.translate]. [function] هي الدالة التي يتم تنفيذها لتكوين التطبيق؛
  • الأسطر 7–14: تأخذ الدالة [$translateProvider.translations] معلمتين:
    • المعلمة الأولى هي مفتاح اللغة. يمكنك استخدام أي اسم تريده. هنا، استخدمنا "fr" للترجمات الفرنسية (السطر 7) و"en" للترجمات الإنجليزية (السطر 16)،
    • والمعلمة الثانية هي قائمة الترجمات في شكل قاموس {'key1':'msg1', 'key2':'msg2', ...
  • الأسطر 7–14: الرسائل باللغة الفرنسية؛
  • الأسطر 16–23: الرسائل الإنجليزية؛
  • السطر 25: تحدد طريقة [preferredLanguage] اللغة الافتراضية. المعلمة الخاصة بها هي إحدى الحجج المستخدمة كمعلمة أولى لدالة [$translateProvider.translations]، لذا فهي هنا إما 'fr' (السطر 7) أو 'en' (السطر 16)؛
  • لاحظ أن هناك ثلاثة أنواع من الرسائل:
    • الرسائل التي لا تحتوي على معلمات أو عناصر HTML (الأسطر 9 و11 و12، ...)،
    • الرسائل التي تحتوي على عناصر HTML (الأسطر 8، 10، ...)،
    • الرسائل التي تحتوي على معلمات (الأسطر 10، 19)؛

نقوم الآن بنسخ [app-11.html] إلى [app-12.html] وإجراء التغييرات التالية:


<div class="container">
  <!-- a first text with HTML elements in it -->
  <h3 class="alert alert-info" translate="{{'msg_header'}}"></h3>
  <!-- a second text with parameters -->
  <h3 class="alert alert-warning" translate="{{msg.text}}" translate-values="{{msg.model}}"></h3>
  <!-- a third text translated by the controller -->
  <h3 class="alert alert-danger">{{msg2}}</h3>
 
  <pre>{{'msg_jour'|translate}}<em>{{jour | date:'fullDate' }}</em></pre>
  <div class="row">
    <!-- the calendar-->
    <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>
    <!-- languages -->
    <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>
  • تحدث الترجمات في الأسطر 3 و 5 و 9 و 13 و 23؛
  • وهناك ثلاث صيغ مختلفة:
    • صيغة [translate={{'msg_key'}}] (السطر 3)، حيث [msg_key] هي إحدى المفاتيح في قاموس الترجمة. هذه الصيغة مناسبة للرسائل التي تحتوي على عناصر HTML أو لا تحتوي عليها، ولكنها غير مناسبة للرسائل التي تحتوي على معلمات؛
    • الصيغة [translate={{'msg_key'}} translate-values={{dictionary]}}] (السطر 5)، وهي مناسبة للرسائل التي تحتوي على عناصر HTML أو لا تحتوي عليها وتحتوي على معلمات؛
    • الصيغة [{{'msg_key'|translate}}] (السطر 9 و13 و23) مناسبة للرسائل التي لا تحتوي على معلمات ولا عناصر HTML؛

دعونا نلقي نظرة على الرسائل المختلفة في هذا العرض:

السطر
الفرنسية
الإنجليزية
3
عيادة طبية<br/>The Associated Doctors
الأطباء المتعاونون
13
التقويم
التقويم
23
اللغات
اللغات
9
اليوم المحدد:
اليوم المحدد:

لننظر الآن إلى السطر 5:


<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

  • السطر 245: تعريف الكائن [msg]؛
  • السطر 245: يحتوي الحقل [text] على القيمة [msg_agenda]، والتي ترتبط بقيمتين:
    • {{title}} {{first_name}} {{last_name}}'s Diary<br/>on {{day}} في القاموس الفرنسي؛
    • {{title}} يوميات {{first_name}} {{last_name}}<br/> في {{day}} في القاموس الإنجليزي؛

وبالتالي، فإن الرسالة المراد عرضها تحتوي على أربعة معلمات [title, first_name, last_name, day]؛

  • السطر 245: حقل [model] هو قاموس يعين قيمًا لهذه المعلمات الأربع. هناك مشكلة في المعلمة [day]. نريد عرض الاسم الكامل لليوم. وهذا يختلف حسب ما إذا كانت اللغة هي الفرنسية أو الإنجليزية. لذلك نستخدم مرشح [date]، الذي تم استخدامه بالفعل في العرض، بالشكل {{ day | date:'fullDate'}}. يمكن استخدام أي مرشح في كود JavaScript بالشكل $filter('filter')(value, optionsحيث $filter هو كائن Angular محدد مسبقًا و'filter' هو اسم المرشح؛
  • السطران 33-34: يتم تمرير كائن $filter المحدد مسبقًا كمعلمة إلى وحدة التحكم، مما يسمح باستخدامه في السطر 245؛

لنعد إلى سطر آخر في العرض المعروض:


  <!-- un troisième texte traduit par le contrôleur -->
<h3 class="alert alert-danger">{{msg2}}</h3>

تم إجراء جميع الترجمات السابقة في العرض باستخدام سمات من وحدة [pascalprecht.translate]. يمكننا أيضًا اختيار إجراء هذه الترجمة على جانب الخادم. وهذا ما يتم هنا. في وحدة التحكم (السطر 247 في لقطة الشاشة أعلاه)، لدينا الكود التالي:


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

نستخدم نفس صيغة مرشح "date" لأن "translate" هو أيضًا مرشح. هنا، نطلب الرسالة ذات المفتاح "msg_meteo".

دعونا ندرس آلية تغيير اللغة. لقد رأينا أن الدالة [config] في الوحدة النمطية [rdvmedecins] قد حددت اللغة الفرنسية كلغة افتراضية (السطر 9 أدناه):


// i18n configuration
angular.module("rdvmedecins")
  .config(['$translateProvider', function ($translateProvider) {
    // french messages
    $translateProvider.translations("fr", {...});
    // english messages
    $translateProvider.translations("en", {...});
    // default language
    $translateProvider.preferredLanguage("fr");
}]);

لاحظ أن الإعدادات المحلية الافتراضية كانت باللغة الفرنسية أيضًا. في تهيئة وحدة التحكم [rdvmedecins]، كتبنا:


// we put the locale in French
angular.copy(locales['fr'], $locale);
  • السطر 2: [locales] هو قاموس أنشأناه؛

لا توجد صلة بين تدويل الرسائل الذي توفره وحدة [pascalprecht.translate] وتوطين التاريخ الذي قمنا بتنفيذه. يستخدم هذا الأخير متغير $locale الذي لا تستخدمه وحدة [pascalprecht.translate]. هاتان عمليتان مستقلتان عن بعضهما البعض.

حان الوقت الآن لنرى ما يحدث عندما يغير المستخدم اللغة:

Image

  • السطر 251: عندما تتغير اللغة، يتم استدعاء الدالة [setLang] بأحد المعلمتين ['fr'، 'en'];
  • الأسطر 252–257: تم شرحها بالفعل — فهي تغير متغير [$locale] الخاص بالتقويم. وهذا لا يؤثر على لغة الترجمات؛
  • السطر 259: نغير لغة الترجمة. نستخدم الكائن [$translate] المقدم من الوحدة النمطية [pascalprecht.translate]. للقيام بذلك، نحتاج إلى إدخاله في وحدة التحكم:

// controller
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', '$locale', '$translate', '$filter',
function ($scope, $locale, $translate, $filter) {

في السطرين 3 و 4 أعلاه، يتم إدراج الكائن $translate؛

  • يجب تعيين المعلمة lang للدالة [$translate.use(lang)] إلى أحد المفاتيح المستخدمة في التكوين كمعلمة أولى للدالة [$translateProvider.translations]، أي إما 'fr' أو 'en'. وهذا هو الحال بالفعل؛
  • السطر 261: نعيد حساب قيمة msg2. لماذا؟ في العرض، بعد تغيير اللغة الذي تم في السطر 259، سيتم إعادة تقييم جميع سمات [translate] الموجودة. لن يكون هذا هو الحال بالنسبة للتعبير {{msg2}}، الذي لا يحتوي على هذه السمة. لذلك، يتم حساب قيمته الجديدة في وحدة التحكم. يجب أن يتم ذلك بعد تغيير اللغة في السطر 259 حتى يتم استخدام اللغة الجديدة لحساب [msg2

إذا توقفنا عند هذا الحد، نلاحظ حالتين شاذتين:

  1. في [1]، يظل اليوم باللغة الفرنسية بينما بقية العرض باللغة الإنجليزية؛
  2. في [2] و[3]، التاريخ المحدد هو 24 يونيو، بينما في [1]، يظل التاريخ محددًا بـ 20 يونيو؛

دعونا نحاول شرح هذه المشكلات قبل إيجاد الحلول. تم إنشاء الرسالة [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]. يمكننا إذن فرض هذا التقييم في وحدة التحكم:


      // ------------------- evts manager
      // language change
      $scope.setLang = function (lang) {
...
        // update msg2
        $scope.msg2 = $filter('translate')('msg_meteo');
        // and msg day
        $scope.msg.model.jour = $filter('date')($scope.jour, 'fullDate');
};

في كل مرة تتغير فيها اللغة، يعيد السطر 8 أعلاه حساب اليوم المعروض. وهذا يحل المشكلة الأولى بشكل فعال ولكنه لا يحل المشكلة الثانية (اليوم المعروض في الرسالة لا يتغير عند اختيار يوم آخر في التقويم). والسبب في هذا السلوك هو كما يلي. يتم عرض الرسالة في العرض باستخدام الكود التالي:


<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

  • السطر 267: معالج النقر على التقويم؛
  • السطر 269: نجبر تحديث اليوم المعروض باستخدام رسالة [msg]؛

3.7.4. المثال 4: خدمة التكوين

دعونا نعيد النظر في بنية تطبيق AngularJS:

هنا، سنركز على مفهوم الخدمة. إنه مفهوم واسع نسبيًا. في حين أن طبقة [DAO] أعلاه هي خدمة بوضوح، يمكن لأي كائن Angular أن يصبح خدمة:

  • تتبع الخدمة صيغة محددة. لها اسم، ويحددها Angular بهذا الاسم؛
  • يمكن لـ Angular حقن الخدمة في وحدات التحكم والخدمات الأخرى؛

ستحتاج بعض الخدمات التي سنقوم بتكوينها في وحدة [rdvmedecins] إلى التكوين. نظرًا لأن الخدمة يمكن حقنها في خدمة أخرى، فمن المغري إجراء التكوين في خدمة سنسميها [config] وحقنها في الخدمات ووحدات التحكم المراد تكوينها. سنقوم الآن بوصف هذه العملية.

نقوم بنسخ [app-13.html] إلى [app-14.html] ونجري التغييرات التالية:


<div class="container">
  <!-- waiting msg control -->
  <label>
    <input type="checkbox" ng-model="waiting.visible">
    <span>Voir le message d'attente</span>
  </label>
 
  <!-- the waiting message -->
  <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>
  • الأسطر 3–6: مربع اختيار يتحكم في عرض رسالة الانتظار في الأسطر 9–15. يتم تخزين قيمة مربع الاختيار في المتغير [waiting.visible] لنموذج M لعرض V. تكون هذه القيمة صحيحة إذا تم تحديد مربع الاختيار، وخاطئة في حالة عدم تحديده. يعمل هذا في كلا الاتجاهين. إذا قمنا بتعيين المتغير [waiting.visible] على true، فسيتم تحديد مربع الاختيار. هناك ارتباط ثنائي الاتجاه بين العرض V ونموذجه M؛
  • الأسطر 9-15: رسالة انتظار مع زر لإلغاء الانتظار (السطر 11)؛
  • السطر 9: لا تظهر الرسالة إلا إذا كانت قيمة المتغير [waiting.visible] هي true. لذلك، عندما نحدد مربع الاختيار في السطر 4:
    • يتم تعيين القيمة true للمتغير [waiting.visible] (ng-model، السطر 4)؛
    • نظرًا لحدوث تغيير في النموذج M، يتم إعادة تقييم العرض V تلقائيًا. ثم تصبح رسالة الانتظار مرئية (ng-show، السطر 9)؛
    • والمنطق مشابه عند إلغاء تحديد مربع الاختيار في السطر 4: يتم إخفاء رسالة الانتظار؛
  • السطر 10: تتم ترجمة رسالة الانتظار (مرشح translate
  • السطر 11: عند النقر على الزر، يتم تنفيذ الأسلوب [waiting.cancel()] (سمة ng-click
  • السطر 12: تتم ترجمة تسمية الزر؛
  • السطر 19: نضع كود JavaScript الخاص بالتطبيق في ملف JS جديد [rdvmedecins-02] حتى لا نفقد الكود الذي تمت كتابته بالفعل ويحتاج الآن إلى إعادة تنظيم؛

ينتج عن ذلك العرض التالي:

  • في [1]، المربع غير محدد؛
  • في [2]، مربع محدد؛

النص البرمجي [rdvmedecins-02] هو إعادة تنظيم للنص البرمجي [rdvmedecins]:

Image

  • السطر 6: وحدة [rdvmedecins] الخاصة بالتطبيق؛
  • السطران 9-10: وظيفة تكوين التطبيق؛
  • السطران 38-39: خدمة [config
  • السطران 283-284: وحدة التحكم [rdvMedecinsCtrl

في السابق، كنا قد عرّفنا القاموس locales={&#x27;fr&#x27;:..., &#x27;en&#x27;: ...} في وحدة التحكم، والتي كان طولها 200 سطر. من الواضح أن هذا القاموس عنصر تكوين، لذا ننقله إلى خدمة [config] في السطرين 38–39. يتم تعريف هذه الخدمة على النحو التالي:

Image

  • السطران 38-39: يتم إنشاء خدمة باستخدام دالة [factory] الخاصة بكائن [angular.module]. تتطابق صيغة هذه الوظيفة مع الصيغ السابقة: factory('service_name', ['O1', 'O2', ..., 'On', function (O1, O2, ..., On){...}]) حيث Oi هي أسماء كائنات معروفة لدى Angular (محددة مسبقًا أو أنشأها المطور) والتي يقوم Angular بإدخالها كمعلمات في وظيفة factory. نظرًا لأن الدالة لا تحتوي على معلمات هنا، فقد استخدمنا صيغة أقصر وصالحة بنفس القدر: factory('service_name', function (){...}]);
  • السطر 40: يجب أن تنفذ دالة [factory] الخدمة باستخدام كائن تعيده. هذا الكائن هو الخدمة. ولهذا السبب تُسمى الدالة factory (مصنع إنشاء الكائنات)؛

بشكل عام، يتخذ كود الخدمة الشكل التالي:


Angular.module('nom_module')
  .factory('nom_service',['O1','O2', ...., 'On', function (O1, O2, ..., On){
     // service preparation
    ...
     // render the object implementing the service
    return {
         // fields
        ...
         // methods
        ...
        }
});
  • السطر 6: نُرجع كائن JavaScript يمكن أن يحتوي على كل من الحقول والطرق. الطرق هي التي تتولى إدارة الخدمة؛

هنا، تحدد خدمة [config] الحقول فقط دون أي طرق. سنضع هنا كل ما يمكن تكوينه في التطبيق:

  • الأسطر 42–47: مفاتيح الرسائل المراد ترجمتها؛
  • الأسطر 59-62: عناوين URL الخاصة بالتطبيق؛
  • الأسطر 64–69: عناوين URL لخدمة الويب البعيدة؛
  • السطر 71: قد يستغرق استدعاء HTTP لخدمة ويب لا تستجيب وقتًا طويلاً. هنا، نحدد الحد الأقصى لوقت انتظار استجابة خدمة الويب بـ 1 ثانية. بعد هذا الوقت، يفشل استدعاء HTTP ويتم إصدار استثناء JavaScript؛
  • السطر 73: قبل كل استدعاء للخادم، سنحاكي فترة انتظار يتم تعيين مدتها هنا بالمللي ثانية. يعني الانتظار 0 أنه لا يوجد انتظار. سيتم تصميم التطبيق بحيث يمكن للمستخدم إلغاء عملية بدأها. لكي تكون قابلة للإلغاء، يجب أن تستمر لبضع ثوانٍ على الأقل. سنستخدم هذا الانتظار المصطنع لمحاكاة العمليات طويلة الأمد؛
  • السطر 75: في وضع [debug=true]، يتم عرض معلومات إضافية في العرض الحالي. بشكل افتراضي، يتم تمكين هذا الوضع. في الإنتاج، سنقوم بتعيين هذا الحقل على false؛
  • الأسطر 77–278: القاموس الخاص باللغتين 'fr' و 'en'. كان موجودًا سابقًا في وحدة التحكم [rdvMedecinsCtrl

مع هذه الخدمة، تتطور وحدة التحكم [rdvMedecinsCtrl] على النحو التالي:

Image

  • السطران 284-285: يتم إدخال خدمة [config] في وحدة التحكم؛
  • السطر 290: يوجد قاموس [locales] الآن في خدمة [config] ولم يعد موجودًا في وحدة التحكم؛
  • السطر 294: الكائن [waiting] الذي يتحكم في عرض رسالة الانتظار. يوجد مفتاح رسالة الانتظار في خدمة [config] (حقل النص). بشكل افتراضي، تكون رسالة الانتظار مخفية (حقل مرئي). يحتوي حقل الإلغاء على اسم الدالة الموجودة في السطر 316 كقيمة له. وبالتالي، فإن هذا الحقل هو طريقة أو دالة؛
  • السطر 316: الدالة [cancel] خاصة (لم نكتب $scope.cancel=function(){}). دعونا نراجع كود زر الإلغاء:

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

عندما ينقر المستخدم على زر الإلغاء، يتم استدعاء الطريقة [$scope.waiting.cancel()]. في النهاية، يتم تنفيذ وظيفة الإلغاء الخاصة من السطر 316. وهي ببساطة تخفي رسالة الانتظار عن طريق تعيين متغير النموذج [waiting.visible] إلى false (السطر 318)؛

3.7.5. المثال 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">
 
  <!-- the waiting message -->
  <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>
 
  <!-- the form -->
  <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>
  • السطر 11: تمنع السمة [ng-cloak] عرض المنطقة حتى يتم تقييم تعبيرات Angular الخاصة بها. وهذا يمنع ظهور المنطقة لفترة وجيزة قبل تقييم السمة [ng-show]، مما سيؤدي في الواقع إلى إخفائها؛
  • السطر 22: سيتم تخزين مدخلات المستخدم (وقت الانتظار) في نموذج [waiting.time] (سمة ng-model
  • السطر 28: تستخدم الصفحة نصًا برمجيًا جديدًا [rdvmedecins-03

النص البرمجي [rdvmedecins-03] هو كما يلي:

Image

  • السطر 6: وحدة Angular التي تدير التطبيق؛
  • السطر 10: وظيفة [config] المستخدمة لتدويل الرسائل؛
  • السطر 41: خدمة [config] التي وصفناها؛
  • السطر 286: خدمة [utils] التي سنقوم بإنشائها؛
  • السطر 315: وحدة التحكم [rdvmedecinsCtrl] التي سنقوم بإنشائها؛

نضيف مفتاح رسالة جديد إلى وظيفة [config] (السطران 6 و 11):


angular.module("rdvmedecins")
  .config(['$translateProvider', function ($translateProvider) {
    // french messages
    $translateProvider.translations("fr", {
...
      'msg_waiting_time_text': "Temps d'attente : "
    });
    // english messages
    $translateProvider.translations("en", {
...
      'msg_waiting_time_text': "Waiting time:"
    });
    // default language
    $translateProvider.preferredLanguage("fr");
}]);

نضيف سطراً جديداً (السطر 6) إلى خدمة [config] لمفتاح الرسالة هذا:


angular.module("rdvmedecins")
  .factory('config', function () {
    return {
      // messages to be internationalized
      ...
waitingTimeText: 'msg_waiting_time_text',

تحتوي خدمة [utils] على طريقتين (السطران 4 و 12):


angular.module("rdvmedecins")
  .factory('utils', ['config', '$timeout', '$q', function (config, $timeout, $q) {
    // display the Json representation of an object
    function debug(message, data) {
      if (config.debug) {
        var text = data ? message + " : " + angular.toJson(data) : message;
        console.log(text);
      }
    }
 
    // waiting
    function waitForSomeTime(milliseconds) {
      // asynchronous waiting milliseconds milliseconds
      var task = $q.defer();
      $timeout(function () {
        task.resolve();
      }, milliseconds);
      // we return the task
      return task;
    };
 
    // service authority
    return {
      debug: debug,
      waitForSomeTime: waitForSomeTime
    }
}]);
  • السطر 2: يتم استدعاء الخدمة [utils] (المعلمة الأولى). وهي تعتمد على ثلاث خدمات: خدمتان محددتان مسبقًا في Angular، وهما $timeout و$q، بالإضافة إلى خدمة config. تتيح خدمة [$timeout] تنفيذ دالة بعد مرور فترة زمنية معينة. وتتيح خدمة [$q] إنشاء مهام غير متزامنة؛
  • السطر 4: دالة محلية [debug
  • السطر 12: دالة محلية [waitForSomeTime
  • الأسطر 23–26: مثيل خدمة [utils]. هذا كائن يعرض طريقتين، هما الموجودتان في السطرين 4 و12. لاحظ أن حقول الكائن يمكن أن تحمل أي أسماء. من أجل الاتساق، أطلقنا عليها أسماء الدوال التي تشير إليها؛
  • الأسطر 4–9: تكتب طريقة [debug] رسالة [message] إلى وحدة التحكم، وإذا كان ذلك ممكنًا، تمثيل JSON لكائن [data]. وهذا يسمح بعرض كائنات بأي درجة من التعقيد؛
  • الأسطر 12-20: تقوم طريقة [waitForSomeTime] بإنشاء مهمة غير متزامنة تستمر [milliseconds] ميلي ثانية؛
  • السطر 14: إنشاء مهمة باستخدام الكائن المحدد مسبقًا [$q] (https://docs.angularjs.org/api/ng/service/$q). فيما يلي واجهة برمجة التطبيقات (API) للمهمة المسماة [deferred] في وثائق Angular:

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);
  • السطر 1: نسترد وعد المهمة؛
  • السطر 2: نحدد الدوال التي سيتم تنفيذها في حالة النجاح أو الفشل. يمكننا اختيار عدم تضمين دالة الفشل. لن يتم تنفيذ دالة [successCallback] إلا إذا اكتملت [المهمة] بنجاح [task.resolve()]. لن يتم تنفيذ دالة [errorCallback] إلا إذا فشلت [المهمة] [task.reject()].
  • السطر 3: نحدد الدالة التي سيتم تنفيذها بعد تنفيذ إحدى الدالتين السابقتين. هنا، نضع الكود المشترك بين الدالتين [successCallback، errorCallback].

لنعد إلى كود دالة [waitForSomeTime]:


    // attente
    function waitForSomeTime(milliseconds) {
      // attente asynchrone de milliseconds millisecondes
      var task = $q.defer();
      $timeout(function () {
        task.resolve();
      }, milliseconds);
      // on retourne la tâche
      return task;
};
  • السطر 4: يتم إنشاء مهمة؛
  • الأسطر 5–7: يسمح لك كائن [$timeout] بتعريف دالة (المعلمة الأولى) يتم تنفيذها بعد فترة تأخير معينة معبر عنها بالميلي ثانية (المعلمة الثانية). هنا، المعلمة الثانية لدالة [$timeout] هي معلمة الطريقة (السطر 1)؛
  • السطر 6: بعد مرور فترة التأخير [milliseconds]، تُنجز المهمة بنجاح؛
  • السطر 9: يتم إرجاع المهمة [task]. من المهم ملاحظة هنا أن السطر 9 يتم تنفيذه فور تعريف الكائن [$timeout]. لا ننتظر انقضاء فترة التأخير [milliseconds]. وبالتالي، يتم تنفيذ الكود في الأسطر 2–10 في وقتين مختلفين:
    • المرة الأولى عند تعريف الكائن [$timeout
    • المرة الثانية عند انقضاء مهلة [milliseconds

هذه دالة غير متزامنة: يتم الحصول على نتيجتها في وقت لاحق عن وقت تنفيذها.

فيما يلي كود وحدة التحكم التي تستخدم خدمة [config]:


// controller
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', '$filter',
    function ($scope, utils, config, $filter) {
      // ------------------- model initialization
      // waiting message
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: undefined};
      $scope.waitingTimeText = config.waitingTimeText;
      // waiting task
      var task;
      // logs
      utils.debug("libellé temps d'attente", $filter('translate')($scope.waitingTimeText));
      utils.debug("locales['fr']=", config.locales['fr']);
 
      // execution action
      $scope.execute = function () {
        // log
        utils.debug('début', new Date());
        // the waiting msg is displayed
        $scope.waiting.visible = true;
        // simulated waiting
        task = utils.waitForSomeTime($scope.waiting.time);
        // end of wait
        task.promise.then(function () {
          // success
          utils.debug('fin', new Date());
        }, function () {
          // failure
          utils.debug('Opération annulée')
        });
        task.promise['finally'](function () {
          // end of wait in all cases
          $scope.waiting.visible = false;
        });
 
      };
 
      // cancel wait
      function cancel() {
        // complete the task
        task.reject();
      }
    }]);
  • السطر 3: يستخدم وحدة التحكم خدمة [config
  • السطر 7: أضفنا الحقل [time] إلى الكائن [$scope.waiting]. يستقبل الكائن [$scope.waiting.time] قيمة وقت الانتظار الذي حدده المستخدم؛
  • السطر 8: يتم وضع مفتاح رسالة الانتظار المعروضة بواسطة العرض في نموذج [$scope.waitingTimeText]. بشكل عام، يجب وضع كل ما يعرضه عرض V في كائن [$scope
  • السطر 10: متغير محلي. لا يتم عرضه على عرض V؛
  • السطران 12-13: استخدام طريقة [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"}

السطر 2: نحصل على تمثيل JSON لكائن locales['fr'].

  • السطر 16: الطريقة التي يتم تنفيذها عندما ينقر المستخدم على زر [Execute
  • السطر 18: يعرض وقت بدء تنفيذ الطريقة؛
  • السطر 22: يتم تشغيل المهمة [waitForSomeTime]. لا ننتظر حتى تنتهي. يستمر التنفيذ بالسطر 24 التالي؛
  • الأسطر 24-30: تحدد الوظائف التي سيتم تنفيذها عند اكتمال المهمة بنجاح (السطر 26) وفي حالة حدوث خطأ (السطر 29)؛
  • السطر 26: يعرض وقت انتهاء تنفيذ الأسلوب؛
  • السطر 29: يعرض أن العملية قد تم إلغاؤها. يحدث هذا فقط عندما ينقر المستخدم على زر [Cancel]. ثم تقوم التعليمات الموجودة في السطر 41 بإيقاف المهمة غير المتزامنة برمز فشل؛
  • الأسطر 31–34: تحدد الوظيفة التي سيتم تنفيذها بعد تنفيذ إحدى الوظيفتين السابقتين؛

من المهم فهم تسلسل تنفيذ هذا الرمز. إذا قام المستخدم بتعيين تأخير مدته 3 ثوانٍ ولم يقم بإلغاء الانتظار:

  • عند النقر على زر [تنفيذ]، يتم تشغيل الدالة [$scope.execute]. يتم تنفيذ الأسطر 16–34 دون انتظار 3 ثوانٍ. في نهاية هذا التنفيذ، تتم مزامنة العرض V مع النموذج M. يتم عرض رسالة الانتظار (ng-show=$scope.waiting.visible=true، السطر 20) ويتم إخفاء النموذج (ng-hide=$scope.waiting.visible=true، السطر 20)؛
  • من هذه النقطة فصاعدًا، يمكن للمستخدم التفاعل مع العرض مرة أخرى. على وجه الخصوص، يمكنه النقر على زر [Cancel
  • إذا لم يفعل ذلك، بعد 3 ثوانٍ، يتم تنفيذ الدالة [$timeout] (انظر الأسطر 5-7 أدناه):

    // attente
    function waitForSomeTime(milliseconds) {
      // attente asynchrone de milliseconds millisecondes
      var task = $q.defer();
      $timeout(function () {
        task.resolve();
      }, milliseconds);
      // on retourne la tâche
      return task;
};
  • بعد 3 ثوانٍ، يتم تنفيذ الكود. يقوم هذا الكود بإكمال المهمة [task] برمز نجاح (resolve). سيؤدي ذلك إلى تشغيل كل الكود الذي كان ينتظر هذا الإكمال (السطر 4 أدناه):

        // simulated waiting
        task = utils.waitForSomeTime($scope.waiting.time);
        // end of wait
        task.promise.then(function () {
          // success
          utils.debug('fin', new Date());
        }, function () {
          // failure
          utils.debug('Opération annulée')
        });
        task.promise['finally'](function () {
          // end of wait in all cases
          $scope.waiting.visible = false;
        });
 
  • وبالتالي سيتم تنفيذ السطر 6 أعلاه (تم الانتهاء بنجاح). ثم سيتم تنفيذ الأسطر 11-14. بمجرد تشغيل هذا الكود، نعود إلى عرض V، والذي سيتم بعد ذلك مزامنته مع نموذج M الخاص به. يتم إخفاء رسالة الانتظار (ng-show=$scope.waiting.visible=false، السطر 13) ويتم عرض النموذج (ng-hide=$scope.waiting.visible=false، السطر 13)؛

ثم تظهر الشاشة على النحو التالي:

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

كما هو موضح أعلاه، هناك تأخير مدته 3 ثوانٍ (06:01–05:58) بين بداية الانتظار ونهايته. على العكس، إذا ألغى المستخدم الانتظار قبل انقضاء الثواني الثلاث، فسيتم عرض ما يلي:

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

أخيرًا، من المهم أن نفهم أنه في أي وقت معين، لا يوجد سوى مؤشر ترابط تنفيذ واحد، يُعرف باسم مؤشر ترابط واجهة المستخدم (UI). يتم الإشارة إلى اكتمال مهمة غير متزامنة بواسطة حدث، تمامًا مثل النقر على زر. لا تتم معالجة هذا الحدث على الفور. بل يتم وضعه في قائمة انتظار الأحداث التي تنتظر التنفيذ. وعندما يحين دوره، تتم معالجته. تستخدم هذه المعالجة مؤشر ترابط واجهة المستخدم، لذا تتجمد الواجهة خلال هذه الفترة. ولا تستجيب لمدخلات المستخدم. ولهذا السبب، من المهم أن تكون معالجة الأحداث سريعة. ونظرًا لأن كل حدث تتم معالجته بواسطة مؤشر ترابط واجهة المستخدم، فلا توجد أبدًا حاجة لحل مشكلات التزامن بين مؤشرات الترابط التي تعمل في وقت واحد. في أي لحظة معينة، يتم تنفيذ مؤشر ترابط واجهة المستخدم فقط.

3.7.6. المثال 6: خدمات HTTP

نقدم الآن خدمة [dao] التي تتواصل مع خادم الويب:

3.7.6.1. طريقة العرض V

سنكتب نموذجًا لطلب قائمة الأطباء:

Image

نقوم بنسخ [app-01.html] إلى [app-16.html]، ثم نقوم بتعديله على النحو التالي:


<div class="container" ng-cloak="">
  <h1>Rdvmedecins - v1</h1>
 
  <!-- the waiting message -->
  <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>
 
  <!-- the request -->
  <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>
 
  <!-- list of doctors -->
  <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>
 
  <!-- the error list -->
  <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>
  • الأسطر 13–31: تنفيذ النموذج. لا يظهر هذا النموذج عند عرض رسالة الانتظار (ng-hide="waiting.visible"). لاحظ أن المدخلات الأربعة مخزنة في (سمات ng-model) [waiting.time (السطر 16)، server.url (السطر 20)، server.login (السطر 24)، server.password (السطر 28)]؛
  • الأسطر 34–39: عرض قائمة الأطباء. هذه القائمة غير مرئية دائمًا (ng-show="medecins.show").
  • السطر 35: بديل للصيغة <div ... translate="{{medecins.title}}" translate-values="{{medecins.model}}"> التي سبق ذكرها؛
  • السطر 36: قائمة غير مرتبة؛
  • السطر 37: توجد قائمة الأطباء في نموذج [medecins.data]. تتيح لك توجيهات Angular [ng-repeat] تكرار قائمة. يوجه بناء الجملة ng-repeat="doctor in medecins.data" علامة <li> إلى التكرار لكل عنصر في قائمة [medecins.data]. يُسمى العنصر الحالي في القائمة [medecin
  • السطر 37: لكل <li>، نعرض اللقب والاسم الأول واسم العائلة للطبيب الحالي المحدد بالمتغير [medecin
  • الأسطر 42-47: عرض قائمة الأخطاء. هذه القائمة ليست مرئية دائمًا (ng-show="errors.show"). يتبع هذا العرض نفس نمط عرض قائمة الأطباء. بشكل عام، لعرض قائمة من الكائنات، نستخدم توجيه Angular [ng-repeat
  • السطر 51: أصبح كود JavaScript الآن في ملف [rdvmedecins-04]

3.7.6.2. وحدة التحكم C ونموذج M

يتغير كود JavaScript على النحو التالي:

Image

  • الأسطر 6–9: تعلن الوحدة النمطية [rdvmedecins] عن تبعية للوحدة النمطية [base64] التي توفرها مكتبة [angular-base64]، وهي إحدى تبعيات المشروع. تُستخدم هذه الوحدة النمطية لترميز السلسلة [login:password] المرسلة إلى خدمة الويب للمصادقة في Base64؛
  • الأسطر 12–13: وظيفة التهيئة التي تحتوي على رسائلنا المُعَلَّمة دوليًا. تظهر رسائل جديدة. لن نتناولها بمزيد من التفصيل؛
  • السطور 69–70: خدمة [config] التي تهيئ تطبيقنا. تمت إضافة مفاتيح رسائل جديدة هنا. لن نتناولها بمزيد من التفصيل؛
  • السطران 318–319: خدمة [utils]، التي تحتوي على طرق المساعدة. تمت إضافة طرق جديدة. سنقدمها؛
  • السطور 385–386: خدمة [dao] المسؤولة عن الاتصال بخدمة الويب. هذا ما سنركز عليه؛
  • السطران 467–468: وحدة التحكم C الخاصة بعرض V الذي ناقشناه للتو. سنغطيها الآن لأنها تعمل كمنسق يستجيب لطلبات المستخدم؛

3.7.6.3. وحدة التحكم C

فيما يلي كود وحدة التحكم:


angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
    function ($scope, utils, config, dao, $translate) {
      // ------------------- model initialization
      // model
      $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;
 
      // asynchronous task
      var task;
 
      // execution action
      $scope.execute = function () {
        // the UI is updated
        $scope.waiting.visible = true;
        $scope.medecins.show = false;
        $scope.errors.show = false;
        // simulated waiting
        task = utils.waitForSomeTime($scope.waiting.time);
        var promise = task.promise;
        // waiting
        promise = promise.then(function () {
          // we ask for the list of doctors;
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
          return task.promise;
        });
        // analyze the result of the previous call
        promise.then(function (result) {
          // result={err: 0, data: [med1, med2, ...]}
          // result={err: n, messages: [msg1, msg2, ...]}
          if (result.err == 0) {
            // we put the acquired data into the model
            $scope.medecins.data = result.data;
            // the UI is updated
            $scope.medecins.show = true;
            $scope.waiting.visible = false;
          } else {
            // there were errors in obtaining the list of doctors
            $scope.errors = { title: config.getMedecinsErrors, messages: utils.getErrors(result), show: true, model: {}};
            // the UI is updated
            $scope.waiting.visible = false;
          }
        });
      };
 
      // cancel wait
      function cancel() {
        // complete the task
        task.reject();
        // the UI is updated
        $scope.waiting.visible = false;
        $scope.medecins.show = false;
        $scope.errors.show = false;
      }
 
    }
  ])
;
  • السطر 2: يحتوي المتحكم على تبعية جديدة، وهي خدمة [dao]؛
  • الأسطر 6–13: يتم تهيئة نموذج M الخاص بعرض V عند عرض العرض لأول مرة؛
  • السطر 8: سيتم استخدام [$scope.server] لاسترداد ثلاث من أربع معلومات من النموذج V؛ أما الرابعة فهي مخزنة في [$scope.waiting.time] (السطر 6)؛
  • السطر 9: سيجمع [$scope.doctors] المعلومات اللازمة لعرض قائمة الأطباء:

  <!-- list of doctors -->
  <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] هي قاموس فارغ وستبقى كذلك. وهي تُستخدم ببساطة لتوضيح استخدام متغير الترجمة المستخدم في السطر 3. السمة [medecins.data]، التي لم يتم تعريفها بعد، ستحتوي على قائمة الأطباء (السطر 5).

  • السطر 10: ستجمع [$scope.errors] المعلومات اللازمة لعرض قائمة الأخطاء:

  <!-- the error list -->
  <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] هي قاموس فارغ وستبقى كذلك. وهي تُستخدم ببساطة لتوضيح استخدام متغير الترجمة المستخدم في السطر 3. السمة [errors.messages]، التي لم يتم تعريفها بعد، ستحتوي على قائمة رسائل الخطأ التي سيتم عرضها (السطر 5).

  • السطر 16: المهمة غير المتزامنة. سيقوم وحدة التحكم بتشغيل مهمتين غير متزامنتين على التوالي. سيتم وضع الإشارات إلى هذه المهام المتتالية في المتغير [task]. سيسمح ذلك بإلغائها (السطر 55)؛
  • السطر 19: الطريقة التي يتم تنفيذها عندما ينقر المستخدم على زر [قائمة الأطباء]:

    <button class="btn btn-primary" ng-click="execute()">Liste des médecins</button>
  • الأسطر 21–23: يتم تحديث واجهة المستخدم: يتم عرض رسالة التحميل، ويتم إخفاء كل شيء آخر؛
  • السطر 25: يتم إنشاء مهمة الانتظار غير المتزامنة. سيتم استلام إشارة (اكتملت المهمة) بعد انقضاء الوقت الذي أدخله المستخدم في النموذج؛
  • السطر 26: نسترد الوعد الخاص بالمهمة غير المتزامنة. يعمل البرنامج الذي يطلق المهمة مع هذا الوعد. ومع ذلك، يجب أن يكون لدينا مرجع إلى المهمة نفسها حتى نتمكن من إلغائها (السطر 55)؛
  • الأسطر 28-32: نحدد العمل الذي يجب القيام به بمجرد اكتمال الانتظار؛
  • السطر 30: نستخدم طريقة [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, ...]} حيث [msg] هي رسالة خطأ و n لا تساوي 0؛
  • السطر 31: نُرجع وعد المهمة. هنا، هناك شيء يجب فهمه. لدينا وعدان:
    • promise.then(): يعيد الوعد الأول [promise1
    • return task.promise: تعيد وعدًا ثانيًا [promise2
    • في النهاية، promise = promise.then(...; return task.promise) هي سلسلة من وعدين [promise2.promise1]. لن يتم تقييم [promise1] إلا بعد حل الوعد [promise2]، أي عند اكتمال المهمة [dao.getData]. الوعد [promise1] لا يعتمد على أي مهمة غير متزامنة. وبالتالي سيتم حله على الفور؛
  • الأسطر 34–50: من التفسير السابق، يترتب على ذلك أن هذه الأسطر لن يتم تنفيذها إلا بمجرد اكتمال المهمة [dao.getData]. يتم إنشاء المعلمة [result] التي تم تمريرها إلى الدالة في السطر 34 بواسطة الطريقة [dao.getData] ويتم تمريرها إلى الكود المستدعي عبر العملية [task.resolve(result)]، حيث يكون لـ [result] الشكل التالي:
    • {err: 0, data: [med1, med2, ...]} حيث [medi] هو كائن يمثل طبيبًا (اللقب، الاسم الأول، الاسم الأخير)،
    • {err: n, messages: [msg1, msg2, ...]} حيث [msg1] هي رسالة خطأ و n لا يساوي 0؛
  • السطر 37: نتحقق من رمز الخطأ [result.err
  • الأسطر 38–42: إذا لم يكن هناك خطأ (result.err == 0)، فإننا نسترد قائمة الأطباء ونعرضها؛
  • الأسطر 44–47: إذا كان هناك خطأ (result.err != 0)، فإننا نسترد قائمة رسائل الخطأ ونعرضها؛
  • الأسطر 53-56: تظل رسالة التحميل مع زر الإلغاء مرئية حتى تكتمل العمليتان غير المتزامنتان. لنرى ما يحدث اعتمادًا على وقت حدوث الإلغاء:
    • أولاً، من المهم أن نفهم أن الأسطر 19-50 يتم تنفيذها دفعة واحدة. لم يتم تشغيل سوى مهمة غير متزامنة واحدة في هذه المرحلة، وهي المهمة الموجودة في السطر 25.
    • بعد هذا التنفيذ الأولي، يتم تحديث العرض V، وبالتالي يصبح شعار الانتظار وزر الإلغاء الخاص به مرئيين. إذا ألغى المستخدم الانتظار قبل اكتمال المهمة الموجودة في السطر 25، يتم عندئذٍ تنفيذ الطريقة الموجودة في السطر 53 ويتم إلغاء المهمة مع حدوث فشل (السطر 55)؛
    • الأسطر 56-59: يتم تحديث الواجهة: يتم إعادة عرض النموذج وإخفاء كل شيء آخر،
    • ثم يعود إلى طريقة العرض V، ويقوم المتصفح بمعالجة الحدث التالي. وبما أن المهمة قد اكتملت، يتم حل الوعد الخاص بهذه المهمة، مما يؤدي إلى إطلاق حدث. ثم تتم معالجته؛
    • ثم يتم تنفيذ الأسطر 28-32. لا توجد دالة محددة لحالة الفشل، لذا لا يتم تنفيذ أي كود. يتم الحصول على وعد جديد، وهو الذي يتم إرجاعه دائمًا بواسطة [promise.then] ويتم حله دائمًا،
    • وبعد معالجة الحدث، يعود التحكم إلى العرض V ويشرع المتصفح في معالجة الحدث التالي. وبما أن [الوعد] في السطر 28 قد تم حله، فسيتم حل الوعد الموجود في السطر 34، مما سيؤدي إلى تشغيل حدث جديد. ثم يتم معالجته؛
    • ثم يتم تنفيذ الأسطر 34-49 بالترتيب، نظرًا لأن الوعد المستخدم في السطر 34 قد تم الوفاء به. مرة أخرى، نظرًا لعدم وجود دالة محددة لحالة الفشل، لا يتم تنفيذ أي كود،
    • وبالتالي نصل إلى السطر 50. لم يعد هناك أي مهمة في انتظار، ويتم عرض العرض الجديد V؛
    • الآن لنفترض أن الإلغاء يحدث أثناء تشغيل المهمة غير المتزامنة الثانية [dao.getData]. ينطبق المنطق السابق مرة أخرى. ستؤدي نهاية المهمة إلى تشغيل الأسطر 34–50 مع فشل المهمة. سنكتشف قريبًا أن طريقة [dao.getData] تقوم بإجراء استدعاء HTTP غير متزامن لخدمة الويب. لن يتم إلغاء هذا الاستدعاء، ولكن لن يتم استخدام نتيجته.

من المهم فهم هذا التبادل المستمر بين عرض العرض V ومعالجة أحداث المتصفح. يتم تشغيل الأحداث بواسطة المستخدم (نقرة) أو بواسطة عمليات النظام مثل إكمال عملية غير متزامنة. حالة الخمول للمتصفح هي عرض العرض V. يتم إخراجه من حالة الخمول هذه بواسطة حدث يحدث، ثم يقوم بمعالجته. بمجرد معالجة الحدث، يعود إلى حالة الخمول. ثم يتم تحديث العرض V إذا كان الحدث المعالج قد عدّل نموذج M الخاص به. يتم إخراج المتصفح من حالة الخمول بواسطة الحدث التالي.

يحدث كل شيء في سلسلة واحدة. لا تتم معالجة حدثين في وقت واحد أبدًا. يتم تنفيذهما بالتتابع. ينتقل المتصفح إلى الحدث التالي فقط عندما يطلق الحدث السابق السيطرة، عادةً لأنه تمت معالجته بالكامل.

هناك نقطة أخرى يجب توضيحها. لعرض رسائل الخطأ، نكتب:


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

يتم توفير قائمة الرسائل بواسطة طريقة [utils.getErrors] المحددة في خدمة [utils]. هذه الطريقة هي كما يلي:


// error analysis in server response JSON
    function getErrors(data) {
      // data {err:n, messages:[]}, err!=0
      // errors
      var errors = [];
      // error code
      var err = data.err;
      switch (err) {
        case 2 :
          // not authorized
          errors.push('not_authorized');
          break;
        case 3 :
          // forbidden
          errors.push('forbidden');
          break;
        case 4 :
          // local error
          errors.push('not_http_error');
          break;
        case 6 :
          // document not found
          errors.push('not_found');
          break;
        default :
          // other cases
          errors = data.messages;
          break;
 
      }
      // if no msg, we put one
      if (! errors || errors.length == 0) {
        errors=['error_unknown'];
      }
      // return the list of errors
      return errors;
    }
  • السطران 2-3: المعلمة [data] المستلمة هي كائن له خاصيتان:
    • [err]: رمز خطأ؛
    • [messages]: قائمة بالرسائل؛
  • السطر 5: سننشئ مصفوفة من رسائل الخطأ. هذه الرسائل مترجمة. ولهذا السبب، فإننا لا نضع الرسائل نفسها في المصفوفة، بل مفاتيح ترجمتها، باستثناء السطر 27. في هذه الحالة، نستخدم السمة [messages] للمعلمة [data]. هذه الرسائل هي رسائل فعلية وليست مفاتيح رسائل. ومع ذلك، ستتعامل طريقة العرض V معها على أنها مفاتيح رسائل، والتي لن يتم العثور عليها بعد ذلك. في هذه الحالة، تعرض الوحدة النمطية [translate] مفتاح الرسالة الذي لم تعثر عليه — في هذه الحالة، رسالة فعلية. هذه هي النتيجة المرجوة؛
  • الأسطر 32–34: تعالج الحالة التي يكون فيها [data.messages] في السطر 27 فارغًا. يحدث هذا مع خدمة الويب المكتوبة. كان ينبغي تجنب هذا السيناريو.

3.7.6.4. خدمة [dao]

تتعامل خدمة [dao] مع تبادلات HTTP مع خدمة الويب / JSON. وفيما يلي شفرة البرمجة الخاصة بها:


angular.module("rdvmedecins")
  .factory('dao', ['$http', '$q', 'config', '$base64', 'utils',
    function ($http, $q, config, $base64, utils) {
 
      // logs
      utils.debug("[dao] init");
 
      // ----------------------------------méthodes privées
      // obtain data from the web service
      function getData(serverUrl, username, password, urlAction, info) {
        // asynchronous operation
        var task = $q.defer();
        // url request HTTP
        var url = serverUrl + urlAction;
        // basic authentication
        var basic = "Basic " + $base64.encode(username + ":" + password);
        // the answer
        var réponse;
        // all http requests must be authenticated
        var headers = $http.defaults.headers.common;
        headers.Authorization = basic;
        // query 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);
        // the task itself is returned so that it can be cancelled
        return task;
 
        // success
        function success(response) {
          // response.data={status:0, data:[med1, med2, ...]} or {status:x, data:[msg1, msg2, ...]
          utils.debug("[dao] getData[" + urlAction + "] success réponse", response);
          // answer
          var payLoad = response.data;
          réponse = payLoad.status == 0 ? {err: 0, data: payLoad.data} : {err: 1, messages: payLoad.data};
          // we return the answer
          task.resolve(réponse);
        }
 
        // failure
        function failure(response) {
          utils.debug("[dao] getData[" + urlAction + "] error réponse", response);
          // status analysis
          var status = response.status;
          var error;
          switch (status) {
            case 401 :
              // unauthorized
              error = 2;
              break;
            case 403:
              // forbidden
              error = 3;
              break;
            case 404:
              // not found
              error = 6;
              break;
            case 0:
              // local error
              error = 4;
              break;
            default:
              // something else
              error = 5;
          }
          // we return the answer
          task.resolve({err: error, messages: [response.statusText]});
        }
      }
 
      // --------------------- service instance [dao]
      return {
        getData: getData
      }
}]);
  • الأسطر 77-79: تحتوي الخدمة على حقل واحد فقط: طريقة [getData]، التي تسترد المعلومات من خدمة الويب / JSON؛
  • السطر 2: تظهر تبعية [$http] لم نواجهها بعد. هذه خدمة Angular محددة مسبقًا تتيح الاتصال عبر HTTP مع كيان بعيد؛
  • السطر 6: سجل لمعرفة في أي مرحلة من دورة حياة التطبيق يتم تنفيذ الكود؛
  • السطر 10: تقبل طريقة [getData] خمسة معلمات:
    • [serverUrl]: عنوان URL الجذر لخدمة الويب (http://localhost:8080
    • [urlAction]: عنوان URL للخدمة المحددة المطلوبة (/getAllMedecins
    • [username]: اسم تسجيل دخول المستخدم؛
    • [password]: كلمة مرور المستخدم؛
    • [info]: كائن يحتوي على معلومات إضافية عند الوصول إلى عنوان URL للخدمة المحددة المطلوبة عبر عملية POST. في حالة عنوان URL (/getAllMedecins)، لم يتم تمرير هذه المعلمة. وبالتالي فهي [undefined
  • السطر 12: يتم إنشاء مهمة غير متزامنة؛
  • السطر 14: عنوان URL الكامل للخدمة المطلوبة (http://localhost:8080/getAllMedecins
  • السطر 16: يتم إجراء المصادقة عن طريق إرسال رأس HTTP التالي:
Authorization:Basic code

حيث [code] هي السلسلة المشفرة بـ Base64 [username:password

السطر 16 ينشئ جزء [الرمز الأساسي] من رأس HTTP؛

  • السطر 18: استجابة خدمة الويب؛
  • السطر 20: يتم تعريف رؤوس HTTP التي يرسلها Angular افتراضيًا في طلب HTTP في الكائن [$http.defaults.headers.common]. لا يتم تضمين رأس [Authorization:Basic code
  • السطر 21: نضيفه إلى رؤوس HTTP ليتم إرساله بشكل منهجي. على الجانب الأيسر من التعيين، لدينا رأس [Authorization] لتهيئته، وعلى الجانب الأيمن، قيمة الرأس — في هذه الحالة، القيمة المحددة في السطر 16. لذا إذا كتبنا:
headers.Authorization = 'x';

فسيقوم Angular بإرسال رأس HTTP:

Authorization : x
  • السطر 23: تُرجع طرق خدمة [$http] وعودًا. سيتم تخزينها في المتغير [promise
  • السطر 27: نظرًا لأن المعلمة [info] هنا لها القيمة [undefined]، يتم تنفيذ السطر 27. يتم طلب عنوان URL (http://localhost:8080/getAllMedecins) باستخدام طلب GET. لتجنب الانتظار لفترة طويلة، نقوم بتعيين مهلة زمنية قصوى لاستلام استجابة الخادم. بشكل افتراضي، تبلغ هذه المهلة ثانية واحدة؛
  • السطر 29: نحدد الطريقتين اللتين سيتم تنفيذهما عند الوفاء بالوعد:
    • [success]: المحددة في السطر 34، هي الطريقة التي يتم تنفيذها عند حل الوعد عند إتمام المهمة بنجاح؛
    • [failure]: محددة في السطر 45، وهي الطريقة التي يتم تنفيذها عند حل الوعد بسبب فشل المهمة؛
    • يتم تعريف كلتا الطريقتين (أو بالأحرى الدالتين) داخل الدالة [getData]. وهذا ممكن في JavaScript. يمكن الوصول إلى المتغيرات المحددة في [getData] داخل الدالتين الداخليتين [success] و [failure
  • السطر 31: نُرجع المهمة التي تم إنشاؤها في السطر 12. هنا، يجب أن نتذكر كود الاستدعاء:

        promise = promise.then(function () {
          // we ask for the list of doctors;
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrMedecins);
          return task.promise;
});

السطر 3 أعلاه يسترد مهمة.

  • السطر 34: يتم تنفيذ الدالة [success] لاحقًا، بمجرد اكتمال طلب HTTP بنجاح. يرتبط مفهوم النجاح هذا بالسطر الأول من استجابة HTTP. ويأخذ الشكل التالي:
HTTP/1.1 code texte

الرمز هو رقم مكون من ثلاثة أرقام يشير إلى ما إذا كان الطلب ناجحًا أم لا. بشكل عام، الرموز 2xx و 3xx هي رموز نجاح، بينما الرموز الأخرى هي رموز فشل. النص هو رسالة توضيحية موجزة. فيما يلي استجابتان محتملتان، واحدة للنجاح والأخرى للفشل:

HTTP/1.1 200 OK
HTTP/1.1 404 Not Found
  • السطر 36: يتم عرض استجابة الخادم على وحدة التحكم. في خطأ [404 غير موجود]، نحصل على شيء مثل:

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

في هذا الرد، سنستخدم فقط الحقول [data] و[status] و[statusText].

  • السطر 38: نسترد حقل [data] من الرد. وسيتخذ أحد الأشكال التالية:
    • {status: 0, data: [med1, med2, ...]} حيث [medi] هو كائن يمثل طبيبًا (اللقب، الاسم الأول، الاسم الأخير)،
    • {status: n, data: [msg1, msg2, ...]} حيث [msg1] هي رسالة خطأ و n لا يساوي 0؛

Image

  • السطر 39: نقوم بإنشاء الاستجابة {0,data} أو {n,messages}. تحتوي الاستجابة الأولى على الأطباء في حقل [data]. أما الرد الثاني فيشير إلى حدوث خطأ على جانب الخادم. وقد عالج الخادم هذا الخطأ، وأنشأ رمز خطأ في [err]، وقائمة برسائل الخطأ في [data]. وفي كلتا الحالتين، يُرجع رمز حالة HTTP 200 الذي يشير إلى أن طلب HTTP قد تمت معالجته بالكامل. ولهذا السبب يتم التعامل مع كلتا الحالتين في نفس الدالة [success
  • السطر 41: تم إكمال المهمة [task.resolve] ويتم إرجاع أحد الاستجابتين:
    • {err: 0, data: [med1, med2, ...]} حيث [medi] هو كائن يمثل طبيبًا (اللقب، الاسم الأول، الاسم الأخير)،
    • {err: n, messages: [msg1, msg2, ...]} حيث [msgi] هي رسالة خطأ و n لا يساوي 0؛

يجب ربط هذا الرمز بكيفية استرداد هذا الرد في رمز استدعاء وحدة التحكم:


        // analyze the result of the previous call
        promise.then(function (result) {
          // result={err: 0, data: [med1, med2, ...]}
          // result={err: n, messages: [msg1, msg2, ...]}
          ...
          }

يتم تخزين الاستجابة من [task.resolve(response)] في المتغير [result] أعلاه.

  • السطر 45: دالة [failure] عندما تنتهي المهمة غير المتزامنة بالفشل. هناك حالتان محتملتان:
    • يُشير الخادم إلى هذا الفشل عن طريق إرجاع رمز حالة ليس 2xx ولا 3xx،
    • يقوم Angular بإلغاء طلب HTTP. في هذه الحالة، لا يتم إرسال أي طلب. تحدث استثناءات Angular، ولكن لا يتم إرجاع أي رمز خطأ HTTP من قبل الخادم. يحدث هذا، على سبيل المثال، إذا تم توفير عنوان URL غير صالح لا يمكن الوصول إليه؛
  • السطر 46: نعرض الاستجابة في وحدة التحكم؛
  • السطر 48: نتذكر أن استجابة الخادم لها التنسيق التالي:

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

السطر 48: نسترد السمة [status] أعلاه؛

  • الأسطر 50–70: استنادًا إلى رمز خطأ HTTP، نقوم بإنشاء رمز خطأ جديد لإخفاء طبيعة HTTP لطريقة [dao.getData] عن الكود المستدعي. يمكننا التحقق من أنه في وحدة التحكم التي تستخدم هذه الطريقة، لا يوجد ما يشير إلى وجود استدعاء HTTP داخل الطريقة؛
    • السطر 51: يتوافق الخطأ [401] مع فشل المصادقة (كلمة مرور غير صحيحة، على سبيل المثال)،
    • السطر 55: الخطأ [403] يتوافق مع طلب غير مصرح به. قام المستخدم بالمصادقة بشكل صحيح ولكنه لا يمتلك أذونات كافية لطلب عنوان URL الذي طلبه. سيحدث هذا مع المستخدم [user / user]. هذا المستخدم موجود بالفعل في قاعدة البيانات ولكنه لا يمتلك إذنًا لاستخدام التطبيق. فقط المستخدم [admin / admin] يمتلك هذا الإذن؛
    • السطر 59: يشير الخطأ [404] إلى عدم العثور على عنوان URL. يمكن أن يكون للخطأ عدة أسباب:
      • أخطأ المستخدم في كتابة عنوان URL للخدمة؛
      • لم يتم تشغيل خدمة الويب؛
      • لم تستجب خدمة الويب بسرعة كافية (الحد الزمني الافتراضي هو ثانية واحدة)؛
    • السطر 63: رمز خطأ HTTP 0 غير موجود. يحدث هذا عندما لا يقوم Angular بإجراء استدعاء HTTP المطلوب لأن عنوان URL الذي أدخله المستخدم غير صالح ولا يمكن الوصول إليه. سنواجه حالات أخرى لاحقًا حيث لا يقوم Angular بتنفيذ استدعاء HTTP المطلوب؛
  • السطر 72: ننجز المهمة بنجاح (task.resolve) عن طريق إرجاع استجابة من النوع {err, messages}، حيث تتكون المصفوفة [messages] حصريًا من الرسالة [response.statusText]. إذا لم يقم Angular بإجراء استدعاء HTTP المطلوب، فسيكون لدينا سلسلة فارغة؛

الآن بعد أن أصبح لدينا نظرة عامة ومفصلة على التطبيق، يمكننا البدء في الاختبار.

3.7.6.5. اختبار التطبيق - 1

لنبدأ بالمدخلات الصحيحة:

Image

  • في [1]، ندخل 0 لتجنب أي تأخير؛
  • في [2]، نحصل على رسالة خطأ على الرغم من صحة المدخلات. لم نتطرق بعد إلى رسائل الخطأ المختلفة. الرسالة المعروضة في [2] هي رسالة عامة مرتبطة بالخطأ 0، والذي يتوافق مع استثناء Angular. واجه Angular مشكلة منعته من إجراء طلب HTTP. في مثل هذه الحالات، تحتاج إلى التحقق من سجلات وحدة التحكم في JavaScript. هناك طريقتان للقيام بذلك:
    • اضغط على [F12] في متصفح Chrome؛
    • استخدام وحدة التحكم في WebStorm؛

في وحدة تحكم WebStorm، نجد رسائل متنوعة، بما في ذلك هذه الرسالة:

XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:63342' is therefore not allowed access.
[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":""}
  • السطر 1: Angular يبلغ عن خطأ، وسنعود إليه لاحقًا؛
  • السطر 2: سجل طريقة [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] الإذن عن طريق إرسال رؤوس HTTP محددة. ولأن خادمنا [2] لم يرسلها هنا، رفض المتصفح إجراء طلب HTTP الذي طلبه التطبيق.

الآن دعونا ندخل في التفاصيل. دعونا نفحص حركة مرور الشبكة التي حدثت أثناء طلب HTTP. للقيام بذلك، في متصفح Chrome، نضغط على [F12] لفتح أدوات المطور ونختار علامة التبويب [الشبكة] لعرض حركة مرور الشبكة:

  • في [1]، نختار علامة التبويب [الشبكة]؛
  • في [2]، نطلب قائمة الأطباء؛

نحصل على المعلومات التالية في علامة التبويب [الشبكة]:

  • في [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. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:63342' is therefore not allowed access.

لذلك، يجب تعديل الخادم الخاص بنا بحيث يرسل رأس HTTP المتوقع.

3.7.6.6. تعديل خادم Web/JSON

نعود إلى Eclipse. للحفاظ على التقدم الذي أحرزناه، نقوم بنسخ الإصدار الحالي من خادم الويب / JSON [rdvmedecins-webapi-v2] إلى [rdvmedecins-webapi-v3] [1]:

نقوم بإجراء تعديل أولي في [ApplicationModel]، وهو أحد عناصر تكوين خدمة الويب:


package rdvmedecins.web.models;
 
...
 
@Component
public class ApplicationModel implements IMetier {
 
    // the [business] layer
    @Autowired
    private IMetier métier;
 
    // data from the [business] layer
    private List<Medecin> médecins;
    private List<Client> clients;
    private List<String> messages;
    // configuration data
    private boolean CORSneeded = true;
 
...
 
    public boolean isCORSneeded() {
        return CORSneeded;
    }
 
}
  • السطر 17: نقوم بإنشاء متغير منطقي يشير إلى ما إذا كان يتم قبول العملاء من خارج نطاق الخادم أم لا؛
  • الأسطر 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;
 
    // sending options to the customer
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // set header CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
        }
 
    }
 
    // list of doctors
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
    public void getAllMedecins(HttpServletResponse response) {
        sendOptions(response);
    }
}
  • الأسطر 28–31: تعريف وحدة تحكم لعنوان URL [/getAllMedecins] عند طلبه باستخدام طريقة HTTP [OPTIONS
  • السطر 29: تأخذ طريقة [getAllMedecins] كائن [HttpServletResponse] كمعلمة، والذي سيتم إرساله إلى العميل الذي قام بالطلب. يتم حقن هذا الكائن بواسطة Spring؛
  • السطر 30: يتم تفويض معالجة الطلب إلى الطريقة الخاصة في الأسطر 19–25؛
  • السطران 15-16: يتم إدخال كائن [ApplicationModel
  • الأسطر 20-23: إذا تم تكوين الخادم لقبول عملاء من خارج نطاقه، يتم إرسال رأس HTTP:

Access-Control-Allow-Origin: *

مما يعني أن الخادم يقبل العملاء من أي مجال (*).

نحن الآن جاهزون لإجراء المزيد من الاختبارات. نقوم بتشغيل الإصدار الجديد من خدمة الويب ونجد أن المشكلة لم تتغير. لم يتغير شيء. إذا أضفنا إخراج وحدة التحكم في السطر 30 أعلاه، فلن يتم عرضه أبدًا، مما يشير إلى أن طريقة [getAllMedecins] في السطر 29 لم يتم استدعاؤها أبدًا.

بعد بعض البحث، نكتشف أن Spring MVC تتعامل مع طلبات HTTP [OPTIONS] بنفسها باستخدام المعالجة الافتراضية. لذلك، فإن Spring هي التي تستجيب دائمًا، وليس أبدًا الطريقة [getAllMedecins] في السطر 29. يمكن تغيير هذا السلوك الافتراضي لـ 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 configuration for CORS headers
    @Bean
    public DispatcherServlet dispatcherServlet() {
        DispatcherServlet servlet = new DispatcherServlet();
        servlet.setDispatchOptionsRequest(true);
        return servlet;
    }
}
  • السطر 8: الفئة هي فئة تكوين Spring. وهي تعلن عن حبوب سيتم وضعها في سياق Spring؛
  • السطر 12: تُستخدم حبة [dispatcherServlet] لتعريف السيرفلت الذي يتعامل مع طلبات العميل. وهي من النوع [DispatcherServlet]. عادةً ما يتم إنشاء هذا السيرفلت بشكل افتراضي. إذا قمنا بإنشائه بأنفسنا، فيمكننا بعد ذلك تكوينه؛
  • السطر 14: نقوم بإنشاء مثيل من النوع [DispatcherServlet
  • السطر 15: نوجه السيرفلت لإعادة توجيه أوامر 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 {
 
}
  • السطر 11: يتم استيراد فئة التكوين الجديدة [WebConfig

3.7.6.7. اختبار التطبيق - 2

نقوم بتشغيل الإصدار الجديد من خدمة الويب / JSON ونحاول استرداد قائمة الأطباء باستخدام عميل Angular الخاص بنا. نفحص حركة مرور الشبكة في علامة التبويب [Network]:

  • في [1]، يمكننا أن نرى أن رأس HTTP [Access-Control-Allow-Origin: *] موجود الآن في استجابة الخادم. ومع ذلك، لا يزال الأمر لا يعمل. نفحص سجلات وحدة التحكم في [2]. وهناك نجد السجل التالي:
XMLHttpRequest cannot load http://localhost:8080/getAllMedecins. Request header field Authorization is not allowed by Access-Control-Allow-Headers

يمكننا أن نرى أن المتصفح يتوقع رأس HTTP جديد [Access-Control-Allow-Headers] يخبره بأن لدينا الإذن لإرسال رأس المصادقة:

Authorization:Basic code

قد يكون هذا مؤشراً إيجابياً. ربما حاولت Angular إرسال طلب HTTP GET. ولكن، نظراً لأن هذا الطلب يتضمن رأس مصادقة، فإنها تتحقق مما إذا كان الخادم يقبله أم لا.

نقوم بتعديل خادم الويب / JSON الخاص بنا لإرسال هذا الرأس. تتغير فئة [RdvMedecinsCorsController] على النحو التالي:


    // sending options to the customer
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // set header CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
            // we authorize the header [Authorization]
            response.addHeader("Access-Control-Allow-Headers", "Authorization");            
}
  • تضيف السطران 6 و7 الرأس المفقود.

نقوم بإعادة تشغيل الخادم ونطلب قائمة الأطباء مرة أخرى باستخدام عميل 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] يملك حق الوصول):

هذه المرة، لم يعد الخطأ [خطأ في المصادقة] بل [رفض الوصول].

3.7.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>
 
  <!-- the waiting message -->
  <div class="alert alert-warning" ng-show="waiting.visible" >
...
  </div>
 
  <!-- the request -->
  <div class="alert alert-info" ng-hide="waiting.visible" >
...
    <button class="btn btn-primary" ng-click="execute()">{{clients.title|translate}}</button>
  </div>
 
  <!-- customer list -->
  <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>
 
  <!-- the error list -->
  <div class="alert alert-danger"  ng-show="errors.show">
   ...
  </div>
 
</div>
....
<script type="text/javascript" src="rdvmedecins-05.js"></script>
  • الأسطر 5-7: لا يتغير شعار التحميل؛
  • الأسطر 10-13: لا يتغير النموذج، باستثناء تسمية الزر (السطر 12)؛
  • الأسطر 28-30: لا يتغير شعار الخطأ؛
  • الأسطر 16-25: يتم عرض العملاء في قائمة منسدلة مصممة بواسطة مكون [Bootstrap-selectpicker] (سمات data-style و class، السطر 19)؛
  • السطر 20: تُستخدم توجيهات [ng-repeat] لتوليد الخيارات المختلفة في القائمة المنسدلة. لاحظ أن تسمية الخيار من النوع [Mme Julienne Tatou] وأن قيمة الخيار من النوع [100]، حيث 100 هو معرّف العميل المعروض؛
  • السطر 34: تم نقل كود JavaScript إلى ملف جديد [rdvmedecins-05

3.7.7.2. وحدة التحكم C ونموذج M

يتم نسخ كود JavaScript الموجود في الملف [rdvmedecins-05] من الملف [rdvmedecins-04]:

Image

لم يتغير شيء تقريبًا، باستثناء وحدة التحكم، التي تم تكييفها الآن لتوفير قائمة العملاء:


angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate',
    function ($scope, utils, config, dao, $translate) {
      // ------------------- model initialization
      // model
      $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;
 
      // asynchronous task
      var task;
 
      // execution action
      $scope.execute = function () {
        // the UI is updated
        $scope.waiting.visible = true;
        $scope.clients.show = false;
        $scope.errors.show = false;
        // simulated waiting
        task = utils.waitForSomeTime($scope.waiting.time);
        var promise = task.promise;
        // waiting
        promise = promise.then(function () {
          // we ask for the customer list;
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
          return task.promise;
        });
        // analyze the result of the previous call
        promise.then(function (result) {
          // result={err: 0, data: [client1, client2, ...]}
          // result={err: n, messages: [msg1, msg2, ...]}
          if (result.err == 0) {
            // we put the acquired data into the model
            $scope.clients.data = result.data;
            // the UI is updated
            $scope.clients.show = true;
            $scope.waiting.visible = false;
            // style the drop-down list
            $('.selectpicker').selectpicker();
          } else {
            // there were errors in obtaining the customer list
            $scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
            // the UI is updated
            $scope.waiting.visible = false;
          }
        });
      };
 
      // cancel wait
      function cancel() {
        // complete the task
        task.reject();
        // the UI is updated
        $scope.waiting.visible = false;
        $scope.clients.show = false;
        $scope.errors.show = false;
      }
    }
  ])
;
  • لم يتغير الكثير في وحدة التحكم. كانت تقدم قائمة بالأطباء. وهي تقدم الآن قائمة بالعملاء؛
  • السطر 9: سيكون [$scope.clients] هو النموذج لشعار العميل في عرض V؛
  • السطر 30: يتم الآن استخدام عنوان URL [/getAllClients
  • السطران 35–36: تنسيقتا الاستجابة اللتان تعيدهما طريقة [dao.getData]. لدينا الآن عملاء بدلاً من أطباء؛
  • السطر 44: تعليمات نادرة إلى حد ما في كود 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. تعديل خدمة الويب - 1

  

تم تحسين فئة [RdvMedecinsController] بإضافة طريقة جديدة:


package rdvmedecins.web.controllers;
 
...
 
@Controller
public class RdvMedecinsCorsController {
 
    @Autowired
    private ApplicationModel application;
 
    // sending options to the customer
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // set header CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
            // we authorize the header [Authorization]
            response.addHeader("Access-Control-Allow-Headers", "Authorization");
        }
 
    }
 
    // list of doctors
    @RequestMapping(value = "/getAllMedecins", method = RequestMethod.OPTIONS)
    public void getAllMedecins(HttpServletResponse response) {
        sendOptions(response);
    }
 
    // customer list
    @RequestMapping(value = "/getAllClients", method = RequestMethod.OPTIONS)
    public void getAllClients(HttpServletResponse response) {
        sendOptions(response);
    }
}
  • الأسطر 29–32: ستتعامل طريقة [getAllClients] مع طلب HTTP [OPTIONS] الذي أرسله المتصفح إليها؛

3.7.7.4. اختبار التطبيق – 1

نحن الآن جاهزون للاختبار. نبدأ تشغيل خادم الويب ثم ندخل قيمًا صالحة في نموذج Angular. نحصل على الاستجابة التالية:

Image

يتم عرض رسالة الخطأ هذه عندما يتعذر على Angular إجراء طلب HTTP المطلوب. يجب علينا بعد ذلك البحث عن الأسباب في سجلات وحدة التحكم. وهناك نجد الرسالة التالية:

XMLHttpRequest cannot load http://localhost:8080/getAllClients. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:63342' is therefore not allowed access.

مشكلة كنا نعتقد أنها حُلّت. لنلقِ نظرة الآن على حركة مرور الشبكة التي حدثت:

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 هو الذي تم إلغاؤه؛

عند دمج ذلك مع الرسالة الأولى، يعني هذا أن Angular تتوقع أيضًا رؤوس CORS لطلب GET. ومع ذلك، في الوقت الحالي، لا ترسل خدمة الويب الخاصة بنا هذه الرؤوس إلا لطلب HTTP [OPTIONS]. من الغريب جدًا مواجهة هذا الخطأ الآن وليس عند عرض قائمة الأطباء. ليس لدي أي تفسير لذلك.

لذلك، نحتاج إلى تعديل خدمة الويب مرة أخرى.

3.7.7.5. تعديل خدمة الويب – 2

  

تتم معالجة طريقتي [GET] و[POST] في فئة [RdvMedecinsController]. نحتاج إلى تعديلها بحيث ترسل هاتان الطريقتان رؤوس CORS. ونقوم بذلك على النحو التالي:


@RestController
public class RdvMedecinsController {
 
    @Autowired
    private ApplicationModel application;
 
    @Autowired
    private RdvMedecinsCorsController rdvMedecinsCorsController;
 
...
 
    // customer list
    @RequestMapping(value = "/getAllClients", method = RequestMethod.GET)
    public Reponse getAllClients(HttpServletResponse response) {
        // headers CORS
        rdvMedecinsCorsController.getAllClients(response);
        // application status
        if (messages != null) {
            return new Reponse(-1, messages);
        }
        // customer list
        try {
            return new Reponse(0, application.getAllClients());
        } catch (Exception e) {
            return new Reponse(1, Static.getErreursForException(e));
        }
    }
...
  • السطر 8: نريد إعادة استخدام الكود الذي وضعناه في وحدة التحكم [RdvMedecinsCorsController]. لذا نقوم بإدراجه هنا؛
  • السطر 14: الطريقة التي تتعامل مع الطلب [GET /getAllClients]. نقوم بإجراء تغييرين:
    • السطر 14: نقوم بإدخال كائن [HttpServletResponse] في معلمات الطريقة،
    • السطر 16: نستخدم طرق فئة [RdvMedecinsCorsController] لتعيين رؤوس CORS في هذا الكائن؛

3.7.7.6. اختبار التطبيق – 2

نطلق الإصدار الجديد من خدمة الويب ونطلب قائمة العملاء مرة أخرى. نحصل على الاستجابة التالية:

  • في [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"} 

إذن، فقد تلقت الدالة بالفعل قائمة العملاء. وبعد التحقق من صحة الكود، بدأنا نشك في الأمر التالي، الذي لا نفهمه تمامًا:


// on style la liste déroulante
$('.selectpicker').selectpicker();

نقوم بتعليق السطر 2 ونحاول مرة أخرى. ثم نحصل على الاستجابة التالية:

وبذلك نكون قد حددنا المشكلة بدقة. إن تطبيق طريقة [selectpicker] على القائمة المنسدلة هو ما يسبب المشكلة. وعندما ننظر إلى شفرة المصدر للصفحة التي تحتوي على الخطأ، نرى ما يلي:

  • نكتشف أنه في [1]، القائمة المنسدلة موجودة بالفعل مع عناصرها ولكنها غير معروضة [style='display:none'];
  • في [2]، يتم عرض زر [bootstrap select]. يجب أن تظهر العناصر الموجودة في القائمة المنسدلة في قائمة <ul role='menu'>. وهي غير موجودة هناك، لذا لدينا قائمة فارغة. يبدو أنه عند تطبيق طريقة [selectpicker] على القائمة المنسدلة، كان محتواها فارغًا في ذلك الوقت؛

أثناء البحث عن حل على الويب، وجدنا هذا الحل. نقوم باستبدال الكود:


// on style la liste déroulante
$('.selectpicker').selectpicker();

بالآتي:


            // on style la liste déroulante
            $timeout(function(){
              $('.selectpicker').selectpicker();
});

يتم تطبيق نمط [bootstrap-select] عبر دالة [$timeout]. لقد سبق أن تعرفنا على هذه الدالة، التي تسمح بتنفيذ دالة ما بعد فترة تأخير معينة. هنا، يعني عدم وجود تأخير أن التأخير يساوي صفرًا. تضع الأسطر السابقة حدثًا في قائمة انتظار الأحداث بالمتصفح. عندما تنتهي معالجة الحدث الحالي (النقر على زر [Client List])، سيتم عرض طريقة العرض V. بعد ذلك مباشرة، سيتحقق المتصفح من قائمة الأحداث الخاصة به. نظرًا لعدم وجود تأخير، سيكون حدث [$timeout] في أعلى القائمة وسيتم معالجته. ثم يتم تطبيق نمط [bootstrap-select] على قائمة منسدلة مملوءة. لنرى النتيجة:

إذا نظرنا مرة أخرى إلى شفرة المصدر للصفحة المعروضة، نرى ما يلي:

أصبح زر [bootstrap-select]، الذي كان فارغًا في السابق، يحتوي الآن على قائمة العملاء.

3.7.7.7. استخدام توجيه

في وحدة التحكم C الخاصة بعرض V، صادفنا الكود التالي:


            // on style la liste déroulante
            $('.selectpicker').selectpicker();

نحن نتعامل مع كائن DOM. يكره العديد من مطوري Angular التعامل مع DOM داخل كود وحدة التحكم. بالنسبة لهم، يجب أن يتم ذلك في توجيه. يمكن النظر إلى توجيه 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] (السطر 3). قيمتها هنا هي دالة تأخذ ثلاث معلمات:
    • النطاق: نموذج العرض الذي تُستخدم فيه التوجيهية؛
    • element: عنصر العرض، هدف التوجيه؛
    • attrs: سمات هذا العنصر؛

لنلقِ نظرة على مثال. يمكن استخدام التوجيه [selectEnable] في السياق التالي:

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

في المثال أعلاه، تطبق السمة [select-enable] التوجيه [selectEnable] على عنصر HTML <div>. يمكن تطبيق توجيه [doSomething] على أي عنصر HTML عن طريق إضافة السمة [do-something] إليه. لاحظ الفرق في التهجئة بين اسم التوجيه والسمة المرتبطة به. ننتقل من [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: هو نموذج العرض الذي يوجد فيه <div>؛
  • element: هو <div> نفسه؛
  • attrs: هي مصفوفة سمات <div>. يمكن استخدامها لتمرير المعلومات إلى التوجيه. في المثال أعلاه، نكتب attrs['selectEnable'] لاسترداد معلومات [data]. لاحظ التغيير في الترميز [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>
  • السطر 1: نطبق التوجيه [selectEnable] على عنصر [select] في HTML. وبما أنه لا توجد معلومات لتمريرها إلى التوجيه، فإننا نكتفي بكتابة [select-enable=""] ;

كما نقوم بتعديل وحدة التحكم عن طريق نسخ ملف JS [rdvmedecins-05.js] إلى [rdvmedecins-05B.js] ونشير إلى ملف JS الجديد في ملف [app-17B.html] وملف التوجيه [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]، نقوم بإزالة الأسطر التالية من وحدة التحكم:


            // on style la liste déroulante
            $timeout(function(){
              $('.selectpicker').selectpicker();
});

يتم الآن التعامل مع هذه العملية بواسطة التوجيه.

3.7.7.8. اختبار التطبيق – 3

عند اختبار التطبيق الجديد [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"}
  • السطر 1: تهيئة خدمة [dao]؛
  • السطر 2: عند العرض الأولي للطريقة V، يتم تنفيذ التوجيه [selectEnable
  • السطر 3: يظهر هذا السطر عندما ينقر المستخدم على زر [Client List]. يمكننا أن نرى أن توجيه [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>

السطر 1: لا يتم إنشاء علامة <select> إلا في حالة وجود [clients.data]. وهذا ليس هو الحال عند عرض العرض V في البداية. لذلك، لن يتم إنشاء علامة <select>، ولن يتم تقييم توجيه [selectEnable]. عندما ينقر المستخدم على زر [قائمة العملاء]، سيكون لـ [clients.data] قيمة جديدة في نموذج M. نظرًا لتغير النموذج M، سيتم إعادة تقييم العلامة <select> وإنشاؤها هنا. وبالتالي، سيتم تقييم التوجيه [selectEnable] أيضًا. عند تقييمه، لم يتم تقييم الأسطر 2-4 من العلامة <select> بعد. وبالتالي، لدينا قائمة فارغة بالعملاء. إذا كتبنا التوجيه [selectEnable] على النحو التالي:


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

سيتم تنفيذ السطر 5 بقائمة فارغة، وسنرى بعد ذلك قائمة منسدلة فارغة على الشاشة. لذلك يجب أن نكتب:


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] في السطر 5، لن يتم تنفيذ السطر 6 إلا بعد أن يتم عرض طريقة العرض V بالكامل، أي في الوقت الذي تحتوي فيه علامة <select> على جميع عناصرها.

3.7.8. المثال 8: جدول مواعيد الطبيب

نقدم الآن تطبيقًا يعرض جدول مواعيد الطبيب.

3.7.8.1. عرض التطبيق V

سنعرض النموذج التالي:

  • في [1]، نطلب جدول أعمال السيدة بيليسييه [2] ليوم 25 يونيو 2014 [3]؛

تم الحصول على النتيجة التالية [4]:

سنقوم بفحص الرأيين بشكل منفصل.

3.7.8.2. النموذج

نقوم بنسخ الملف [app-17.html] إلى [app-18.html] ثم نعدل الكود على النحو التالي:


<div class="container">
  <h1>Rdvmedecins - v1</h1>
 
  <!-- the waiting message -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    ...
  </div>
 
  <!-- the request -->
  <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>
 
  <!-- the error list -->
  <div class="alert alert-danger" ng-show="errors.show">
...
  </div>
 
  <!-- the diary -->
  <div id="agenda" ng-show="agenda.show">
...
  </div>
</div>
...
<script type="text/javascript" src="rdvmedecins-06.js"></script>
  • الأسطر 5-7: لا تتغير رسالة التحميل؛
  • الأسطر 12-19: قائمة الأطباء باستخدام مكون [bootstrap select
  • الأسطر 20-26: تقويم [ui-bootstrap] الذي قدمناه سابقًا. لاحظ أن اليوم المحدد يتم وضعه في نموذج [calendar.day] (سمة ng-model
  • السطر 28: الزر الذي يطلب التقويم؛
  • الأسطر 32–34: قائمة الأخطاء تبقى دون تغيير؛
  • الأسطر 37-39: التقويم، الذي سنعرضه لاحقًا؛
  • السطر 42: يتم نقل كود JS إلى ملف [rdvmedecins-06.js] عن طريق نسخ ملف [rdvmedecins-05.js

3.7.8.3. وحدة التحكم C

يصبح كود JS للتطبيق كما يلي:

Image

لن تتأثر بالتغييرات سوى خدمة [utils] ووحدة التحكم [rdvMedecinsCtrl].

يصبح وحدة التحكم [rdvMedecinsCtrl] كما يلي:


// controller
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
    function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
       // ------------------- model initialization
      // model
      $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()};
      // style the drop-down list
      $timeout(function () {
        $('.selectpicker').selectpicker();
      });
      // for the French local calendar
      angular.copy(config.locales['fr'], $locale);
 ...
    }
  ])
;
  • السطر 7: نحدد مهلة انتظار مدتها 3 ثوانٍ قبل إرسال طلب HTTP؛
  • السطر 8: العناصر المطلوبة لاتصال HTTP مكتوبة بشكل ثابت؛
  • الأسطر 10-17: قائمة الأطباء مكتوبة برمجيًا؛
  • السطر 18: يقوم نموذج [agenda] بتكوين عرض التقويم في العرض؛
  • السطر 19: يقوم نموذج [calendar] بتكوين عرض التقويم في العرض. نحدد التاريخ الأدنى [minDate] على اليوم والتاريخ الحالي على اليوم أيضًا؛
  • الأسطر 21-23: يتم تصميم القائمة المنسدلة باستخدام الطريقة التي رأيناها سابقًا؛
  • السطر 25: قمنا بتعيين لغة التطبيق على 'fr'. بشكل افتراضي، تكون 'en'؛

الطريقة التي يتم تنفيذها عند طلب التقويم هي كما يلي:


// exécution action
      $scope.execute = function () {
        // les infos du formulaire
        var idMedecin = $('.selectpicker').selectpicker('val');
 
        // vérification
        utils.debug("[homeCtrl] idMedecin", idMedecin);
        utils.debug("[homeCtrl] jour", $scope.calendar.jour);
 
        // on met le jour au format yyyy-MM-dd
        var formattedJour = $filter('date')($scope.calendar.jour, 'yyyy-MM-dd');
        // mise à jour de la vue
        $scope.waiting.visible = true;
        $scope.errors.show = false;
        $scope.agenda.show = false;
...
      };
  • السطر 4: نسترد السمة [value] الخاصة بالطبيب المحدد. هنا، نستخدم مرة أخرى طريقة [selectpicker] من ملف [bootstrap-select.min.js]. تذكر تنسيق خيارات القائمة المنسدلة:

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

وبالتالي، فإن قيمة (سمة value) الخيار هي [id] الطبيب.

  • السطر 11: نقوم بتنسيق التاريخ الذي اختاره المستخدم على النحو التالي [yyyy-mm-dd]، وهو تنسيق التاريخ الذي يتوقعه خادم الويب؛
  • الأسطر 13-15: عند انتهاء طريقة [execute]، سيتم عرض شعار التحميل وإخفاء كل شيء آخر؛

يستمر الكود على النحو التالي:


// simulated waiting
        var task = utils.waitForSomeTime($scope.waiting.time);
        // we ask for the doctor's diary
        var promise = task.promise.then(function () {
          // the URL service path
          var path = config.urlSvrAgenda + "/" + idMedecin + "/" + formattedJour;
          // we ask for the agenda
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
          // we return the promise of task completion
          return task.promise;
        });
        // we analyze the result of the call to service [dao]
        promise.then(function (result) {
          // end of wait
          $scope.waiting.visible = false;
          // mistake?
          if (result.err == 0) {
            // we prepare the agenda model
            $scope.agenda.data = result.data;
            $scope.agenda.show = true;
            // timetable display formatting
            angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
              creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
            });
            // we create an evt to style the table after the view is displayed
            $timeout(function () {
              $("#creneaux").footable();
            });
          } else {
            // mistakes were made in obtaining the agenda
            $scope.errors = {
              title: config.getAgendaErrors,
              messages: utils.getErrors(result),
              show: true
            };
}
  • السطر 2: المهمة غير المتزامنة التي تنتظر لمدة 3 ثوانٍ؛
  • الأسطر 5–10: الكود الذي سيتم تنفيذه عند انتهاء فترة الانتظار هذه؛
  • السطر 6: يتم إنشاء عنوان URL المطلوب [/getAgendaMedecinJour/1/2014-06-25
  • السطر 8: يتم الاستعلام عن عنوان URL. تبدأ مهمة غير متزامنة؛
  • السطر 10: نجعل هذه المهمة غير متزامنة؛
  • الأسطر 14-38: الكود الذي سيتم تنفيذه بمجرد عودة طلب HTTP برده؛
  • السطر 13: [result] هو الرد المرسل بواسطة طريقة [dao.getData]. هنا، يجب أن نتذكر تنسيق رد خادم الويب:

المعلمة [result.data] في السطر 19 هي السمة [data] [1] المذكورة أعلاه. تحتوي هذه السمة بدورها على السمة [creneauxMedecin] [2] المذكورة أعلاه. وهي عبارة عن مصفوفة من الفترات الزمنية، تحتوي كل منها على معلومتين:

  • [rv]: تمثيل JSON لموعد، أو [null] إذا لم يتم تحديد موعد لتلك الفترة الزمنية؛
  • [hDeb, mDeb, hFin, mFin]: معلومات الوقت الخاصة بالفترة؛

لنعد إلى كود وحدة التحكم:

  • السطر 15: انتهى الانتظار؛
  • السطر 19: نملأ نموذج [$scope.agenda]، الذي يتحكم في عرض التقويم؛
  • السطر 20: يتم إظهار التقويم؛
  • الأسطر 22–24: نكرر كل عنصر C في المصفوفة [creneauxMedecin] التي ناقشناها للتو؛
  • السطر 23: يحتوي كل عنصر C على سمة [slot] تمثل الفترة الزمنية. يتم تعزيز ذلك بسمة [text] التي ستكون التمثيل النصي للفترة الزمنية بالتنسيق [10:20–10:40]؛
  • الأسطر 26–28: نجعل جدول HTML المستخدم لعرض فترات التقويم متجاوبًا. وقد تناولنا هذا المفهوم في القسم 3.6.7؛
 
  • السطر 27: لجعل الجدول متجاوبًا، يجب أن نطبق عليه طريقة [footable]. هنا نواجه نفس الصعوبة التي واجهناها مع مكون [bootstrap-select]. إذا كتبنا السطر 17 ببساطة، نرى أن الجدول غير متجاوب. نحل هذه المشكلة بنفس الطريقة باستخدام دالة [$timeout] (السطر 26)؛
  • الأسطر 31–34: الحالة التي فشل فيها طلب HTTP. ثم يتم عرض رسائل الخطأ؛

3.7.8.4. عرض التقويم

نعود الآن إلى كود التقويم في ملف [app-18.html]. وهو كما يلي:


<!-- the diary -->
  <div id="agenda" ng-show="agenda.show">
    <!-- case of a doctor without consultation slots -->
    <h4 class="alert alert-danger" ng-if="agenda.data.creneauxMedecin.length==0"
        translate="agenda_medecinsanscreneaux"></h4>
    <!-- doctor's diary -->
    <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>
  • السطران 4-5: تذكر أن [agenda.data] هو التقويم، وأن [agenda.data.creneauxMedecin] هو مصفوفة من الكائنات من النوع [creneauMedecin]. كل عنصر من هذا النوع له سمة [creneauMedecin.creneau] وهي فترة زمنية. كل فترة زمنية لها عنصران يهماننا:
    • [doctorSlot.slot.appointment] وهو الموعد (إن وجد؛ الموعد ≠ null) المحدد للفتحة؛
    • [doctorSlot.slot.text]، وهو نص [start:end] للفتحة الزمنية؛
  • السطر 4: يعرض رسالة خاصة إذا لم يكن لدى الطبيب أي فترات زمنية. هذا أمر غير مرجح، ولكن اتضح أن قاعدة البيانات لدينا غير مكتملة وأن هذا السيناريو يحدث بالفعل. يتم التحكم في عرض الرسالة بلغة HTML أم لا بواسطة توجيه [ng-if

Image

يختلف التوجيه [ng-if] عن التوجيهات [ng-show، ng-hide]. فهذه الأخيرة تخفي ببساطة منطقة موجودة في المستند. إذا كان [ng-if='false'], فإن المنطقة تُزال من المستند. وقد استخدمناها هنا لأغراض التوضيح؛

  • السطر 9: السمة [id='creneaux'] مهمة. يتم استخدامها في العبارة التالية:

$("#creneaux").footable();
  • الأسطر 10–22: تعرض رؤوس الجدول [1]؛
  • الأسطر 23–45: عرض محتوى الجدول [2]؛
  • السطر 24: نقوم بالتكرار عبر المصفوفة [agenda.data.creneauxMedecin
  • الأسطر 26-29: يتم عرض النص [3]. تُستخدم توجيهات [ng-class] لإنشاء سمة [class] للعنصر. هنا، إذا كان [creneauMedecin.rv == null]، فهذا يعني أن الموعد متاح، ويتم إعطاء النص خلفية خضراء. وإلا، يتم إعطاؤه خلفية حمراء؛
  • السطر 32: نكتب اسم العميل الذي تم تحديد الموعد له [4]. إذا كان [rv==null]، فإن هذه المعلومات غير موجودة، لكن Angular يتعامل مع هذه الحالة بشكل صحيح ولا يظهر خطأً؛
  • الأسطر 34-39: عرض أحد الزرين: [حجز] أو [حذف]. يحدد وجود الموعد من عدمه الزر الذي يتم تحديده؛

3.7.8.5. تعديل خادم الويب

كما في الأمثلة السابقة، يجب تعديل خادم الويب بحيث يرسل عنوان URL [/getAgendaMedecinJour] رؤوس CORS:

  

في فئة [RdvMedecinsCorsController]، أضف طريقة جديدة:


    // doctor's diary
    @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) {
        // headers CORS
        rdvMedecinsCorsController.getAgendaMedecinJour(response);
...
}

3.7.8.6. استخدام التوجيهات

كما فعلنا سابقًا، سننقل معالجة DOM إلى التوجيهات. لدينا عمليتا معالجة DOM:

  • عند عرض العرض لأول مرة:

      // on style la liste déroulante
      $timeout(function () {
        $('.selectpicker').selectpicker();
});
  • عند عرض التقويم:

            // we create an evt to style the table after the view is displayed
            $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>
  • السطر 1: قم بتطبيق توجيه [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>
  • السطر 3: يتم تطبيق توجيه [footable] (عبر السمة [footable]) على جدول HTML الخاص بالتقويم؛

<script type="text/javascript" src="rdvmedecins-06B.js"></script>
<!-- directives -->
<script type="text/javascript" src="selectEnable.js"></script>
<script type="text/javascript" src="footable.js"></script>
  • السطران 3-4: إشارة إلى ملفات JS لكلا التوجيهين؛
  • السطر 1: كود JS من [app-18B.html] هو كود JS من [app-18.html] المكرر في الملف [rdvmedecins-06B.js

ملف [rdvmedecins-06B.js] مطابق لملف [rdvmedecins-06.js] باستثناء تفصيلين. تمت إزالة الأسطر التي تتعامل مع DOM:


      // on style la liste déroulante
      $timeout(function () {
        $('.selectpicker').selectpicker();
});

            // we create an evt to style the table after the view is displayed
            $timeout(function () {
              $("#creneaux").footable();
});

بمجرد الانتهاء من ذلك، يؤدي تشغيل تطبيق [app-18B.html] إلى نفس النتائج التي ينتجها تشغيل [app-18.html].

3.7.9. المثال 9: إنشاء الحجوزات وإلغاؤها

نقدم الآن تطبيقًا يتيح لك إنشاء الحجوزات وإلغاءها.

3.7.9.1. الطريقة V للتطبيق

سنعرض النموذج التالي:

  • في [1]، يمكنك إجراء حجز. سيتم إجراء الحجز لعميل عشوائي؛
  • في [2]، يمكنك حذف الحجوزات التي قمت بها؛

نقوم بنسخ ملف [app-18.html] باسم [app-19.html]، ثم نعدل الكود على النحو التالي:


<div class="container">
  <h1>Rdvmedecins - v1</h1>
 
  <!-- the waiting message -->
  <div class="alert alert-warning" ng-show="waiting.visible">
  ...
  </div>
 
  <!-- the error list -->
  <div class="alert alert-danger" ng-show="errors.show">
...
  </div>
 
  <!-- the diary -->
  <div id="agenda" ng-show="agenda.show">
..
    <!-- doctor's diary -->
    <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>
  • الأسطر 5-7: رسالة التحميل هي نفسها كما في الإصدار السابق؛
  • الأسطر 10-12: رسالة الخطأ هي نفسها الموجودة في الإصدار السابق؛
  • الأسطر 15-36: التقويم هو نفسه كما في الإصدار السابق، مع استثناءين:
    • السطر 26: يتم التعامل مع النقر على زر [book] (سمة ng-click) بواسطة طريقة [reserve] للنموذج M في العرض V. ويتم تمرير رقم فترة الحجز إليها؛
    • السطر 26: يتم التعامل مع النقر على زر [delete] بواسطة طريقة [reserve] للنموذج M في العرض V. ويتم تمرير رقم الموعد المراد حذفه؛
  • السطر 39: يوجد كود JavaScript الذي يدير التطبيق في الملف [rdvmedecins-07.js
  • السطر 40: كود JS لتوجيه [footable] المطبق في السطر 20؛

3.7.9.2. وحدة التحكم C

يتم إنشاء كود JavaScript الخاص بـ [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) {
      // ------------------- model initialization
      // model
      $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"}
      ];
      // for the date
      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}};
 
 
      // ---------------------------------------------------------------- agenda initial
      // the global asynchronous task
      var task;
      // we ask for the agenda
      getAgenda();
 
      // ------------------------------------------------------------------ réservation
      $scope.reserver = function (creneauId) {
....
      };
 
      // ------------------------------------------------------------ suppression RV
      $scope.supprimer = function (idRv) {
...
      };
 
      // obtaining the agenda
      function getAgenda() {
 ...
      }
 
      // cancel wait
      function cancel() {
...
      }
} ]);
  • السطر 6: تكوين رسالة الانتظار. بشكل افتراضي، سننتظر 3 ثوانٍ قبل إجراء طلب HTTP؛
  • السطر 7: المعلومات المطلوبة لطلبات HTTP؛
  • السطر 8: تكوين رسالة الخطأ؛
  • الأسطر 9–17: أسماء الأطباء المحددة مسبقًا؛
  • السطر 18: طبيب معين. سيتم إجراء الحجوزات لمواعيد هذا الطبيب؛
  • الأسطر 19-24: عملاء مبرمجون بشكل ثابت؛
  • السطر 26: نريد التعامل مع التواريخ باللغة الفرنسية؛
  • السطر 27: سيتم تحديد المواعيد ليوم اليوم؛
  • السطر 28: تتوقع خدمة الحجز عبر الويب أن تكون التواريخ بالصيغة 'yyyy-mm-dd
  • السطر 29: تاريخ اليوم بالتنسيق [الخميس، 26 يونيو 2014]؛
  • السطر 30: تكوين التقويم. تحمل السمة [model] معلمات الرسالة الدولية المراد عرضها:

        agenda_title: "Agenda de {{titre}} {{prenom}} {{nom}} le {{jour}}"
  • السطر 35: تمثل المتغير العام [task] المهمة غير المتزامنة التي يتم تنفيذها حاليًا في لحظة معينة؛
  • السطر 37: يتم طلب التقويم الأولي؛

هذا هو كل ما يتم خلال التحميل الأولي للصفحة. إذا سارت الأمور على ما يرام، تعرض الواجهة تقويم السيدة بيليسييه لهذا اليوم.

Image

3.7.9.4. استرداد التقويم

يتم استرداد التقويم باستخدام طريقة [getAgenda] التالية:


      // obtaining the agenda
      function getAgenda() {
        // the URL service path
        var path = config.urlSvrAgenda + "/" + médecin.id + "/" + formattedDay;
        // we ask for the agenda
        task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path);
        // waiting msg
        $scope.waiting.visible = true;
        // we analyze the result of the call to service [dao]
        task.promise.then(function (result) {
          // end of wait
          $scope.waiting.visible = false;
          // mistake?
          if (result.err == 0) {
            // we prepare the agenda model
            $scope.agenda.data = result.data;
            $scope.agenda.show = true;
            // timetable display formatting
            angular.forEach($scope.agenda.data.creneauxMedecin, function (creneauMedecin) {
              creneauMedecin.creneau.text = utils.getTextForCreneau(creneauMedecin.creneau);
            });
          } else {
            // mistakes were made in obtaining the agenda
            $scope.errors = {title: config.getAgendaErrors, messages: utils.getErrors(result), show: true};
          }
        });
}

هذا الكود هو نفسه الذي تمت دراسته في التطبيق السابق. هناك تغييران:

  • لا يوجد انتظار محاكاة قبل استدعاء HTTP؛
  • السطر 4: نستخدم الطبيب الذي تم إنشاؤه أثناء تهيئة وحدة التحكم بالإضافة إلى اليوم المنسق الذي تم إنشاؤه؛

تم عزل هذا الكود في دالة لأنه يُستخدم أيضًا من قبل دالتي [reserve] و [delete].

3.7.9.5. حجز فترة زمنية

تذكر أن العملاء يتم اختيارهم عشوائيًا.

رمز الحجز هو كما يلي:


$scope.reserver = function (creneauId) {
        utils.debug("réservation du créneau", creneauId);
        // we create a RV with a random customer in the slot identified by [id]
        var idClient = clients[Math.floor(Math.random() * clients.length)].id;
        utils.debug("réservation du créneau pour le client", idClient);
        // simulated waiting
        $scope.waiting.visible = true;
        var task = utils.waitForSomeTime($scope.waiting.time);
        // we add the
        var promise = task.promise.then(function () {
          // the URL service path
          var path = config.urlSvrResaAdd;
          // data to be sent to the service
          var post = {jour: formattedDay, idCreneau: creneauId, idClient: idClient};
          // start the asynchronous task
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
          // we return the promise of task completion
          return task.promise;
        });
 
        // task result analysis
        promise = promise.then(function (result) {
          if (result.err != 0) {
            // there were errors in validating the appointment
            $scope.errors = {title: config.postResaErrors, messages: utils.getErrors(result, $filter), show: true};
          } else {
            // we ask for the new agenda
            getAgenda();
          }
        });
 
      };
  • السطر 1: لاحظ أن معلمة دالة [reserve] هي رقم الخانة (سمة id
  • السطر 4: يتم اختيار عميل عشوائيًا من قائمة العملاء المحددة مسبقًا في كود التهيئة. ونحتفظ بمعرّفه [id]؛
  • السطران 7-8: فترة الانتظار لمدة 3 ثوانٍ؛
  • السطور 11-18: لا يتم تنفيذ هذه السطور إلا بعد انقضاء 3 ثوانٍ؛
  • السطر 12: عنوان URL لخدمة الحجز [/ajouterRv]. يختلف عنوان URL هذا عن العناوين التي صادفناها حتى الآن. يتم تعريفه على النحو التالي في خدمة الويب:

    @RequestMapping(value = "/ajouterRv", method = RequestMethod.POST, consumes = "application/json; charset=UTF-8")
public Reponse ajouterRv(@RequestBody PostAjouterRv post, HttpServletResponse response) {
  • (تابع)
    • السطر 1: لا يحتوي عنوان URL على معلمات ويتم طلبه عبر POST؛
    • السطر 2: المعلمات المرسلة تأتي في شكل كائن JSON. سيتم تحويلها إلى المعلمة [post] (@RequestBody

لقد رأينا مثالاً على هذا الطلب POST (القسم 2.12.2):

  • في [0]، عنوان URL لخدمة الويب؛
  • في [1]، يتم استخدام طريقة POST؛
  • في [2]، نص JSON للمعلومات المرسلة إلى خدمة الويب في النموذج {day, clientId, slotId
  • في [3]، يُعلم العميل خدمة الويب بأنه يرسل بيانات JSON؛

لنعد إلى كود JS الخاص بوظيفة [reserve]:

  • السطر 14: نقوم بإنشاء القيمة المراد نشرها في شكل كائن JS. سيقوم Angular بتسلسلها إلى JSON عند نشرها؛
  • السطر 16: يتم إجراء طلب HTTP. القيمة المراد نشرها هي المعلمة الأخيرة لوظيفة [dao.getData]. عند وجود هذه المعلمة، تقوم وظيفة [dao.getData] بإجراء POST بدلاً من GET (انظر الكود في القسم 3.7.6.4
  • السطر 18: يتم إرجاع الوعد من استدعاء HTTP؛
  • الأسطر 23-29: يتم تنفيذها فقط عندما يعود استدعاء HTTP برده؛
  • السطر 23: المعلمة [result] تكون في صيغة [err,data] أو [err,messages]، حيث [err] هو رمز الخطأ؛
  • الأسطر 23–26: في حالة وجود أخطاء، يتم عرض رسالة الخطأ؛
  • السطر 28: إذا نجح الحجز، يتم عرض التقويم الجديد مرة أخرى؛

3.7.9.6. تعديل الخادم

  

في فئة [RdvMedecinsCorsController]، نضيف الطريقة التالية:


    // sending options to the customer
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // set header CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
            // we authorize the header [authorization]
            response.addHeader("Access-Control-Allow-Headers", "authorization");
        }
 
    @RequestMapping(value = "/ajouterRv", method = RequestMethod.OPTIONS)
    public void ajouterRv(HttpServletResponse response) {
        sendOptions(response);
}

يتم إجراء الإضافة في الأسطر 10–13. سيتم إرسال الرؤوس في الأسطر 2–8 لعنوان URL [/addAppt] (السطر 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) {
        // headers CORS
        rdvMedecinsCorsController.ajouterRv(response);
...

بالنسبة لطريقة [POST] (السطر 1) وعنوان URL [/addAppointment] (السطر 1)، يتم استدعاء الطريقة التي أضفناها للتو إلى [RdvMedecinsCorsController] (السطر 4)، وبالتالي يتم إرجاع نفس رؤوس HTTP المستخدمة في طريقة HTTP [OPTIONS].

3.7.9.7. الاختبارات

دعونا نجري اختبارًا أوليًا حيث نحجز أي موعد متاح:

 

كما هو الحال دائمًا في مثل هذه الحالات، نحتاج إلى التحقق من سجلات وحدة التحكم:


[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 header field Content-Type is not allowed by 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:


    // sending options to the customer
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // set header CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
            // certain headers are allowed
            response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
            // the POST is authorized
            response.addHeader("Access-Control-Allow-Methods", "POST");
        }
}
  • السطر 7: أضفنا تفويضًا لرؤوس HTTP [accept, content-type
  • السطر 9: أضفنا تفويضًا لطريقة POST؛

أعدنا تشغيل الاختبار بعد إعادة تشغيل الخادم:

 

هذه المرة، تم الحجز بنجاح.

3.7.9.8. حذف موعد

فيما يلي كود وظيفة [delete]:


$scope.supprimer = function (idRv) {
        utils.debug("suppression rv n°", idRv);
        // simulated waiting
        $scope.waiting.visible = true;
        task = utils.waitForSomeTime($scope.waiting.time);
        // we add the
        var promise = task.promise.then(function () {
          // the URL service path
          var path = config.urlSvrResaRemove;
          // data to be sent to the service
          var post = {idRv: idRv};
          // start the asynchronous task
          task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, path, post);
          // we return the promise of task completion
          return task.promise;
        });
 
        // task result analysis
        promise = promise.then(function (result) {
          if (result.err != 0) {
            // there have been errors deleting the rv
            $scope.errors = {title: config.postRemoveErrors, messages: utils.getErrors(result, $filter), show: true};
            // the UI is updated
            $scope.waiting.visible = false;
          } else {
            // we ask for the new agenda
            getAgenda();
          }
        });
      };
  • السطر 1: تذكر أن معلمة الدالة هي معرف الموعد المراد حذفه. هذا الكود مشابه جدًا لكود الحجز. سنعلق فقط على الاختلافات؛
  • السطر 9: عنوان URL للخدمة هنا هو [/deleteAppointment]، وكما في السابق، يتم الوصول إليه عبر طلب 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 الخاص بوظيفة [delete]:

  • السطر 11: نقوم بإنشاء الكائن المنشور. سيقوم Angular تلقائيًا بتحويله إلى JSON؛

باقي الكود مشابه لكود الحجز.

3.7.9.9. التغييرات على جانب الخادم

على جانب الخادم، نقوم بإجراء التغييرات التالية:

  

في فئة [RdvMedecinsCorsController]، نضيف الطريقة التالية:


    // sending options to the customer
    private void sendOptions(HttpServletResponse response) {
        if (application.isCORSneeded()) {
            // set header CORS
            response.addHeader("Access-Control-Allow-Origin", "*");
            // certain headers are allowed
            response.addHeader("Access-Control-Allow-Headers", "accept, authorization, content-type");
            // the POST is authorized
            response.addHeader("Access-Control-Allow-Methods", "POST");
        }
    }
...
    @RequestMapping(value = "/supprimerRv", method = RequestMethod.OPTIONS)
    public void supprimerRv(HttpServletResponse response) {
        sendOptions(response);
}

تمت الإضافة في الأسطر 13–16. سيتم إرسال الرؤوس من الأسطر 2–10 لعنوان URL [/deleteAppointment] (السطر 13) وطريقة HTTP [OPTIONS] (السطر 13).

يتم تعديل فئة [RdvMedecinsController] على النحو التالي:


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

بالنسبة لطريقة [POST] (السطر 1) وعنوان URL [/deleteAppointment] (السطر 1)، يتم استدعاء الطريقة التي أضفناها للتو إلى [RdvMedecinsCorsController] (السطر 4)، وبالتالي يتم إرجاع نفس رؤوس HTTP المستخدمة في طريقة HTTP [OPTIONS].

3.7.10. مثال 10: إنشاء وإلغاء المواعيد - 2

نقدم الآن نفس التطبيق كما في السابق، ولكن بدلاً من الحجز لعميل عشوائي، سيتم اختيار العميل من قائمة منسدلة.

3.7.10.1. طريقة العرض V للتطبيق

سنقدم النموذج التالي:

سيتم اختيار العملاء في [1].

الرمز مشابه لرمز التطبيق السابق، لذا سنعرض فقط الاختلافات الرئيسية.

نقوم بنسخ الملف [app-19.html] إلى [app-20.html]، ثم ننشئ الكود الخاص بقائمة العملاء المنسدلة [1]:


<!-- customer list -->
  <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>
  • الأسطر 8–12: سيتم تنفيذ القائمة المنسدلة باستخدام مكون [bootstrap-select
  • السطر 1: يتم تطبيق توجيه [selectEnable] عبر السمة [select-enable
  • السطر 1: يتم إنشاء العلامة <select> فقط في حالة وجود [clients.data] (# null، undefined). هذه النقطة مهمة وقد تم شرحها في القسم 3.7.7.8؛

بالإضافة إلى ذلك، نقوم باستيراد ملفات JS جديدة:


<script type="text/javascript" src="rdvmedecins-08.js"></script>
<!-- directives -->
<script type="text/javascript" src="selectEnable.js"></script>
<script type="text/javascript" src="footable.js"></script>
  • السطر 1: يتم إنشاء الملف [rdvmedecins-08.js] عن طريق نسخ الملف [rdvmedecins-0.js
  • السطران 3-4: يتم استيراد الملفات الخاصة بكلا التوجيهين؛

3.7.10.2. وحدة التحكم C

يتطور كود وحدة التحكم C على النحو التالي:


// controller
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao', '$translate', '$timeout', '$filter', '$locale',
    function ($scope, utils, config, dao, $translate, $timeout, $filter, $locale) {
      // ------------------- model initialization
...
      // our customers
      $scope.clients = {title: config.listClients, show: false, model: {}};
 
      //------------------------------------------- initilisation vue
      // the global asynchronous task
      var task;
      // we ask for the customers, then the agenda
      getClients().then(function () {
        getAgenda();
      });
...
 
      // execution action
      function getClients() {
....
      };
} ]);
  • السطر 8: يقوم الكائن [$scope.clients] بتكوين قائمة العملاء المنسدلة في العرض V؛
  • الأسطر 14–16: بشكل غير متزامن، نطلب أولاً قائمة العملاء، ثم، بمجرد الحصول عليها، نطلب جدول أعمال السيدة PELISSIER لليوم. لا تعمل الصيغة المستخدمة هنا إلا لأن الدالة [getClients] تُرجع وعدًا؛

تسترد طريقة [getClients] قائمة العملاء:


function getClients() {
        // the UI is updated
        $scope.waiting.visible = true;
        $scope.clients.show = false;
        $scope.errors.show = false;
        // we ask for the customer list;
        task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
        var promise = task.promise;
        // analyze the result of the previous call
        promise = promise.then(function (result) {
          // result={err: 0, data: [client1, client2, ...]}
          // result={err: n, messages: [msg1, msg2, ...]}
          if (result.err == 0) {
            // we put the acquired data into the model
            $scope.clients.data = result.data;
            // the UI is updated
            $scope.clients.show = true;
            $scope.waiting.visible = false;
          } else {
            // there were errors in obtaining the customer list
            $scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
            // the UI is updated
            $scope.waiting.visible = false;
          }
        });
        // we return the promise
        return promise;
      };

هذا هو الكود الذي سبق أن تناولناه وناقشناه. الجزء المهم الذي يجب ملاحظته هو السطر 31:

  • السطر 27: نُرجع الوعد من السطر 10، أي آخر وعد تم الحصول عليه في الكود. لن يتم الوفاء بهذا الوعد إلا بعد أن يعود طلب HTTP برده؛

تتغير طريقة [reserve] قليلاً:


      $scope.reserver = function (creneauId) {
        utils.debug("réservation du créneau", creneauId);
        // on crée un RV pour le client sélectionné
        var idClient = $(".selectpicker").selectpicker('val');
        ...
        });
  • السطر 4: لم نعد نقوم بالحجز لعميل عشوائي، بل للعميل الذي تم اختياره من قائمة العملاء.

3.7.11. المثال 11: توجيه [selectEnable2]

يعيد هذا المثال النظر في التوجيهات.

3.7.11.1. طريقة العرض V

يعرض التطبيق العرض التالي:

 

3.7.11.2. كود HTML للطريقة

كود HTML لعرض [app-21.html] هو كما يلي:


<div class="container">
  <h1>Rdvmedecins - v1</h1>
 
  <!-- the waiting message -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    ...
  </div>
 
  <!-- the error list -->
  <div class="alert alert-danger" ng-show="errors.show">
   ...
  </div>
 
  <!-- customer list -->
  <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>
 
  <!-- list of doctors -->
  <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>
<!-- guidelines -->
<script type="text/javascript" src="selectEnable2.js"></script>
  • الأسطر 19–23: القائمة المنسدلة للعميل؛
  • السطر 19: يتم تطبيق توجيه [selectEnable2] (السمة [select-enable2])؛
  • السطر 19: فقط إذا لم تكن [clients.data] فارغة؛
  • السطر 19: يتم تحديد القائمة المنسدلة بواسطة السمة [id="selectpickerClients"
  • الأسطر 33-37: القائمة المنسدلة للأطباء؛
  • السطر 33: يتم تطبيق توجيه [selectEnable2] (السمة [select-enable2])؛
  • السطر 33: فقط إذا لم يكن [doctors.data] فارغًا؛
  • السطر 33: يتم تحديد القائمة المنسدلة بواسطة السمة [id="selectpickerMedecins"];
  • السطر 43: يتم استيراد ملف JS جديد [rdvmedecins-09.js
  • السطر 45: يتم استيراد ملف 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();
      })
    }
  }
}]);
  • السطر 4: نعرض قيمة المعلمة [attrs] للمساعدة في فهم كيفية عمل الكود. سنرى أن attrs['id']='selectpickerClients' لقائمة العملاء؛
  • السطر 6: لتحديد موقع عنصر بـ [id='x'] في DOM، نكتب [$('#x')]. لذلك، يجب أن نكتب [$('#selectpickerClients')] لتحديد موقع قائمة العملاء. يتم تحقيق ذلك باستخدام الصيغة [$('#' + attrs['id'])];

وبالتالي، تستخدم توجيهات [selectEnable2] المعلومات التي تحملها إحدى سمات عنصر HTML الذي يتم تطبيقها عليه.

3.7.11.4. وحدة التحكم C

توجد وحدة التحكم C في ملف JS [rdvmedecins-09.js] وتتخذ الشكل التالي:


// controller
angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
    function ($scope, utils, config, dao) {
      // ------------------- model initialization
      // the waiting msg
      $scope.waiting = {text: config.msgWaiting, visible: false, cancel: cancel, time: 3000};
      // login information
      $scope.server = {url: 'http://localhost:8080', login: 'admin', password: 'admin'};
      // errors
      $scope.errors = {show: false, model: {}};
      // the doctors
      $scope.medecins = {title: config.listMedecins, show: false, model: {}};
      // our customers
      $scope.clients = {title: config.listClients, show: false, model: {}};
 
      // the global asynchronous task
      var task;
      // ---------------------------------------------------- initialisation vue
      // the UI is updated
      $scope.waiting.visible = true;
      $scope.clients.show = false;
      $scope.medecins.show = false;
      $scope.errors.show = false;
      // we ask for customers, then doctors
      getClients().then(function () {
        getMedecins();
      });
 
      // customer list
      function getClients() {
        ...
      }
 
      // list of doctors
      function getMedecins() {
...
      }
 
      // cancel wait
      function cancel() {
...
      }
    } ]);
  • الأسطر 26–28: أولاً نستعلم عن العملاء، ثم الأطباء؛

3.7.11.5. الاختبارات

اختبر هذه النسخة الجديدة.

3.7.12. المثال 12: توجيه [list]

سنستخدم نفس المثال السابق، لكننا نريد تبسيط كود HTML باستخدام توجيه. حالياً، لدينا كود HTML التالي:


<!-- customer list -->
  <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>
  <!-- list of doctors -->
  <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>

الأسطر 14–26 مطابقة للأسطر 1–13. وهي تنطبق على الأطباء بدلاً من العملاء. نود أن نتمكن من كتابة ما يلي:


  <!-- la liste des clients -->
  <list model="clients" ng-if="clients.show"></list>
  <!-- la liste des médecins -->
<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) {
    // instance de la directive retournée
    return {
      // élément HTML
      restrict: "E",
      // url du fragment
      templateUrl: "list.html",
      // scope unique à chaque instance de la directive
      scope: true,
      // fonction lien avec le document
      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();
        })
      }
    }
}]);
  • السطر 2: يحدد توجيهًا باسم 'list
  • السطر 6: تحدد السمة [restrict] كيفية استخدام التوجيه. [restrict: "E"] تعني أنه يمكن استخدام توجيه [list] كعنصر HTML <list ...>...</list>. [restrict: "A"] تعني أن التوجيه [list] يمكن استخدامه كسمة، على سبيل المثال <div ... list='...'>. [restrict: "AE"] تعني أن التوجيه [list] يمكن استخدامه كسمة وكعنصر؛
  • السطر 8: تحدد السمة [templateUrl] اسم جزء HTML الذي سيتم استخدامه عند العثور على العلامة. سيكون هذا الجزء هو نص العلامة؛
  • السطر 10: تحدد السمة [scope] نطاق قالب التوجيه. [scope: true] تعني أن عنصري <list> سيكون لكل منهما قالبه الخاص. بشكل افتراضي (scope غير مهيأ)، يتشاركان قوالبهم؛
  • السطر 12: الدالة [link]، التي استخدمناها عدة مرات بالفعل؛

لفهم الكود أعلاه، عليك أن تتذكر كيف سيتم استخدام التوجيه:


  <!-- la liste des clients -->
  <list model="clients" ng-if="clients.show"></list>
  <!-- la liste des médecins -->
<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>
  • السطر 3: attrs['model'] له القيمة 'clients' هنا؛
  • السطر 3: scope[attrs['model']] له القيمة scope['clients'] وبالتالي يمثل [$scope.clients]، أي حقل [clients] في نموذج العرض. سيكون لهذا الحقل القيمة {id:'...', data:[client1, client2, ...], show:..., title:'...'};
  • السطر 3: نضيف حقل [model] إلى نموذج التوجيه. يرث هذا الحقل من نموذج العرض الذي يوجد فيه. لذلك يجب أن نتجنب التضارب مع أي حقل [model] قد يحتوي عليه العرض أيضًا. هنا، لن يكون هناك تضارب؛
  • السطر 4: نعرض [scope.model] لفهم الكود بشكل أفضل؛
  • الأسطر 5-7: نرى كودًا سبق أن صادفناه من قبل. الفرق هو أن معرف المكون كان يُسترد سابقًا من سمة attrs['id']. هنا، سيتم استرداده من [scope.model.id

الآن، دعونا نلقي نظرة على كود HTML الذي تم إنشاؤه بواسطة التوجيه. بسبب سمة [templateUrl: "list.html"] الخاصة بالتوجيه، نحتاج إلى البحث عنها في ملف [list.html]:


<!-- a list of customers or doctors -->
<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 الخاص بالتوجيه؛
  • السطر 2: استخدام [model.show] لإظهار/إخفاء العرض الذي أنشأته التوجيهية؛
  • السطر 5: استخدام [model.title] لتعيين عنوان؛
  • السطر 6: استخدام [model.id] لتعيين معرف لعلامة <select>. يستخدم كود JavaScript الخاص بالتوجيه هذا المعرف؛
  • السطر 6: استخدام [model.data] لإنشاء <select> فقط في حالة وجود بيانات لعرضها؛
  • الأسطر 7-9: استخدام [model.data] لإنشاء عناصر القائمة المنسدلة؛

3.7.12.2. كود HTML

كود HTML للتطبيق [app-22.html] هو كما يلي:


<div class="container">
  <h1>Rdvmedecins - v1</h1>
 
  <!-- the waiting message -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    ...
  </div>
 
  <!-- the error list -->
  <div class="alert alert-danger" ng-show="errors.show">
    ...
  </div>
 
  <!-- customer list -->
  <list model="clients" ng-if="clients.show"></list>
  <!-- list of doctors -->
  <list model="medecins" ng-if="medecins.show"></list>
</div>
...
<script type="text/javascript" src="rdvmedecins-10.js"></script>
<!-- guidelines -->
<script type="text/javascript" src="list.js"></script>
  • السطر 22: لا تنسَ تضمين كود JS الخاص بالتوجيه؛

3.7.12.3. وحدة التحكم C

لم تتغير وحدة التحكم C إلا قليلاً:


angular.module("rdvmedecins")
  .controller('rdvMedecinsCtrl', ['$scope', 'utils', 'config', 'dao',
    function ($scope, utils, config, dao) {
      // ------------------- model initialization
...
      // the doctors
      $scope.medecins = {title: config.listMedecins, show: false, id: 'medecins'};
      // our customers
      $scope.clients = {title: config.listClients, show: false, id: 'clients'};
...
  • السطران 7 و9: نضيف السمة [id] إلى نماذج الأطباء والعملاء؛

3.7.12.4. الاختبارات

تُظهر الاختبارات نفس النتائج كما في المثال السابق.

3.7.13. المثال 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>
 
  <!-- le message d'attente -->
  <div class="alert alert-warning" ng-show="waiting.visible">
    ...
  </div>
 
  <!-- la liste d'erreurs -->
  <div class="alert alert-danger" ng-show="errors.show">
    ...
  </div>
 
  <!-- le bouton -->
  <div class="alert alert-warning">
    <button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
  </div>
 
  <!-- la liste des clients -->
  <list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-11.js"></script>
<!-- directives -->
<script type="text/javascript" src="list2.js"></script>

التغييرات عن التطبيق السابق هي كما يلي:

  • الأسطر 15–17: إضافة زر؛
  • السطر 20: استخدام توجيه جديد [list2
  • السطر 23: استخدام ملف JS جديد؛
  • السطر 25: استيراد ملف JS من التوجيه [list2

3.7.13.3. التوجيه [list2]

التوجيه [list2] في [list2.js] هو كما يلي:


angular.module("rdvmedecins")
  .directive("list2", ['utils', '$timeout', function (utils, $timeout) {
    // instance de la directive retournée
    return {
      // élément HTML
      restrict: "E",
      // url du fragment
      templateUrl: "list.html",
      // scope unique à chaque instance de la directive
      scope: true,
      // fonction lien avec le document
      link: function (scope, element, attrs) {
        utils.debug('directive list2');
        scope.model = scope[attrs['model']];
        $timeout(function () {
          $('#' + scope.model.id).selectpicker('refresh');
        })
      }
    }
}]);

الفرق الوحيد عن توجيه [list] هو السطر 16: باستخدام طريقة [selectpicker('refresh')]، نطلب من مكون [Bootstrap-select] التحديث. الفكرة من وراء ذلك هي أنه في كل مرة يطلب فيها المستخدم قائمة جديدة بالعملاء، سيتم تحديث القائمة المنسدلة. لن يعمل ذلك، لكن هذه هي الفكرة الأساسية.

3.7.13.4. وحدة التحكم C

توجد وحدة التحكم في ملف [rdvmedecins-11.js]، الذي تم إنشاؤه عن طريق نسخ ملف [rdvmedecins-10.js]:


      // our customers
      $scope.clients = {title: config.listClients, show: false, id: 'clients', data: []};
...
      // customer list
      $scope.getClients = function getClients() {
        // the UI is updated
        $scope.waiting.visible = true;
        $scope.errors.show = false;
        // we ask for the customer list;
        task = dao.getData($scope.server.url, $scope.server.login, $scope.server.password, config.urlSvrClients);
        var promise = task.promise;
        // analyze the result of the previous call
        promise = promise.then(function (result) {
          // result={err: 0, data: [client1, client2, ...]}
          // result={err: n, messages: [msg1, msg2, ...]}
          if (result.err == 0) {
             // put the acquired data into a new model to force the view to refresh
            $scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};
            // the UI is updated
            $scope.clients.show = true;
            $scope.waiting.visible = false;
          } else {
            // there were errors in obtaining the customer list
            $scope.errors = { title: config.getClientsErrors, messages: utils.getErrors(result), show: true, model: {}};
            // the UI is updated
            $scope.waiting.visible = false;
          }
        });
}
  • السطر 1: للسماح بضم المصفوفات في [clients.data]، يتم تهيئة هذا الكائن بمصفوفة فارغة؛
  • السطر 18: نقوم بربط قائمة العملاء الجديدة بتلك الموجودة بالفعل في المصفوفة [clients.data

في السابق، كنا قد كتبنا:

// we put the acquired data into the model
$scope.clients.data = result.data;

الآن نكتب:

// put the acquired data into a new model to force the view to refresh
$scope.clients = {title: $scope.clients.title, data: $scope.clients.data.concat(result.data), show: $scope.clients.show, id: $scope.clients.id};

لفهم هذا الكود، عليك أن تتذكر كيف يتم استخدام النموذج M في العرض V في حالة التوجيه [list2]:


  <!-- la liste des clients -->
<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]. لست على دراية بتفاصيل JavaScript، ولكن لن يكون من المستغرب أن تكون [clients] مؤشرًا، مثل [clients.data]. لا يتغير المؤشر [clients] عندما نغير المؤشر [clients.data]. وبالتالي، لا يتم إعادة تقييم التوجيه [list2]. وهذا بالفعل ما نلاحظه عند تصحيح أخطاء التطبيق (F12 في Chrome).

بكتابة:

$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"}
  • السطر 1: يتم إنشاء مثيل لخدمة [dao]؛
  • السطر 2: تسترد خدمة [dao] قائمة أولية بالعملاء؛
  • السطر 3: يتم تنفيذ توجيه [list2
  • السطر 4: تسترد خدمة [dao] قائمة ثانية من العملاء؛

يأتي الناتج في السطر 2 من الكود التالي في التوجيه:


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

دعونا ندرس دورة حياة التوجيه [list2]:

  • بين السطرين 1 و 2، لا يتم تنشيطها على الرغم من عرض العرض لأول مرة. ويرجع ذلك إلى سمة [ng-if="clients.show"] في العرض V:

<list2 model="clients" ng-if="clients.show"></list2>
  • السطر 3: بعد استرداد القائمة الأولى للأطباء، تصبح [clients.show] صحيحة ويتم تنشيط التوجيه؛
  • بعد استرداد القائمة الثانية للعملاء، نرى أن كود التوجيه [list2] لم يتم استدعاؤه. ولهذا السبب لا نرى القائمة الثانية؛

لحل هذه المشكلة، نقوم بتعديل التوجيه [list2] على النحو التالي:


angular.module("rdvmedecins")
  .directive("list2", ['utils', '$timeout', function (utils, $timeout) {
    // instance de la directive retournée
    return {
      // élément HTML
      restrict: "E",
      // url du fragment
      templateUrl: "list.html",
      // scope unique à chaque instance de la directive
      scope: true,
      // fonction lien avec le document
      link: function (scope, element, attrs) {
        // à chaque fois que attrs["model"] change, le modèle de la directive doit changer également
        scope.$watch(attrs["model"], function (newValue) {
          utils.debug("directive list2 newValue", newValue);
          // on met à jour le modèle de la directive
          scope.model = newValue;
          $timeout(function () {
            $('#' + scope.model.id).selectpicker('refresh');
          })
        });
      }
    }
}]);
  • السطر 14: تتيح لك الدالة [scope.$watch] مراقبة قيمة في النموذج. وصيغتها هي [scope.$watch('var'), f]، حيث [var] هو معرف متغير في النموذج و f هي الدالة التي سيتم تنفيذها عندما تتغير قيمة ذلك المتغير. هنا، نريد مراقبة المتغير [clients]. لذا يجب أن نكتب [scope.$watch('clients)']. وبما أن لدينا attrs['model']='clients'، فإننا نكتب [scope.$watch(attrs["model"], function (newValue)];
  • السطر 14: المعلمة الثانية لدالة [scope.$watch] هي الدالة التي سيتم تنفيذها عندما تتغير قيمة المتغير المراقب. المعلمة [newValue] هي القيمة الجديدة للمتغير، أي بالنسبة لنا، القيمة الجديدة لمتغير [clients] في النموذج؛
  • السطر 17: يتم تعيين هذه القيمة الجديدة لحقل [model] في نموذج التوجيه؛

بعد إجراء هذا التغيير، تتغير السجلات:

في الأعلى، نرى أنه بعد الحصول على القائمة الثانية من العملاء، يتم بالفعل تنفيذ التوجيه [list2] مرة أخرى، كما تؤكد النتيجة [2].

3.7.14. المثال 14: التوجيهات [waiting] و [errors]

لنعد إلى كود HTML من التطبيق السابق:


<div class="container">
  <h1>Rdvmedecins - v1</h1>
 
  <!-- le message d'attente -->
  <div class="alert alert-warning" ng-show="waiting.visible">
  ...
  </div>
 
  <!-- la liste d'erreurs -->
  <div class="alert alert-danger" ng-show="errors.show">
  ...
  </div>
 
  <!-- le bouton -->
  <div class="alert alert-warning">
    <button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
  </div>
 
  <!-- la liste des clients -->
  <list2 model="clients" ng-if="clients.show"></list2>
</div>
  • الأسطر 5-7: رسالة التحميل؛
  • الأسطر 10-12: رسالة الخطأ؛

قررنا وضع كود HTML لهاتين الرسالتين داخل توجيهات.

3.7.14.1. كود HTML الجديد

كود HTML الجديد [app-24.html] هو كما يلي:


<div class="container">
  <h1>Rdvmedecins - v1</h1>
 
  <!-- le message d'attente -->
  <waiting model="waiting"></waiting>
 
  <!-- la liste d'erreurs -->
  <errors model="errors"></errors>
 
  <!-- le bouton -->
  <div class="alert alert-warning">
    <button class="btn btn-primary" ng-click="getClients()">{{clients.title|translate}}</button>
  </div>
 
  <!-- la liste des clients -->
  <list2 model="clients" ng-if="clients.show"></list2>
</div>
...
<script type="text/javascript" src="rdvmedecins-12.js"></script>
<!-- directives -->
<script type="text/javascript" src="list2.js"></script>
<script type="text/javascript" src="errors.js"></script>
<script type="text/javascript" src="waiting.js"></script>
  • السطر 5: التوجيه الخاص برسالة الانتظار؛
  • السطر 8: التوجيه الخاص برسالة الخطأ؛
  • السطر 19: ملف JS الجديد المرتبط بالتطبيق؛
  • الأسطر 21–23: ملفات JS الخاصة بالتوجيهات الثلاثة؛

3.7.14.2. التوجيه [waiting]

يوجد كود JS للتوجيه [waiting] في ملف [waiting.js] التالي:


angular.module("rdvmedecins")
  .directive("waiting", ['utils', function (utils) {
    // returned directive instance
    return {
      // element HTML
      restrict: "E",
      // fragment url
      templateUrl: "waiting.html",
      // scope unique to each directive instance
      scope: true,
      // function link to document
      link: function (scope, element, attrs) {
        // each time attr["model"] changes, the page model must also change
        scope.$watch(attrs["model"], function (newValue) {
          utils.debug("[waiting] watch newValue", newValue);
          scope.model = newValue;
        });
      }
    }
  }]);

يتبع هذا الرمز نفس منطق توجيه [list2] الذي تمت مناقشته سابقًا.

في السطر 8، نشير إلى الملف [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 على النحو التالي:


// the waiting msg
$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) {
    // returned directive instance
    return {
      // element HTML
      restrict: "E",
      // fragment url
      templateUrl: "errors.html",
      // scope unique to each directive instance
      scope: true,
      // function link to document
      link: function (scope, element, attrs) {
        // each time attr["model"] changes, the page model must also change
        scope.$watch(attrs["model"], function (newValue) {
          utils.debug("[errors] watch newValue", newValue);
          scope.model = newValue;
        });
      }
    }
}]);

يتبع هذا الرمز نفس منطق توجيه [list2] الذي تمت مناقشته سابقًا.

في السطر 8، نشير إلى الملف [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 على النحو التالي:


// there were errors in obtaining the customer list
$scope.errors = { title: { text: config.getClientsErrors, values: {}}, messages: utils.getErrors(result), show: true, model: {}};

3.7.15. المثال 15: التنقل

حتى الآن، استخدمنا تطبيقات ذات صفحة واحدة. في هذا المثال، سنتناول التطبيقات متعددة الصفحات والتنقل بينها.

3.7.15.1. طرق العرض V للتطبيق

  • في [1]، عنوان URL للعرض رقم 1؛
  • في [2]، محتواه؛
  • في [3]، ننتقل إلى الصفحة 2؛
  • في [4]، العرض رقم 2؛
  • في [5]، ننتقل إلى الصفحة 3؛
  • في [6]، عرض رقم 3؛
  • في [7]، ننتقل إلى الصفحة 1؛
  • في [8]، نعود إلى العرض رقم 1؛

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">
        <!-- the navigation bar -->
        <ng-include src="'views/navbar.html'"></ng-include>
 
        <!-- the current view -->
        <ng-view></ng-view>
    </div>
 
...
<!-- the module -->
<script type="text/javascript" src="modules/rdvmedecins-13.js"></script>
<!-- controllers -->
<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>
  • السطر 7: يتم التحكم في نص الحاوية بواسطة [mainCtrl
  • السطر 9: تسمح لك توجيهات [ng-include] بتضمين ملف HTML خارجي، وهو في هذه الحالة شريط التنقل؛
  • السطر 12: يتم عرض طرق العرض المختلفة التي يعرضها الحاوية ضمن توجيه [ng-view]. في النهاية، لدينا حاوية تعرض:
    • شريط التنقل نفسه دائمًا (السطر 9)؛
    • طرق عرض مختلفة في السطر 12؛
  • الأسطر 16-22: نقوم باستيراد ملفات JS من وحدة التطبيق [rdvmedecins-13.js] ووحدات التحكم الخاصة بها؛

3.7.15.4. وحدة التطبيق

يحدد ملف [rdvmedecins-13.js] وحدة التطبيق والتوجيه بين العروض:


// --------------------- Angular module
angular.module("rdvmedecins", [ 'ngRoute' ]);
 
angular.module("rdvmedecins").config(["$routeProvider", function ($routeProvider) {
// ------------------------ routage
  $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"
    });
}]);
  • السطر 1: يحدد الوحدة النمطية [rdvmedecins]. وهي تعتمد على الوحدة النمطية [ngRoute] التي توفرها مكتبة [angular-route.min.js]. تتيح هذه الوحدة النمطية التوجيه المحدد في الأسطر 6–24؛
  • السطر 4: يحدد وظيفة [config] للوحدة النمطية [rdvmedecins]. لاحظ أن هذه الوظيفة يتم تنفيذها قبل إنشاء أي مثيل للخدمة. إنها وظيفة تكوين الوحدة النمطية. هنا، يتم تكوين التوجيه الخاص بها. يتم ذلك باستخدام الكائن [$routeProvider] المقدم من الوحدة النمطية [ngRoute
  • الأسطر 6-10: تحدد العرض الذي سيظهر عندما يطلب المستخدم عنوان URL [/page1]. هذا توجيه داخلي داخل التطبيق. عنوان URL هو في الواقع [/rdvmedecins-angular-v1/app-21.html#/page1]. يمكننا أن نرى أنه لا يزال يتم استخدام عنوان URL للحاوية [/rdvmedecins-angular-v1/app-21.html]، ولكن مع معلومات إضافية تلي الحرف #. هذه المعلومات الإضافية هي التي يتعامل معها توجيه Angular؛
  • السطر 8: يحدد جزء HTML المراد إدراجه في توجيه [ng-view] للحاوية:
  • السطر 9: يحدد اسم وحدة التحكم لهذا الجزء؛
  • الأسطر 11–15: تحدد العرض الذي سيتم عرضه عندما يطلب المستخدم عنوان URL [/page2
  • الأسطر 16-20: تحدد العرض الذي سيتم عرضه عندما يطلب المستخدم عنوان URL [/page3
  • الأسطر 21-24: تحدد التوجيه الذي سيتم تنفيذه عندما لا يكون عنوان URL المطلوب أحد العناوين الثلاثة السابقة (وإلا، انظر السطر 21)؛
  • السطر 23: يعيد التوجيه إلى عنوان URL [/page1]، وبالتالي إلى العرض المحدد في الأسطر 6-10؛

3.7.15.5. وحدة التحكم في حاوية العرض

لقد رأينا أن حاوية العرض أعلنت عن وحدة تحكم:


<div class="container" ng-controller="mainCtrl">

تم تعريف وحدة التحكم [mainCtrl] في ملف [mainController.js]:


// controller
angular.module("rdvmedecins")
  .controller('mainCtrl', ['$scope', '$location',
    function ($scope, $location) {
 
      // page templates
      $scope.page1 = {};
      $scope.page2 = {};
      $scope.page3 = {};
      // global model
      var main = $scope.main = {};
      main.text = "[Modèle global]";
 
      // methods exposed to view
      main.showPage1 = function () {
        $location.path("/page1");
      };
      main.showPage2 = function () {
        $location.path("/page2");
      };
      main.showPage3 = function () {
        $location.path("/page3");
      }
}]);
  • السطر 3: تحتاج وحدة التحكم [mainCtrl] إلى الكائن [$location] الذي توفره وحدة التوجيه [ngRoute]. يتيح لك هذا الكائن تغيير طرق العرض (الأسطر 16 و19 و22)؛

لنعد إلى كود الحاوية:


    <div class="container" ng-controller="mainCtrl">
        <!-- the navigation bar -->
        <ng-include src="'views/navbar.html'"></ng-include>
 
        <!-- the current view -->
        <ng-view></ng-view>
</div>
  • تقوم وحدة التحكم [mainCtrl] بإنشاء النموذج الخاص بالمنطقة 1-7؛
  • كما أن العرض المضمن في السطر 6 يحتوي أيضًا على وحدة تحكم. على سبيل المثال، يحتوي العرض [page1] على وحدة التحكم [page1Ctrl]. تقوم وحدة التحكم هذه بإنشاء النموذج للمنطقة المعروضة في السطر 6. وبذلك يكون لدينا نموذجان في هذه المنطقة:
    • النموذج الذي أنشأته وحدة التحكم [mainCtrl
    • النموذج الذي أنشأته وحدة التحكم [page1Ctrl

هناك قاعدة لتسمية النماذج. في العرض الموضح في السطر 6، تظهر نماذج وحدات التحكم [mainCtrl] و[pagexCtrl] معًا. إذا كان هناك متغيران في هذين النموذجين يحملان نفس الاسم، فسيحل أحدهما محل الآخر. لتجنب هذا التضارب في التسمية، نقوم بإنشاء أربعة نماذج بأربعة أسماء مختلفة:

page
وحدة التحكم
النموذج
سطر الكود
الحاوية
mainCtrl
الرئيسي
11
page1
page1Ctrl
الصفحة 1
7
الصفحة 2
الصفحة 2Ctrl
الصفحة 2
8
الصفحة 3
الصفحة 3Ctrl
الصفحة 3
9
  • السطر 12: يحدد عنصر [text] في القالب [main

الأسطر 7–11 لها تأثير محدد للغاية: فهي تحدد [$scope] لوحدة التحكم [mainCtrl]، وتقوم بإنشاء أربعة متغيرات [main، page1، page2، page3] بداخلها. سيتم استخدام هذه المتغيرات الأربعة كنماذج لكل من الحاوية والعروض الثلاثة التي ستحتويها بدورها.

3.7.15.6. شريط التنقل

يتم تعريف شريط التنقل على النحو التالي في الحاوية:


    <div class="container" ng-controller="mainCtrl">
        <!-- the navigation bar -->
        <ng-include src="'views/navbar.html'"></ng-include>
 
        <!-- the current view -->
        <ng-view></ng-view>
</div>

يتم تعريف شريط التنقل في السطر 3. وهذا يعني أنه لا يعرف سوى القالب [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>
  • في الأسطر 16 و21 و26، يتم استخدام طرق من نموذج [main
  • السطر 16: سيؤدي النقر على رابط [Page1] إلى تشغيل طريقة [$scope.main.showPage1]. يتم تعريف هذه الطريقة في وحدة التحكم [mainCtrl] على النحو التالي:

      // global model
      var main = $scope.main = {};
      main.text = "[Modèle global]";
 
      // methods exposed to view
      main.showPage1 = function () {
        $location.path("/page1");
};
  • السطر 6: من الكود أعلاه، يمكننا أن نرى أن الطريقة [main.showPage1] هي في الواقع الطريقة [$scope.main.showPage1]. لذا فهذه هي الطريقة التي سيتم تنفيذها؛
  • السطر 7: نغير عنوان 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] مرئي في العرض الذي تم إدراجه في الحاوية. وهذا ما نريد التحقق منه في السطر 4. بالإضافة إلى ذلك، يحدد وحدة التحكم [page1Ctrl] الخاصة بجزء [views/page1.html] قالب [page1]. وهذا هو القالب المستخدم في السطر 5.

فيما يلي كود وحدة التحكم [page1Ctrl]:


angular.module("rdvmedecins")
  .controller('page1Ctrl', ['$scope',
    function ($scope) {
 
      // page 1 template
      var page1=$scope.page1;
      page1.text="[Modèle local dans page 1]";
}]);
  • السطر 2: [$scope] المُدرج هنا ليس فارغًا. نظرًا لأن وحدة التحكم [page1Ctrl] تتحكم في منطقة مُدرجة في حاوية تتحكم فيها وحدة التحكم [mainCtrl]، فإن [$scope] في السطر 2 يحتوي على عناصر [$scope] المُعرَّفة بواسطة وحدة التحكم [mainCtrl]. من المهم فهم هذا الأمر. يحتوي [$scope] المحدد بواسطة وحدة التحكم [mainCtrl] على العناصر التالية: [main، page1، page2، page3]. وهذا يعني أن لدينا إمكانية الوصول إلى نماذج جميع طرق العرض. وهذا ليس أمرًا مرغوبًا بالضرورة، ولكنه هو الحال هنا. في الإصدار النهائي لعميل Angular، سنستخدم هذه الميزة لتخزين المعلومات التي يجب مشاركتها بين طرق العرض في نموذج [main]. سيكون هذا مشابهًا لمفهوم "الجلسة" من جانب الخادم؛
  • السطر 6: نسترد نموذج [page1] للصفحة 1 من [$scope] ثم نعمل عليه (السطر 7). ثم نحصل على العرض التالي:
 

تم إنشاء عرضي [/page2] و [/page3] على نفس نموذج عرض [/page1] (انظر لقطات الشاشة في الصفحة 240).

3.7.15.8. التحكم في التنقل

نريد الآن التحكم في التنقل على النحو التالي [الصفحة 1 --> الصفحة 2 --> الصفحة 3 --> الصفحة 1]. وبالتالي، إذا كان المستخدم في الصفحة 1 [/page1] وقام بكتابة عنوان URL [/page3] في متصفحه، فلا ينبغي قبول هذا التنقل، ويجب أن يظل المستخدم في الصفحة 1.

لتحقيق ذلك، نقوم بتعديل وحدات التحكم في الصفحات على النحو التالي:


angular.module("rdvmedecins")
  .controller('page1Ctrl', ['$scope', '$location',
    function ($scope, $location) {
      // authorized navigation?
      var main = $scope.main;
      if (main.lastUrl && main.lastUrl != '/page3') {
        // we return to the last URL
        $location.path(main.lastUrl);
        return;
      }
      // we store the URL of the page
      main.lastUrl = '/page1';
      // page template
      var page1 = $scope.page1;
      page1.text = "[Modèle local dans page 1]";
    }]);
  • السطر 12: عند عرض صفحة ما، نقوم بتخزين عنوان URL الخاص بها في نموذج [main.lastUrl]. هنا نستخدم المفهوم الذي ناقشناه سابقًا: استخدام نموذج [main] لتخزين المعلومات المشتركة بين جميع طرق العرض. في هذه الحالة، هو آخر عنوان URL تمت زيارته؛
  • يتم تكرار الكود في الأسطر 4-12 وتكييفه ليتناسب مع العروض الثلاثة. نحن هنا في عرض [/page1
  • السطر 5: نسترد نموذج [main
  • السطر 6: إذا كان نموذج [main.lastUrl] موجودًا ويختلف عن [/page3]، فإن التنقل محظور (عنوان URL الأخير الذي تمت زيارته موجود وليس /page3
  • السطر 8: نعود بعد ذلك إلى آخر عنوان URL تمت زيارته؛

دعونا نجرب ذلك:

  • في [1]، نحن في الصفحة 1 ونكتب عنوان URL للصفحة 3 في [2]؛
  • في [3]، لم يحدث التنقل وعُدنا إلى عنوان URL للصفحة 1؛

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.4، الصفحة 134.

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é">
  <!-- on 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>
<!-- controller [appCtrl], model [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>
<!-- angular js -->
<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>
<!-- modules -->
<script type="text/javascript" src="modules/main.js"></script>
<!-- services -->
<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>
<!-- guidelines -->
<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>
<!-- controllers -->
<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>
  • السطر 18: لاحظ أن [appCtrl] هو وحدة التحكم في الصفحة الرئيسية؛
  • الأسطر 19–21: محتوى الصفحة الرئيسية؛

هذا المحتوى هو كما يلي:


<div class="container">
  <!-- navigation bars -->
  <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>
  <!-- the jumbotron -->
  <ng-include src="'views/jumbotron.html'"></ng-include>
  <!-- page title -->
  <div class="alert alert-info" ng-show="app.titre.show" translate="{{app.titre.text}}"
       translate-values="{{app.titre.model}}"></div>
  <!-- page errors -->
  <errors model="app.errors" ng-show="app.errors.show"></errors>
  <!-- the waiting message -->
  <waiting model="app.waiting" ng-show="app.waiting.show"></waiting>
  <!-- the current view -->
  <ng-view></ng-view>
  <!-- debug -->
  <debug model="app" ng-show="app.debug.on"></debug>
</div>

بغض النظر عن العرض المعروض، سيحتوي دائمًا على العناصر التالية:

  • السطران 3 و4: شريط الأوامر. الشريطان الموجودان في السطرين 3 و4 متنافيان؛

Image

Image

  • السطر 6: شعار/نص التطبيق:

Image

  • السطر 8: عنوان

Image

  • السطر 11: رسالة خطأ:

Image

  • السطر 13: رسالة تحميل:

Image

  • السطر 17: معلومات التصحيح:

Image

يتم التحكم في جميع العناصر المذكورة أعلاه بواسطة توجيه [ng-show / ng-hide]، مما يعني أنه حتى لو كانت موجودة، فهي ليست بالضرورة مرئية.

3.8.4. طرق عرض التطبيق

في كود الصفحة الرئيسية، لدينا:


<div class="container">
  ...
  <!-- the current view -->
  <ng-view></ng-view>
  ...
</div>

يستقبل السطر 4 طرق العرض المختلفة للتطبيق. يتم تعريف هذه الطرق في الوحدة النمطية [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]؛
  • في [4]، تطلب عرض جدول مواعيد الطبيب المحدد لليوم المختار؛
  • بمجرد عرض جدول مواعيد الطبيب، يمكنك حجز موعد [5]؛
  • في [6]، حدد المريض الذي سيحضر الموعد وقم بتأكيد اختيارك في [7]؛

بمجرد تأكيد الموعد، ستعود تلقائيًا إلى الجدول الزمني حيث يظهر الموعد الجديد الآن. يمكن حذف هذا الموعد لاحقًا [7].

تم وصف الميزات الرئيسية. وهي بسيطة. أما الميزات التي لم يتم وصفها فهي وظائف التنقل للعودة إلى عرض سابق. لنختتم بإعدادات اللغة:

  • في [1]، تتحول اللغة من الفرنسية إلى الإنجليزية؛
  • في [2]، تتحول طريقة العرض إلى الإنجليزية، بما في ذلك التقويم؛

3.8.6. وحدة [main.js]

تحدد الوحدة النمطية [main.js] الوحدة النمطية Angular التي ستتحكم في التطبيق:

 
  • السطر 4: يُسمى الوحدة النمطية [rdvmedecins
  • السطر 5: تُستخدم الوحدة النمطية [ngRoute] لتوجيه عناوين URL؛
  • السطر 6: تُستخدم الوحدة النمطية [translate] لتدويل النص؛
  • السطر 7: تُستخدم الوحدة النمطية [base64] لترميز السلسلة 'login:password' بتنسيق Base64؛
  • السطر 8: تُستخدم الوحدة النمطية [ngLocale] لتدويل التقويم؛
  • السطر 9: تُستخدم الوحدة النمطية [ui.bootstrap] للتقويم؛
  • السطر 12: تكوين المسار؛
  • السطر 40: تدويل الرسائل؛

3.8.7. وحدة التحكم في الصفحة الرئيسية

دعونا نراجع كود HTML للصفحة الرئيسية [app.html]:


<body ng-controller="appCtrl">
<div class="container">
...

السطر 1: يتم التحكم في نص الصفحة الرئيسية بالكامل بواسطة وحدة التحكم [appCtrl]. ونظرًا لموقعها، فإنها تُعد وحدة التحكم العامة والرئيسية للتطبيق. وكما هو موضح في القسم 3.7.15، فإن النموذج الذي تنشئه وحدة التحكم هذه يتم توريثه إلى جميع طرق العرض التي سيتم إدراجها في الصفحة الرئيسية.

فيما يلي كودها:


angular.module("rdvmedecins")
  .controller("appCtrl", ['$scope', 'config', 'utils', '$location', '$locale',
    function ($scope, config, utils, $location, $locale) {
 
      // debug
      utils.debug("[app] init");
 
      // ----------------------------------------initialisation page
      // templates for # pages
      $scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
      $scope.login = {};
      $scope.home = {};
      $scope.agenda = {};
      $scope.resa = {};
      // current page template
      var app = $scope.app;
      ...
 
      // ---------------------------------- méthodes
 
      // cancel current job
      app.cancel = function () {
...
      };
 
      // disconnect
      app.deconnecter = function () {
        ...
      };
 
      // this code must remain here as it refers to the preceding [cancel] function
      app.waiting = {title: {text: config.msgWaitingInit, values: {}}, cancel: app.cancel, show: true};
    }])
;

تحدد الأسطر 10–14 النماذج الخمسة المستخدمة في التطبيق:

النموذج
عرض
وحدة التحكم
$scope.app
app.html
appCtrl
$scope.login
login.html
loginCtrl
$scope.home
home.html
homeCtrl
$scope.reservation
reservation.html
resaCtrl
$scope.agenda
agenda.html
agendaCtrl

من المهم أن نفهم أن الكائن [$scope]، باعتباره نموذج وحدة التحكم في الصفحة الرئيسية، يتم توريثه إلى جميع العروض ووحدات التحكم. وبالتالي، فإن وحدة التحكم [loginCtrl] لديها حق الوصول إلى العناصر [$scope.app، $scope.login، $scope.home، $scope.resa، $scope.agenda]. بعبارة أخرى، تتمتع وحدة التحكم بحق الوصول إلى نطاقات وحدات التحكم الأخرى. يتجنب التطبيق قيد الدراسة استخدام هذه الإمكانية بعناية. وبالتالي، على سبيل المثال، تعمل وحدة التحكم [loginCtrl] مع نطاقين فقط:

  • نطاقها الخاص [$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]، الذي يعمل كذاكرة مشتركة بين وحدات التحكم، باسم "جلسة" في التعليقات، محاكاةً للجلسة المستخدمة في تطبيقات الويب التقليدية، والتي تشير إلى الذاكرة المشتركة بين طلبات HTTP المتتالية.

لنعد إلى كود وحدة التحكم [appCtrl]:


      // templates for # pages
      $scope.app = {waitingTimeBeforeTask: config.waitingTimeBeforeTask};
      $scope.login = {};
      $scope.home = {};
      $scope.agenda = {};
      $scope.resa = {};
      // current page template
      var app = $scope.app;
      // [app.debug] and [utils.verbose] must always be synchronized
      app.debug = utils.verbose;
      app.debug.on = config.debug;
      // no page title for the moment
      app.titre = {show: false};
      // no navigation bars
      app.navbarrun = {show: false};
      app.navbarstart = {show: false};
      // no errors
      app.errors = {show: false};
      // local default
      angular.copy(config.locales['fr'], $locale);
      // the current view
      app.view = {url: undefined, model: {}, done: false};
      // the current task
app.task = app.view.model.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};
  • السطر 8: [$scope.app] سيكون نموذج الصفحة الرئيسية. كما سيكون بمثابة الذاكرة المشتركة بين مختلف وحدات التحكم. بدلاً من كتابة [$scope.app.field=value] في كل مكان، يتم تعيين المؤشر [$scope.app] إلى المتغير [app]، لذا نكتب [app.field=value]. فقط تذكر أن [app] هو النموذج المعروض على الصفحة الرئيسية؛
  • السطر 11: [app.debug.on] هو قيمة منطقية تتحكم في وضع تصحيح أخطاء التطبيق. بشكل افتراضي، يتم تعيينها على true. ترتبط قيمتها بمربع الاختيار [debug] في أشرطة التنقل؛
  • السطر 15: [app.navbarrun.show] يتحكم في عرض شريط التنقل التالي:

Image

  • السطر 16: [app.navbarstart.show] يتحكم في عرض شريط التنقل التالي:

Image

  • السطر 18: [app.errors] هو القالب الخاص بشعار الخطأ؛

Image

  • السطر 22: [app.view] سيحتوي على معلومات حول العرض الحالي — الذي يتم عرضه حاليًا بواسطة علامة [ng-view] في الصفحة الرئيسية. سنقوم بتضمين المعلومات التالية هناك:
    • [url]: عنوان URL للعرض الحالي، على سبيل المثال [/agenda
    • [model]: نموذج العرض الحالي، على سبيل المثال [$scope.agenda
    • [done]: عندما تكون القيمة true، تشير إلى أن العرض الحالي قد انتهى من عمله وأننا ننتقل إلى عرض آخر؛

تُستخدم معلومات هذه للتحكم في التنقل.

  • السطر 24: يطلق مهمة غير متزامنة، وهي انتظار محاكاة. تتم الإشارة إلى المهمة غير المتزامنة بواسطة مؤشرين [app.view.model.task.action] و [app.task

تم تضمين طريقتين في وحدة التحكم [appCtrl]:


      // cancel current job
      app.cancel = function () {
...
      };
 
      // disconnect
      app.deconnecter = function () {
        ...
};
  • السطر 2: تُستخدم الدالة [app.cancel] لإلغاء المهمة الحالية التي تُعرض لها رسالة التحميل حاليًا. تعرض جميع طرق العرض هذه الرسالة، لذا سيتم إلغاء المهمة هنا؛
  • السطر 7: تعيد الدالة [app.logout] المستخدم إلى صفحة تسجيل الدخول. توفر جميع طرق العرض، باستثناء طريقة العرض [/login]، هذا الخيار؛

وظيفة [app.deconnecter] هي كما يلي:


      // disconnect
      app.deconnecter = function () {
        // we return to the login page
        $location.path(config.urlLogin);
};
  • السطر 4: العودة إلى صفحة تسجيل الدخول على الرابط [/login

3.8.8. إدارة المهام غير المتزامنة

في تطبيقنا، في أي وقت معين، سيتم تشغيل مهمة غير متزامنة واحدة فقط. من الممكن تشغيل مهام متعددة. على سبيل المثال، عند بدء تشغيل التطبيق، يطلب قائمة الأطباء من خدمة الويب ثم قائمة العملاء بطلبين HTTP متتاليين. يمكننا القيام بنفس الشيء بطلبين HTTP متزامنين. يوفر Angular الأدوات اللازمة لذلك. هنا، لم نختر هذا النهج.

يتم إلغاء المهمة قيد التشغيل حاليًا باستخدام الكود التالي في وحدة التحكم [appCtrl]:


      // cancel current job
      app.cancel = function () {
        utils.debug("[app] cancel task");
        // cancel the current view's asynchronous task
        var task = app.view.model.task;
        task.isFinished = true;
        task.action.reject();
 
        ...
};
  • السطر 5: يتم استرداد المهمة من [app.view.model.task]. لذلك، ستضمن جميع وحدات التحكم أن مهامها غير المتزامنة تتم الإشارة إليها بواسطة هذا الكائن؛
  • السطر 6: للإشارة إلى أن المهمة قد انتهت؛
  • السطر 7: لإنهاء المهمة بفشل. يختلف هذا الترميز عن ذلك المستخدم في أمثلة Angular التي تمت دراستها:
    • في الأمثلة، كان الكائن [task] كائن [$q.defer()] يمكن إنهاؤه؛
    • في النسخة النهائية، كائن [task] هو كائن يحتوي على الحقول [action, isFinished]، حيث [action] هو كائن [$q.defer()] الذي يمكن إنهاؤه و[isFinished] هو قيمة منطقية تشير إلى اكتمال الإجراء؛

دعونا نفحص دورة حياة كائن [task] باستخدام مثال. عند بدء التشغيل، بعد وحدة التحكم [appCtrl]، تتولى وحدة التحكم [loginCtrl] عرض [views/login.html]. رمز التهيئة الخاص بها هو كما يلي:


      // retrieve the parent model
      var login = $scope.login;
      var app = $scope.app;
      // current view
app.view = {url: config.urlLogin, model: login, done: false};

في السطر 5، لدينا [model=login]. هذا يعني أنه عندما نقوم بتعديل الكائن [login]، فإننا نقوم بتعديل الكائن [app.view.model]، أي [$scope.app.view.model]. عندما نريد محاكاة انتظار في وحدة التحكم [loginCtrl]، نكتب:


// simulated waiting
var task = login.task = {action: utils.waitForSomeTime(app.waitingTimeBeforeTask), isFinished: false};

بإضافة الحقل [task] إلى الكائن [login]، يتم إضافته بالتالي إلى الكائن [$scope.app.view.model]. إذا ألغى المستخدم الانتظار، فإن الكود الموجود في [appCtrl.cancel]:


// current page template
var app = $scope.app;
...
var task = app.view.model.task;
task.isFinished = true;
task.action.reject();

سيكمل بنجاح الانتظار المحاكي (الأسطر 4–6).

3.8.9. التحكم في التنقل

قواعد التنقل المستخدمة في التطبيق هي كما يلي:

عنوان URL الهدف
عنوان URL السابق
التنقل المسموح به
/login
أي
نعم
/الصفحة الرئيسية
/login
نعم إذا أشارت وحدة التحكم [loginCtrl] إلى أنها انتهت من عملها

/home
نعم

/التقويم
نعم
/التقويم
/الصفحة الرئيسية
نعم إذا أشار جهاز التحكم [homeCtrl] إلى أنه قد انتهى من عمله

/إعادة_التعيين
نعم

/جدول الأعمال
نعم
/القرار
/التقويم
نعم إذا أشار جهاز التحكم [homeCtrl] إلى أنه قد أنهى عمله

/إعادة_التعيين
نعم

يتم تنفيذ ذلك باستخدام الكود التالي:

بالنسبة إلى [agendaCtrl]:

Image

  • الأسطر 11–20: تنفيذ قاعدة التنقل؛
  • السطر 26: عرض حالي جديد؛

بالنسبة إلى [resaCtrl]:

Image

  • الأسطر 12–20: تنفيذ قاعدة التنقل:
  • السطر 27: عرض حالي جديد؛

بالنسبة لـ [loginCtrl]:

Image

  • لا يوجد عنصر تحكم في التنقل هنا لأن القاعدة تنص على أنه يمكن الوصول إلى عنوان URL [/login] من أي مكان. لذلك، إذا كتب المستخدم عنوان URL هذا في متصفحه، فسيعمل بغض النظر عن العرض الحالي؛
  • السطر 16: العرض الحالي الجديد؛

تم توفير كود وحدة التحكم [homeCtrl] في القسم 3.8.7.

أخيرًا، بالنسبة لقاعدة مثل:

/agenda
/home
نعم، إذا أشارت وحدة التحكم [homeCtrl] إلى أنها قد أنهت عملها

إليك مثال على كود ينتقل من عنوان URL [/home] إلى عنوان URL [/agenda]:

 

في الأعلى، نحن في طريقة [displayCalendar] الخاصة بوحدة التحكم [homeCtrl]. وقد طلب المستخدم عرض تقويم الطبيب.

  • السطر 107: وعد مهمة HTTP؛
  • السطر 109: تم تهيئة المتغير [app] بقيمة [$scope.app]. كما رأينا، يُستخدم هذا الكائن كقالب لعرض [app.html]. يُستخدم هذا القالب [$scope.app] أيضًا لتخزين المعلومات التي يجب مشاركتها بين العروض؛
  • السطر 111: يتم تحليل رمز الخطأ الذي أرجعته المهمة؛
  • السطر 113: يتم وضع النتيجة [result.data] في نموذج [app]؛
  • السطر 116: سيقوم وحدة التحكم [homeCtrl] بتسليم المهمة إلى وحدة التحكم [agendaCtrl]. وهذا يشير إلى أنه قد انتهى من عمله مع الكود الموجود في السطر 115. سيتم استخدام هذا الكود بواسطة وحدة التحكم [agendaCtrl] على النحو التالي:

Image

  • السطر 11: يتم استرداد الكائن [$scope.app.view
  • السطر 15: معالجة الحقل [$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]: نرى أن طول الكود يبلغ حوالي 250 سطراً. ويتضمن الجزء الأكبر من هذا الكود إخراج مفاتيح الرسائل الدولية [2]. ونحن نتجنب تضمين هذه المفاتيح بشكل ثابت في الكود مباشرةً؛

خدمة [utils]

 
  • السطر 8: لم نصل بعد إلى المتغير [verbose]. وهو يتحكم في وظيفة [debug] على النحو التالي:
 
  • الأسطر 22–25: لا تقوم الدالة [utils.debug] بأي شيء إذا كانت قيمة [verbose.on] هي false. هذا المتغير مرتبط بمتغير في وحدة التحكم [appCtrl]:
 
  • السطر 21: [app.debug] يأخذ قيمة المؤشر [utils.verbose]. لذلك، فإن أي تغيير يتم إجراؤه على [app.debug] سيتم إجراؤه أيضًا على [utils.verbose
  • السطر 22: يتم أخذ القيمة الأولية لـ [app.debug.on] من ملف التكوين. بشكل افتراضي، يتم تعيينها على true. قد تتغير هذه القيمة بمرور الوقت. يمكن للمستخدم تغييرها عبر أشرطة التنقل:
 
  • السطر 45: مربع اختيار (type=checkbox) يسمح لك بتغيير قيمة [app.debug.on] (سمة ng-model

خدمة [dao]

 

3.8.11. التوجيهات

  

التوجيهات [errors، footable، list، waiting] هي تلك التي تم وصفها بالفعل في النظرة العامة على Angular:

  • تم تقديم التوجيه [footable] في القسم 3.7.8.
  • تم تقديم التوجيه [list] في القسم 3.7.12؛
  • تم تقديم التوجيهات [errors] و [waiting] في القسم 3.7.14؛

لم نكن قد صادفنا التوجيه [debug] من قبل. وهو كما يلي:

 

ملف [debug.html] المشار إليه في السطر 11 هو كما يلي:

 
  • السطر 2: تعرض توجيهات [debug] قالبها بتنسيق JSON في لافتة Bootstrap (السطر 1)؛

يُستخدم هذا التوجيه فقط في الصفحة الرئيسية [app.html]:

 
  • يُستخدم التوجيه [debug] في السطر 35. وبالتالي، فإنه يعرض تمثيل JSON لنموذج [$scope.app] عند التواجد في وضع التصحيح (السمة ng-show). وينتج عن ذلك إخراج مثل التالي:

يتطلب هذا فهمًا جيدًا للكود لتفسيره، ولكن بمجرد اكتساب هذا الفهم، تصبح المعلومات المذكورة أعلاه مفيدة في عملية التصحيح. هنا، قمنا بتمييز عناصر نموذج [$scope.app] المعروض. تذكر أن [$scope.app] هي الذاكرة المشتركة بين وحدات التحكم؛

  • [waitingBeforeTask]: وقت الانتظار المحاكي قبل أي طلب HTTP؛
  • [debug]: وضع التصحيح — يكون بالضرورة صحيحًا إذا تم عرض هذا الشعار؛
  • [navbarrun]: قيمة منطقية تتحكم في عرض شريط التنقل التالي:

Image

  • [navbarstart]: قيمة منطقية تتحكم في عرض شريط التنقل التالي:

Image

  • [errors]: قالب لتوجيه [errors
  • [view]: يغلف المعلومات المتعلقة بالعرض المعروض حاليًا؛
  • [waiting]: قالب للتوجيه [waiting
  • [serverUrl, username, password]: بيانات اعتماد تسجيل الدخول لخدمة الويب؛
  • [doctors]: نموذج لتوجيه [list] المطبق على الأطباء؛
  • [clients]: كما هو مذكور أعلاه بالنسبة للعملاء؛
  • [menu]: يتحكم في خيارات القائمة المعروضة. يتم تعريف هذه الخيارات في [navbar-run.html]:

Image

توجد خيارات القائمة في الأسطر 16 و23 و29 و36.

  • [formattedDay]: اليوم المحدد في التقويم بتنسيق 'yyyy-mm-dd
  • [agenda]: جدول مواعيد الطبيب. يحتوي على المواعيد المتاحة (rv==null) والمواعيد المحجوزة. بالنسبة للأخيرة، يتضمن اسم العميل الذي قام بالحجز؛
  • [selectedCreneau]: الفترة الزمنية المحددة لإجراء الحجز؛

3.8.12. وحدة التحكم [loginCtrl]

  

ترتبط وحدة التحكم [loginCtrl] بصفحة العرض [views/login.html]، والتي تنتج، عند دمجها مع الصفحة الرئيسية، الصفحة التالية:

Image

وحدة التحكم [loginCtrl] هي كما يلي:

Image

  • السطر 13: سيكون [login] هو النموذج للعرض الحالي؛
  • السطر 14: [app] هي الذاكرة المشتركة بين وحدات التحكم؛
  • السطر 16: يتم ملء [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};
  • السطر 18: قد تتذكر أن [appCtrl] أطلقت انتظارًا محاكىًّا يشير إليه الكائن [app.task.action]. نستخدم [promise] لهذه المهمة لانتظار انتهائها؛
  • السطر 39: تتولى طريقة [login.setLang] تبديل اللغة؛
  • السطر 47: تتولى طريقة [login.authenticate] مصادقة المستخدم؛

دعونا نلقي نظرة على الخطوات الرئيسية لطريقة المصادقة:

Image

  • السطران 50-51: [app.waiting] هو نموذج لافتة التحميل؛
  • السطر 53: [app.errors] هو نموذج لافتة الخطأ؛
  • السطر 55: يتم بدء انتظار محاكاة. يتم الإشارة إلى الكائن [action, isFinished] بواسطة [login.task] وبالتالي، وبما أن [app.view.model=login]، بواسطة [app.view.model.task]. تذكر أن هذا هو الشرط لإلغاء المهمة؛
  • السطر 57: بعد انتهاء الانتظار المحاكي، يتم تحميل الأطباء؛
  • السطر 62: بمجرد استرداد الأطباء، يتم تحليل طلبهم. إذا تم الحصول على الأطباء، يتم عندئذٍ طلب العملاء؛
  • السطر 83: يتم تحليل الاستجابة وعرض العرض النهائي. يتم ذلك باستخدام الكود التالي:

Image

  • السطر 87: يتم تعيين القيمة المنطقية [task.isFinished] على true في الحالات التالية:
    • ألغى المستخدم الانتظار؛
    • انتهى طلب الأطباء بخطأ؛
  • الأسطر 91-98: الحالة التي يكون لدينا فيها العملاء؛
  • السطر 93: [app.clients] هو النموذج لتوجيه [list] الذي سيعرض العملاء في قائمة منسدلة؛
  • السطور 97–98: نستعد لتغيير العروض (السطر 98) ولكننا نشير أولاً إلى أن وحدة التحكم قد أنهت عملها (السطر 97). تذكر أن [$scope.app.view.done] تُستخدم للتحكم في التنقل؛

النقطة المهمة التي يجب ملاحظتها هنا هي أن الأطباء والعملاء قد تم تخزينهم مؤقتًا في المتصفح. لن يتم طلبهم بعد الآن من خدمة الويب.

3.8.13. وحدة التحكم [homeCtrl]

  

يرتبط عنصر التحكم [homeCtrl] بعرض [views/home.html]، والذي ينتج، عند دمجه مع الصفحة الرئيسية، الصفحة التالية:

Image

هيكل وحدة التحكم [homeCtrl] هو كما يلي:

Image

  • الأسطر 12–20: هذا هو عنصر التحكم في التنقل. تحتوي جميع وحدات التحكم على هذا العنصر باستثناء [loginCtrl] لأن الصفحة [/login.html] يمكن الوصول إليها دون شروط؛

Image

  • الأسطر 25–28: نجد هنا أسطرًا مشابهة لتلك الموجودة في وحدة التحكم [loginCtrl]. وبالتالي، فإن [home] هو قالب العرض المرتبط بوحدة التحكم؛
  • السطر 33: سمة لم نلتق بها بعد. هذا هو النموذج لشريط رأس العرض:

Image

  • السطر 36: [home.datepicker] هو نموذج التقويم؛
  • السطر 38: [app.menu] هو نموذج قائمة شريط التنقل. هنا، سيكون خيار [Schedule] موجودًا. وهذا ما يسمح لك بطلب جدول مواعيد الطبيب؛

أخيرًا، يحتوي وحدة التحكم على طريقتين:

Image

تمت تغطية عرض الجدول (السطر 51) في القسم 3.7.8.

3.8.14. وحدة التحكم [agendaCtrl]

  

ترتبط وحدة التحكم [agendaCtrl] بعرض [views/agenda.html]، والذي ينتج، عند دمجه مع الصفحة الرئيسية، الصفحة التالية:

Image

هيكل وحدة التحكم [agendaCtrl] هو كما يلي:

Image

  • تتولى الأسطر 10–20 التحكم في التنقل؛

Image

  • الأسطر 23–26: سيكون [agenda] هو قالب العرض المرتبط بوحدة التحكم [agendaCtrl
  • الأسطر 36–44: [app.title] هو القالب لشريط العنوان التالي:

Image

  • السطر 46: ستحتوي القائمة على خيار [Home]:

Image

طرق وحدة التحكم هي كما يلي:

Image

  • السطر 95: تمت مناقشة طريقة [agenda.delete] في القسم 3.7.9؛

طريقة [agenda.home] هي طريقة تنقل بحتة:

Image

طريقة [agenda.reserve] هي كما يلي:

Image

  • السطر 73: معلمة دالة [reserve] هي رقم الخانة (id)؛
  • الأسطر 77-86: تهدف إلى العثور على الفتحة الزمنية التي تحمل هذا المعرف؛
  • السطر 82: يتم وضع الفتحة الزمنية التي تم العثور عليها في الذاكرة المشتركة [app]. سيستخدم جهاز التحكم [resaCtrl]، الذي سيتولى المهمة (السطر 90)، هذه المعلومات لعرض شريط العنوان الخاص به؛
  • السطور 89-90: الانتقال إلى [/resa.html

3.8.15. وحدة التحكم [resaCtrl]

  

يرتبط وحدة التحكم [resaCtrl] بالعرض [views/resa.html]، والذي ينتج، عند دمجه مع الصفحة الرئيسية، الصفحة التالية:

Image

هيكل وحدة التحكم [resaCtrl] هو كما يلي:

Image

  • الأسطر 12–20: عنصر التحكم في التنقل؛

Image

  • الأسطر 24-27: ستكون [resa] هي القالب للعرض الحالي؛
  • الأسطر 38–45: [app.titre] هو القالب لشريط العنوان التالي:

Image

  • السطر 47: يتم عرض خيارين من القائمة:

Image

طرق وحدة التحكم هي كما يلي:

Image

تمت مناقشة طريقة [resa.valider] في القسم 3.7.9.

3.8.16. إدارة اللغة

توفر جميع وحدات التحكم الطريقة [setLang] التالية:

Image

كان من الممكن دمجها في وحدة التحكم [appCtrl].