الوسم : Angular

30يوليو

( تعلم Angular ) – التوجيه Routing في AngularJS

تقتصر بعض تطبيقات Angular على كونها أدوات مساعدة في صفحة ويب تقليدية، إلا أنّ الأغلبية العُظمى لتطبيقاتها هي التطبيقات وحيدة الصفحة (single-page applications)، التي تستبدل تصفح الويب المعتمد على المتصفّح بواجهاتٍ وانتقالاتٍ مشابهة في صفحةٍ واحدة، مقدمة للمستخدم تجربة تفاعليّة رائعة. إلّا أنّ كتابة تطبيقات وحيدة الصفحة بطريقةٍ بسيطة يجعلنا نخسر شيئًا قيّمًا في التطبيقات المعتمدة على المتصفّح وهو الرّابط URL. ففي تطبيقٍ بسيطٍ وحيد الصفحة، سيكون هناك رابط URL واحد فقط، ولن تكون هناك طريقة لمشاركة روابطَ مخصصة لكلّ مورد (resource) على حدة، كتعليق محدّد على تدوينة ما على سبيل المثال. وعلى العكس، فسيقوم تطبيق تقليدي في طرف المخدّم يتبع نمط REST (تبادل الحالة ضمن رابط HTML) بإدارة الواجهات كبنية هرميّة من الروابط المنظّمة سهلة القراءة والتي تظهر حالة التّطبيق الحاليّة بوضوح. تُقدّم هذه الروابط طريقةً للمستخدم ليعود إلى حالةٍ من حالات التطبيق في وقتٍ لاحق، أو ليشارك هذه الحالة مع الآخرين.

يُعرَّف التوجيه في تطبيق ويب تقليديّ بأنّه صلة الوصل بين عمليّات HTTP مثل (GET وPOST وما إلى ذلك) وأنماط الروابط وبين المتحكّمات. إن لم تكن معتادًا على التوجيه من طرف المخدّم فسيقدّم لك دليل التوجيه السريع مقدّمةً سهلةً للتوجيه في JavaScript. على أي حال، فالتوجيه من طرف المستخدم يقلب الحالة رأسًا على عقب. فبدلًا من أن يتمّ التفاعل مع الأفعال التي ينقلها لنا المتصفّح، فسنحتاج إلى إبقاء المتصفّح متابعًا للتحديثات بينما يتفاعل التطبيق مباشرةً مع مدخلات المستخدم. كمثالٍ على ذلك، كيف يمكننا الإبقاء على شريط التّنقل محدّثًا بروابط تمثّل الحالة بدقّة، وفي نفس الوقت نستجيب لروابط تُدخل فيه أو تُحمّل من العلامات المرجعية؟ لحُسن الحظ، فقد تمّ إيجاد الوحدة ngRoute لتساعدنا في هذا العمل.

لنتمكّن من استخدام هذه الوحدة، سنحتاج إلى تحميل ملف angular-route.js إضافةً إلى ملف مكتبة Angular الرئيسي. كما سنقوم بتحميل بيئة Bootstrap لاستخدام أنماط CSS الخاصة بها.

<base href="/">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.2/angular.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.2/angular-route.js"></script>

لاحظ تعريف العنصر base في السطر الأول في المثال السابق. يُعرِّف هذا العنصر رابط URL القاعديّ لتطبيق Angular الحالي، وهذا العنصر يطلبه دعم Angular لـتوابع HTML5 الخاصة بالتاريخ (سنتحدث عنها أدناه). إن كنت تتساءل لمَ تم إسناد الخاصية href إلى مسار الجذر، بدلًا من أن تكون مسندةً إلى مسار الصفحة، فهذا لأنّ أمثلة هذه السلسلة تعمل داخل أُطُر iframe خاصة لكل واحد منها.

<body ng-app="app">
  <!-- كل الأمثلة توضع هنا -->
</body>

بعد ذلك، سيكون علينا جلب ngRoute عند تهيئة الوحدة الجذر، وقد قمنا بتغطية هذه الفكرة بعمق في فصل الوحدات.

angular.module('app', ['ngRoute']);

وهكذا نكون قد أتممنا تحميل الوحدة ngRoute في تطبيقنا.

routeProvider$

لنتمكن من استخدام ngRoute، سنحتاج إلى ربط مسار URL نسبي واحدٍ أو أكثر مع معالجاتٍ (handlers). يتيح لنا التابع module.config إعداد وحداتٍ مثل ngRoute بعد أن تقوم Angular بتحميلها. كل ما علينا هنا هو معرفة اسم الخدمة (service) أو المزوّد (provider) الذي سيقوم بإعداد الوحدة. في حالة ngRoute يُسمّى المزوِّد بـrouteProvider$. سنتمكن عند وضع هذا الاسم في تابع الاستدعاء الخلفي config من عمل سلسلةٍ من عمليّات ربط المسارات باستخدام التابع when. الوسيط الأول للتابع when هو المسار النسبي الخاص بوجهة التوجيه، أما الوسيط الثاني فهو كائن الإعدادات الذي يحدد الكيفية التي سيتم بها معالجة محتوى وجهة التوجيه.

angular.module('app')
  .config(function($routeProvider) {
    $routeProvider
      .when('/', {
        template: "<a href='#/about'>About</a>"
      })
      .when('/about', {
        template: "<a href='#/'>Home</a>"
      });
  });

يشبه الخيار template الذي يظهر في المثال السابق ذلك الذي استخدمناه في إعداد التوجيهات سابقًا. لا يحوي المثال السابق أي محتوىً متغيّر، فقط نص HTML ثابت. يُقدّم كلّ قالبٍ رابطًا إلى المسار النسبي للآخرين، لذا سنتمكن في المثال السابق من التبديل بين الاثنين. قد تتساءل أين سيتم وضع المحتوى المعالَج، حسنًا، تُقدّم الوحدة ngRoute توجيهًا مُميّزًا، هو ng-view، ويجب أن يكون هذا التوجيه موجودًا في وحدتنا لتعمل. تتكوّن القوالب الرئيسي في هذا الفصل كلّها من عنصرdiv خالٍ يوجد فيه التوجيه ng-view، وكلّ ما سوى ذلك سيقوم المعالج (handler) بمعالجته للمسار الحالي.

<div ng-view></div>

جرب النقر على الرابط الناتج لتتم معالجة الرابط للمورد الآخر. لم يعمل؟ حسنًا، لقد تجاهلنا أمرًا هامًا، إن نظرت إلى شريط التنقل الخاص بمتصفحك أثناء نقر الرابط فلن تراه قد تغير ليلائم المسار النسبي الصحيح، في الواقع، ليس هناك أي أمرٍ خاطئ في النص البرمجي الخاص بالمثال، وفي التطبيقات الحقيقية ستلاحظ التغيير، فكلّ ما في الأمر أن البيئة التفاعلية التي تمّ استضافة الأمثلة في داخلها ضمن هذه السلسلة لا تسمح لنا برؤية التغيير في الصفحة الرئيسية للفصل.

الخدمة location$

جميع أمثلة هذه السلسلة موضوعة داخل صناديق sandbox داخل أُطر iframe الخاصة بكل واحد منها. (يُمكنك إلقاء نظرة على الرابط التالي Codecademy/stuff.js إن كان لديك فضول لمعرفة طريقة القيام بذلك.) وهذا ممتازٌ لعزل البيئة البرمجية، إلا أنّه يمنعك من رؤية رابط الـURL الخاص بالأمثلة في شريط الانتقال. ومن حسن الحظ أنّه توجد طريقةٌ للتحايل على هذا الأمر، مع فائدةٍ إضافيّة في استكشاف خدمةٍ مفيدةٍ في Angular تسمح بمعاينة رابط URL الحالي، وهي الخدمة location$.

angular.module('app')
  .controller('LocationController', function($scope, $location) {
    $scope.location = $location.absUrl();
  });

يتم في المثال السابق التصريح عن متحكم بسيطٍ يستقبل الخدمة location$ عن طريق حقن التبعية، ويستخدمها للدخول إلى رابط URL المُطلق الحالي للتطبيق، حيث تكون القيمة هي دمجًا للرابط الثابت للجذر الخاص بالصفحة المغلّفة للمثال مع رابط الامتداد الذي تُديره الوحدة ngRoute.

المتحكم

يُمكننا أن نقوم بتحميل المتحكّم LocationController في المثال السابق بالطريقة المعيارية باستخدام التّوجيه ng-controller، إلّا أنّه بإمكاننا أيضًا أن نقوم بإعداد متحكّمٍ كخيار (option)، كما سنضيف خاصّيّة المجال location إلى قالبنا.

angular.module('app')
  .config(function($routeProvider) {
    $routeProvider
      .when('/', {
        controller: 'LocationController',
        template: '<div class="well well-sm" ng-bind="location"></div>\
                   <a href="#/about">About</a>'
      })
      .when('/about', {
        controller: 'LocationController',
        template: '<div class="well well-sm" ng-bind="location"></div>\
                   <a href="#/">Home</a>'
      });
  });

ها نحن ذا، صار لدينا الآن محاكٍ لشريط الانتقال لأمثلتنا، ولكن للأسف لن تتمكّن من تعديلها لرؤية الكيفية التي ستدير بها الوحدة ngRoute التغييرات بمعالجتها وإظهار محتوىً جديد، وسيكون عليك اختبار ذلك في تطبيقاتك الخاصة.

رمز Hashbang

