اذهب إلى المحتوى

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


Mostafa Ata العايش

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

angularjs-collections.thumb.png.15e02645

لا تملك 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 لذلك، الوحدات.

ترجمة وبتصرّف للفصل الخامس من كتاب: Angular Basics لصاحبه: Chris Smith.


تفاعل الأعضاء

أفضل التعليقات



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...