تتوقّع وحدة ngRoute في الحالة الافتراضية أن تبدأ مسارات URL النسبيّة برمز hash (#)، ويُمكنك بسهولةٍ تغييره إلى البادئة hashbang متمثّلة بالرمز (!#) بإضافة إعداداتٍ إلى الخدمة locationProvider$ كما يبيّن المثال التالي. لاحظ أنّه يجب علينا أيضًا إضافة رمز ! إلى الروابط في قوالبنا.

angular.module('app')
  .config(function($locationProvider) {
    $locationProvider.hashPrefix('!');
  })
  .config(function($routeProvider) {
    $routeProvider
      .when('/', {
        controller: 'LocationController',
        template: '<div class="well well-sm" ng-bind="location"></div>\
                   <a href="#!/about">About</a>'
      })
      .when('/about', {
        controller: 'LocationController',
        template: '<div class="well well-sm" ng-bind="location"></div>\
                   <a href="#!/">Home</a>'
      });
  });

لا يزال رمز البادئة hashbang مصادقًا عليه من Google في دليل Webmasters الخاص بها، وتحديدًا في المقطع Making AJAX Applications Crawlable، الذي ينص على أنّ “أقسام الـhash يجب أن تبدأ بعلامة تعجّب”، على أيّ حالٍ، طالما أنّك تقوم بالخطوات اللازمة للتّأكّد من ملاءمة موقعك لقواعد SEO فقد تجد أنّه من الأفضل أن يكون تطبيقك مخدومًا بأسلوبٍ خالٍ من البادئة، وهذا الأسلوب مفعّلٌ في HTML5 الآن.

الكائن History من HTML5

يُقدّم الكائن window.history تحكّمًا بالتّنقّل في المتصفّحات الحديثة، مما يقدّم تجربة مستخدمٍ سلسة.

angular.module('app')
  .config(function($locationProvider) {
    $locationProvider.html5Mode(true);
  });

يُمكننا كتابة قيم href الخاصة بنا كمسارٍ بسيط، بدون البادئة # ولا البادئة !#.

angular.module('app')
  .config(function($routeProvider) {
    $routeProvider
      .when('/', {
        controller: 'LocationController',
        template: '<div class="well well-sm" ng-bind="location"></div>\
                   <a href="/about">About</a>'
      })
      .when('/about', {
        controller: 'LocationController',
        template: '<div class="well well-sm" ng-bind="location"></div>\
                   <a href="/">Home</a>'
      });
  });

استخدام الكائن history من HTML5 وسيلةٌ ممتازةٌ إن كنت غير مضطرٍّ لدعم متصفحاتٍ قديمة في تطبيقك.

العنصر templateUrl

لنقم الآن بتحليل تطبيقنا البسيط إلى مكوناته الأولية. كما قمنا سابقًا مع التوجيهات بجعل شيفراتنا البرمجية أكثر قابلية للإدارة عن طريق استخراج قوالبنا (حتى الصغيرة منها) إلى ملفاتٍ مستقلة، فسيكون علينا للقيام بذلك فقط استبدال العناصر template في إعداداتنا بالعناصر templateUrl.

angular.module('app')
  .config(function($routeProvider) {
    $routeProvider
      .when('/', {
        controller: 'LocationController',
        templateUrl: '/views/index.html'
      })
      .when('/about', {
        controller: 'LocationController',
        templateUrl: '/views/about.html'
      });
  });

دون تغييرٍ على صفحة About التي سنضعها في مجلد views.

<div class="well well-sm" ng-bind="location"></div>
<a href="/">Home</a>
<h4>About</h4>

سنتكلّم في الفقرات التالية عن فكرة التعامل مع روابط URL غير الصالحة، لذا لنقم بإضافة واحدٍ منها الآن.

<div class="well well-sm" ng-bind="location"></div>
<a href="/about">About</a> |
<a href="/bad-path">Bad path</a>
<h4>Home</h4>

جرّب النقر على Bad path في المثال السابق، ستنتهي التّسلية للأسف، وستضطر لإعادة إنعاش الصفحة لاستعادتها.

التابع otherwise

ما الذي يمكننا فعله لتجنب النهايات الحزينة كما حدث قبل قليل؟ قد يكون أحد الإجابات هو تجنب تقديم روابط غير صالحة، ولكن في التطبيقات الحقيقيّة لا يمكننا إيقاف المستخدم عن الكتابة في شريط التنقل. يُمكننا أن نُعِدّ المُخدّم ليقوم بإعادة توجيهٍ للروابط غير الصالحة إلى رابط الجذر لتطبيق Angular، ولكن هذا يعني إعادة تحميل الصفحة من جديد، مما يعني إعادة تشغيل التطبيق من البداية. كيف يُمكننا بناء تطبيقٍ من طرف المستخدم ليقوم بإعادة توجيهٍ للروابط غير الصالحة؟

لو افترضنا بأننا نريد أن نُعلم المستخدم بأن الرابط غير صالح قبل أن نقوم بإعادة التوجيه، فالأمر الأول الذي نحتاجه هو الواجهة التي ستظهر لأي رابط غير صالح. سنسُمّي هذا القالب 404 ليتناسب مع معناه، وطبعًا يمكنك تسميته بأي اسمٍ آخر.

<div class="well well-sm" ng-bind="location"></div>
<a href="/">Home</a>
<h4 class="text-danger">404 - Not Found</h4>

كلّ ما نحتاجه الآن هو عبارة when أخرى لإضافة المسار 404/. ثم لربط أي رابط غير متوافق مع الروابط الموجودة بالمسار 404/، وسنقوم بذلك بإضافة استدعاءٍ للتابع otherwise الذي يقوم بإضافة المسار إلى العنصر redirectTo فيه. في المثال التالي، سيتم تشغيل الاستدعاء للتابع config إضافةً إلى كل الإعدادات السابقة. (يُمكنك تسجيل عددٍ غير محدودٍ من الاستدعاءات الخلفية (callbacks) للتابع config في المزوِّد (provider))

angular.module('app')
  .config(function($routeProvider) {
    $routeProvider
      .when('/404', {
        controller: 'LocationController',
        templateUrl: '/views/404.html'

      })
      .otherwise({
        redirectTo: '/404'
      });
  });

جّرب النقر الآن على Bad path. صحيحٌ أنّ بيئة الأمثلة لا تسمح لك بإدخال مسارٍ عشوائيّ في شريط التصفح، إلّا أنّه يمكنك تعديل “href=”/bad-path في القالب السابق إلى أيّ قيمة تريدها لترى كيف سيتم التعامل معها. ربما يجب عليك دومًا إضافة معالجٍ (handler) ليتعامل مع كل الاحتمالات الباقية في إعدادات التوجيه الخاصة بك.

الأحداث (Events)

تُقدّم Angular تطبيقًا للرسائل من نمط (publish-subscribe) لأحداث التطبيق، وهي تعتمد على ثلاث توابع: emit$ وbroadcast$ وon$، حيث يقوم التابع emit$ و broadcast$ بتفعيل نشر أحداثٍ مخصصة، أما التابع on$ فيقوم بتسجيل معالجات (handlers) للأحداث التي تقوم بنشرها الوحدة ngRoute. فمثلًا، عندما يتمّ تغيير المسار بنجاح سينتج عن ذلك نشر الأحداث التالية:

هذه الأحداث مرتبةٌ في القائمة بنفس ترتيب ظهورها أثناء دورة حياة عملية تغيير المسار، لنقُم بإثبات ذلك.

تسجيل معالج للحدث (event handler)

لنستخدم التابع on$ لتسجيل معالجٍ بسيط لأحداث المسار، حيث سيقوم هذا المعالج بجمع أسماء الأحداث في مصفوفة، ثم سنقوم لاحقًا بطباعة أسماء الأحداث في الصفحة بنفس الترتيب الذي حُفظت به.

angular.module('app')
  .value('eventsLog', [])
  .factory('logEvent', function(eventsLog) {
    return function(event) {
      eventsLog.push(event.name);
    };
  });

في هذا المثال، سنقوم بتسجيل الأحداث أثناء انتقالنا من مسار الجذر (والمسؤول عنه هو المتحكم HomeController) إلى المسار events/ (والمسؤول عنه المتحكم EventsController). سنقوم أيضًا بجعل المتحكّم يضيف اسمه إلى القائمة أيضًا لنعرف وقت استدعاء هذا المتحكم.

angular.module('app')
  .controller('HomeController', function($scope, $location, $rootScope, eventsLog, logEvent) {
    $scope.location = $location.absUrl();
    $scope.link = {path: '/events', title: 'Events'};
    eventsLog.push("HomeController: " + $location.path());

    $rootScope.$on('$routeChangeStart', logEvent);
    $rootScope.$on('$locationChangeStart', logEvent);
    $rootScope.$on('$locationChangeSuccess', logEvent);
    $rootScope.$on('$routeChangeSuccess', logEvent);

    $scope.eventsLog = eventsLog;
  });

يقوم المتحكم EventsController بإضافة اسمه إلى المصفوفة eventsLog ثم يقوم بكشف (expose) السجل للمجال بحيث يمكننا رؤيته في الصفحة.

angular.module('app')
  .controller('EventsController', function($scope, eventsLog, $location) {
    $scope.location = $location.absUrl();
    $scope.link = {path: '/', title: 'Home'};

    eventsLog.push("EventsController: " + $location.path());
    $scope.eventsLog = eventsLog;
  });

نفس الواجهة ستعمل في كلا المتحكّمين. وسيتم عرض شريط انتقالٍ متغيّر إضافةً إلى محتويات السجل باستخدام التوجيه ng-repeat.

<div class="well well-sm" ng-bind="location"></div>
<a ng-href="{{link.path}}">{{link.title}}</a>
<ol>
  <li ng-repeat="event in eventsLog track by $index">
     {{event}}
  </li>
</ol>

كملاحظةٍ جانبية، يُعدّ المرشح json المبني في Angular مفيدًا في التنقيح وإنشاء السجلات، فإن أردنا رؤية ما هو أكثر من اسم الحدث، فيُمكننا استخدامه لعرض كامل المحتويات لكائن الحدث.

سيكون علينا لإتمام المثال أن نكتب إعداداتٍ مباشرةً للمسارين.

angular.module('app')
  .config(function($routeProvider) {
    $routeProvider
      .when('/', {
        controller: 'HomeController',
        templateUrl: '/views/events/index.html'
      })
      .when('/events', {
        controller: 'EventsController',
        templateUrl: '/views/events/index.html'
      });
  });

انقر على الرابط في المثال السابق لرؤية الأحداث المسجلة في الصفحة.

الموارد التي تتبع أسلوب REST

يُقدّم أسلوب REST في تطوير تطبيقات الويب مجموعةً من المعايير لتنظيم عمليات CRUD (إنشاء، قراءة، تحديث و حذف) على مجموعات الموارد والموارد المفردة أيضًا. لن تحتاج عند استخدامك لـ Angular أن تتّبع أسلوب REST في التوجيه إلا إذا قمت بتحميل الوحدة ngResource التي لم نقم بتغطيتها في السلسلة. على أيّ حال، سنختم هذا الفصل بالقليل من التوجيه المتّبع لأسلوب REST لتحصل على خبرةٍ في بعض الأمور العمليّة في التوجيه.

سنحتاج إلى مجموعة موارد لنتمكّن من البدء، وسنعرض في الفصل التالي HTTP كيف نقوم بتحميل البيانات من مخدّمٍ في النهاية الخلفية (backend). أما الآن فسنقوم فقط بحقن مصفوفةٍ من كائناتٍ من نوع item في داخل المتحكّم.

angular.module('app')
  .factory('Item', function(filterFilter) {

    var items = [
      {id: 1, name: 'Item 1', color: 'red'},
      {id: 2, name: 'Item 2', color: 'blue'},
      {id: 3, name: 'Item 3', color: 'red'},
      {id: 4, name: 'Item 4', color: 'white'}
    ];

    return {
      query: function(params) {
        return filterFilter(items, params);
      },
      get: function(params) {
        return this.query(params)[0];
      }
    };
  });

لكلّ عنصرٍ في المصفوفة قيمة فريدة للخاصية id فيه، مما يسمح لنا بالتعامل معه كموردٍ منفرد. تقوم الخدمة التي يُنشئها التابع factory بتغليف الوصول إلى المصفوفة items بتابعين مفيدين هما: query لمجموعة الموارد، وget للموارد المنفردة. يستخدم التابع query المرشّح ذا الاسم سيء الحظ، filter (محقونٍ مع filterFilter) لتنفيذ جلبٍ حسب المثال باستخدام الوسيط params، أمّا التابع get فيقوم باستدعاء التابع query ليقوم بإعادة موردٍ واحد. (راجع فصل المرشحات إن كنت قد تجاوزته، لمعلوماتٍ إضافيّة عن المرشّحات في Angular.)

أوّل شيءٍ نحتاجه في تطبيقٍ يتبع أسلوب REST هو دليل (index) أو مسارٌ للقائمة ليتم كشف المجموعة items كاملةً. وكلّ ما على المتحكم القيام به هو كشف كامل المحتويات للمصفوفة items باستدعاء التابع query دون وُسطاء.

angular.module('app')
  .controller('ItemsController', function($scope, $location, Item) {
    $scope.location = $location.absUrl();
    $scope.items = Item.query();
  });

واجهة هذا المتحكم سهلةٌ أيضًا، وسنستخدم التوجيه ng-repeat لإظهار العناصر.

<div class="well well-sm" ng-bind="location"></div>
<a ng-href="/">Home</a>
<ol>
  <li ng-repeat="item in items">
     {{item.name}} - {{item.color}}
  </li>
</ol>

في هذا المثال الأساسيّ، ستربط إعدادات التوجيه بين مسار الجذر (/) وبين واجهة المجموعة view.

angular.module('app')
  .config(function($routeProvider) {
    $routeProvider
      .when('/', {
        controller: 'ItemsController',
        templateUrl: '/views/items/index.html'
      });
  });

لا يُحقّق المثال السّابق أسلوب REST فعلًا، وذلك لأنّ المسار ليس اسم مجموعة الموارد (items/). المشكلة هي أننا نحتاج إلى ننشئ شيئًا ما في مسار الجذر ليكون المكان الافتراضي الذي يتم تحميل تطبيق Angular منه. سنرى الحل في المثال التالي، وهو إعادة التوجيه الفوري من المسار الجذر إلى المسار items/.

العنصر redirectTo

لنتمكّن من القيام بإعادة توجيهٍ تلقائيّة من مسارٍ لآخر، سيكون علينا استخدام العنصر redirectTo بدلًا من متحكّم وقالب.

angular.module('app')
  .config(function($routeProvider) {
    $routeProvider
      .when('/', {
        redirectTo: '/items'
      })
      .when('/items', {
        controller: 'ItemsController',
        templateUrl: '/views/items/index.html'
      });
  });

كما يُمكنك أن ترى في شريط الموضع في المثال السابق، فالمجموعة items يتم عرضها في المسار items/. ومهما كان عدد المرات التي تنقر بها على الرابط Home، فلن تتمكن من الانتقال إلى المسار الجذر / وسيتم دومًا إعادة توجيهك إلى المسار items/. هذا استخدامٌ بسيط للخيار redirectTo. إن كنت تريد تطبيق بعض العمليات على إعادة التوجيه، فيُمكنك توفير تابعٍ بدلًا من مسارٍ نصّيّ.

التابع search

كما ناقشنا في بداية هذا الفصل، تقدّم روابط URL النصّية تمثيلًا لحالة التطبيق (مثل عملية ترشيحٍ للموارد) يُمكن أن يتم حفظها ومشاركتها بسهولة. كيف يُمكننا معالجة نصّ طلب (query) يطلب فقط الحصول على العناصر التي تكون قيمة العنصر color فيها هو لونًا معيّنًا؟

angular.module('app')
  .config(function($routeProvider) {
    $routeProvider
      .when('/', {
        controller: 'LocationController',
        template: '<div class="well well-sm" ng-bind="location"></div>\
                   <a ng-href="/items?color=red">Red items</a>'
      })
      .when('/items', {
        controller: 'ItemsController',
        templateUrl: '/views/items/index.html'
      });
  });

في هذا المثال، تحتوي واجهة مسار الجذر الخاص بالتطبيق على رابط التّنقّل الحاوي على نصّ الطلب color=red. يُمكننا جلب قيمة هذا الوسيط بسهولةٍ باستخدام التابع search للخدمة location$.

angular.module('app')
  .controller('ItemsController', function($scope, $location, Item) {
    $scope.location = $location.absUrl();
    $scope.items = Item.query({color: $location.search().color});
  });

عند النّقر على الرابط Red items في المثال السابق، سترى كيف استخدم التابع query (الذي قُمنا بتعريفه سابقًا في الخدمة items) المُرشّح filter للقيام بعملية جلبٍ حسب المثال.

الخدمة routeParams$

يعتمد أسلوب REST نموذجيًّا على إلحاق معرّف المورد الفريد في نهاية المسار بدلًا من وضعه في نصّ الطلب. كيف يُمكننا القيام بمعالجةٍ صحيحة للمسار بحيث يحوي المعرّف الفريد؟ أو بعبارةٍ أدقّ، كيف يُمكننا استخراج المعرّف الفريد 3 من المسار items/3/ على سبيل المثال؟

لنقم بتحديث واجهة المجموعة لتتضمّن هذا الأسلوب من رابط التّنقّل لكلّ مورد مفرد على حدة.

<div class="well well-sm" ng-bind="location"></div>
<p class="lead">Items</p>
<ol>
  <li ng-repeat="item in items">
    <a ng-href="/items/{{item.id}}">
      {{item.name}}
    </a>
  </li>
</ol>

تُقدّم الخدمة routeParams$ وصولًا ملائمًا للعناصر في المسار، حيث يقوم بكشفها (expose) كعناصر مُسمّاة. يُمثّل القسم الأخير من المسار المعرّف الفريد لموردٍ وحيد يتبع أسلوب REST. فمثلًا، يجب أن يعيد المسار items/3/ تمثيلًا للمورد Item بالمعرّف الفريد 3.

في إعدادات التوجيه الخاصة بنا، يُمكننا استخدام البادئة الخاصّة : لتعريف متغيّراتٍ ذات أسماءٍ متغيرّة قابلةٍ للاستخراج من المسار. تمّ الاتّفاق على تسمية الوسيط المعرّف للمورد بالاسم id:، لذا سيكون نصّ المسار هو items/:id/.

angular.module('app')
  .config(function($routeProvider) {
    $routeProvider
      .when('/', {
        redirectTo: '/items'
      })
      .when('/items', {
        controller: 'ItemsController',
        templateUrl: '/views/items/linked-index.html'
      })
      .when('/items/:id', {
        controller: 'ItemController',
        templateUrl: '/views/items/show.html'
      });
  });

تقوم الوحدة بوضع الوسطاء المستخرجة من المسار في الخدمة routeParams$ التي يجب أن نقوم بحقنها في المتحكّم ItemController جنبًا إلى جنبٍ مع الخدمة Item التي أنشأناها.

angular.module('app')
  .controller('ItemController', function($scope, $location, Item, $routeParams) {
    $scope.location = $location.absUrl();
    $scope.item = Item.get({id: $routeParams.id});
  });

سنقوم باستدعاء التّابع Item.get مستخدمين القيمة التي تمّ إسنادها في routeParams.id$، وهذا التّابع يستخدم المرشّح filter لإيجاد موقع النموذج الصحيح. (يُمكن للتابع ()Array.prototype.find الموجود في المكتبة المركزيّة أن يُقدّم طريقةً أفضل للقيام بذلك عندما ينتشر هذا التابع على نطاقٍ واسع.)

طريقة عرض المورد item المفرد سهلةٌ ومباشرة، سنقوم بتضمين رابطٍ للعودة إلى المسار items/ بحيث نتمكّن من العودة إلى واجهة عرض القائمة.

<div class="well well-sm" ng-bind="location"></div>
<a ng-href="/items">Items</a>
<p class="lead">{{item.name}}</p>
<p>Color: {{item.color}}</p>

صحيحٌ أنّ الخدمة routeParams$ والتابع ()location.search$ كلاهما سهل الاستخدام نوعًا ما، إلّا أنّهما معًا يقدّمان حجر بناءٍ هامًّا في أيّ عمليّة توجيه متضمّنةٍ لأسلوب REST.

خاتمة

تعلّمنا في هذا الفصل أساسيات طريقة Angular الخاصة بها في التوجيه. ورغم أنّ ngRoute لا تُقدّم بعض الميزات المعقّدة للتوجيه، كدعم الموارد المتداخلة، أو آلة الحالة من الدرجة الأولى، أو توليد مسارات URL، إلا أنّها تعطيك مقدّمةً ممتازةً للتوجيه في Angular. ستجد أنّ العديد من التطبيقات الحقيقية تستخدم UI-Router من مشروع AngularUI بدلًا من ذلك، ولن نتطرّق إليها في هذه السلسلة، بل سيكون الفصل الأخير مقدّمةً لتحميل البيانات من المخدّم في النهاية الخلفية (backend) باستخدام الخدمة http$ في Angular.

30يوليو

( تعلم Angular ) – مجالات التوجيه (Directive Scopes) في AngularJS

يقترنُ المتحكّم بمجالٍ جديد يتمّ إنشاؤه عند إنشاء المتحكّم، أمّا التّوجيه فلا يتمّ إعطاؤه مجالًا خاصًّا به عند إنشائه في الحالة الافتراضيّة، بل يستخدم المجال المتاح، وذلك اعتمادًا على مكانه في المستند. بالنّسبة لي فأنا أعتبر هذه الحالة الافتراضيّة سيّئة، وذلك لأنّ معظم التّوجيهات تُكتب كمكوّناتٍ سيتمّ إعادة استخدامها مع حالتها المغلَّفة. ولكن هذه الحالة الافتراضيّة تُفيد في جعل تعلُّم استخدام التّوجيهات سهلًا في البداية، وذلك كما رأينا في الفصل السّابق.

والآن بعد أن اعتدنا على التّوجيهات المخصّصة، فقد حان الوقت لنتعلّم كيفيّة إدارة حالة التّوجيه (directive state) بطريقةٍ مغلّفة، مما يسمح بإعادة استخدامٍ آمنٍ للتّوجيه المخصّص.

عزل المجال

يُشار إلى عبارة عزل المجال (isolate scope) غالبًا كأحد المصطلحات الصّعبة في Angular. ولكن كما هو الحال غالبًا في علوم الحاسوب (وأيضًا في شبه علوم الحاسوب)، فالمفاهيم خلف الأسماء الرّنّانة غالبًا ما تكون بسيطةَ الفهم.

عزل المجال يعني فقط إعطاء التّوجيه مجالًا خاصًّا به بحيث لا يكون موروثًا من المجال الحالي.

قبل أن نبدأ، نحتاج إلى الاهتمام بالإعدادات المُعتادة. أوّلًا، لنقُم بإضافة Angular وBootstrap إلى الصّفحة.

<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.2/angular.js"></script>

لابدّ من تعريف وحدةٍ للتطبيق.

angular.module('app', []);

والآن سيكون علينا تحميل وحدة الجذر الخاصّة بنا عن طريق تمرير اسمها إلى التّوجيه ng-app.

<body ng-app="app">
  <!-- الأمثلة توضع هنا -->
</body>

والآن بعد أن أصبحت Angular ووحدة الجذر مُعدّتين جيّدًا، لنقُم بتعريف متحكّمٍ يقوم بالكشف (expose) عن ثلاث عناصر لتخزين سلاسل نصّيّة، وكلّ عنصرٍ سيُخزّن اسم المكوِّن الذي سيستخدمه في العرض. السلسلة النصية الأولى تُعرّف عن المتحكّم نفسه، والسلسلتان النصيتان الباقيتان لنسختين منفصلتين من توجيهٍ سنقوم بتعريفه لاحقًا.

angular.module('app')
  .controller('MessageController', function($scope) {
    $scope.message  = "the controller";
    $scope.message1  = "directive 1";
    $scope.message2  = "directive 2";
  });

وفيما يلي شيفرة التّوجيه، وفيها نقوم أيضًا بإنشاء عنصرٍ اسمه message في نسخة عنصر المجال الذي يتم تمريره إلى تابع الرّبط.

angular.module('app')
  .directive('message', function() {       
    return {
      template: "<p>hello, from {{message}}</p>",
      link: function(scope, element, attrs) {
        scope.message = scope.$eval(attrs.message);
      }  
    };
  });

لقد ناقشنا في الفصل السابق استخدام scope.$eval للوصول إلى عنصرٍ في المجال تمّ تمريره كقيمة نصّيّة لوسيط التّوجيه، وما يهمّنا تحديدًا في هذا المثال هو ما سيحدث للعنصر scope.message الموجود في المتحّكم ونسختي التّوجيه. سنضيف خانات إدخالٍ مرتبطةً بعناصر المجالات الثلاثة التي تمّ إنشاؤها في المتحكّم.

قبل أن تنظر إلى استخدام المثال ومخرجاته، ألقِ نظرةً سريعةً مرّةً أخرى على المتحكّم والتّوجيه السابقين. هل ترى أيّ إمكانيّةٍ للتضارب بينهما؟

<div ng-controller="MessageController">       
  <h4>hello, from {{message}}</h4>
  <input class="form-control" ng-model="message"> 
  <input class="form-control" ng-model="message1"> 
  <input class="form-control" ng-model="message2">
  <br>
  <div class="well" message="message1"></div>     
  <div class="well" message="message2"></div>     
</div>

خلال الوقت الذي أنهى فيه الاستخدام الثّاني للتّوجيه عمله، تمّ إسناد القيمة “2 directive” للعنصر message في مجال المتحكّم. إن قمتَ بكتابة شيءٍ داخل خانة الإدخال العُلويّة، والتي ترتبط بالعنصر message، فسترى أنّ التغيير أدّى إلى تحديث مخرجات نسختي التّوجيه معًا. في هذا المثال، تمّت مشاركة مجال المتحكّم ضمن النّسخ الثّلاثة، مما أدّى إلى حدوث هذه المشكلة، فنحن لا نريد أن تقوم أيّ نسخةٍ من نُسخ التّوجيه بتعديل المجال الذي يُشاركه المتحكّم MessageController مع نُسخ التّوجيه الأخرى. إذًا، كيف يُمكننا إعطاء كلّ نسخةٍ للتّوجيه مجالها الخاصّ المعزول؟ هل يُمكنك اكتشاف الحلّ بنفسك؟

الحلُّ هو التّصريح عن عنصر scope في كائن الإعدادات الخاص بالتّوجيه. الخيار الأكثر سهولةً هو إسناد القيمة true إلى هذه الخاصّية، مما يعطي نُسخ التّوجيه مجالاتٍ خاصّة بها. هذه المجالات الجديدة ترث من المجال الحاليّ، تمامًا كما لو كانت نُسخ التّوجيه نُسخًا لمتحكّمٍ ما.

angular.module('app')
  .directive('message', function() {       
    return {
      template: "<p>hello, from {{message}}</p>",
      link: function(scope, element, attrs) {
        scope.message = scope.$eval(attrs.message);
      },
      scope: true  
    };
  });

لقد استطعنا حلّ مشكلة الحالة الابتدائيّة لكلّ العناصر. ولكن هل انتهينا؟ كلّا. جرّب الكتابة في أيّ خانةٍ من خانات الدّخل.

إذا أردنا أن تؤدّي التحديثات للعنصرين message1 وmessage2 التّابعين لمجال المتحكّم إلى تحديثات في التّوجيه، فسيكون علينا القيام بأكثر مما قمنا به حتّى الآن، لأنّ تابع الرّبط لا يتمّ تشغيله إلّا مرّةً واحدة، عندما يتمّ إخراج (render) التّوجيه للمرّة الأولى، وعند استخدام eval$ لتغيير قيمة عنصرٍ في التّوجيه فلن يكون هذا التّغيير دائمًا ولن يعمل الرّبط ثنائيّ الاتّجاه بين مُخرجات التّوجيه والنّموذج الأصليّ. إن لم تكن قد جرّبت تغيير القيَم في خانات الدّخل بعد، فأنصحك بشدّةٍ بتجربة ذلك لتتأكّد بنفسك من أنّ القيم لا يتمّ تحديثها.

إذًا ما الذي يجب علينا إضافته لتابع الرّبط ليعمل الرّبط ثنائيّ الاتّجاه؟ لا نحتاج إلى شيء، علينا حذف تابع الرّبط. فالشيفرة الأمريّة (imperative) المطلوبة في تابع الرّبط مملّةٌ ومتوقّعة. لم لا نقوم باستبدالها بإعداداتٍ تصريحيّة ونقوم بأتمتة العمل البرمجيّ؟ هذا بالضّبط هو ما أراد مخترعوا Angular القيام به.

طرق ربط المجال المعزول

يتمّ التصريح عن طرق ربط المجال المعزول باستخدام الرّموز التّالية:

  • الرّمز = للرّبط ثنائيّ الاتّجاه (two-way binding)
  • الرّمز & للرّبط وحيد الاتّجاه (one-way binding)
  • الرّمز @ للرّبط النّصّي (text binding)

لنلقِ نظرةً على كلّ واحدةٍ من تلك الطّرق على حدة، ولنبدأ بالطريقة المفيدة، الرّبط ثنائيّ الاتّجاه.

الربط ثنائي الاتجاه (=)

لحلّ مشكلة تحديث مخرجات التّوجيه، فلن يكون علينا إلّا استبدال الطريقة الأمريّة (imperative) في كتابة الشّيفرة، والتي اعتدنا عليها عند كتابة تابع الرّبط، بكائن إعداداتٍ يتمّ إسناده للعنصر scope.

angular.module('app')
  .directive('message', function() {       
    return {
      template: "<p>hello, from {{message}}</p>",
      scope: {
        message: '='
      }
    };
  });

لاحظ بأنّ قيمة العنصر scope أصبحت الآن كائنًا بدلًا من القيمة true البوليانيّة الأوّليّة (primitive). ما قمنا به يؤدّي إلى قطع العلاقة الوراثيّة بين المجالات. ولنتمكّن من رؤية ذلك عمليًّا، يُمكنك استبدال scope: true في المثال الأسبق، بـ {} :scope. لا يعمل تابع الرّبط في ذلك المثال دون الوصول إلى العناصر في المجال الأب.

لنتمكّن من أتمتة العمل الذي يقوم به تابع الرّبط ذاك، فيجب علينا إضافة عنصرٍ إلى كائن الإعدادات scope بحيث يكون اسمه بنفس اسم عنصر المجال المحلّي الذي نريد ربطه بعنصرٍ ما في المجال الخارجيّ، فبما أنّ توجيهنا يستخدم العنصر message في القالب الخاصّ به، فسنضيف عنصرًا اسمه message إلى كائن الإعدادات، ونُسند إليه القيمة =، مما يدلّ على أنّنا نريد أن يتمّ ربطه باستخدام الرّبط ثنائيّ الاتّجاه مع العنصر في المجال الخارجيّ الذي يتم تمرير اسمه إلى الخاصّيّة message. قد لا يكون الأمر سهلًا، ولكنّه يعمل جيّدًا ويُمكنك تجريب ذلك بكتابة شيءٍ ما في خانات الإدخال لتحديث عناصر المتحكّم في المثال السابق. وبهذا نكون قد أتممنا كتابة المتحكّم message أخيرًا.

الربط وحيد الاتّجاه (&)

قد تحتاج التّوجيهات أحيانًا إلى طريقةٍ لاستدعاء التّوابع في المجال الشّامل رغم أنّها تكون في مجالٍ معزول. بعبارةٍ أخرى، قد تحتاج إلى معالجة عبارة Angular ضمنالسياق الأصلي. ومن أجل ذلك تمّ إيجاد طريقة الرّبط وحيد الاتّجاه، أو خيار المعالجة. لنبدأ بمتحكّمٍ يقوم بالكشف عن تابع استدعاءٍ خلفيٍّ (callback) اسمه kaboom.

angular.module('app')
  .controller('ClickController', function($scope) {
    $scope.message = "Waiting...";
    $scope.kaboom = function() {
      $scope.message = "Kaboom!";
    }
  });

يعرض المثال التالي كيف يُمكن استخدام المتحكّم ClickController، حيث يمكن استخدامه مع التّوجيه العام bootstrap-button الّذي يُمكن أن يتمّ إعداده ليقوم باستدعاء التّابع kaboom عندما يتم نقر الزّر.

<div ng-controller="ClickController">
  <h4>
    {{message}}
  </h4>
  <bootstrap-button the-callback="kaboom()"></bootstrap-button>
</div>

لنقُم الآن بكتابة شيفرة التّوجيه bootstrap-button، في داخل قالبه البسيط، سنستخدم التّوجيه ng-click المدمج في Angular لربط حدث النّقر بتابع الاستدعاء الخلفي.

views/button.html

<button type="button" class="btn btn-default" ng-click="theCallback()">
  Submit
</button>

بما أنّ التّوجيه العام الذي سنكتبه لا يُمكنه معرفة اسم تابع الاستدعاء الخلفيّ الحقيقي الذي سيتم تضمينه، سنستخدم اسمًا عشوائيًّا theCallback كاسمٍ للتّابع. حسنًا، والآن كيف سنقوم بربط theCallback (في الواقع هي عنصرٌ في المجال) مع kaboom؟

ليست هذه بمشكلةٍ، فلدينا الرّبط وحيد الاتّجاه.

angular.module('app')
  .directive('bootstrapButton', function() {
    return {
      restrict: 'E',
      scope: {
        theCallback: '&'
      },
      templateUrl: '/views/button.html'
    }
  });

يقوم الرّمز (&) بعمليّة الرّبط المطلوبة.

والآن ماذا عن خيار الإعداد الأخير، @، المُسمّى بخيار الرّبط النّصّيّ؟ سنحتاج إلى الانتقال مؤقّتًا إلى مثالٍ آخر من المصطلحات الصّعبة في Angular، وذلك كي نتمكّن من تبيان فائدة هذا الخيار بأفضل ما يمكن.

الاستبدال Transclusion

لابدّ من وجود اسمٍ آخر أفضل من Transclusion، فمُستخدموا Ruby on Rails سيلاحظون تشابه آلية عمل الـTransclusion مع yield، وهذا المصطلح أسهل للفهم.

تعني عمليّة الاستبدال transclude لعنصرٍ ما، أن نقوم بالتّحكّم بإعادة إنتاج المكوّن الذي قام بتضمين التّوجيه، وفي حالتنا يكون هذا المكوّن هو القالب الذي يتضمّن التّوجيه. إن لم تكن هذه العبارة واضحةً لك، فيُمكنك التّفكير بأنّ الاستبدال (transclusion) يعطي الأمر التّالي: “قم بقصّ محتويات القالب الموجود داخل شيفرة التّوجيه، ثمّ ألصقها هنا“. ويعتمد مكان اللصق على التّوجيه المدمج ng-transclude الذي يُمكننا وضعها في مكانٍ ما داخل القالب الموجود في التّوجيه المخصّص.

كمثالٍ بسيط، لنقم بكتابة التّوجيه box الذي نريد تضمينه حول محتوىً ما، عن طريق وضعه في عنصرٍ div كغلاف.

<div box>
  This <strong>content</strong> will be <em>wrapped</em>.
</div>

ما سيقوم به التّوجيه box هو تغليف المحتوى الدّاخليّ بعنصر p مع بعض التّنسيق من Bootstrap. هناك جزآن يجب الاهتمام بهما عند استخدام الاستبدال (transclusion)، أوّلهما، يجب أن نقوم بإضافة transclude: true إلى إعدادات التّوجيه، وثانيهما، لابُدّ من استخدام التّوجيه ng-transclude في مكانٍ ما من القالب الخاصّ بالتّوجيه، وذلك لاختيار المكان الذي نريد أن نقوم فيه بإضافة المحتوى الجديد.

angular.module('app')
  .directive('box', function() {
    return {
      template: '<p class="bg-primary text-center" ng-transclude></p>',
      transclude: true
    };
  });

ألم يكن هذا سهلًا؟ بالطّبع.

يُمكننا جعل المثال السابق أسهل من ذلك بعدم استخدام التّوجيه من الأساس، وإضافة تنسيق CSS للعنصر المغلِّف مباشرةً. ولذلك سنقوم باستخدام الاستبدال (transclusion) في مثالٍ أكثر واقعيّة لتغليف التّعقيدات الخاصة بتنسيق Bootstrap لـلوحةٍ مع ترويسة. سيسمح التّوجيه panel لمستخدمه بتحديد النّص الخاص بشريط العنوان في رأس اللوحة، وأيضًا بتحديد المحتوى المطلوب إدخاله في جسم اللوحة. بالنّسبة لمحتوى اللوحة، سيتمّ توظيف الاستبدال (transclusion) تمامًا كما في المثال السّابق، أمّا بالنّسبة لنصّ العنوان، فسنستخدم الخيار المتبقّي لربط المجال المعزول الذي تركناه قبل قليل.

الربط النصي (@)

عندما يكون كلّ ما تحتاجه هو نسخ سلسلة نصّيّة إلى مجال التّوجيه الخاصّ بك، عندها سيكون الرّبط البسيط وحيد الاتّجاه (@) هو الطّريقة التّصريحيّة للقيام بذلك. تُبيّن الشّيفرة التّالية القالب الخاصّ بالتّوجيه. هدفنا من العنصر title هو أن نسمح للنّصّ الذي تحويه بأن يصبح أحد خصائص التّوجيه.

views/panel.html

<div class="panel panel-default">
  <div class="panel-heading">
    <h3 class="panel-title">
      {{title}}
      <small>
        Static text in the directive template
      </small>
    </h3>
  </div>
  <div class="panel-body" ng-transclude></div>
</div>

وفي إعدادات التّوجيه الخاصّ بنا، سنقوم بالحدّ من استخدامه للعناصر، وسنصرّح عن طريقة ربط الخاصّيّة title مع العنصر title في مجاله.

angular.module('app')
  .directive('panel', function() {
    return {
      restrict: 'E',
      templateUrl: '/views/panel.html',
      scope: {
        title: '@'
      },
      transclude: true
    };
  });

المثال التّالي يوضّح طريقة استخدام التّوجيه panel لكتابة النّصّين الثّابتين (static) في العنوان، وذلك في الوقت نفسه الذي يتمّ فيه استخدام الرّبط ثنائيّ الاتّجاه في المحتوى المُستبدل (transcluded).

<div ng-controller="MessageController">
  <panel title="Static text in the client template">
    <h3>
      <em>Hello</em>, from <strong>{{message}}</strong>
    </h3>
    <input class="form-control" ng-model="message"> 
  </panel>
</div>

لا تصلح طريقة الرّبط التي استخدمناها للعنوان باستخدام @ إلّا مع السّلاسل النّصّيّة التي لن يتمّ التّعديل عليها، فهذه الطّريقة لا يتمّ فيها مراقبة التّغييرات التي تطرأ على العنصر الأصليّ.

خاتمة

بناءً على خبرتك الحاليّة، فقد يكون هذا الفصل هو الأصعب ضمن هذه السلسلة، لذا أُهنّئُك على إنهائك له. يُفترض بك الآن أن تكون قادرًا على تعريف توجيهاتٍ مخصّصة يُمكن استخدامها دون الخوف من التّأثيرات الجانبيّة النّاتجة عن الحالة المشتركة، وهذه خطوةٌ كبيرة في طريقك نحو إكمال سيطرتك على التّوجيهات في Angular. في هذه النّقطة، سنحوّل تركيزنا عن الفروقات الدّقيقة في استخدام التّوجيهات، وسنتوجّه إلى مفهومٍ أساسيٍّ في مجال تطوير الويب من طرف المُستخدم: التسيير (routing) و الـURLs.

30يوليو

( تعلم Angular ) – التوجيهات (Directives) في AngularJS

لَعِبت التّوجيهات المبنيّة داخل Angular مثل ng-bind وng-model وinput دور النّجوميّة في هذه السلسلة منذ الفصل الأوّل، المبادئ، حتّى أنّ فصل المجموعات كان يركّز كلّيًّا على استخدام توجيهٍ واحدٍ (قويٍّ إلى حدّ كبير) هو التّوجيه ng-repeat. قد تظنّ بعد ذلك أن قائمة التّوجيهات المبنيّة في داخل Angular تحوي حلولًا لجميع الحالات، ولكنّ هذا غير صحيحٍ بالطّبع. لقد تصوّر مصمّموا Angular بأنّها ستكون طريقةً للسّماح لغير المبرمجين ببناء صفحاتٍ تفاعليّة، إلّا أنّها تطوّرت لتصبح منصّةً تعطي مطوّري الويب القوّة لإنشاء امتداداتٍ وتخصيص HTML العاديّة، وهذا يعني إنشاء التّوجيهات الخاصّة بك، وهذا تمامًا ما سنقوم به في هذا الفصل.

يكون إنشاء توجيهٍ مخصّص بشكله الأبسط مشابهًا كثيرًا لإنشاء مرشّح مخصّص، وهذا هو سبب الحديث عن المرشّحات قبل التّوجيهات في فصلٍ سابق. التّابع الخارجي الذي سنقوم بتمريره إلى التّابع directive هو غلافٌ لحقن التّبعيّة ويُدعى بتابع الصّناعة (factory function)، ويُعرف بالوصفة (recipe) ضمن توثيق Angular. يتمّ استدعاء هذا التّابع مرّةً على الأكثر، أو لا يتمّ استدعاؤه أبدًا إن لم يتمّ استخدام التّوجيه، كما تعلّمنا سابقًا في فصل الخدمات. في داخل تابع الصناعة يوجد تابعٌ آخر تقوم Angular فعليًّا باستدعائه وهو المكان الذي نقوم فيهذر عن بالعمل الحقيقيّ للتّوجيه، ويُسمّى بتابع الربط link.

التابع directive

نستخدم التّابع directive لتسجيل توجيهٍ مخصّص، وهذا التّابع ذو الاسم المناسب لوظيفته جزءٌ من الواجهة البرمجيّة للوحدات، لذا سنحتاج إلى إنشاء وحدةٍ جذرٍ لتطبيقنا.

angular.module('app', []);

ثمّ نقوم بتحميل الوحدة الجذر عن طريق تمرير اسم التّطبيق إلى التّوجيه ng-app.

<body ng-app="app">
  <!-- الأمثلة توضع هنا -->
</body>

وبذلك أصبحنا جاهزين لإنشاء توجيه مخصّص، ولنبدأ بتوجيهٍ بسيطٍ جدًّا اسمه hello.

angular.module('app')
  .directive('hello', function() {
    return function(scope, element) {
      element.text("hello");
    };
  });

للقيام بأيّ شيءٍ مفيدٍ سنحتاج إلى استخدام الوسيط الثّاني لتابع الرّبط، والّذي يكون غلافًا (wrapper) لعنصرٍ من المستند نجلبه من مكتبة jqLite الخاصّة بـAngular أو من مكتبة jQuery حسب الوسيط الذي قمت بتضمينه. في هذا المثال، قمنا باستبدال المحتوى السّابق للعنصر بالعبارة hello.

The message is <span hello></span>.

الناتج:

The message is hello.

أليس هذا رائعًا؟ لقد أنشأنا لتوّنا خاصّيّة HTML جديدة خاصّةً بنا.

الوسيط scope

في المثال السابق قمنا يدويًا (hardcoded) بكتابة النصّ التي نريد كتابته وهذا لا يفيدنا كثيرًا، لذا لنرى إن كان بإمكاننا استبدال النّصّ بقيمةٍ متغيّرة. ربّما كنت تتساءل بخصوص الوسيط الأوّل scope، لنقم بإضافة متحكّمٍ يقوم بإنشاء العنصر message في مجاله.

angular.module('app')
  .controller('MessageController', function($scope) {
    $scope.message = 'hello';
  });

والآن لنقم باستخدام هذا العنصر في التّوجيه، وسنُسمّيه message أيضًا.

angular.module('app')
  .directive('message', function() {
    return function(scope, element) {
      element.text(scope.message);
    };
  });

من الواضح أنّه يتوجّب علينا استخدام هذا التّوجيه في المكان داخل المستند الذي يكون تابعًا للمتحكّم MessageController.

<p ng-controller="MessageController">
  The message is <span message></span>.
</p>

الناتج:

The message is hello.

قد تتساءل، لماذا يتمّ في المتحكّم تعريف وسيط المجال مسبوقًا برمز الدولار scope$ بينما في تابع الرّبط يكون هذا الوسيط بدون علامة الدولار في بدايته scope، والسّبب هو أنّ أسماء الوسطاء في تابع الرّبط غير مهمّة (إلّا بالنّسبة لنا فقط) على عكس الوسطاء التي يتم حقنها.

الوسطاء

كما تعلّمنا سابقًا عند استخدام التّوجيهات المبنيّة داخل Angular، فالتّوجيهات تقبل تمرير الوسطاء، وهذه الوسطاء تُمرّر داخل الوسيط الثّالث لتابع الرّبط، وهو الوسيط attrs. مرّةً أخرى، وعلى عكس حقن التّبعيّات، يُمكننا تسمية الوُسطاء بأيّ اسمٍ نريده، ويُفضّل استخدام أسماءٍ مفهومةٍ لتبقى الشّيفرة مفهومة.

angular.module('app')
  .directive('welcomeMessage', function() {
    return function(scope, element, attrs) {
      element.text(attrs.welcomeMessage);
    };
  });
The message is <em welcome-message="bonjour"></em>.

الناتج:

The message is bonjour.

لاحظ أنّه عندما نستخدم أكثر من كلمةٍ واحدةٍ في كتابة اسم التّوجيه بأنّنا نستخدم نمط camel case حيث نكتب أوّل حرفٍ في الكلمة الثّانية بحرفٍ كبير، وهذا هو النّمط المتعارف عليه في JavaScript، ولكنّ النمط المستخدم في HTML هو فصل الكلمات باستخدام إشارة (-) وهو ما يُدعى hyphenated style، وخلاصة الكلام هي أنّك عندما تريد استخدام welcome-message لتضمين التّوجيه الخاص بك في القالب، سيكون عليك استخدام الاسم welcomeMessage عندما تقوم بتعريفها في JavaScript.

قد تكون لاحظت بأنّ بعض التّوجيهات المدمجة في Angular تقوم بمعالجة (evaluate) العبارات، إلّا أنّ هذا السلوك ليس هو السلوك الافتراضي.

The message is <em welcome-message="'bon' + 'jour'"></em>.

الناتج:

The message is 'bon' + 'jour'.

بما أنّنا نعمل داخل قالب Angular، سيكون بإمكاننا دومًا تمرير نتيجة عبارةٍ معالجَة (evaluated).

The message is <em welcome-message="{{'bon' + 'jour'}}"></em>.

الناتج:

The message is bonjour.

يُمكنك أيضًا إدخال سلسلةٍ نصّيّة معرّفة مسبقًا داخل السلسلة النّصّية العاديّة.

<p ng-init="greeting = 'bonjour'">
  The message is <span welcome-message="I say {{greeting}}, you say hello"></span>.
</p>

الناتج:

The message is I say bonjour, you say hello.

لتتمكّن من معالجة العبارة الممرّرة إلى التّوجيه كسلسلةٍ نظاميّة، ستحتاج إلى استخدام التّابع eval$.

التّابع scope.$eval

بالرغم من أنّ Angular لا تعتبر أنّ الوسيط المُمرّر إلى التّوجيه عبارةٌ تحتاج إلى معالجة، إلّا أنّه يمكننا ببساطةٍ أن نعالج هذه العبارة باستخدام التّابع eval$، ولأخذ العلم، فهذه الطّريقة لا تؤدّي إلى إعادة تحديث العرض (view) عند تغيُّر النّموذج، وهذا مفيدٌ فقط في حالات الاستخدام الأساسيّة.

angular.module('app')
  .directive('welcomeMessage', function() {
    return function(scope, element, attrs) {
      var result = scope.$eval(attrs.welcomeMessage);
      element.text(result);
    };
  });

لنقم بتجربة حالة استخدامٍ بسيطة نقوم فيها بوصل سلسلتين نصّيّتين.

The message is <em welcome-message="'bon' + 'jour'"></em>.

الناتج:

The message is bonjour.

يُمكننا أيضًا باستخدام scope.$eval أن نقوم بتمرير كائنات، مّما يتيح لنا طريقةً لتلقّي العديد من الوسطاء معًا، وسنلقي نظرةً في فقرةٍ لاحقةٍ من هذا الفصل علىتوجيهات العناصر، والتي تقبل أيضًا العديد من الوسطاء المُسمّاة عن طريقة إتاحة وجود العديد من الخصائص للعنصر، أمّا الآن فسنرى في المثال التالي معالجةً لكائنٍ من نوع options، حيث نقوم بتمرير العديد من الوسطاء إلى التّوجيه.

angular.module('app')
  .directive('welcomeMessage', function() {
    return function(scope, element, attrs) {
      var options = scope.$eval(attrs.welcomeMessage);
      var result = options.emoticon + ' ' + options.message + ' ' + options.emoticon;
      element.text(result);
    };
  });

وبهذه الطّريقة استطعنا إدخال وسيطين بفعّاليّة، message وemoticon، واستطعنا إنجاز الوصل بينهما داخل التّوجيه.

The message is <em welcome-message="{message: 'bonjour', emoticon: '\u263a'}"></em>.

الناتج:

The message is  bonjour ☺.

صِرنا الآن نفهم كيفيّة استخدام توجيهاتٍ مخصّصةٍ مع المجالات والمتحكّمات. ولكن ماذا لو أردنا استخدام خدماتٍ أخرى داخل التّوجيه؟

حقن التبعية

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

على سبيل المثال، لنفترض بأنّنا نريد توجيهًا يُمكنه تطبيق حسمٍ على السّعر وأيضًا القيام بتنسيقٍ للعملة لقيمة المتغيّر price في المجال. سيحتاج هذا التّوجيه للوصول إلى الخدمة المخصّصة calculateDiscount المُعرّفة كما يلي.

angular.module('app')
  .value('discountRate', 0.8)
  .factory('calculateDiscount', function(discountRate) {
    return function(amount) {
      return amount * discountRate;
    };
  });

والآن سنستخدم تابع الوصفة (recipe) التّغليفيّ لحقن الخدمة calculateDiscount مع المُرشّح currency أيضًا.

angular.module('app')
  .directive('discount', function(calculateDiscount, currencyFilter) {
    return function(scope, element, attrs) {
      var price = scope.$eval(attrs.discount);
      var discountPrice = calculateDiscount(price);
      element.html(currencyFilter(discountPrice));
    };
  });

لاحظ بأنّنا قمنا بتمرير العنصر price كقيمةٍ لوسيط التّوجيه في القالب، ويُمكننا الحصول على هذه القيمة للوسيط عن طريق الوسيط attrs حيث نصل إلى الخاصّيّة (discount) وهي اسم التّوجيه الخاصّ بنا. في المثال التّالي، سنقوم بإسناد قيمة هذا العنصر في المجال (الّتي نحصل عليها عن طريق scope.$eval) إلى المتغيّر المحلّي المُسمّى price.

<table ng-init="price = 100">
  <tr>
    <td>Full price:</td>
    <td ng-bind="price | currency"></td>
  </tr>
  <tr>
    <td>Sale price:</td>
    <td discount="price"></td>
  </tr>
</table>

الناتج:

Full price:	$100.00
Sale price:	$80.00

قد ترغب في وضع مُرشّحٍ بدلًا من التّوجيه، فالتّحويل الذي قمنا به بسيطٌ للغاية، حيث لم نضِف إلى المستند أيّ سلوكٍ تفاعليّ. لننتقل الآن إلى مثالٍ أكثر تعقيدًا بحيث يحتاج إلى إضافة وسمٍ باستخدام القالب.

القوالب

كُلّ العمليّات التي قمنا بتنفيذها في الأمثلة السّابقة على الوسيط element كانت سهلةً إلى حدٍّ ما، ولكن ماذا لو أردنا إضافة قطعةٍ متغيّرة الحجم من محتوىً جديدٍ إلى المستند؟

لنقم بمحاولة حلّ هذه المشكلة بالتّدريج. في البداية، لنقُل بأننا نريد فقط تغليف سلسلةٍ نصّيّةٍ نُمرّرها كوسيطٍ للتّوجيه الخاصّ بنا باستخدام عنصر strong.

The message is <span strong-message="a strong hello!"></span>

يُمكننا دومًا استخدام عمليّة وصل السّلاسل (concatenation).

angular.module('app')
  .directive('strongMessage', function() {
    return function(scope, element, attrs) {
      element.html('<strong>' + attrs.strongMessage + '</strong>');
    };
  });

الناتج:

The message is a strong hello!

أو يُمكننا بناء المحتوى بطريقةٍ برمجيّة باستخدام jqLite أو jQuery.

angular.module('app')
  .directive('strongMessage', function() {
    return function(scope, element, attrs) {
      element.html('<strong>');
      element.find('strong').text(attrs.strongMessage);
    };
  });

الناتج:

The message is a strong hello!

عمليّة وصل السّلاسل والتّغييرات البرمجيّة على المستند طريقةٌ جيّدةٌ عند استخدامها للأمور الصّغيرة كعنصرٍ واحدٍ مثلًا، ولكن ماذا سنفعل عندما يكون المحتوى يتضمّن العديد من العناصر المتداخلة؟ أو عندما يكون لدينا قائمةٌ متغيّرة الحجم؟ سيجيبك عن هذا السّؤال أيّ خبيرٍ في تطوير النهاية الأماميّة (front-end) بأنّ مكتبة القوالب ستسهّل الأمر كثيرًا، ولحسن الحظّ، فـAngular مكتبة قوالب، وستجعل الأمر أكثر سهولة.

يُمكن لـAngular أن تقوم بإخراج (render) قوالب متداخلةٍ يقدّمها التّوجيه الخاصّ بنا، وذلك في الوقت نفسه الّذي تقوم فيه بإخراج قالب التّطبيق الخاصّ بنا.

لإضافة قالبٍ للتّوجيه، سنحتاج إلى تغيير أسلوبنا السّابق، فبدلًا من إعادة (return) تابع الرّبط فقط، سنقوم بإعادة كائنٍ يحوي تابع الربط ومعه قالب. هذا تغيير كبير، لذا فلنكرّر ذلك: سيقوم المُغلّف الوصفيّ (recipe) الآن بإعادة كائنٍ بدلًا من تابع، وسنستخدم العنصر link في هذا الكائن المُعاد للوصول إلى التّابع.

فيما يلي مثالٌ يستخدم قالبًا لإنشاء عددٍ غير محدّدٍ مسبقًا من عناصر li.

angular.module('app')
  .directive('wordList', function() {
    return {
      link: function(scope, element, attrs) {
        scope.words = attrs.wordList.split(" ");
      },
      template: "<li ng-repeat='word in words'>\
                   {{word}}\
                 </li>"
    };
  });

ستستلم Angular مهمّة تضمين القالب في المجال الصحيح، ثمّ إلحاق المخرجات بالعنصر الذي استخدم التّوجيه. بما أنّ مخرجات هذا التّوجيه ستكون عناصر في قائمة، لنقم بتضمين التّوجيه في العنصر ul.

<ul word-list="we love templates"></ul>

الناتج:

. we
. love
. templates

أظنّ بأن المثال بدأ يوضّح لك قوّة وأناقة التّوجيهات المخصّصة.

العنصر templateUrl

استخدمنا في المثال البسيط السّابق رمز الشرطة الخلفيّة (\) مرّتين لنتمكّن من كتابة نصّ القالب، وكما ترى، فالقوالب السّطريّة سُرعان ما تصبح مستهجنةً داخل شيفرة JavaScript، ولذلك سنستعين بالقوالب الخارجيّة التي تدعمها Angular، سواء كانت عناصر خاصّة مخفيّة في المستند، أو موارد ويب منفصلةً تمامًا في مكانٍ مستقل.

<li ng-repeat="word in words">
  {{word}}
</li>

لقد قمنا باستخراج شيفرة القالب إلى مورد ويب موجودٍ في الرابط النّسبيّ views/word-list.html، والآن سنقوم بإضافة العنصر templateUrl إلى كائن الإعدادات الذي يعيده التّوجيه الخاص بنا، وسنقوم بإسناد مسار القالب إليه، وذلك بدلًا من استخدام العنصر template وإسناد شيفرة القالب إليه مباشرةً.

angular.module('app')
  .directive('wordList', function() {
    return {
      link: function(scope, element, attrs) {
        scope.words = attrs.wordList.split(" ");
      },
      templateUrl: '/views/word-list.html'
    };
  });

أمّا الاستخدام فيبقى كما هو دون تغيير.

 

<ul word-list="external templates rock"></ul>

الناتج:

. external
. templates
. rock

القوالب الخارجيّة عمومًا أسهلُ بكثيرٍ للقراءة وللتّعديل من القوالب السّطريّة.

العنصر replace

لا بُدّ من استخدام التّوجيه في المثال السابق ضمن قائمة، ولكن ماذا لو لم يقم المستخدم باستخدام التّوجيه بشكلٍ صحيح؟ أحد الحلول هو استبدال العنصر الذي يتضمّن التّوجيه بدلًا من إلحاق العناصر به. إذًا لنقُم بإضافة عنصر ul لتغليف مخرجات قالبنا، سنحتاج أيضًا إلى عنصرٍ جديدٍ في كائن الإعدادات هو العنصر replace، وسنُسند إليه القيمة true. (القيمة الافتراضيّة له هي بالطّبع false.)

لنقم أوّلًا بإضافة العنصر ul إلى قالبنا.

<ul>
  <li ng-bind="word" ng-repeat="word in words"></li>
</ul>

التّغيير الوحيد الهامّ في التّوجيه هو استخدام العنصر الجديد replace.

angular.module('app')
  .directive('wordList', function() {
    return {
      link: function(scope, element, attrs) {
        scope.words = attrs.wordList.split(" ");
      },
      templateUrl: '/views/word-list-ul.html',
      replace: true
    };
  });

بما أنّ العنصر الذي يتضمّن التّوجيه سيتمّ استبداله مهما كان، يُمكننا تضمين التّوجيه في عنصرٍ غريبٍ وغير ملائمٍ مثل h1.

 

<h1 word-list="lists not headlines"></h1>

الناتج:

. lists
. not
. headlines

يبيّن المثال السّابق قوّة تأثير التّوجيهات على المستند، ولكنّني أُفضّل أن يكون التّصميم أكثر وضوحًا، ودلالته صحيحة، ولذلك فبدلًا من استبدال وسوم HTML، أرى أنّ الصّواب هو التّحقّق من صحّة العنصر الذي قام بتضمين التّوجيه، وإن لم يكن صحيحًا يتمّ إلقاء خطأ (throw an error).

العنصر controller

في بداية هذا الفصل، رأينا مثالًا يحوي متحكّمًا يقوم بتحضير عنصرٍ في المجال، ثمّ استخدمنا هذا العنصر لاحقًا داخل تابع الربط الخاصّ بالتّوجيه. والآن بعد أن تعرّفنا على طريقة استخدام القوالب، لنقُم بإعادة كتابة هذا المثال، واستبدال تابع الرّبط الذي يحدّد نصّ العنصر في المستند بقالب.

angular.module('app')
  .controller('MessageController', function($scope) {
    $scope.message = 'hello, from the external controller';
  })
  .directive('message', function() {
    return {
      template: "<strong>{{message}}</strong>"
    };
  });

بما أنّ المتحكّم والتّوجيه منفصلان، فكلاهما يحتاج إلى التّضمين بشكلٍ منفصلٍ في المثال التالي. يُمكننا أن نضع المتحكّم في نفس العنصر الذي يحوي التّوجيه أو في عنصرٍ مغلّف، فلا فرق.

The message is <span message ng-controller="MessageController"></span>.

الناتج:

The message is hello, from the external controller.

كما ترى في المثال السّابق، لقد قُمنا بإزالة تابع الرّبط، فوجوده أمرٌ اختياريّ، واقتصرنا على العنصر template، وذلك لأنّ استخدام تابع الرّبط يكون للقيام بالتّغييرات البرمجيّة على المستند بطريقةٍ أكثر أُلفةً لمطوّري النّهاية الأماميّة. أمّا الآن بعد أن انتقلنا إلى استخدام القوالب، فسنحتاج إليه للقيام بمهمّاتٍ نحتاج فيها إلى الوصول إلى العنصر الحاوي للتّوجيه أو إلى خصائص العنصر (مُتضمّنةً التّوجيه بذاته، كما رأينا سابقًا.)

إضافةً إلى ذلك، فإنّ تحضير بيانات النّموذج لا يحتاج إلى الوصول إلى العنصر أو إلى خصائصه، وهو من مهمّات المتحكّم، وهذا بالرّغم من إمكانيّة القيام بذلك داخل تابع الرّبط كما بيّنّا في فقرة القوالب. لذا، سيكون من الأفضل أن ننقل مسؤوليّة تحضير النّموذج إلى داخل المتحكّم، إلّا أنّ المشكلة الآن هي أنّ المتحكّم منفصلٌ ويحتاج إلى إعداداتٍ خاصّةٍ به، فهل يُمكننا نقل المتحكّم إلى داخل التّوجيه؟ نعم، يمكننا ذلك.

angular.module('app')
  .directive('message', function() {
    return {
      template: "<strong>{{message}}</strong>",
      controller: function($scope) {
        $scope.message = 'hello, from the internal controller';
      }
    };
  });

أليس هذا رائعًا؟ لم نحصل الآن على مكوِّنٍ مغلّفٍ جيّدًا وحسب، بل تخلّصنا الآن من تضمين المتحكّم واستخدام التّوجيه ng-controller في الوسم.

The message is <span message></span>.

الناتج:

The message is hello, from the internal controller.

ويمكننا طبعًا القيام بكلّ الأشياء المعتادة مع المتحكّم، كتعريف تابعٍ في المجال وتضمين هذا التّابع من عنصر تحكّمٍ داخل القالب. راجع فصل المتحكّمات للاستزادة.

الخيار restrict

تُوجد أربع طرقٍ لتضمين توجيهٍ ما، إلّا أنّنا لم نرَ حتّى الآن إلّا طريقة تضمين التّوجيهات كخصائص لعناصر HTML، وذلك لأنّه الأسلوب الأكثر شيوعًا وفائدةً للقيام بذلك، وهي أيضًا القيمة الافتراضيّة للخيار restrict الذي يُمكن أن يملك واحدةً أو أكثر من الخيارات التّالية:

  • A‘ يتمّ تضمينه كخاصّيّة (Attribute)
  • C‘ يتمّ تضمينه كفئة (Class)
  • E‘ يتمّ تضمينه كعنصر (Element)
  • M‘ يتمّ تضمينه كتعليق (Comment)

بالنّسبة للأساليب الثّلاثة الأخرى في التّضمين (الفئة، العنصر والتّعليق) فأسلوب التضمين كعنصرٍ هو الأكثر إثارةً للاهتمام من النّاحية العمليّة، فأسلوبا الفئة والتّعليق موجودان لدعم الحالات الهامشيّة مثل وجود تحقّق صارمٍ لصحّة نصوص HTML.

أعتبر أنّ عنصر التّوجيه يُشبه مدفعًا كبيرًا، يجب عليك ألا تستخدمه إلّا إن كان الأمر يستحق ذلك. نموذجيًّا، يتمّ استخدام هذه الطّريقة عندما نحتاج إلى وضع عددٍ كبيرٍ من الوسوم داخل مكوّنٍ من مكوّنات HTML ويكون أيضًا عليك تمرير العديد من الوسطاء إليه، فعندما تستخدم عنصر التّوجيه، ستتمكّن من تمرير قيم الوسطاء على شكل خصائص لهذا العنصر.

في المثال التّالي لن يكون لدينا هذا العدد الكبير من الوسوم إلّا أنّنا سنصمّم جدولًا مع كلّ الزخرفات الخاصة به.

<table class="table table-condensed">
  <thead>
    <tr>
      <th ng-bind="column" ng-repeat="column in columns"></th>
    </tr>
  </thead>
  <tbody>
    <tr ng-repeat="model in models">
      <td ng-bind="model[column]" ng-repeat="column in columns"></td>
    </tr>
  </tbody>
</table>

سيستخدم تابع الرّبط الآن وسيطين في تعريف التّوجيه الخاصّ بنا، وسنصل إلى هذا الوسيطين عن طريق attrs.models و attrs.columns.

angular.module('app')
  .directive('modelsTable', function() {
    return {
      restrict: 'E',
      templateUrl: '/views/models-table.html',
      link: function(scope, element, attrs) {
        scope.models = scope.$eval(attrs.models);
        scope.columns = attrs.columns.split(",");
      }
    };
  });

قد ترغب بإسناد القيمة true إلى العنصر replace في عنصر التّوجيه إن كانت البيئة التي تعمل عليها تُسبّب لك بعض المشاكل عند استخدام أسماء عناصر غير معياريّة، على الجانب الآخر، تركُ هذا العنصر المخصص في مستندك قد يساعدك أثناء تصحيح الأخطاء في الشيفرة.

سنقوم مرّةً أخرى باستخدام متحكّمٍ خارجيّ في هذا المثال.

angular.module('app')
  .controller('ItemsController', function($scope) {
    $scope.items = [
      {name: 'Item 1', color: 'green', price: 5.0},
      {name: 'Item 2', color: 'blue', price: 4.93}
    ];
  });

وأخيرًا، سنقوم بإضافة Bootstrap إلى صفحتنا لجعل مثالنا الأخير أكثر أناقة.

<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.2/angular.js"></script>

الخطوة الأخيرة الآن هي تضمين عنصر التّوجيه الخاصّ بنا مع وسيطيه models وcolumns حيث سنضيفهما على شكل خصائص للوسم.

<p ng-controller="ItemsController">
  <models-table models="items" columns="name,color,price"></models-table>
</p>

رائع، لقد قمنا بإخفاء الشيفرات المزعجة الخاصّة بجدول HTML بإحكام.

خاتمة

لقد قدّم لك هذا الفصل عن التّوجيهات معرفةً ستعطيك قوّةً كبيرةً عند استخدامك لـAngular في تطوير تطبيقات طرف المستخدم، فكما رأينا في المثال الأخير، يُمكنك الآن تعريف توجيهٍ مخصّصٍ يستخدم قوّة قوالب Angular ويستخدم التّوجيهات المدمجة معها، مع قدرةٍ كبيرةٍ على التّقليل من الشّيفرات المزعجة ومن ثمّ زيادة الإنتاجيّة.

لا يزال علينا فهم أمورٍ أخرى بخصوص التّوجيهات، خصوصًا عندما تدخل مشكلة الحالة المشتركة إلى السّاحة. سيعرض الفصل القادم هذه السيناريوهات المتقدّمة، فإن كنت تشعر بأنك حصلت على المعرفة الكافية لخدمة أهدافك، فيُمكنك القفز مباشرةً إلى فصل HTTP. أما إن كنت ترغب في تعلُّم المزيد، فتابع مباشرةً إلى الجزء الثّاني من فصل التّوجيهات.

30يوليو

( تعلم Angular ) – المرشحات (filters) في AngularJS

تَستخدم المُرشّحات في Angular نمط خطّ الأنابيب (pipeline) بشكلٍ مشابه للـUnix shell، فقد استعارت رمز الخطّ العمودي المشابه لرمز الأنبوب من Unix لتستخدمه لتمرير عبارات التّهيئة أو خواص المجال للمُرشّح وأيضًا لربط المُرشّحات معًا.

سنقوم بتحميل إطار عمل Bootstrap الخاصّ بـCSS لأننا سنقوم بإنشاء بعض الجداول في أمثلة هذا الفصل ممّا سيجعلها أكثر ترتيبًا، وسنجلبه من شبكة توصيل المحتوى (CDN) كما تُبيّن الشّيفرة التّالية.

<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.2/angular.js"></script>

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

angular.module('app', []);

سنقوم بإسناد اسم وحدة الجذر لتطبيقنا إلى التّوجيه ng-app.

<body ng-app="app">
  <!-- الأمثلة توضع هنا -->
</body>

مفهوم المُرشّحات سهلُ التّعلّم، لذا سيكون هذا الفصل مرورًا سريعًا على المُرشّحات المبنيّة داخل Angular، ثُمّ سنستعرض كيف يمكننا إنشاء المُرشّحات الخاصّة بنا.

lowercase و uppercase

لنبدأ جولتنا مع المُرشّحات المبنيّة في Angular ببعض التّحويلات على السّلاسل النّصية، والمُرشّح lowercase بدايةٌ ممتازة. نبدأ بكتابة أيّ عبارة (في مثالنا استخدمنا عبارة “AngularJS”)، ثم نضع رمز الأنبوب (|)، وبعده نكتب اسم المُرشّح (lowercase).

<p>{{"AngularJS" | lowercase}}</p>

الناتج:

angularjs

يُمكننا تطبيق المرشّح uppercase على النّتيجة عن طريق إضافة رمز أنبوبٍ آخر، ثمّ اسم المرشّح، وهذا ما نُسمّيه بسلسلة المُرشّحات.

<p>{{"AngularJS" | lowercase | uppercase }}</p>

الناتج:

ANGULARJS

بالطّبع ليس هناك فائدةٌ من وضع lowercase قبل uppercase، وسنرى في نهاية هذا الفصل مثالًا أفضل على سَلسَلة المُرشّحات، ولكن على الأقل رأينا كيف نقوم بذلك الآن.

number

المُرشّح التّالي سهلٌ أيضًا، المُرشّح number يُقدّم المساعدة الأساسيّة لتنسيق الأعداد وتقريبها.

<p>{{1000.12345 | number}}</p>

الناتج:

1,000.123

بالنّسبة لإعداداتي المحلّيّة، en-us، المُخرجات في المثال هي 1,000.123، يقوم المُرشّح بتقريب العدد المُمرّر إليه إلى 3 خاناتٍ بعد الفاصلة، ومن الواضح أنّ هذا لن يناسب جميع الاستخدامات، ولحسن الحظ، يمكننا تمرير وسطاء إلى المرشّحات التي تقبل القيام بذلك، والمُرشّح number يقبل ذلك. الوسيط الأول الذي نقوم بتمريره هو القيمة التي نريد تطبيق المُرشّح عليها، أما الوسيط الإضافي الثّاني فيجب أن يتبع اسم المُرشّح ويكون مفصولًا عنه بنقطتين (:).

الوسيط الإضافيّ الثّاني للمُرشّح number هو fractionSize، وهو يسمح لنا بالتّحكّم بعدد المنازل التي يتم عرضها بعد الفاصلة، وبهذا يمكننا رؤية القيمة كاملةً.

<p>{{1000.12345 | number:5}}</p>

الناتج:

1,000.12345

ماذا يحدث لو قمنا بتمرير قيمةٍ سالبةٍ إلى الوسيط fractionSize؟ استبدل 5 بـ 3- ثمّ جرّب 4-، هل يُمكنك تخمين السّبب؟ لا مشكلة، حتى أنا لا يمكنني ذلك.

currency

المُرشّح التّالي مفيدٌ بعض الشّيء في التّطبيقات العمليّة: يُقدّم المُرشّح currency الدّعم الأساسيّ لتنسيق العملة وتقريب قيمة العدد.

<p>{{1999.995 | currency}}</p>

الناتج:

$2,000.00

وهو يقبل وسيطين إضافيين، أوّلهما هو رمز العملة، استبدل currency بـ’€’:currency في المثال السّابق لتجريبه.

الوسيط الثّاني هو fractionSize. انتبه عند استخدامك للمُرشّح currency عند تقريب المبَالِغ الماليّة، فليس هناك طريقةٌ واحدةٌ فقط هي الصّحيحة عندما ننظر للأمر على أنّه مسألةٌ ماليٌّةٌ وليست تقنيًّة، فهذا المُرشّح يستخدم تقريب النصف للأعلى وقد لا يكون هذا جيّدًا دومًا، كما رأينا في المثال السّابق. لا يوجد حاليًّا طريقةٌ موثّقةٌ لتجاوز طريقة المُرشّح في التّقريب، ما رأيك بأن تحاول التّعديل على المثال السّابق لتجعل المُرشّح يعرض القيمة الصّحيحة.

date

سيبيّن المُرشّح date لك إلى أيّ مدىً تذهب بك إعدادات المُرشّحات.

لا تسمح Angular بتهيئة كائنٍ من نوع Date داخل عبارة، لذا سنستخدم متحكّمًا لتحضير النّموذج الخاصّ بالمثال.

angular.module('app')
  .controller('DateController', function($scope) {
    $scope.now = Date.now();
  });

يبدأ المثال التّالي بعنصرٍ في المجال اسمه now، بدلًا من كتابة عبارة.

<p ng-controller="DateController">
{{now | date}}
</p>

الناتج:

Oct 28, 2015

إن قمت بإزالة date | من المثال السّابق سترى القيمة الأصليّة للعنصر now، وهي عدد الميللي ثانية منذ شهر كانون الثّاني عام 1970 حسب التوقيت العالميّ UTC. والآن قم بتجربة بعض التّنسيقات المُعرّفة سابقًا، والتي يدعمها هذا المُرشّح، قم بتجربة ‘date:’medium | ثمّ ‘date:’shortTime | وبعدها’date:’yyyy-MM-dd HH:mm:ss Z |، وجرّب بعض التّعديلات من عندك بعد ذلك. يُسمح بكتابة السّلاسل المحرفية داخل نمط التّنسيق، مثلًا “date:”MMMM ‘of the year’ yyyy |، وبدلًا من استخدام محارف الهروب مثل % لوضع رموز التّنسيق، سيكون عليك وضع السّلاسل المحرفية بين علامتَي تنصّيّصٍ مفردة (‘) وأن تكون عبارة التّنسيق الكاملة بين علامتَي تنصّيّصٍ مزدوجة (“). كما أنّ الفراغات في عبارة التّنسيق تدخل في تنسيق العبارة.

orderBy

يُمكن للمُرشّحات القيام بما هو أكثر من تنسيق المخرجات، حيث يُمكن استخدمها لتحويل المجموعات، كما سنرى في المثال التّالي.

لنفترض أنّ خدمةً في النهاية الخلفية (backend) أَرجعت سجلًّا مرتًبًا بطريقةٍ غير مرغوبةٍ لأسباب تتعلّق بالعرض. (لتبسيط المثال، ستكون البيانات مكتوبةً يدويًّا داخل الخدمة items التي تراها أدناه، وذلك بدلًا من التّعامل مع نهايةٍ خلفيّة بعيدة حقيقيّة.)

angular.module('app')
  .value('items', [
    {name: 'Item 3', position: 3, color: 'red', price: 2.75},
    {name: 'Item 1', position: 1, color: 'red', price: 0.92},
    {name: 'Item 4', position: 4, color: 'blue', price: 3.09},
    {name: 'Item 2', position: 2, color: 'red', price: 1.95}
  ]);

سنقوم بعد ذلك بحقن المصفوفة items في داخل متحكّم.

angular.module('app')
  .controller('ItemsController', function($scope, items) {
    $scope.items = items;
  });

بناءً على ترتيب العناصر الحالي في المصفوفة، كيف يمكننا ترتيب العناصر حسب قيمة العنصر position؟

إن كنتَ قد قفزتَ من مقعدك الآن وصرخت قائلًا “Underscore” (أو “lodash“)، فتستحقّ منّي وسام الوفاء لـJavaScript، ولكنّها ليست الإجابة التي نبحث عنها، فـAngular تملك مُرشّحًا للقيام بذلك، إنّه المُرشّح orderBy.

<ol ng-controller="ItemsController">
  <li ng-repeat="item in items | orderBy:'position'">
     {{item.name}}
  </li>
</ol>

الناتج:

1. Item 1
2. Item 2
3. Item 3
4. Item 4

هذا رائع، ولكن ماذا لو أردنا أن يكون التّرتيب بالجهة المعاكسة؟ في هذا المثال البسيط يمكننا القيام بذلك بطريقتين، الأولى هي إضافة الرمز – قبل اسم العنصر، أي أن نكتب ‘orderBy:’-position بدلًا من ‘orderBy:’position. (يمكنك أيضًا إضافة الرمز + لاختيار التّرتيب التّصاعدي، إلا أنّه لا يؤثّر على مثالنا السّابق.) أمّا الطّريقة الثّانية فهي إعطاء قيمةٍ للوسيط البولياني reverse بعد اسم العنصر المطلوب التّرتيبُ حسب قيمته. في مثالنا، قم بكتابة orderBy:’position’:true بدلًا من ‘orderBy:’position.

فائدةٌ إضافيّة: عدّل سجلّ البيانات في المتحكّم بحيث تجعل عنصرين في المصفوفة لهما القيمة نفسها بالنّسبة للعنصر position، ألا يزال بإمكانك استخدام المُرشّحorderBy لترتيب العناصر بشكلٍ صحيح؟ ماهي الخاصّيّة التي استخدمتها؟

انتبه للتحديثات

بفضل طريقة Angular في الربط ثنائيّ الاتّجاه، سيكون من السّهل جدًّا أن تجعل النّماذج في قائمة أو جدول العرض قابلةً للتّعديل، كلّ ما تحتاجه هو إضافة خانة إدخالٍ داخل الحلقة مع إسناد اسم عنصر المجموعة إلى التّوجيه ng-model. ولكن انتبه، فبما أنّ Angular جاهزةٌ دومًا وتنتظر إعادة توليد العرض عند أيّ تغيير في الحالة، فقد يتمّ إعادة ترتيب قائمتك قبل أن تنتهي من تعديل العنصر حتّى.

<table class="table table-condensed" ng-controller="ItemsController">
  <tr ng-repeat="item in items | orderBy:'name'">
    <td ng-bind="item.name"></td>
    <td><input type="text" ng-model="item.name"></td>
  </tr>
</table>

جرّب تعديل اسم أحد العناصر في المثال السّابق بحذف المحتوى أوّلًا ثمّ كتابة الأحرف، وستلاحظ مشكلةً كبيرةً في الاستخدام. فـAngular تقوم بإعادة توليد الجدول كاملًا، ونقل السطر إلى مكانٍ آخر قبل أن تنتهي من كتابته. أفضل الحلول لهذه المشكلة قد يكون استخدام عرضٍ(view) آخر منفصلٍ للتّعديل، كما يمكنك نقل المُرشّح orderBy من العرض إلى المتحكّم حيث يتمّ استدعاؤه مرّةً واحدة، وهذا ما سنتكّلم عنه في الفقرة القادمة، استخدام المرشحات في JavaScript.

limitTo

المُرشّح limitTo مفيدٌ عندما ترغب بعرض مجموعةٍ جزئيّةٍ فقط من مجموعةٍ مرتّبة. مثلًا، يمكننا استخدامها بالتّعاون مع المُرشّح orderBy لنعرض فقط أغلى عنصرين في المجموعة.

<ol ng-controller="ItemsController">
  <li ng-repeat="item in items | orderBy:'-price' | limitTo:2">
     {{item.price | currency}}
  </li>
</ol>

الناتج:

1. $3.09
2. $2.75

لا تُقدّم Angular طريقةً لتمرير وسيط offset إلى المُرشّح limitTo حاليًّا. ولتقديم حلٍّ كاملٍ للتصحيف (pagination) سيتضمّن ذلك استخدام التّابع slice داخل المتحكّم.

filter

اسم هذا المُرشّح ظريفٌ نوعًا ما، فهو مرشّحٌ اسمه filter، ربّما لم يكن من المناسب تسمية المُرشّحات بهذا الاسم العام (ربّما يكون الاسم مزخرِفات أو مساعِدات أفضل)، إلّا أنّ هذا المرشّح يقوم فعلًا بالتّرشيح. فهو يقوم بإعادة مصفوفةٍ تحوي فقط على العناصر التي تطابق الشرط المُسند. لنرى ما هي العناصر التي ستطابق وضعنا لشرطٍ يقول بأنّ القيمة العدديّة تساوي 3.

<table class="table table-condensed" ng-controller="ItemsController">
  <thead>
    <tr><th>name</th><th>color</th><th>price</th></tr>
  <thead>
  <tbody>
    <tr ng-repeat="item in items | filter:3">
      <td ng-bind="item.name"></td>
      <td ng-bind="item.color"></td>
      <td ng-bind="item.price | currency"></td>
    </tr>
  <tbody>
</table>

لقد حصلنا على تطابقات في كلا العنصرين name وprice. ماذا سيحدث لو استبدلنا الشرط 3 بالسّلسلة النّصية ‘3’ أو بسلسلة نصّيّة خالية؟ أو لو أضفنا فراغًا في البداية (‘3 ‘)؟

لتجنُّب مطابقة أيّ عنصرٍ، يمكنك تمرير كائنٍ باعتباره شرط التّطابق.

<table class="table table-condensed" ng-controller="ItemsController">
  <thead>
    <tr><th>name</th><th>color</th><th>price</th></tr>
  <thead>
  <tbody>
    <tr ng-repeat="item in items | filter:{price: 2, color: 'red'}">
      <td ng-bind="item.name"></td>
      <td ng-bind="item.color"></td>
      <td ng-bind="item.price | currency"></td>
    </tr>
  <tbody>
</table>

في المثال السّابق، لم يتمّ مطابقة 2 مع item 2 لأننا حدّدنا اسم عنصر التّطابق بأنّه price.

أكثر ما يجعل المُرشّح filter رائعًا، هو السهولة الفائقة في دمجه مع الربط ثنائيّ الاتّجاه في Angular، لإنتاج صندوق ترشيحٍ قويّ.

Filter: <input type="text" ng-model="q">
<table class="table table-condensed" ng-controller="ItemsController">
  <thead>
    <tr><th>name</th><th>color</th><th>price</th></tr>
  <thead>
  <tbody>
    <tr ng-repeat="item in items | filter:q">
      <td ng-bind="item.name"></td>
      <td ng-bind="item.color"></td>
      <td ng-bind="item.price | currency"></td>
    </tr>
  <tbody>
</table>

إن كان هناك مثالٌ واحدٌ يبيّن بأفضل طريقةٍ مرونة وقوّة امتدادات HTML التي توفّرها Angular، فقد يكون هذا هو.

استخدام المرشحات في JavaScript

تحتاج أحيانًا إلى استخدام مُرشّح ما عندما يكون لديك القوّة الكاملة لـJavaScript، بدلًا من أن تكون محدودًا بما تسمح لك العبارات بالقيام به. مثلًا، أمرٌ بسيطٌ كمعرفة عدد العناصر التي أنتجها المُرشّح، لا يبدو ممكنًا. ربما يمكنك أن تقول بأنّه عندما تصبح العبارة كبيرةً وتحتاج إلى منطقٍ برمجيٍّ فمن الأفضل نقل شيفرتها إلى داخل متحكّم.

<p ng-controller="ItemsController">
  <!-- Invalid code! -->
  {{items | filter:{color:'red'}.length}} red items
</p>

الناتج:

[{"name":"Item 3","position":3,"color":"red","price":2.75},{"name":"Item 1","position":1,"color":"red","price":0.92},{"name":"Item 4","position":4,"color":"blue","price":3.09},{"name":"Item 2","position":2,"color":"red","price":1.95}] red items

يُمكنك حقن المُرشّحات أينما أردت، كما أنّه ليس من الغريب استخدام المُرشّح filter للتعامل مع مصفوفةٍ داخل متحكّم.

يكون اسم التّابع الذي يقوم بالترشيح من الشكل filtername>Filter>. مثلًا لاستخدام المُرشّح date نقوم بحقن dateFilter، لاستخدام المُرشّح currency نقوم بحقن currencyFilter، ولاستخدام المُرشّح filter نقوم بحقن filterFilter. (أعرف ذلك، أعرف ذلك)

angular.module('app')
  .controller('FilteredItemsController', function($scope, items, filterFilter) {
    $scope.redItemsCount = filterFilter(items, {color: 'red'}).length;
  });
<p ng-controller="FilteredItemsController">
  {{redItemsCount}} red items
</p>

الناتج:

3 red items

ما عليك إبقاؤه في ذهنك بشكلٍ أساسيّ هو أنّ استخدام المُرشّح داخل المتحكّم بدلًا من أن يكون في العرض، سيؤدي إلى عدم استدعاء المُرشّح عند حدوث تغييرات في واجهة المستخدم إن كانت تغييراتٍ لا تتضمّن إعادة استدعاء المتحكّم. ولحلّ هذه المشكلة يمكنك ربط استخدام المُرشّح يدويًّا بعنصرٍ في المجال باستخدام التابع scope.$watch.

مرشح مخصص

المُرشّح المخصّص هو أفضل مكانٍ لتضع فيه الشيفرات التي تقوم بتحويل بيانات النّموذج للعرض، وذلك لأنّ المُرشّح نموذجيًّا مكوِّنٌ ثابت الحالة (stateless) ويُمكن استخدامه في أيّ مكانٍ داخل القوالب دون خوفٍ من أيّ تأثيراتٍ جانبيّة، كما أنّ إنشاء مُرشّح مخصّص ليس صعبًا، فبشكلٍ مشابهٍ لبقيّة المكوّنات في Angular، نقوم بتسجيل وصفةٍ للمُرشّح داخل الوحدة، باستخدام التّابع filter.

angular.module('app')
  .filter('smiley', function() {
    return function(text) {
      return '\u263A ' + text + ' \u263A';
    };
  });

في المثال السّابق، المُرشّح الفعليّ هو التابع الدّاخليّ الذي يقوم بوصل السّلسلة النّصية الممرّرة إليه عن طريق الوسيط text مع رمز الوجه الضاحك. يتم استدعاءُ هذا التابع في كلّ مرّة يتمّ فيها استدعاء المُرشّح، أمّا التابع الخارجيّ الذي يحيط بالمُرشّح فهو وصفة (recipe) إنشائية، وقد ناقشنا هذه الفكرة بعمقٍ في الفصل السّابق، الخدمات. يتمّ تشغيل الوصفة مرّةً واحدةً، أو لا يتمّ تشغيلها أبدًا إن لم يكن لها استخدامٌ في العرض.

المُرشّحات المخصّصة تكون متوفّرة تلقائيًّا في أيّ مكانٍ في العرض، ويتمّ استخدامها تمامًا مثل المُرشّحات الأصليّة في المكتبة، حيث نقوم ببساطةٍ بإدخال العبارة فيأنبوب الترشيح داخل العرض، وذلك باستخدام رمز الأنبوب (|)، ويتم تمرير قيم الوسطاء الإضافيّة باستخدام رمز النّقطتين (:).

<strong ng-bind="'hello' | smiley"></strong>

الناتج:

 hello 

إليك هذا التّحدّي: هل يمكنك إعادة تشكيل المُرشّح بحيث يصبح بإمكاننا تمرير رمز الوجه الضّاحك كوسيط؟

حقن التبعية

بالرغم من كون المُرشّحات ثابتة الحالة وبسيطةً نوعًا ما، إلّا أنّها يمكن أن تملك تبعيّاتٍ تحتاج إلى أن تُحقن.

يسمح تابع الوصفة الإنشائيّة الخارجيّ بالتّصريح عن التّبعيّات تمامًا كما نقوم بذلك في الوحدات. فلنلقِ نظرةً على الكيفيّة التي نقوم بها بحقن التّبعيّات في داخل مُرشّح مخصّص للمحلّيّات (localization). سنقوم بتسمية المُرشّح بالاسم localize، ولكن قبل ذلك نحتاج إلى شيءٍ ما لنقوم بالحقن. يمكننا إنشاء جدول لمحتوى المتغيّر locales، حيث يتمّ تعريفها كخدمةٍ قابلةٍ للحقن باستخدام التّابع value.

angular.module('app')
  .value('locales', {
    'de': {greeting: 'guten Tag'},
    'en-us': {greeting: 'howdy'},
    'fr': {greeting: 'bonjour'}
  });

يمكننا حقن هذه البيانات في داخل مُرشّحنا البسيط المخصّص بالتّصريح عن وسيطٍ اسمه locales، ولنتمكّن من الحصول على الموضع الحاليّ للمستخدم، يُمكننا استخدام الخدمة locale$ المبنيّة داخل Angular .

angular.module('app')
  .filter('localize', function(locales, $locale) {
    return function(key) {
      var locale = locales[$locale.id] || locales['en-us'];
      return locale[key];
    };
  });

سيعمل المثال السّابق معك فقط إن كانت إعدادات جهازك المحلّيّة من ضمن الأماكن المحدّدة في المثال، إن لم يكن موضعك موجودًا فقم بإضافته بنفسك مع عبارةٍ ترحيبيّة بلغتك.

<p>
  The app says <strong ng-bind="'greeting' | localize"></strong>.
</p>

الناتج:

The app says howdy.

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

خاتمة

المُرشّحات المبنيّة في Angular جزءٌ هامٌّ من قدراتها الرائعة، كما أنّ استخدامها سهلٌ وبديهيّ بشكلٍ عامٍّ، كما أنّها مرنة بفضل الوسطاء الإضافيّة وسَلسلة المُرشّحات، وأيضًا فإنّ إنشاء مرشّحٍ مخصّص سيكون سهلًا فورَ إتقانك لنمط الوصفة الإنشائيّة (creation recipe) التي تسمح لك بحقن التّبعيّات. والآن بعد أن اعتدنا استخدام هذه الأنماط، فنحن الآن جاهزون للتّحدّي في الفصل القادم، حيث سنقوم بإعداد التّوجيهات الخاصّة بنا.

30يوليو

( تعلم Angular ) – الخدمات (Services) في AngularJS

تعاملنا حتّى الآن مع نوعٍ واحد من مكوّنات JavaScript المعدّلة في Angular، وهو المتحكّم. المتحكّمات هي نوعٌ مخصّص من المكوّنات، وكذلك سنجد المرشحات والتوجيهات، التي سنغطّيها قريبًا. تدعم Angular مكوّنًا غير مخصّص، يُسمّى بالخدمات، تعريف الخدمات في Angular فضفاضٌ نوعًا ما، ولكنّ السّمة المميّزة لها هي عدم اقترانها مباشرةً بالقالب، مما يفترض قرابةً بينها وبين نمط طبقة الخدمة في الهيكليّة التّقليديّة للمشاريع.

تتضمّن وحدة القلب ng في Angular عددًا من الخدمات المدمجة، ونذكر الخدمات location$ وlog$ وq$وwindow$ على سبيل المثال، وسنستكشف الخدمة http$ في الفصل الأخير من هذه السلسلة، فصل HTTP.

في تطبيقٍ نموذجيٍّ ستحتاج إلى تعريف خدماتك الخاصّة لأيّ سلوكٍ مشترك بين مكوّنات JavaScript المخصّصة التي تنشئها، مثل المتحكّمات. يُمكن للخدمات أن تكون متابعةً للحالة (stateful) إن احتجت إلى ذلك، فمثلًا، إن أردت مكانًا لتخزين نقاط اللاعب في لعبةٍ ما، يُمكنك إنشاء خدمة score لتتمكّن من جلب وعرض النّقاط الحاليّة في عدّة أماكن ضمن تطبيقك. جميع الخدمات متفرّدة (singletons) أي أنه لا يوجد غير نسخةٍ واحدةٍ من خدمةٍ معيّنة طوال دورة حياة تطبيق Angular.

الخدمة في Angular يُمكن أن تكون كائنًا، تابعًا، أو حتّى قيمةً أوّليّة (كالأعداد مثلًا)، أنت من يحدّد ذلك. في الواقع، هذا الفصل لن يشرح إلا القليل من الأمثلة عن الخدمات، وذلك لأنّ الخدمات يمكن أن تحوي أيّ شيفرة من شيفرات JavaScript العاديّة. ما سيُغطّيه هذا الفصل هو كيفيّة تسجيل (register) الخدمات في Angular. نقوم بذلك عن طريق توابع معرّفة بواسطة الخدمة provide$، ومغلّفة بواسطة angular.Module. لنتمكّن من تسجيل الخدمة نحتاج إلى مرجعٍ (reference) إلى وحدةٍ ما. لنقُم بتعريف وحدةٍ جذريّة لتطبيقنا الآن.

angular.module('app', []);

كما تمّ الشّرح في فصل الوحدات، نقوم بتحميل وحدة الجذر في تطبيقنا عن طريق تمرير اسمها إلى التّوجيهng-app ضمن مستند HTML.

<body ng-app="app">
  <!-- الأمثلة توضع هنا -->
</body>

جيّد، أصبحت وحدتنا جاهزة.

تُقدّم الخدمة provide$ خمس توابع مختلفةً للقيام بتسجيل الخدمة، يُمكننا تصنيف هذه التّوابع إلى صنفين، أوّلهما سهلٌ جدًّا والثّاني أصعبُ قليلًا.

إنشاء خدمات دون استخدام حقن التبعية

قد لا تحتاج إلى حقن أيّ تبعيّاتٍ إلى خدمتك. (قُمنا بتغطية حقن التّبعيّة في الفصل الماضي. وكمثالٍ على تبعيّةٍ محقونة، فالخدمة المدمجة http$ يُمكن لخدمتك أن تستخدمها لجلب بياناتٍ من النّهاية الخلفيّة (backend) البعيدة.) ولكن حتّى لو لم تكن تريد حقن تبعيّاتٍ في خدمتك، ستحتاج إلى وضعها ضمن وحدةٍ لتجعلها متاحةً للحقن ضمن مكوِّناتٍ أخرى. إنّ جعل الخدمة متاحةً للحقن هو ما يجعلها خدمةً في Angular، وإلّا ستكون مجرّد شيفرة JavaScript عاديّة.

هناك تابعان خدميّان يسمحان بتسجيل الخدمات من دون تبعيّات: التّابع value والتّابع constant. الفرق بينهما في Angular معقّدٌ قليلًا ويخُصّ المحترفين، فـالثّوابت تكون متاحًةً لتطبيق Angular أثناء الـbootstrapping، أما القِيمة فلا. لن نقوم بتغطية constant في هذه السلسلة.

التابع value

لنفترض أنّنا نريد كتابة شيفرةٍ للعبة حيث يحصل فيها اللاعب على نقاطٍ تبدأ من الصفر. يمكننا باستخدام التّابع value من الخدمة provide$ أن نقوم بتسجيل (register) قيمةٍ عدديّةٍ أوّليّة باسم ‘score’ وستكون متاحةً للمتحكّمات، الخدمات، ومكوّناتٍ أخرى.

angular.module('app')
  .value('score', 0);

يُمكننا حقن خدمتنا score عن طريق وضعها ضمن قائمة الوسطاء للمتحكّم، وقد شرحنا ذلك بالتّفصيل في فصل حقن التّبعيّة.

angular.module('app')
  .controller('ScoreController', function($scope, score) {
    $scope.score = score;
    $scope.increment = function() {
      $scope.score++;
    };
  });

يستخدم قالب هذا المثال نُسختين من المتحكّم ScoreController لإثبات الطّبيعيّة المتفرّدة (singleton) للخدمة score.

<p ng-controller="ScoreController">
  Score: {{score}}
</p>
<p ng-controller="ScoreController">
  <button ng-click="increment()">Increment</button>
</p>

أعتذر، لقد تعمّدت إفشال المثال السّابق، فعند النّقر على زرّ Increment لن يتمّ تغيير قيمة المتغيّر scoreالمعروضة في النّسخة الأولى من المتحكّم. هذه ليست مشكلة مجالات، ولكنّها بسبب الطّبيعة الثّابتة (immutable) للخدمات التي تمثّل قيمًا أوّليّة. يُمكننا إصلاح المثال السّابق بتغيير الخدمة من قيمةٍ أوّليّة إلى كائنٍ يحوي العنصر points.

angular.module('app')
  .value('score', {points: 0});

سنستخدم نفس جسم المتحكّم تقريبًا، إلّا أنّنا سنغيّر ++scope.score$ إلى++scope.score.points$.

angular.module('app')
  .controller('ScoreController', function($scope, score) {
    $scope.score = score;
    $scope.increment = function() {
      $scope.score.points++;
    };
  });

بطريقةٍ مماثلة، سنغيّر في القالب العبارة score إلى score.points.

<p ng-controller="ScoreController">
  Score: {{score.points}}
</p>
<p ng-controller="ScoreController">
  <button ng-click="increment()">Increment</button>
</p>

لقد استخدمنا للتّو خدمةً تشارك بياناتٍ متغيّرة (mutable) ضمن تطبيقنا، هذا رائع. إضافةً إلى القيمة الأوّليّة والكائنات، يمكن لخدمات Angular أن تمثّل توابعًا. لنفترض أنّنا بحاجةٍ إلى خدمةٍ ثابتةٍ (stateless) تُعيد عدًدا عشوائيًّا بين 1 و 10.

angular.module('app')
  .value('randomScore', function() {
     return Math.ceil(Math.random() * 10);
  });

إنّ حقن هذه الخدمة أمرٌ سهل، سنقوم ببساطةٍ بإضافة اسمها randomScore إلى قائمة الوسطاء في متحكّمنا.

angular.module('app')
  .controller('ScoreController', function($scope, score, randomScore) {
    $scope.score = score;
    $scope.increment = function() {
      $scope.score.points += randomScore();
    };
  });

لقد تعلّمنا الآن كيفيّة تعريف الخدمات وحقنها ضمن المتحكّم كتبعيّات. ولكن ماذا لو أردنا حقن تبعيّاتٍ إلى خدماتنا؟

إنشاء خدمات باستخدام حقن التبعية

لعبتنا تبدأ دومًا بنقاطٍ قيمتها صفر، هذا منطقيٌّ، ولكن لنفترض أننا نريد أن تكون القيمة البدائيّة عددًا عشوائيًّا. كيف يمكننا جلب مرجعٍ للخدمة randomScore عندما ننشئ الكائن score للمرّة الأولى؟ إنّ التّابعvalue الذي كُنّا نستخدمه بسيط جدًّا، فأيّ شيءٍ نقوم بتمريره إليه سيكون هو القيمة أو التّابع أو الكائن الكامل والنّهائي الذي ستقوم Angular بحقنه لاحقًا، وهذا يعني بأنّنا لن نحظى بأيّ فرصةٍ لحقن أيّ تبعيّاتٍ لاحقًا.

تُقدّم Angular عدّة حلول لهذه المشكلة، وسنبدأ بالحلّ الأوّل، التّابع service كائنيُّ التّوجّه.

التابع service

تدعم JavaScript أسلوب البرمجة كائنيّة التّوجّه، ولذلك يمكننا كتابة خدمة في Angular تقبل التّبعيّات عن طريق الحقن بواسطة الباني (constructor injection). كُلّ ما نحتاج إليه هو كتابة التّابع الباني لخدمتناscore، بدلّا من تهيئتها بقيمةٍ بدائيّة عن طريق مهيِّئ الكائن ({}) الذي استخدمناه قبل قليل.

يجب أن يكون أوّل حرفٍ من اسم الباني في JavaScript حرفًا كبيرًا، ولذلك سنُسمّي الباني بالاسم Score، وسنقوم بوضع الخدمة randomScore ضمن قائمة الوسطاء لهذا الباني.

function Score(randomScore) {
  this.points = randomScore();
}

التّابع service يحتاج إلى تمرير الباني الخاصّ بالخدمة بدلًا من تمرير الخدمة ذاتها، وعندما تقوم Angular باستدعاء الباني عن طريق العمليّة new ستقوم آليّة حقن التّابعيّة بإسناد الخدمة randomScore إلى وسيط الباني.

angular.module('app')
  .service('score', Score);

توصف طريقة إنشاء نُسخة الخدمة في Angular بأنّها كسولة، أي أنّ النّسخة ستُنشأ فقط عندما تُشكِّل تبعيّةً لأحد المكوّنات التي يتمّ إخراجها في القالب.

التابع factory

إن كانت خدمتك شيئًا آخر غير الكائن، أو إن أردت الحصول على مرونةٍ أكبر من طريقة إنشاء بانٍ للكائن، عندها يُمكنك استخدام التّابع factory بدلًا من service، حيث نقوم بتمرير تابع استدعاءٍ خلفيٍّ (callback) وسيتمّ حقنه لاحقًا بالتّبعيّات التي نكتب أسماءها في قائمة الوسطاء. يُمكنك كتابة ما تشاء داخل هذا التّابع، ولكن يجب عليك في النّهاية أن تعيد قيمةً، تابعًا أو كائنًا يُمثّل الخدمة. يُطلق على هذا النّوع من الإنشاء عن طريق الاستدعاء الخلفيّ في المرجع الرّسميّ اسمُ الوصفة(recipe).

angular.module('app')
  .factory('score', function(randomScore) {
     return  {points: randomScore()};
  });

المثال السّابق مكافئٌ للمثال الذي يستخدم تركيبة الباني مع التّابع service الذي رأيناه في الفقرة الماضية.

التابع decorator

إن كنت تريد تعديل خدمةٍ موجودةٍ سابقًا، يُمكنك استخدام التّابع decorator الّذي تُوفّره الخدمةprovide$. هذا التّابع ليس مُغلّفًا باستخدام angular.Module كحال بقيّة التّوابع، لذا يجب أن يتمّ استدعاؤه مباشرةً من الخدمة provide$. يُمكنك الحصول على مرجعٍ للخدمة provide$ عن طريق تسجيل تابع استدعاءٍ خلفيٍّ (callback) باستخدام التّابع config.

angular.module('app')
  .config(function($provide) {
    $provide.decorator('score', function($delegate) {
      $delegate.points = 1000000;
      return $delegate;
    });
  });

يُمكن لتابع decorator أن يقوم بإعادة الخدمة الأصليّة التي تمّ تمريها ضمن الوسيط ذي علامات الاقتباس ‘ ‘ أو أن يقوم بإعادة نُسخةٍ (instance) من خدمةٍ جديدةٍ كُلّيًّا. ولكن عليك بالحذر من التّأثيرات الجانبيّة غير المرغوبة.

التغليف (Encapsulation)

في الفقرة السّابقة أسأنا استخدام التّابع decorator، ونتيجةً لذلك تمّ تقييد الوصول إلى العنصر points. لحسن الحظّ، يُقدّم التّغليف الخاصّ بحقن التّابع factory مجالًا مُغلقًا (closure)، ممّا يسمح لنا بإخفاء بعض المعلومات، أو بعبارةٍ أخرى، التّغليف (encapsulation).

سنقوم باستبدال العنصر points المرئيّ في المجال العام بالمتغيّر المحلّي points المحدود الرؤية ضمن التّابع الّذي يغلّفه، وهذا سيسمح لنا بحمايته من التّعديل خارج التّابع. والآن سنقوم بجعل كائن الخدمة يكشف عن تابعٍ للوصول إلى قيمة المتغيّر، getPoints، وعن تابعٍ يقيَّد التعديل فيه فقط، increment.

angular.module('app')
  .factory('score', function(randomScore) {
    var points = randomScore();
    return {
       increment: function() {
         return ++points;
       },
       getPoints: function() {
         return points;
       }
    };
  });

سنحتاج إلى تغيير بسيطٍ في المتحكّم كي نسمح له باستدعاء التّابع increment من الخدمة.

angular.module('app')
  .controller('ScoreController', function($scope, score) {
    $scope.score = score;
    $scope.increment = function() {
      $scope.score.increment();
    };
  });

وسنغيّر أيضًا القالب، ليكون مرتبطًا بتابع الوصول ()score.getPoints بدلًا من الوصول إلى العنصرpoints مباشرةً.

<p ng-controller="ScoreController">
  Score: {{score.getPoints()}}
</p>
<p ng-controller="ScoreController">
  <button ng-click="increment()">Increment</button>
</p>

بما أنّ التابع increment يقوم أيضًا بإعادة قيمة المتغيّر points بعد التّعديل، إذًا بإمكاننا الكشف (expose) عنها ضمن العرض (view) في عبارة. قد تُفاجئك النّتيجة، قُم باستبدال الاستدعاء في السّطر الثاني، وضع()score.increment بدلًا من ()score.getPoints، ثُمّ قُم بنقر الزّر عدّة مرّات. هل يُمكنك معرفة سبب زيادة القيمة بسرعةٍ كبيرة؟ هذا صحيح: تقوم Angular غالبًا باستدعاء العناصر والتّوابع المرتبطة بالقالب عدّة مرّات قبل أن تنتهي دورة الإخراج. هذه معلومةٌ هامّة يجب معرفتها لنفهم التّأثير الجانبيّ لها كالمثال السّابق، وأيضًا لتحسين كفاءة التّطبيق.

خاتمة

وصلنا إلى ختام الفصول الثّلاثة التي تتحدّث عن دعم Angular للبرمجة باستخدام الوحدات. بدأنا مع فصلالوحدات، وتابعنا مع الفصل القصير عن حقن التّبعيّة، وختمنا هذه الثّلاثيّة بهذا الفصل عن الخدمات. قد تستغرب إفراد الخدمات بفصلٍ مستقلٍّ عندما ترى أنّها ليست سوى JavaScript المعتادة ليس إلّا، ولكنّنا لم نقم بتغطيةٍ كاملة لما يجب معرفته، فإضافةً إلى التّوابع constant وvalue و service وfactory وdecorator لا يزال هناك تابعٌ منخفض المستوى هو التّابع provider الذي يقدّم تعلّقات دورة الحياة (lifecycle hooks) لإعداد خدماتك بطريقةٍ متقدّمة.

إن كنت تتساءل فيما إذا كنت تحتاج إلى مساعدة Angular في هذا المجال بالفعل، فلتبقِ في ذهنك بأنّك لست مضطرًّا لإدارة كلّ شيفراتك باستخدام Angular، ورغم ذلك فإنّ Angular 2.0 ستنتقل إلى نظام وحدات ES6 كما أشار العرض التّقديمي “RIP angular.module” الّذي قُدّم في خطاب فريق التّطوير في ng-europe 2014. وإلى أن يتمّ ذلك فإنّك تحتاج بالفعل إلى أن تضع شيفراتك داخل وحدات Angular عندما تحتاج إلى استخدام مكوّناتٍ مدمجة في Angular (أو مكوّناتٍ طوّرها طرفٌ ثالث) يتمُّ الوصول إليها عن طريق حقن التّبعيّة. لقد قمتُ بعرض بديلٍ لحقن التّبعيّة في نهاية الفصل الماضي. فقط أبقِ في ذهنك أنّ دعم اختبار الوحدة (unit testing support) في Angular قد عزّز من أهمّيّة استخدام حقن التّبعيّة، لذا عليك أن تهتمّ باستخدامك له أيضًا.

هناك أيضًا بعض المكوِّنات المخصّصة التي يجب عليك تسجيلها باستخدام نظام الوحدات في Angular، لتجعلها متوفّرة ضمن القالب. أحد هذه المكوّنات هو المُرشّحات، وسنُغطّيها في الفصل القادم.

30يوليو

( تعلم Angular ) – حقن التبعية (Dependency-injection) في AngularjS

تدير Angular الموارد باستخدام نظام الوحدات، ولهذا السّبب فهي تحتاج أيضًا إلى تقديم طريقةٍ للوصول إلى هذه الموارد المنظّمة ضمن حاويات (container-managed)، حيث تقوم Angular بذلك باستخدام نمطٍ إنشائيّ يُدعى حقن التبعية.

 

إن لم يكن لديك معرفةٌ مسبقةٌ بهذا المفهوم فسأقوم هنا بأفضل ما لديّ لتلخيصه لك: لنفترض أنّك تحتاج إلى الحصول على مَوردٍ (resource) من Angular، المَورد هو الحاوية. للحصول عليه يجب أن تكتب تابعًا يُعرّف وسيطًا له نفس اسم هذا المَورد بالضّبط. تقوم الحاوية باكتشاف التّابع الذي كتبته، وتتعرّف على توافق الوسيط مع المَورد المطلوب، فتقوم بإنشاء نُسخةٍ (instance) من هذا المَورد (أو تقوم بأخذ النُّسخة المنفردة (singleton) من هذا المَورد، إن كان من النّوع المنفرد) ثُمّ تقوم باستدعاء التّابع الخاص بك وجعل هذه النّسخة هي قيمة الوسيط الخاص بالتابع. بما أنّ الحاوية هنا تلعب الدّور الفعّال (على عكس الطلب الذي تقوم به للحصول على نسخة من المَورد)، لذلك يُعتبر حقن التّابعيّة عكسًا للتّحكّم (inversion of control)، ويُقدّم على أنّه تطبيق لمبدأ Hollywood، “لا تكلّمنا، نحن سوف نكلِّمُك”.

أعتقد بأنّ مخترعي Angular قد اختاروا طريقة حقن التّابعيّة بسبب تاريخ Google الطّويل مع منصّة Java، بيئة برمجة ساكنة يقوم فيها حقنُ التّابعيّة بدورٍ فعّال في عمليّات تغيير الأنواع أثناء وقت التّشغيل. (لقد تخطّت Google الحدود في ذلك إلى درجة أنّها أنشأت إطار عمل Java خاصٍّ بها، Guice، الذي يعتمد على حقن التّابعيّة كُلّيًّا.) أمّا في JavaScript، وهي البيئة المرنة والديناميكيّة، حيث يمكن أن تتغيّر الأنواع أثناء وقت التّشغيل، فقد حظِيَ فيها أسلوب محدّد موضع الخدمة (service locator) البسيط والمباشَر بشعبيّة هائلة عن طريق الواجهة البرمجيّة لـCommonJS، وهي عبارة require المستخدمة في Node.js وbrowserify. والأكثر من ذلك، أنّ الطريقة البدائيّة في القيام بحقن التّابعيّة قد تبيّن أنّ لها نقطة ضعفٍ عند استخدامها في JavaScript من طرف العميل، وذلك بسبب تعديل أسماء الوُسطاء باستخدام المُقصّرات (minifiers). ولنستطيع التّعامل مع هذه المشكلة، من الضّروريّ أن توجد أداة بناءٍ بديلة، أو أن نتعلّم ونستخدم حواشي التّبعيّات، وسنصل إلى ذلك بعد قليل، ولكن لنتعرّف أوّلًا على النّموذج البدائيّ.

التبعيات الضمنية

لن نستخدم خارج هذا الفصل للقيام بحقن التّبعيّة غير طريقة التّبعيّات الضّمنيّة لأنّها الأسهل بين جميع الطُّرق. كما ذكرت سابقًا، يمكننا جلب مرجعٍ لأحد الموارد المتاحة عن طريق وضعه كوسيطٍ في تابعنا الإنشائيّ، ولقد قمنا بذلك للمرّة الأولى عندما أضفنا الوسيط scope$ الذي يعطي تابع التّحكّم مرجعًا إلى كائن المجال المنظّمٍ ضمن حاوية (container-managed).

يقوم نظام حقن التّبعيّة في Angular بالعثور على نسخةٍ للمَورد المطلوب (أو بإنشاء نسخةٍ لهذا المَورد) وتمريره إلى المتحكّم. لنرى مرّةً أخرى كيف نقوم بذلك على المَورد المدمج في Angular، الخدمة locale$.

نحتاج إلى تعريف وحدتنا app لأنّ بعض أمثلة هذا الفصل تستخدم نظام الوحدات.

angular.module('app', []);

وبعد ذلك سنقوم بتحميل الوحدة عن طريق تمرير اسمها إلى التّوجيه ng-app. (أكرّر التّنبيه إلى أنّ الاسم app ليس إجباريًّا ويمكن استخدام أيّ اسم آخر.)

<body ng-app="app">
  <!-- الأمثلة توضع هنا -->
</body>

نحن الآن جاهزون للانطلاق مع مثالنا الأوّل، وبطريقةٍ مماثلةٍ لإضافة الوسيط scope$ سنضيف الوسيط locale$، وستلاحظ Angular هذين الوسيطين وتقوم بحقنهما. تقوم Angular بحقن الموارد داخل التّابع لأنّ هذا التّابع يمثّلُ متحكّمًا.

angular.module('app')
  .controller('LocaleController', function($scope, $locale) {
    $scope.locale = $locale.id;
  });

داخل جسم المتحكّم قمنا ببساطةٍ بإسناد المتغيّر locale الذي يمكننا تغيير اسمه إلى أيّ اسمٍ آخر، إلى العنصر id من الوسيط locale$ الذي لا يمكننا استخدام اسمٍ آخر له.

<p ng-controller="LocaleController">
  Your locale is <strong ng-bind="locale"></strong>
</p>

الناتج:

Your locale is en-us

يُفترض أن ترى شيئًا مماثلًا لعبارة “Your locale is en-us” في المخرجات. ما الذي سيحدث لو أنّك أضفت الوسيط myResource إلى تابع التّحكّم؟ هل عطّلت عمل المثال؟ هل يمكنك قراءة الخطأ في الـconsole الخاصة بـJavaScript في متصفّحك؟ تعرض أخطاء Angular غالبًا رابطا مفيدا، يمكنك عن طريق هذا الرابط أن تحصل على درسٍ رائع وقصير عن حلّ مشاكل حقن التّبعيّة، انقر على الرّابط السّابق واقرأ الشرح إن أردت.

الحواشي (Annotations)

تمثّلُ الحاشية في لغة برمجةٍ مثل Java، معلوماتٍ إضافيّة تُضاف إلى الشيفرة المصدريّة ويتجاهلها المترجم. أمّا في سياق الحديث عن حقن التّبعيّة في Angular، فتعني كلمة الحاشية التّصريح عن الموارد التي نريد حقنها بطريقةٍ صريحةٍ تحافظ على أسماء الوسطاء قصيرةً باستخدام أسلوب التّصغير (minification). (استخدام أداة البناء UglifyJS مثالٌ على أسلوب التّصغير.) لنتابع الآن طريقة Angular التي تسمح لنا فيها بإضافة الحواشي إلى التّبعيّات.

ng-annotate

من الطرق الملائمة للتخلص من إزعاج مهمّة بناء ما، استخدام مهمّة بناءٍ أخرى. ستقوم أداة البناء ng-annotate بإضافة الحواشي للتّبعيّات، ومن ثمّ ستسمح لنا بمواصلة استخدام طريقة التّبعيّات الضّمنيّة التي شرحناها في الفقرة السّابقة. ستحتاج فقط إلى تشغيل ng-annotate قبل أن تُشغّل المقصّر (minifier) الخاصّ بك. هذه الأداة المركزيّة صُمّمت لتُستخدم عن طريق سطر الأوامر، إلّا أنّه يوجد غلافٌ لها لتفادي التّعامل مع سطر الأوامر، كما يوجد غلافٌ لمعظم أدوات البناء وحزم الأدوات، من ضمنها grunt-ng-annotate وgulp-ng-annotate وbrowserify-ngannotate وng-annotate-webpack-plugin.

الحاشية السطرية

هناك أسلوبان لكتابة الحواشي، وأكثرهما شُهرةً يُعرف باسم الحاشية السّطريّة (inline annotation)، وهي تتضمّن وضع تابع التّحكّم داخل مصفوفةٍ كما يلي.

angular.module('app')
  .controller('LocaleController', ['$scope', '$locale', function(s, l) {
    s.locale = l.id;
  }]);

الناتج:

Your locale is en-us

في المثال السّابق، قُمت بمحاكاة تأثير التّقصير عن طريق تغيير أسماء الوُسطاء من scope$ و locale$ إلى s وl بالترتيب. بالطّبع لا يمكن لـAngular أن تجد مَوردين لهما الأسماء s وl لو اعتمدنا على أسلوب إضافة الحواشي الضّمنيّ، ولكن هنا، ولأنّ المصفوفة تحوي أسماء الوسطاء الحقيقيّين ستتمكّن من حقنهم بنجاح.

إضافة الحاشية بواسطة inject$

إن كان قد انتابك شعورٌ بأن طريقة إضافة الحواشي السّطريّة، قبيحة، فالأرجح أنك ستجد الطريقة البديلة هذه أكثر قبحًا. فهي تعتمد على إنشاء عنصرٍ ذا اسمٍ مخصّص، inject$، في التّابع الإنشائيّ، وهذا سبب تسمية الطّريقة بهذا الاسم.

var LocaleController = function(s, l) {
  s.locale = l.id;
};
 
LocaleController['$inject'] = ['$scope', '$locale'];
 
angular.module('app')
  .controller('LocaleController', LocaleController);

الناتج:

Your locale is en-us

المهمّ أنّك الآن تعرف الخيارات المتاحة للقيام بإضافة الحواشي.

angular.injector

أرجو أن تكون عند هذه النّقطة قد قمت بإضافة ng-annotate إلى بُنيتك، لتتمكّن من استخدام الأسلوب البدائيّ في التّبعيّات الضّمنيّة التي ستراها في هذه السلسلة دون مشاكل. على أيّ حال، إن لم تكن قد قمت بذلك، لديّ اقتراح لك، إلّا أنّه ليس اقتراحًا جادًّا: قم بكتابة تابع require الخاصّ بك، وأفصح عن التّبعيّات الّتي تريد ضمن عمليّة البحث المباشر عن الخدمات، وذلك بدلًا من استخدام أسلوب حقن التّبعيّة. يمكنك القيام بذلك عن طريق استدعاء تابع angular.injector مباشرةً، وعليك تمرير وسيط وحيدٍ إليه هو مصفوفةٌ فيها أسماء الوحدات التي ترغب بالبحث ضمنها، ويجب عليك تمرير اسم الوحدة ng دومًا وجعلها في بداية القائمة.

var require = function(name) {
  return angular.injector(['ng','app']).get(name);
};

angular.module('app')
  .controller('LocaleController', ['$scope', function($scope) {
    var $locale = require('$locale');
    $scope.locale = $locale.id;
  }]);

الناتج:

Your locale is en-us

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

مخاطر

المثال التّالي سيستبق الأحداث، ويأخذنا إلى الفصل القادم، الخدمات. سنستخدم في هذا المثال التّابع factory، ورغم أنّك لم تألف استخدامه من قبل إلّا أنّه بإمكانك تخمين وظيفته، فهو يقوم بإعداد مصنعٍ للمكوّن باستخدام نظام الوحدات. لاحظ أيضًا أنّ الخدمة http$ (التي سنناقشها في فصلٍ لاحق HTTP) ستكون خدمةً نموذجيّةً ومثالًا من الحياة العمليّة لاستخدام حقن التّبعيّة داخل خدماتٍ كهذه.

angular.module('app')
  .factory('fullPrice', ['$http', function($http) {
    return function() {
      // Use $http to fetch remote data.
      return 100;
    }
  }])
  .factory('discountPrice', ['$http', function($http) {
    return function() {
      // Use $http to fetch remote data.
      return 40;
    }
  }]);

استلهمتُ المثال التّالي من تطبيق Angular حقيقيّ، حيث استُبدلت التّبعيّات بقائمةً طويلةً من الخدمات المدمجة في Angular، ولكنّ طول القائمة هو نفسه تقريبًا.

angular.module('app')
  .controller('PriceController',
    ['$scope',
    '$anchorScroll',
    '$animate',
    '$cacheFactory',
    '$compile',
    '$controller',
    '$document',
    '$exceptionHandler',
    '$filter',
    '$http',
    '$httpBackend',
    '$interpolate',
    '$interval',
    'fullPrice',
    'discountPrice',
    '$locale',
    '$location',
    '$log',
    '$parse',
    '$q',
    '$rootElement',
    '$rootScope',
    '$sce',
    '$sceDelegate',
    '$templateCache',
    '$timeout',
    '$window',
    function(
      $scope,
      $anchorScroll,
      $animate,
      $cacheFactory,
      $compile,
      $controller,
      $document,
      $exceptionHandler,
      $filter,
      $http,
      $httpBackend,
      $interpolate,
      $interval,
      discountPrice,
      fullPrice,
      $locale,
      $location,
      $log,
      $parse,
      $q,
      $rootElement,
      $rootScope,
      $sce,
      $sceDelegate,
      $templateCache,
      $timeout,
      $window) {
        $scope.fullPrice = fullPrice();
        $scope.discountPrice = discountPrice();
  }]);

من الواضح أنني اختصرت جسم المتحكّم PriceController وكذلك اختصرت شيفرة العرض التّالية.

<table ng-controller="PriceController">
  <tr>
    <td>Full price:</td>
    <td>{{fullPrice}}</td>
  </tr>
  <tr>
    <td>Discount price:</td>
    <td>{{discountPrice}}</td>
  </tr>
</table>

الناتج:

Full price:	    40
Discount price:	100

لقد تعمّدت القيام بخطأٍ في المثال السابق، فالمفترض أن يكون السعر الكلّي أكبر دومًا من السّعر المخفّض، ولكنّهما انعكسا لسببٍ ما، والمشكلة ليست في شيفرة العرض أو في جسم المتحكّم، هل يمكنك إيجادها؟ ماذا تستنتج من ذلك بخصوص كتابة حاشية التّبعيّات يدويًّا في المشاريع الحقيقيّة؟

30يوليو

( تعلم Angular ) – الوحدات (Modules) في AngularJS

إنّ هدف الوحدات عمومًا هو توزيع المهام عن طريق تعريف توابع الواجهة البرمجية (API) والحدّ من رؤية السلوك (أجسام التوابع) والبيانات (العناصر والمتغيّرات).

معظم المنصّات البرمجيّة تتضمّن دعمًا داخليًّا للوحدات، حتّى أصبح استخدامها أمرًا مسلّمًا به. ولكنّ JavaScript المستخدمة من طرف العميل لا تستخدم الوحدات حاليًّا، وتؤدي إلى احتدام النقاشات المليئة بين مؤيّد ومعارض للحلّ المشهور (الإضافات add-ons) مثل CommonJS و تعريف الوحدة غير المتزامن (AMD).

أبقِ في ذهنك عندما تقوم بمقارنة الحلول المطروحة، أنّ نظام الوحدات في Angular لا يقوم بتحميل الوحدة. فهو لا يقوم بتحميل الشيفرة المصدرية من ملفّاتٍ أو من الإنترنت. فالحلّ الذي تقدّمه Angular هو نظام الوحدات المدمج فيها، والذي يرتبط ارتباطًا وثيقًا بنظام حقن التابعية المدمج أيضًا في Angular. معًا، يكوّن هذان النّظامان جزءًا كبيرًا من البنية التّحتيّة لـAngular. ولكن هل نحن بحاجةٍ إليها حقًّا؟ صحيحٌ أنّ بإمكان أيّ مطوّر تعلّمها، ولكن لمَ نحتاجها؟

تُشكّل الوحدات وحقن التّابعيّة كلّ بنية Angular، ولكن كما رأينا فإنّ Angular تسمح لنا بالبدء دون استخدام نظام الوحدات. حتّى الآن، كان بإمكاننا إنجاز الكثير ببساطة عن طريق تعريف المتحكمات كتوابع معرّفة في المجال العامّ. ولكنّ ذلك لم يكن ليعمل لولا القليل من الإعدادات، وهذه الإعدادات سمحت للتّوجيه ng-controller بالبحث عن توابع التّحكّم في المجال العام وأيضًا في نظام الوحدات. هذه الطريقة السهلة في استخدام المجال العام مسموحةٌ في Angular، ولكن فقط إلى هذه النقطة.

لماذا نستخدم وحدات Angular؟

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

لنبدأ بنظرةٍ عامّة إلى المزايا الهامّة التي تدفعنا لاستخدام نظام الوحدات:

  • مكوّنات مخصّصة – لتعريف المتحكّمات الخاص بك، التّوجيهات، المرشّحات، والمؤثّرات الخاصّة، لا بدّ من استخدام نظام الوحدات.(يمكننا استثناء المتحكّمات كما أشرنا سابقًا.)
  • حقن التّابعيّة – الخدمات ليست إلّا كائنات وتوابع JavaScript عاديّة، إلّا أنّ الخدمات التي يتمّ إنشاؤها في نظام الوحدات يمكن أن تُحقن بسهولة مع التّبعيّات الخاصّة بها، وذلك إن كانت تتيح حقن التّابعيّة فيها.
  • الوحدات الخارجيّة – هناك نظامٌ بيئيّ مثيرٌ للاهتمام، مجّانيٌّ ومفتوح المصدر من الإضافات لـAngular، وقد قام بكتابة بعض هذه الإضافات فريق تطوير قلب Angular وكذلك المطوّرون كطرفٍ ثالث. لاستخدام أيٍّ من هذه المكاتب في تطبيقك لا بُدّ من استخدام نظام الوحدات.
  • إعدادات وقت-التّحميل – يسمح نظام الوحدات في Angular بالوصول إلى تعلّقات دورة الحياة (lifecycle hooks) المتعلّقة بالإعدادات الدّاخليّة لـAngular، وكذلك المتعلّقة بإعدادات الوحدات المضافة، وسنتطرّق لها في نهاية الفصل الأخير، حيث سنقوم بإعداد رأس HTML مخصص.
  • الاختبار – إنّ البنية التّحتيّة الخاصّة بالاختبار في Angular تبيّن فاعليّة نظام حقن التّابعيّة والّذي يعتمد بدوره على نظام الوحدات.

سيبيّن فصلٌ لاحقٌ، فصل الخدمات، كيف نقوم بتعريف مقدم لمكوّنات JavaScript المخصّصة. سنرى في هذا الفصل كيف نعرّف متحكّمًا، وسنقوم أيضًا بتحميل وحدةٍ خارجيّةٍ، وأثناء تنفيذ ذلك سنتعلّم أساسيّات نظام الوحدات في Angular.

إنشاء وحدة التطبيق

يمكن تهيئة تطبيق Angular باستخدام وحدةٍ جذريّة واحدة فقط. سنقوم بتسمية الوحدة في أمثلة هذا الفصل بالاسم app ويمكنك اختيار أيّ اسمٍ آخر إن أردت، إلّا أنّ هذا الاسم شائع الاستخدام في مرجع Angular.

angular.module('app', []);

أرجو أن تركّز على الوسيط الثاني للتابع module في مثالنا السابق، ورغم أنّه يبدو فارغًا، وله مظهر مصفوفة بريئة خالية ([]) إلّا أنّ وجود هذا الوسيط الثاني شديد الأهمّيّة، فبدونه سيتغيّر سلوك التّابع angular.module كليا، وسأشرح لك سلوك هذا التابع كما يلي:

عند استدعاء التابع angular.module مع وجود الوسيط الثاني، سيعمل التّابع في نمط الإنشاء (create)، وسيقوم بإنشاء وحدةٍ ويسمّيها app إن لم يكن هناك وحدة بنفس الاسم سابقًا، أمّا عند استدعائه دون الوسيط الثّاني فسيعمل في نمط البحث (lookup)، فإن عثر على وحدةٍ بالاسم المعطى سيقوم بإعادتها، أمّا إن لم يجدها فسيقوم برمي خطأٍ يطلب تمرير الوسيط الثاني. قمنا في المثال السابق بالاستخدام الصحيح للتابع، ولكن كن حذرًا دومًا من الفرق بين الطريقتين في استدعاء هذا التّابع, أمّا بالنّسبة لهذه المصفوفة التي تمرر كوسيط، فهي تُستخدم لجلب الوحدات الخارجية التي سنناقشها في هذا الفصل، باستخدام وحدة animation في أمثلتنا.

تحميل وحدة التطبيق

في مستند HTML الذي يستضيف التّطبيق، يمكننا أن نأمر Angular بتحميل وحدة هذا التّطبيق عن طريق تمرير اسمه إلى التّوجيه ng-app.

<body ng-app="app">
  <!-- الأمثلة توضع هنا -->
</body>

ستكون جميع المكوّنات التي نضيفها إلى وحدة التّطبيق متاحةً للاستخدام. لنرى الآن كيف نقوم بتعريف متحكّم داخل وحدةً بدلًا من تعريفه في المجال العام.

تعريف متحكم

تتضمّن الواجهة البرمجية للعنصر module توابع لتعريف مكوّنات Angular المخصّصة. والمكوّن الوحيد الذي تعرفنا عليه سابقًا هو المتحكّم، لذا لنقم باستخدام التابع controller لإنشاء متحكّم.

سنحتاج أوّلًا إلى جلب مرجعٍ لوحدة التّطبيق، وكما ذكرنا سابقًا، يجب أن نقوم باستدعاء التابع angular.module بدون تمرير الوسيط الثاني، وذلك كي يعمل في نمط البحث lookup.

var app = angular.module('app');

app.controller('MessageController', function($scope) {
  $scope.message = "This is a model.";
});

والآن، في مكان ما ضمن العنصر الذي قمنا فيه باستدعاء الوحدة عن طريق “ng-app=”app، يمكننا استخدام التّوجيه ng-controller لتحميل المتحكّم.

<p ng-controller="MessageController">
  {{message}}
</p>

الناتج

This is a model.

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

سلسلة التعريفات

لنفترض بأننا نريد تعريف متحكّمين للقالب التالي.

<p ng-controller="MessageController">
  {{message}}
</p>
<p ng-controller="AnotherMessageController">
  {{message}}
</p>

إنّ توابع التعريف ضمن العنصر Module قابلةٌ للسَّلسَلة، حيث يمكننا إنشاء متحكّمين باستخدام عبارةٍ واحدة.

angular.module('app')

  .controller('MessageController', function($scope) {
    $scope.message = "This is a model.";
  })

  .controller('AnotherMessageController', function($scope) {
    $scope.message = "This is another model.";
  });

الناتج

This is a model.

This is another model.

لاحظ أن الاستدعاء الأول للتابع controller لم تتبعه فاصلةٌ منقوطة.

إن لم تعجبك هذه الطريقة في سَلسَلة الاستدعاءات، يمكنك استدعاء التابع module كلّما أردت الحصول على مرجعٍ للوحدة، أو يمكنك تخزين هذا المرجع في متغيّر، كما يبيّن المثال التالي. عند استخدامك لمتغيّر، سيكون من الجيّد أن تضعه ضمن تابعٍ يُستدعى آنيًّا (IIFE) أو أيّ مجالٍ مغلقٍ آخر، لمنع ذلك المتغيّر من أن يُنشأ في المجال العام.

var app = angular.module('app');

app.controller('MessageController', function($scope) {
  $scope.message = "This is a model.";
});

app.controller('AnotherMessageController', function($scope) {
  $scope.message = "This is another model.";
});

يمكنك اختيار أيٍّ من الطريقتين، ولكنّ طريقة السَّلسلة هي الأكثر شيوعًا.

تحميل الوحدات

يُعدّ التحريك (animations) ميزةً جديدةً في Angular تمّت إضافتها إلى حزمة منفصلة اسمها ngAnimate. الخطوة الأولى لاستخدام التحريك هي تضمين ملف JavaScript المحتوي على الوحدة. الشيفرة المصدريّة للوحدة جزءٌ من قلب Angular، ولكنّها موجودةٌ في ملفٍّ منفصل، كما نرى أدناه.

<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.2/angular.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.2/angular-animate.js"></script>

حان الوقت لنستخدم الوسيط الثّاني للتابع module كما يجب، فبدلًا من تمرير مصفوفةٍ خاليةٍ فيه سنقوم الآن بالتصريح عن الوحدات المتعلّقة بوحدتنا.

angular.module('app', ['ngAnimate']);

لقد قمنا الآن بتحميل ngAnimate إلى داخل وحدة التّطبيق، والآن ستقوم التّوجيهات ng-show و ng-hide و ng-class و العديد من التّوجيهات الأخرى بالتّحقّق من التحريك الخاصّ بـCSS وJavaScript أيضًا الذي يمكن تطبيقه على أحداث المستند مثل add و enter و leave و move و remove، وتطبيق هذا التّحريك شفّاف فهو لا يتطلّب أيّ تغيير على شيفرة القالب المكتوبة، وإن لم نكن قد طبّقنا هذا التّحريك سواء على شكل CSS أو كـJavaScript عن طريق تسجيل الحركة عبر module.animation، فإنّ الشيفرة ستسلك السّلوك الافتراضيّ لها بشكل طبيعيّ.

<input type="checkbox" ng-model="showMessage">
    Check the box to show the message
<h2 ng-show="showMessage">
    The secret message.
</h2>

بالنّقر على الـcheckbox ستقوم ng-show بعملها كالمعتاد، وسيتمّ التّبديل بين إظهار وإخفاء العنصر بشكلٍ فوريّ دون تدرّج أو تأثير اختفاءٍ بسيط، إلى أن نكتب شيفرة التّحريك في CSS أو JavaScript. في المثال التّالي سنستخدم CSS، وهي عمومًا الخيار الأفضل للتحريك بسبب الأسلوب التصريحي في كتابتها. يمكنك على أيّ حالٍ استخدام التابع animate في jQuery ليقوم بالمهمّة إن وجدت ذلك أسهل، أما Angular فتقوم بذلك عن طريق الوحدة module.animation.

يتطلّب التّحكّم بتأثير الاختفاء التّدريجيّ استخدام أربع فئات CSS تقوم بالتّعلّق (hook)، وذلك حسب توثيق الواجهة البرمجية الخاصّ بالتّوجيه ng-show. هذه الفئات الأربعة هي ng-hide-add و ng-hide-add-active و ng-hide-remove و ng-hide-remove-active.

.ng-hide-add,
.ng-hide-remove {
  transition: all linear 1s;
  display: block !important;
}

.ng-hide-add.ng-hide-add-active,
.ng-hide-remove {
  opacity: 0;
}

.ng-hide-add,
.ng-hide-remove.ng-hide-remove-active {
  opacity: 1;
}

قد تحتاج إلى إضافة خصائص محدّدة من قبل المصنّع لبعض المتصفّحات حسب زوّار موقعك، مثل webkit-transition- الخاص بمتصفّح سفاري 6 لأنظمة iOS. (يُعدّ موقع caniuse.com مرجعًا جيّدًا لتوافق المتصفّحات مع HTML5 و CSS3.) أرجو أن تنتبه أيضًا إلى أنّ هذا التّحريك سيتمّ تطبيقه في كلّ الأماكن التي ستستخدم فيها ng-show و ng-hide في تطبيقك. يمكنك إضافة فئة CSS مخصّصة لتزيد قدرتك على التّحكّم، وذلك للعنصر ولمحدّد CSS الذي تتعامل معه، مثلًا يمكنك كتابة my-class.ng-hide-add. و my-class.ng-hide-remove. وهكذا.

خاتمة

أتمنى أن تكون سهولة إضافة ngAnimate قد أقنعتك بأهمّية فهم نظام الوحدات في Angular. ومن المفرح أن تعلم بأنّ ngAnimate ليست إلّا البداية ضمن الكثير من الوحدات المُضافة إلى Angular. يمكنك أن تختار وحداتٍ أخرى متاحةّ بشكلٍ مجّانيّ من هذا النّظام البيئيّ سريع النموّ الذي يعتمد على إبداعات المطوّرين من مواقع مثل GitHub، وذلك إضافةً إلى الوحدات التي تتيحها Angular أصلًا مثل ngResource وngRoute. أحد أكثر المشاريع شُهرةً هو AngularUI، الذي يحوي وحداتٍ عظيمة الأهمّيّة مثل UI Router وUI Bootstrap، ومعظم هذه الوحدات يمكن تحميله بسهولة في مشروعك باستخدام Bower، مدير حزم الويب، ولربّما ستبدأ بنشر وحدات Angular الخاصّة بك مفتوحة المصدر على GitHub وBower عندما تملك زمام Angular، وهذا ما أتمنّاه.

تقدّم الوحدة ngAnimate إثباتًا واضحًا للقوّة المتاحة في الوحدات المضافة، ولكنّها لا تتضمّن استخدام البنية التّحتيّة لحقن التّابعيّة في Angular بسبب أسلوبها الجزئيّ في التكامل. سنغطّي حقن التّابعيّة في الفصل القادم، وذلك لأنّه يعمل يدًا بيد مع الوحدات.

30يوليو

( تعلم Angular ) – المتحكمات (Controllers) في AngularJS

طلبت منك في الفصل السابق (المبادئ) أن تكبح جماح JavaScript بداخلك ريثما نستكشف القيمة الحقيقيةّ التي تقدّمها Angular، وهي تقديم امتدادات قويّة لـHTML لمطوّر النّهاية الأمامية front-end.

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

أكثر الطرق شيوعًا لتعديل العرض في Angular باستخدام JavaScript هي عن طريق استخدام متحّكم، وأبسط طريقة لكتابة متحكّم هي باستخدام تابع بناء (constructor) بسيط.

لنبدأ بمثالٍ بسيط لنفهم تمامًا ما الذي يحدث، المثال التّالي لا يقوم بأي شيء، ولا يطبع “!Hello World” حتّى.

ها هو متحكّمنا البسيط.

function EmptyController() {

};

سنحتاج إلى تضمين مكتبة Angular ضمن الصفحة لتتم معالجة خصائص المجال التي تم تحضيرها عن طريق المتحكّم، ولذا يجب نسخ الشّيفرة التّالية ولصقها داخل العنصر head في كلّ الأمثلة.

<html>
    <head>
        <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.4.2/angular.js"></script>
    </head>
    <body ng-app="app">
        <!-- كل الأمثلة توضع هنا -->
    </body>
</html>

يجب علينا ضبط إعدادات تطبيق Angular داخل مستند HTML ليقوم بتحميل الوحدة app، فقد تعرفّنا في الوحدة الأولى المبادئ على التّوجيه ng-app الذي نضيفه لأيّ عنصر في المستند لتقوم Angular بمعالجة ما بداخله، انظر الآن إلى استخدام ng-app في المثال التّالي، هل يمكنك الانتباه إلى الإضافة الجديدة؟

<body ng-app="app">

</body>

يُمكن تعريف المتحكّمات كتوابع بناء معرّفة في المجال العام (global scope)، وهذه هي الطّريقة التي سنستخدمها في أمثلتنا في هذا الفصل.

كان الشكل البسيط للمتحكّمات مدعومًا في Angular حتى النسخة 1.3 لتسهيل انطلاق المطوّرين مع المكتبة، ولكنّ ذلك يتطلب الآن إنشاء وتسمية وحدة للتطبيق.

سنناقش تفاصيل استخدام angular.module في الفصول القادمة الوحدات، حقن التابعة، والخدمات.

يمكنك التعامل مع المثال التّالي على أنّه شيفرة معياريّة مطلوبة:

angular.module('app', []);
angular.module('app').config(['$controllerProvider', function($controllerProvider) {
    $controllerProvider.allowGlobals();
}]);

والآن بعد كتابة هذه الشّيفرة الخارجة عن سياق الحديث، لنعد إلى متحكّمنا عديم الفائدة.

ng-controller

كالعادة، الطّريقة التي سنتّبعها للقيام بأيّ شيء، هي استخدام الموجّهات، سنستخدم الموجه ng-controller الذي يقوم بالعثور على التّابع الذي يتمّ تمرير اسمه إليه ثم استدعائه.

<p ng-controller="EmptyController">

</p>

وكما توقّعتَ، لن يقوم هذا التّابع بأيّ شيء، والآن إذًا ما هي هذه المتحكّمات وكيف نستخدمها؟

بناء النموذج

ينصّ المرجع الرسمي في النظرة العامة للمفهوم على أنّ وظيفة المتحكّم هي “الكشف عن المتغيرات والوظائفيّة للعبارات والتّوجيهات” وتشير كلمة “الوظائفية” إلى توابع الاستدعاء الخلفي (callback)، وسنتطرق إليها قريبًا، أمّا الآن فسنتكلّم عن تهيئة “المتغيّرات”، أو بعبارة أخرى تحضير النّموذج.

أرجو منك أن تبقي في ذهنك أنّ وحدة Angular ليست سوى JavaScript عاديّة يمكن الوصول إليها ضمن مجال عبارة ما، وبذلك سيكون تحضير النّموذج أمرًا سهلًا للغاية.

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

function MessageController() {
    this.message = "This is a model.";
}

أمرٌ بسيط، أليس كذلك؟ هل قمنا الآن بإنشاء وحدة؟ ماذا تتوقّع؟
الإجابة هي “تقريبًا”، فعندما يكون بإمكاننا الوصول إلى العنصر message ضمن مجال العرض، ستكون هذه وحدةً بالفعل.

أحد طرق القيام بذلك هي الكشف عن كامل المتحكّم كعنصرٍ بداخل المجال، ولن يكون هذا صعبًا كما تظنّ، كلّ ما عليك معرفته هو التركيب النحويّ المناسب للقيام بذلك.

Controller as propertyName

يشرح توثيق المكتبة بالنّسبة للتّوجيه ng-controller بأنه يمكنك تمرير عبارة Controller as propertyName كوسيط للتّوجيه ng-controller، وهذه الميزة متاحة في النسخة 1.2 فما فوق.

<p ng-controller="MessageController as controller">
    {{controller.message}}
</p>

الناتج

This is a model.

رائع، لقد قمنا للتّو بتحضير بيانات النّموذج باستخدام متحكّم.

صحيحٌ بأنّ هذه الطّريقة في ربط الخاصّية بالمرجع this مباشرة وليس فيها الكثير من التّعقيد، إلّا أنها تعطي انطباعًا خاطئًا بأنّ المتحكّم جزءٌ من النّموذج، وهذا غير صحيح، فالمتحكّم في الحقيقة يقوم بـتحضير النّموذج فقط.

سيكون اختبار الشّيفرة وتتبّع أخطائها أكثر سهولةً عندما يتم الحدّ من الوصول إلى البيانات بمقدار ما يحتاج إليه العمل لا أكثر، كما أنّ الطّريقة السّابقة تضيف بعض الضجّة إلى شيفرة العرض، فجميع العناصر سيتم الوصول إليها عبر مرجع (reference) للمتحكّم.

شيفرة العرض هي المكان الأكثر حساسيةً للضجيج في تطبيق ويب، فهي أوّل مكانٍ قد نبدأ فيه بفقد القدرة على فهم الهدف من الشّيفرة بلمحة سريعة.

أخيرًا، المتحكّمات تعمل يدًا بيد مع المجال، لذا ولأهدافٍ تعليميّة سيكون من الأفضل رؤية مرجع (reference) للمجال وامتلاك القدرة على التّحكم به، ومع متحكّم كهذا سيكون بإمكاننا الكشف عن العناصر التي نحتاجها في العرض فقط، بدل الكشف عن المتحكّم بما فيه، وسنتّبع في هذا الكتاب هذه الطّريقة بالكشف الصّريح عن العناصر المستخدمة في العرض داخل المتحكّم.

أوّل سؤالٍ يتبادر إلى الذّهن الآن هو: “كيف سنحصل على مرجعٍ للمجال؟”، هل سننشئ واحدًا عن طريق new أم سنطلب الحصول على واحدٍ بطريقة ما؟

حقن التابعية

سنحصل عليه عن طريق حقن التابعية، ربما تكون قد لاحظت أيضًا أنني لم أكتب أي شيفرة لإنشاء المتحكّم مثل:

var controller = new MessageController();

مثلًا، فمن الذي يقوم بإنشاء المتحكّم المستخدم في العرض إذًا؟

Angular هي من يقوم بذلك طبعًا، فـAngular هي حاوية لتبادل التحكم Inversion of Control تدير دورة حياة مكونات التّطبيق، فعندما نحتاج إلى متحكّم جديد أو إلى مكوّنٍ آخر، ستقوم Angular ببنائه تلقائيًّا، وهذا يوفر الجهد، ولكنّ الأمر الأكثر أهمية هو أنه يسمح لـAngular بـحقن المصادر، أو التّبعيّات إلى المكوّن الجديد.

scope$

يحتاج متحكّمنا فقط إلى مُعامل يُسمّى scope$ وذلك بفضل إطار العمل الذي يُدار عن طريق حقن التّابعية، كما أنّ تسمية هذا المُعامل هامّة جدًّا.
ويجب عليك أن تعرف خطورة تقصير شفرة JavaScript داخل مشروعك، فهذا قد يؤدي إلى تدمير آلية عمل حقن التّابعية. (ولكن لا تقلق، فهناك حلّ التفافيّ لهذه المشكلة، سنتطرّق إليه في فصل حقن التابعية).

function MessageController($scope) {
   $scope.message = "This is a model.";
}

بإضافتنا للمعامل scope$ إلى تابع البناء نكون قد أخبرنا Angular بحاجتنا إلى مرجع لمجال العرض، والآن بدلًا من تعريف العنصر message داخل this (أي داخل المتحكّم)، قمنا بتعريفه مباشرةً داخل المجال.

في المثال التّالي، قمنا بإزالة as controller من التّوجيه ng-controller، كما أنّ العبارة controller.message أصبحت فقط message وهي العنصر الوحيد الذي قمنا بربطه بالمجال.

<p ng-controller="MessageController">
    {{message}}
</p>

الناتج

This is a model.

رغم أن الطّريقتين تعملان بشكل صحيح، إلا أننا سنستخدم طريقة المرجع scope$، فكما ترى، حقن التّابعية هو جزء مكمّل لاستخدام Angular، ويجب أن يصير مألوفًا لنا.

النموذج-العرض-المتحكم MVC

يشير تعريف المتحكّم في المرجع الرسمي إلى أنّه يكشف “الوظائفيّة” للعرض، وقبل أن ننتقل إلى هذا الجزء يجدر بنا الحديث عن الفرق بين متحكّمات Angular وبين نموذج MVC التقليدي.(قد ترغب في تجاوز هذه الفقرة).

تنصّ ويكيبيديا في مقال النّموذج-العرض-المتحكّم (MVC) على أنّ المتحكّم “يستلم المدخلات ويحوّلها إلى أوامر للنموذج أو للعرض.”، قد تكون عبارة “أوامر للنّموذج أو للعرض” صعبة الفهم في سياق الحديث عن Angular، فـAngular قامت بتبسيط نموذج MVC الخاص بها، باستخدام النّماذج الضّعيفة (anemic models) التي لا تحوي منطقًا برمجيًّا فيها.

النّمط السّائد في Angular هذه الأيّام هو وضع كل منطق العمل، أو ما يُعرف أيضًا بمنطق النّطاق (domain logic) داخل المتحكّم.
بعبارةٍ أخرى، تتّجه Angular نحو نموذج نحيل ومتحكّم سمين.

إذا كنت تألف بعض الأنماط مثل نموذج النطاق الغنيّ (rich domain model) فربّما تجد طريقة Angular متخلّفةً بعض الشيء، أما أنا فأرى الأمر مجرّد طريقة لترتيب الأولويّات.

فعند برمجة طرف المستخدم يكون الفرق الأكثر أهمّية هو بين شيفرة العرض التّريحية الموجّهة نحو المستند من جهة، وبين شيفرة JavaScript التي تتبع أسلوب الأوامر والمقادة بالبيانات والتي تعالج منطق العمل والبنية التحتية للتطبيق من جهة أخرى.

أنا سعيدٌ لأن Angular ركّزت على ذلك فهذا انتصارٌ كبيرٌ لها، وقد يصبح ذات يومٍ فصل منطق العمل عن باقي مسؤوليات المتحكّم هو الأمر الأكثر أولويّة، وقد نرى توجّهات نحو نموذج أغنى.

التوابع

لنقم بكتابة متحكّم يكشف عن تابع بسيطٍ جدًّا، فقط لنختبر إمكانية استدعاء التوابع داخل العرض.

لا بد من أنّك تذكر من الفصل الماضي أن Angular لا تسمح بتعريف التوابع داخل العرض.

المتحكّم التّالي يقوم بإسناد تابع بسيط إلى عنصرٍ في العرض.

function CountController($scope) {
    $scope.count = function() { return 12; }
}

ويمكننا استدعاء التّابع في العرض بكتابة شيفرة JavaScript عاديّة، اسم العنصر مع أقواس، فقط.

<p ng-controller="CountController">
    There are {{count()}} months in a year.
</p>

الناتج

There are 12 months in a year.

لاحظ أن التّابع لم يتم استدعاؤه ونسيانه بعد ذلك ببساطة، بل تمّ ربطه بالنّموذج.

ما الذي يعنيه ذلك؟ ربّما بدأت بالتسليم بأن البنية التحتية في Angular هي الربط (binding)، ومرّة أخرى سأكرّر: لا تقوم Angular باستدعاء التّابع فقط عندما يتم إخراج العرض للمرة الأولى فقط، بل في أيّ وقتٍ يتم فيه تغيير النّموذج المرتبط، لنتمكّن من رؤية ذلك أثناء العمل سيتوجّب علينا كتابة تابع يستخدم عنصرًا من النّموذج.

يستخدم التّابع add في المثال التّالي عنصرين في النّموذج، تم تعريفهما في المجال، وهما operand1 و operand1.
ستقوم Angular باستدعاء التّابع وإخراج النتيجة كلّما تغيّر أحد العنصرين أو كليهما.

function AdditionController($scope) {
  $scope.operand1 = 0;
  $scope.operand2 = 0;
  $scope.add = function() {
    return $scope.operand1 + $scope.operand2;
  }
  $scope.options = [0,1,2,3,4];
}

لقد قمنا في الفصل الماضي بتغطية العديد من الأمثلة عن التّوجيه input في Angular، لذا سنستخدم الآن التّوجيه select لتغيير قيم النّموذج.
لا بدّ من أنك لاحظت السّطر الأخير في الشّيفرة السابقة، حيث تمّ فيه تحضير نموذج خيارات options، وسنستخدم هذا النّموذج لبناء سلسلة داخل وسيط التّوجيه ng-options.
سنستخدم العبارة x for x في بناء السّلسلة، ورغم أنها قد تبدو بلا فائدة لأنها تعيد عناصر السلسلة الأصلية كما هي، إلا أن التّوجيه يحتاج إلى كتابتها على أي حال.(عندما يكون النّموذج مكوّنًا من مصفوفة كائنات objects وهو الأكثر شيوعًا، يمكنك كتابة بانٍ للسّلسلة يقوم بإخراج العنصر name من الخيار المحدد، وذلك باستخدام x.name for x.

<p ng-controller="AdditionController">
  <select ng-model="operand1" ng-options="x for x in options"></select>
  + <select ng-model="operand2" ng-options="x for x in options"></select>
  = {{add()}}
</p>

إنه يعمل بشكل جيّد، ولكن بإمكاننا تحسين تصميم التّابع add عن طريق التّعميم، الذي سيكون مفيدًا إن أردنا استخدام التّابع مع وسطاء غير operand1 و operand2.

فلنبدأ إذا بتعديل الشفرة ولنقم باستخراج الوسطاء من التابع.

function AdditionController($scope) {
  $scope.number = 2;
  $scope.add = function(operand1, operand2) {
    return operand1 + operand2;
  }
}

يمكنك تمرير العناصر والقيم الفوريّة داخل العبارات، كما في JavaScript المعتادة.

<p ng-controller="AdditionController">
  {{add(number, 2)}} is not the same as {{add(number, "2")}}
  <br>
  2 + 2 + 2 + 2 = {{add(2, add(2, add(2, 2)))}}
</p>

الناتج

4 is not the same as 22 
2 + 2 + 2 + 2 = 8

لنقم الآن بالكشف عن تابع استدعاء خلفي (callback) يمكنه معالجة عملٍ ما.

الاستدعاءات الخلفية (Callbacks)

في الفصل السابق، قمنا بتمرير عبارة إلى التّوجيه ng-click واستخدمناها للتّقليب بين قيمتين بوليانيتين للعنصر authorized، حيث قمنا بتهيئة القيمة الابتدائية للنموذج authorized باستخدام التّوجيه ng-init ثم تلاعبنا بقيمته عن طريق استدعاء خلفيّ سطريّ، فكتبنا “ng-click=”authorized = !authorized، لنقم الآن بتعديل المثال عن طريق نقل التهيئة ووضع المتغيّر في مكانه الصحيح، في المتحكّم.

function AuthController($scope) {
  $scope.authorized = true;
  $scope.toggle = function() {
    $scope.authorized = !$scope.authorized
  };
}

والآن صار التّابع toggle متاحًا للاستخدام داخل المجال، والوسيط الذي سيتمّ تمريره للتوجيه ng-click سيبدو كأنه استدعاء للتابع: ()toggle ولكنه ليس كذلك كما ذكرنا سابقًا، إنها فقط سلسلة نصّيّة سيتم معالجتها لاحقًا عندما يقوم المستخدم بالنقر على الزر.

<div ng-controller="AuthController">
  <p>
    The secret code is
    <span ng-show="authorized">0123</span>
    <span ng-hide="authorized">not for you to see</span>
  </p>
  <input class="btn btn-xs btn-default" type="button" value="toggle" ng-click="toggle()">
</div>

لا يزال المثال يعمل، والآن صار منطق التّابع toggle البسيط في مكانٍ أفضل.

تشرح الكتب عادةً تقنيّاتٍ لإدارة التعقيدات باستخدام أمثلة شديدة البساطة لإيصال الإحساس العام بفائدة هذه التقنية، وقد قمنا بذلك هنا أيضًا.

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

أحد الأسباب التّقليدية لنقل الشّيفرة من سياقٍ معقّد(كالقالب) إلى واحدٍ أبسط (كالمتحكّم) هو تسهيل الاختبار وتصحيح الأخطاء لاحقًا.

تتضح الفائدة من تسليم التعامل مع سلوك المستخدم إلى المتحكّم عندما نحتاج إلى كتابة شيفرات معقّدة، مثل مزامنة بيانات النّموذج مع المخدّم البعيد.

خلاصة

تعرفنا في هذا الفصل على JavaScript بداخل Angular، مكتوبة على شكل متحكّمات، والتي تتحمل مسؤولية تحضير البيانات للعرض كما ينصّ نمط MVC.
تجعل المتحكّمات البيانات متاحةً للعرض عن طريق التصريح عن العناصر في كائن المجال scope$.

في الفصل القادم سنأخذ نظرة أقرب لكائنات المجال، وسنتعرّف على كيفيّة تنظيمها في بنيةٍ هرميّة تتبع تقريبًا بنية الجزء الذي نعالجة في تطبيقنا ضمن مستند.

30يوليو

( تعلم Angular ) – المجموعات (Collections) في AngularJS

في الفصل السابق، المجالات، تعلّمنا أنّ Angular تقوم بإنشاء مجالٍ جديد في كلّ مرّةٍ يتمّ فيها استدعاء الباني الخاصّ بالمتحكّم عن طريق ng-controller. هناك حالاتٌ أخرى أيضًا تقوم فيها Angular بإنشاء مجالاتٍ جديدة، وربّما تكون الحالات الأكثر شيوعًا هي عند التّعامل مع مجموعاتٍ من كائناتٍ متشابهة، كما سنرى في هذا الفصل.

لا تملك Angular مكوّنًا اسمه Collection على عكس Backbone، ولكنّ دعم Angular الواسع للمجموعات ذات الكائنات المتشابهة يستحقّ إفراد فصلٍ كاملٍ لها، كما سنرى الآن.

التهيئة

قمنا سابقًا بتضمين المكتبة المستضافة عند Google في رأس مستندات HTML الخاصة بأمثلة الفصول السابقة، وسنضيف في هذا الفصل أسلوب Bootstrap في تنسيق الجداول والقوائم لإعطائها مظهرًا أجمل.

<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.4.2/angular.js"></script>

بعد ذلك نقوم بتحميل وحدة app بتمرير اسمها إلى التّوجيه ng-app. يمكنك اختيار أيّ اسم للوحدة بدلًا من app.

<body ng-app="app">
  <!-- الأمثلة توضع هنا -->
</body>

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

angular.module('app', []);
angular.module('app').config(['$controllerProvider', function($controllerProvider) {
    $controllerProvider.allowGlobals();
}]);

نحن جاهزون الآن لنتقدّم في استكشافنا التّفاعليّ للمجموعات (collections) والمرور على العناصر (iteration) ولـAngular.

المرور على العناصر

في JavaScript المعتادة، عندما تمرّ على عناصر مجموعة من كائنات متشابهة عن طريق حلقة for، قد تقوم بالتّصريح عن متغيّر محلّيّ ليحتفظ بمرجعٍ للعنصر الحاليّ، على سبيل المثال:

var items = [{name: "Item 1"},{name: "Item 2"}];
for (var i = 0; i < items.length; i++) {
    var item = items[i];
}
document.body.innerHTML = item.name;

الناتج

Item 2

ربما تظنّ (تتمنّى؟ تأمل؟ ترجو؟) أن يكون المتغيّر item في المثال السابق موجودًا ضمن مجال أسماءٍ تُنشئه JavaScript عند كل دورةٍ لحلقة for، للأسف، JavaScript لا تقوم بذلك كما يبيّن السطر الأخير في المثال السّابق.فالمتغيّر item متاح للاستخدام خارج الحلقة، لقراءة المزيد عن هذه النقطة، يُرجى مراجعة دليل Mozilla في JavaScript.

ولكنّ Angular تتجنّب هذا المأزق عن طريق دعمها الدّاخليّ للمرور على العناصر.وكي نرى كيف تقوم بذلك لنقم أوّلًا بنقل المصفوفة items إلى داخل متحكّم.

function ItemsController($scope) {
  $scope.items = [
    {name: 'Item 1', price: 1.99},
    {name: 'Item 2', price: 2.99}
  ];
}

تخيّل لو أنّ items مجموعةٌ غير معروفة الطّول، ونحتاج إلى المرور على جميع عناصرها، وإظهار قيمة العنصر name لكلّ عنصرٍ فيها، ما الذي سنقوم به؟

ng-repeat

رأينا سابقًا كيف تقوم Angular بإنشاء مجالٍ للعبارات لحمايتنا من إنشاء المتغيّرات في مجال JavaScript العام، وبطريقةٍ مماثلةٍ لذلك، يقوم التّوجيه ng-repeat بحمايتنا من الحالة التي رأيناها في المثال الأوّل، وذلك بإنشاء مجال Angular لكلّ دورة تكرار للحلقة.

<ol ng-controller="ItemsController">
  <li ng-repeat="item in items" ng-bind="item.name"></li>
  <li ng-bind="item.name"></li>
</ol>

الناتج

ْ1. Item 1
2. Item 2
3.

كما ترى، العنصر item غير متاحٍ خارج حلقة ng-repeat.

تقوم ng-repeat بإنشاء مجالٍ ابنٍ جديد لكلّ عنصرٍ جديد من المجموعة مع الخصائص التي تحددها العبارة المحددة للمجموعة. في مثالنا السّابق، كان اسم الخاصّيّة هو item ولكن يمكن أن نستبدله بأيّ اسم. جرّب تغيير المثال السّابق وغيّر item إلى شيءٍ آخر، جرّب i أو x.

عناصر الكائنات

التركيب النّحوي (مفتاح، قيمة) للكائن يسمح لك بالمرور على عناصر الكائنات. سيفيدك ذلك إن كنت تحتاج إلى كتابة محتوى كائنٍ ما بشكلٍ كامل في العرض.

<table class="table table-condensed">
  <tr ng-repeat="(propertyName, propertyValue) in {b: 'two', a: 1.0, c: 3}">
    <td ng-bind="propertyName"></td>
    <td ng-bind="propertyValue"></td>
  </tr>
</table>

الناتج

b	two
a	1
c	3

ماذا لو أردنا استخراج عنصرٍ معيّن؟ كيف نقوم باستخراج عنصرٍ له اسمٌ محدّد من جميع الكائنات الموجودة في المجموعة؟

التركيب النحوي item in items الذي استخدمناه في مثال ng-repeat السابق، يشبه كثيرًا باني القائمة، ولكنّ ng-repeat لا تسمح للأسف بإرجاع أيّ شيءٍ غير عناصر الكائن أو المصفوفة في الطرف الأيمن للعبارة، لنحاول على أيّ حال.

<ol ng-controller="ItemsController">
  <!-- Invalid code! Syntax error, because 'for' is not supported! -->
  <li ng-repeat="item.name for item in items" ng-bind="item.name"></li>
</ol>

لم تعمل كما أخبرتك. باني القائمة الحقيقيّ يسمح لك بإرجاع أيّ شيء تريده من التعداد الأصليّ، عادةً باستخدام الكلمة المفتاحيّة for. تقدّم CoffeeScript مثالًا ممتازًا على ذلك.

index$

تقوم ng-repeat بإسناد الرقم التسلسلي للعنصر الحالي إلى متغيّرٍ خاصٍ هو index$ وذلك إضافةً إلى المتغيّر الّذي يحوي قيمة العنصر. في المثال التّالي سنقوم بإعادة اختراع العجلة، وسننشئ طريقةً جديدةً لكتابة قوائم HTML المرقّمة، حيث سنستخدم قيمة المتغيّر index$ في الإخراج.

<div ng-controller="ItemsController">
  <div ng-repeat="item in items">
     {{$index + 1}}. {{item.name}}
  </div>
</div>

الناتج

1. Item 1
2. Item 2

لنحاول الآن القيام باستخدامٍ متداخلٍ للتّوجيه ng-repeat. أوّلًا، لننشئ هيكل نموذج بياناتٍ أكثر تعقيدًا حيث يحوي كلّ عنصرٍ في الطبقة العلويّة مصفوفةً من الأبناء.

function ItemsController($scope) {
  $scope.items = [
    {name: 'Item 1',
      items: [
       {name: 'Nested Item 1.1'},
       {name: 'Nested Item 1.2'}
      ]
    },
    {name: 'Item 2',
      items: [
       {name: 'Nested Item 2.1'},
       {name: 'Nested Item 2.2'}
      ]
    }
  ];
}

والآن لنقم بإدخال حلقةٍ داخل أخرى، ستقوم الشّيفرة التّالية بإضافة اسم كلّ عنصرٍ إلى قائمةٍ مرقّمة.

<div ng-controller="ItemsController">
  <ol>
    <li ng-repeat="item in items">
      {{item.name}}
      <ol>
        <li ng-repeat="item in item.items">
          {{item.name}}
        </li>
      </ol>
    </li>
  </ol>
</div>

الناتج

1. Item 1
    1. Nested Item 1.1
    2. Nested Item 1.2
2. Item 2
    1. Nested Item 2.1
    2. Nested Item 2.2

ولكن ماذا لو أردنا إنشاء عدّاداتٍ متداخلة؟ كيف سنمنع المتغيّر index$ الّذي تمّت تحديد قيمته في الحلقة الخارجيّة من أن يتمّ تظليله بالمتغيّر ذي الاسم نفسه الّذي تُنشئه الحلقة الدّاخليّة؟

ng-init

ربّما تتذكّر من الفصل الأول، المبادئ، أنّ Angular تسمح لنا بتهيئة متغيّرات المجال في منطقة العرض باستخدام التّوجيه ng-init. حلّ مشكلتنا يكمُن في استخدام ng-init للقيام بإعادة تعيين، أو عمل اسمٍ مستعارٍ، لمتغيّر الحلقة الخارجيّة index$ قبل أن يتمّ تظليله.

<div ng-controller="ItemsController">
  <div ng-repeat="item in items" ng-init="outerCount = $index">
    {{outerCount + 1}}. {{item.name}}
    <div ng-repeat="item in item.items">
       {{outerCount + 1}}.{{$index + 1}}. {{item.name}}
    </div>
  </div>
</div>

الناتج

1. Item 1
1.1. Nested Item 1.1
1.2. Nested Item 1.2
2. Item 2
2.1. Nested Item 2.1
2.2. Nested Item 2.2

لا تكتفي ng-repeat بالمتغيّر index$ فهي تقوم بإسناد عددٍ من المتغيّرات البوليانيّة في كلّ دورٍ للحلقة، وهي: first$ وmiddle$ وlast$ وeven$ وodd$. يمكنك تجربة كلِّ واحدٍ منها ضمن المثال التّالي، وهذا المثال يستخدم التّوجيه المفيد ng-class حيث يتمّ وسم الخانة باللون الأخضر عندما تكون العبارة الشّرطيّة محقّقة. هل يمكنك اكتشاف الطّريقة الّتي تجعل العنصرين الأوّل والأخير فقط موسومين باللون الأخضر؟ (ملاحظة: ستحتاج إلى إضافة العمليّة ! فقط.)

<ol>
  <li ng-repeat="val in [1,2,3,4,5]">
    <span class="label label-default"
          ng-class="{'label-success': $middle}">
      {{val}}
    </span>
  </li>
</ol>

هل لاحظت شيئًا عند استخدام المتغيّرين even$ وodd$؟ تقوم Angular بتحديد قيمة المتغيّرات وفق الطّريقة التّقليديّة التي تعتمد بدء التّرقيم من الصّفر في حلقات for. قد لا يبدو الأمر مثيرًا للاهتمام للوهلة الأولى، ولكن لو لاحظت جيّدًا فترقيم العناصر يبدأ من الصّفر بينما يبدأ ترقيم القائمة المرتّبة من الواحد، وهذا يعني أنّ المتغيّرين even$ وodd$ سيعملات بشكلٍ معاكسٍ لاسمهما بالنّسبة لترقيم القائمة.

التفرد

كملاحظةٍ جانبيّة، يجب عليك الانتباه عند استخدام المتغيّرات البدائيّة (primitives) داخل ng-repeat بأن لا تكون جميع عناصر المجموعة متفرّدة، أي أنّه لا يوجد أي تكرار لأي قيمة في المجموعة، ومن يحدّد تطابق العناصر هو عمليّة المساواة المتشدّدة === في JavaScript.

المساواة المتشددة

لنقم ببعض التّجارب لإنعاش معلوماتنا في كيفيّة عمل عمليّة المساواة الصّارمة === في JavaScript. يختبر القالب التّالي المساواة بين أعدادٍ، سلاسل نصّيّةٍ وكائنات.

<table class="table table-condensed">
  <tr>
    <td>1 === 1</td>
    <td>{{1 === 1}}</td>
  </tr>
  <tr>
    <td>'1' === '1'</td>
    <td>{{'1' === '1'}}</td>
  </tr>
  <tr>
    <td>1 === '1'</td>
    <td>{{1 === '1'}}</td>
  </tr>
  <tr>
    <td>{} === {}</td>
    <td>{{ {} === {} }}</td>
  </tr>
  <tr>
    <td>{name: 1} === {name: 1}</td>
    <td>{{ {name: 1} === {name: 1} }}</td>
  </tr>
</table>

الناتج

1 === 1	                    true
'1' === '1'	                true
1 === '1'	                false
{} === {}	                false
{name: 1} === {name: 1}	    false

بيّنّا بأنّ التّساوي بين عناصر المجموعة مرفوض، وسيبيّن المثال التّالي الخطأ الّذي سينتج عن تكرار العدد 1 في المجموعة.

<ol>
  <!-- Invalid code! Duplicate element error, because '1' is repeated! -->
  <li ng-repeat="val in [1,2,1]" ng-bind="val"></li>
</ol>

قم بتعديل المثال السّابق، واستخدم خليطًا من الأعداد والسّلاسل النّصّيّة بدلًا من الأعداد فقط، جرّب المصفوفة التّالية [1,2,’1′]. ماذا كانت النّتيجة؟ والآن حاول تغييرها باستخدام الكائنات بدلًا من الأعداد، جرّب المصفوفة التّالية [{name: 1},{name: 2},{name: 1}]. ستحتاج إلى تغيير العبارة “ng-bind=”val إلى “ng-bind=”val.name لتتمكّن من رؤية القيم.

track by

إنّ حلّ مشكلة مصفوفة الأعداد السّابقة يكمُن في إضافة تعليمة track by إلى عبارة ng-repeat وذلك لكي يتمّ تجاوز اختبار المساواة الّذي يتمّ القيام به تلقائيًّا للعناصر. يمكنك تتبّع عناصر المجموعة باستخدام أيّ متغيّر متفرّد (لا تتكرّر القيم الّتي يأخذها عند كلّ عنصر)، وإن لم يكن لديك متغيّر متفرّد (فالقيم العدديّة الموجودة داخل المصفوفة ليست كذلك)، يمكنك استخدام المتغيّر index$.

<ol>
  <li ng-repeat="val in [1,2,1] track by $index" ng-bind="val"></li>
</ol>

الناتج

1. 1
2. 2
3. 1

يجب عليك استخدام متغيّرات متفرّدة في نماذج كائناتك كلّما أمكنك ذلك، كأن تستخدم معرّفًا رقميًّا متفرّدًا يتمّ إنشاؤه عن طريق مخزن بيانات في الـbackend أو عن طريق نوعٍ ما من مولّد UUID في طرف المستخدم. إن لم يكن لديك مفرٌّ من استخدام المتغيّر index$ فكن حذرًا لأنّ التّغييرات في المجموعة قد تٌسبّب مشاكل في الأحداث الخاصة بالصفحة.

توابع الاستدعاء الخلفي

تسهّل Angular الحصول على مرجعٍ لعنصرٍ ما في المجموعة لتتمكّن من استخدامه داخل متحكّم ما. ليس عليك إلّا أن تمرّر عنصر المجموعة إلى تابع استدعاءٍ خلفيّ (callback) في التّوجيه الذي يسمح بتمرير تابع استدعاءٍ خلفيّ، مثل ng-click.

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

function ItemsController($scope) {
  $scope.items = [
    {id: 1, name: 'Item 1'},
    {id: 2, name: 'Item 2'},
    {id: 3, name: 'Item 3'},
    {id: 4, name: 'Item 4'}
  ];

  $scope.destroy = function(item) {
    var index = $scope.items.indexOf(item);
    $scope.items.splice(index, 1);
  };
}

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

كُلّ ما بقي هو إضافة “(ng-click=”destroy(item إلى عنصرٍ ما داخل الحلقة. كما يشير التّوجيه ng-click، فالزر هو الخيار الأفضل، ولكن لا بدّ من الإشارة إلى أنّه يمكننا استخدام التّوجيه ng-click مع أيّ عنصرٍ قابلٍ للنّقر.

<div ng-controller="ItemsController">
  <h4 ng-pluralize count="items.length"
      when="{'one': '1 item', 'other': '{} items'}">
  </h4>
  <table class="table table-condensed">
    <tr ng-repeat="item in items">
      <td ng-bind="item.name"></td>
      <td>
        <button class="btn btn-xs btn-default" ng-click="destroy(item)">
          destroy
        </button>
      </td>
    </tr>
  </table>
</div>

أعتبرُ طريقة Angular في ربط تابع الاستدعاء الخلفيّ destroy مثالًا واضحًا على الأسلوب التّصريحيّ الذي تتبعه Angular. كفائدةٍ إضافيّة، يعرض المثال السّابق استخدام التّوجيه ng-pluralize لكتابة نصٍّ يميّز بين المفرد والجمع بشكلٍ مشروط، حسب عدد العناصر ضمن مجموعة ما. إعداد هذا التّوجيه صعبٌ قليلًا ولكنك قد تحتاج لاستخدامه.

start- وend-

رغم أنه ليس شائعًا، إلّا أنك قد ترغب بإخراج عناصر “إخوة” لكلّ عضوٍ في المجموعة. وكمثالٍ على ذلك قائمة الوصف، أو العنصر dl، الذي يحوي زوجي العناصر dt وdd. وهنا تظهر المشكلة، فالتّوجيه ng-repeat مُصمّمٌ ليتمّ تطبيقه على عنصر HTML واحد. ولكنّ حلّ هذه المشكلة هو بتوسيع نطاق التنفيذ لهذا التّوجيه ليشمل العديد من عناصر HTML باستخدام اللاحقتين start- وend-.

<dl ng-controller="ItemsController">
  <dt ng-repeat-start="item in items">name</dt>
  <dd ng-bind="item.name"></dd>
  <dt>price</dt>
  <dd ng-repeat-end ng-bind="item.price"></dd>
</dl>

هناك العديد من التّوجيهات الأخرى التي تستخدم اللاحقتين start- وend- وليس التّوجيه ng-repeat فقط، ويجب عليك أن تتأكد من عدم انتهاء التّوجيهات المخصصة التي تقوم بإنشائها (كما ستتعلم في فصل التوجيهات اللاحق) بأيٍّ من اللاحقتين start- وend-.

خلاصة

تدعم Angular المجموعات بقوّةٍ ومرونة عن طريق التّوجيه ng-repeat، وتسمح لنا ببناء واجهة مستخدمٍ سريعة لتطبيقات CRUD. تنتهي في هذا الفصل نظرتنا العامّة للوظائف الغنيّة في تطوير الويب التي يمكننا تعلّمها دون الغوص كثيرًا في بنية Angular الدّاخليّة. ولكن من الآن فصاعدًا، سنبدأ بالغوص أعمق فأعمق في Angular الموسّعة، ولنتمكّن من القيام بذلك سيكون علينا تعلُّم كيفيّة قيام Angular بإدارة المكوّنات. وسيغطّي الفصل القادم النّظام الّذي طوّرته Angular لذلك، الوحدات.

30يوليو

( تعلم Angular ) – المجالات (scopes) في AngularJS

ما الذي تعنيه كلمة “مجال” برأيك؟ قد تبدو من اسمها بأنّها تشير إلى جزءٍ من شيفرة التّطبيق، ربّما تم عملها لنتجنّب استخدام المجال العام الذي يسبّب العديد من المشاكل. يبدو أنّ الأمر بسيط، وإنّه لمن الحكمة أن يقوم إطار العمل بتطبيق شيءٍ كهذا، ربّما ليس علينا التّفكير في أمر المجالات أكثر من ذلك، هل يمكننا الانتقال للفصل التالي؟

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

قبل أن نبدأ، لنقم بإعداد البيئة لتُلائم أمثلة هذا الفصل.

التهيئة

في فصل المبادئ، تعلّمنا كيف نقوم بإضافة التّوجيه ng-app إلى العنصر الذي نرغب بأن تقوم Angular بمعالجته.

<body ng-app="app">
  <!-- الأمثلة هنا -->
</body>

الوسيط الذي نمرره إلى التّوجيه ng-app هو اسم الوحدة التي تشكل جذر التطبيق الحالي (في المثال قمنا باستخدام الاسم app على سبيل الاصطلاح).

سنقوم بتغطية الوحدات بعمق في فصل لاحق. أما الآن فاعتبر السّطور التّالية مجرّد شيفرات اصطلاحية يمكنك نسخها دون فهم محتواها.

angular.module('app', []);
angular.module('app').config(['$controllerProvider', function($controllerProvider) {
    $controllerProvider.allowGlobals();
}]);

والآن بعد كتابة تلك الشّيفرات الخارجة عن الدّرس، يمكننا الدخول في الموضوع.

scope$

في الفصل الماضي، المتحكمات، تعلّمنا كيفيّة تحضير النّموذج عن طريق ربط العناصر (الخصائص properties) إلى المرجع scope$. لنقم بتكرار التمرين ثانيةّ.

function NameController($scope) {
    $scope.name = "First";
}

باستخدام التّوجيه ng-controller يمكننا استدعاء تابع التّحكم المكتوب أعلاه، وذلك ضمن سياق أحد عناصر المستند، وعندها ستكون أيّ بيانات قمنا بإسنادها للمجال متاحةً للاستخدام داخل العنصر.

<p ng-controller="NameController">
  {{name}}
</p>

الناتج

First

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

<div>
  <p>
    Outside the scope: {{name}}
  </p>
  <div ng-controller="NameController">
    <p>
      Inside the scope: {{name}}
    </p>
  </div>
</div>

الناتج

Outside the scope:

Inside the scope: First

وهذا المثال يوضّح ارتباط عمل المتحكّم والمجال معًا، والآن لننظر إلى الحالة المعاكسة، تطبيق Angular بمجال واحد.

rootScope$

تحاول Angular جاهدةً أن تتأكد من ابتعادنا عن المشاكل أثناء استخدام المجالات، لذا تقوم بإنشاء مجالٍ جديد لكلّ متحكّم.

يتمّ بناء المجالات وفق بنيةٍ هرميّة، ويتربّع في جذر هرم المجالات في كلّ تطبيقات Angular مجالٌ أبٌ وحيدٌ لجميع المجالات، يمكننا الوصول إلى هذا الجذر باستخدام الوسيط ذو الاسم المخصص rootScope$ داخل المتحكّم، واستخدامه بدلًا من المرجع المعتاد (والمستحسن) scope$.

function RootNameController($rootScope) {
  $rootScope.name = "First";
}

الشّيفرة واضحة وليس فيها صعوبة، وهي تعمل بشكل طبيعي.

<p ng-controller="RootNameController">
  {{name}}
</p>

الناتج

First

ولكنّ المشاكل تبدأ بالظّهور عندما نحاول الوصول إلى عنصر له الاسم نفسه من متحكّم آخر.

function SecondRootNameController($rootScope) {
  $rootScope.name = "Second";
}

هذا لا يبشّر بالخير، فالمرجع rootScope$ وحيد (singleton) داخل التّطبيق، ولا يمكن أن يكون هناك سوى عنصر واحد له الاسم name.

<p ng-controller="RootNameController">
  {{name}}
</p>
<p ng-controller="SecondRootNameController">
  {{name}}
</p>

الناتج

Second

Second

من الواضح أنّ المتحكّم SecondRootNameController قد استبدل القيمة التي أسندها RootNameController للعنصر name. حسنًا، هذه هي مشكلة المتغيرات العامة، أليس كذلك؟

العزل

تقدم Angular لنا بيئة آمنة عن طريق إنشاء مجالٍ خاصّ بكلّ متحكّم، لنقم بإعادة كتابة المتحكّمات السّابقة لتقوم بتهيئة النّموذج تهيئةً صحيحة، باستخدام المرجع scope$ بدلًا من rootScope$، وحاول مقارنة ذلك مع المثال السّابق الذي استخدمنا فيه rootScope$ لتعرف سبب إنشاء مجال لكلّ متحكّم، سنستخدم المتحكّمين التاليين:

function SecondNameController($scope) {
  $scope.name = "Second";
}
<p ng-controller="NameController">
  {{name}}
</p>
<p ng-controller="SecondNameController">
  {{name}}
</p>

الناتج

First

Second

أظهر المثال المخرجات الصحيحة، القيمة الصحيحة للعنصر name لكلا المتحكّمين.

هذا العزل هو النّتيجة التي نحصل عليها عندما لا يكون أيٌّ من المتحكّمين ابنًا للآخر، أيّ أننا لم نقم بتعريف أحد المتحكّمين داخل عنصر في المستند محتوًى داخل متحكّمٍ آخر. ما الذي سيحدث لو قمنا بجعلهم متداخلين بدلًا من ذلك؟

المجالات المتداخلة

بتعديلٍ صغيرٍ على المثال السابق، سنقوم بتحريك SecondNameController إلى عنصرٍ ابن للعنصر div الذي يقوم بتحميل المتحكّم NameController، وهذه الحالة هي ما يسمّى بالمجالات المتداخلة (nested).

<div ng-controller="NameController">
  <p>
    {{name}}
  </p>
  <p ng-controller="SecondNameController">
    {{name}}
  </p>
</div>

الناتج

First

Second

الأمور تسير على ما يُرام، ولا يزال العنصر name معزولًا في كلا المجالين، ماذا لو قمتَ بعكس ترتيب العنصرين p في المثال السابق؟ هيا قم بتجربة ذلك، يُفترض بأنك سترى أنّ العنصرين name قد انعكس ترتيبهما.

في الحقيقة، لا يعني ذلك بأنّ المجالين معزولين، فـAngular تقوم بتنظيم المتحكّمات في بنية هرميّة بالاعتماد على موضع المتحكّم في بنية المستند، والمتحكّم الابن يرث عناصر وخصائص أبيه.

يرجع السبب في عدم حدوث تغيير في قيمة العنصر name في المثال السابق، إلى خاصّيّة التظليل، حيث يقوم العنصر name في الابن بتظليل قيمة العنصر name في الأب.

لنجرّب الحصول على دليلٍ على هذا السلوك عن طريق تغيير اسم العنصر في المتحكّم الابن.

function ChildController($scope) {
  $scope.childName = "Child";
}

سنحاول إخراج قيمتي العنصرين في كلا المجالين، الأب والابن.

<div ng-controller="NameController">
  <p>
    {{name}} and {{childName}}
  </p>
  <p ng-controller="ChildController">
    {{name}} and {{childName}}
  </p>
</div>

الناتج

First and

First and Child

من الواضح أنّ المتحكّم الأب NameController لا يملك صلاحيّةً للوصول إلى عناصر المتحكّم الابن، بينما يمكن للمتحكّم الابن ChildController أن يصل إلى عناصره وعناصر أبيه.

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

<div ng-controller="NameController">
  <p>
    {{name}}
  </p>
  <p ng-controller="ChildController">
    {{name}}
  </p>
  <input type='text' ng-model='name'>
</div>

إذا حاولت تعديل قيمة العنصر name في المثال السابق، ستجد أنّه يعمل كما هو متوقّع، فقيمة العنصر name تتغيّر في كلا المجالين، ولكن لاحظ بأنّنا نقوم بالتّغيير ضمن مجال الأب فقط.

الوراثة

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

في العرض الخاصّ بالمثال، قم بالخطوات التالية بنفس الترتيب الذي سأذكره لك:أوّلًا، غيّر القيمة في خانة الإدخال العليا، ستلاحظ أنّ جميع القيم المرتبطة بالعنصر name قد تمّ تحديثها.ثانيًا، غيّر القيمة في خانة الإدخال السّفلى.

<div ng-controller="NameController">
  <p>
    name: {{name}}
    <br>
    <input type='text' ng-model='name'>
  </p>
  <p ng-controller="ChildController">
    name: {{name}}
    <br>
    <input type='text' ng-model='name'>
  </p>
</div>

هل فاجَأَتك النّتيجة؟

تستخدم Angular طريقة JavaScript في وراثة الهيكل الخارجي (prototypal inheritance) هذا جيّدٌ إن كنت متمرّسًا فيها، فليس عليك تعلّم أيّ شيءٍ جديدٍ هنا، ولكنّه سيّءٌ لمن لم يواجه هذا النّوع من الوراثة من قبل، ففهمه ليس أمرًا بديهيًّا.

تقول القاعدة: “إنّ تغيير قيمة عنصر ما في كائن في JavaScript، يؤدّي إلى إنشاء هذا العنصر في الكائن”، وهذه القاعدة البسيطة تشكّل مشكلةً أثناء التّعامل مع العناصر الموروثة التي تكون مظلّلة بالعنصر الخاص بالكائن الابن.

حدث الأمر كالتّالي: في البداية لم يكن هناك أيّ عنصر اسمه name داخل الابن، ولكن عندما قمنا بتعديل النّصّ في خانة الإدخال السّفلى قامت Angular بإسناد النّص إلى العنصر name في الابن حيث تمّ إنشاء هذا العنصر في الابن، وعند حدوث ذلك، قام هذا العنصر بتظليل العنصر name في الأب، ومن ثمّ لم يعُد بإمكاننا الوصول إليه من الابن.

كيف يمكننا التّعامل مع هذا الأمر في Angular، بحيث نتمكّن من تعديل بيانات النّموذج في مجالٍ موروث؟

لن يكون ذلك صعبًا، سيكون علينا فقط نقل العنصر name إلى داخل كائنٍ آخر.

function InfoController($scope) {
  $scope.info = {name: "First"};
}
function ChildInfoController($scope) {
  $scope.info.childName = "Child";
}
<div ng-controller="InfoController">
  <p>
    {{info.name}} and {{info.childName}}
    <br>
    <input type='text' ng-model='info.name'>
  </p>
  <p ng-controller="ChildInfoController">
    {{info.name}} and {{info.childName}}
    <br>
    <input type='text' ng-model='info.name'>
  </p>
</div>

لاحظ أن ChildInfoController يعتمد على أبيه في إنشاء الكائن info، ما الذي سيحدث لو قمنا بتعديل شيفرة المتحكّم ChildInfoController واستبدلنا جسم التابع بالعبارة:

scope.info = {childName: "Second$"};

جرّب ذلك، سترى بأننا عدنا إلى إنشاء عنصرٍ جديد في الابن، مع تأثير التظليل الذي رأيناه سابقًا.

scope.$watch

تتعامل Angular أثناء عمليات الربط ثنائيّ الاتجاه مع العناصر في المجال بالطريقة التالية: عندما تستخدم خانة الدّخل المرتبطة للقيام بأيّ تغيير، يتمّ تحديث واجهة المستخدم في كلّ مكان، وهذا يختلف عن “الخصائص المحسوبة”(computed properties) وهي البيانات المأخوذة من مجال بيانات آخر. في المثال التالي، العنصر sum هو خصيصة محسوبة.

function SumController($scope) {
  $scope.values = [1,2];
  $scope.newValue = 1;
  $scope.add = function() {
    $scope.values.push(parseInt($scope.newValue));
  };

  // Broken -- doesn't trigger UI update
  $scope.sum = $scope.values.reduce(function(a, b) {
    return a + b;
  });
}

يتم حساب قيمة العنصر sum فعليًّا عن طريق عملية بسيطة تستخدم التابع reduce في العبارة الأخيرة في المتحكّم SumController.

في القالب الخاص بهذا المثال، سنستخدم أداة الدّخل select للسّماح للمستخدم باختيار الرّقم (2،1 أو 3) لإضافته إلى نهاية المصفوفة values.(كنقطة جانبيّة: لاحظ أنّ المتحكّم يعطي قيمةً ابتدائيّة للمتغيّر newValue, ولو لم نقم بذلك لكانت Angular ستضيف الاختيار الفارغ للقائمة في العنصر select، وذلك لتجنّب القيمة العشوائيّة التي يخزّنها newValue للخيار الأوّل المولّد عن طريق التّوجيه ng-options. هذا السلوك لا علاقة له بالمجالات ولكنّ العلم به أمرٌ مفيد.)

<p ng-controller="SumController">
  <select ng-model="newValue" ng-options="n for n in [1,2,3]"></select>
  <input type="button" value="Add" ng-click="add()">
  The sum of {{values}} is {{sum}}.
</p>

عند النقر على Add ستتغيّر القيمة المعروضة لـsum، ولكن للأسف، الشّيفرة الخاصة بالمتحكّم تحتوي على أخطاء، ولن يعمل المتحكّم كما توقّعنا.

لنقم الآن بتصحيح الخطأ، وذلك بنقل العبارة التي تقوم بالحساب الحقيقي للقيمة sum إلى تابع استدعاءٍ خلفيّ (callback). وعندما سنمرر هذا التابع كوسيط إلى scope.$watch$ مع وسيط آخر يمثّل عبارة المتابعة (في هذه الحالة هو اسم العنصر الذي يتم حساب sum منه)، سيؤدّي ذلك إلى جعل sum يتمّ إعادة حسابها كلّما تغيّر values.

function SumController($scope) {
  $scope.values = [1,2];
  $scope.newValue = 1;
  $scope.add = function() {
    $scope.values.push(parseInt($scope.newValue));
  };
  $scope.$watch('values', function () {
    $scope.sum = $scope.values.reduce(function(a, b) {
      return a + b;
    });
  }, true);
}

والآن ستتغيّر قيمة العنصر sum ديناميكيًّا عند إضافة العناصر للمصفوفة.

scope.$apply

جميع التّوجيهات المدمجة في Angular والخاصة بعمليّة الربط ثنائيّ الاتجاه كاملة المزايا، ولكنّك قد تجد من فترة إلى أخرى سلوكًا تحتاج إلى إضافته. مثلًا، ماذا لو أردنا أن يقوم المستخدم بمسح الحالتين الحالة الحاليّة لخانة الدخل النّصّيّة و الحالة المرتبطة بها وذلك عندما يضغط زرّ esc؟ كيف يمكننا كتابة شيفرة التّعامل مع هذا الحدث؟

<div ng-controller="EscapeController">
  <input type="text" ng-model="message">
  is bound to
  "<strong ng-bind="message"></strong>".
  Press <code>esc</code> to clear it!
</div>

يجب علينا أوّلًا أن نصرّح عن المتغيّر ذو الاسم المخصّص element$ وتمريره كوسيط للمتحكّم، وذلك للسّماح لـAngular بحقن مرجعٍ في العنصر المرتبط بالمتحكّم.

يمكننا استخدام التابع bind لتسجيل استدعاء خلفيّ لحدث keyup الذي يختبر ضغط الزّرّ esc، وداخل تابع الاستدعاء الخلفيّ هذا، سنقوم بتحديث عنصر المجال.

قم بتجربة المثال التالي، اكتب شيئًا ثم اضغط esc.

function EscapeController($scope, $element) {
  $scope.message = '';
  $element.bind('keyup', function (event) {
    if (event.keyCode === 27) { // esc key

      // Broken -- doesn't trigger UI update
      $scope.message = '';
    }
  });
}

ليس بعد، بما أنّنا نتعامل مباشرةً (تقريبًا) مع عناصر المستند، سنحتاج إلى إخبار Angular عندما نريد أن تقوم بإعادة رسم العرض. نقوم بذلك عن طريق تغليف التّغييرات التي نقوم بها بتابع استدعاءٍ خلفيّ نمرّره إلى scope.$apply$.

function EscapeController($scope, $element) {
  $scope.message = '';
  $element.bind('keyup', function (event) {
    if (event.keyCode === 27) { // esc key

      $scope.$apply(function() {
        $scope.message = '';
      });
    }
  });
}

جرّبها الآن، بعد أن أخبرنا Angular بما نريد، سيعمل كلّ شيء كما يجب.

خلاصة

إن حاولت تطبيق مفاهيم نموذج-عرض-متحكم (Model-view-controller (MVC على Angular، ستكون المجالات لغزًا بالنّسبة لك، قد تظنّ بأنّ الأمر بسيط، وأنّ المجالات هي جزءٌ من طبقة النّموذج، ولكن في Angular لا يكون الكائن نموذجًا حتّى يكون قابلًا للوصول إليه كعنصرٍ في المجال. ولكنّ القصّة تصبح أكثر إثارةً عندما ترى طريقة ارتباط المجالات بالمستند عن طريق المتحكّمات والتّوجيهات. بوضع سؤالنا الأكاديميّ جانبًا، فإن المجالات بديهيّةٌ وسهلة الاستخدام كما بيّنت أمثلة هذا الفصل.

© Copyright 2014, All Rights Reserved For Haitham.me