أساسيات angularjs مجالات التوجيه (Directive Scopes) في AngularJS


مصطفى عطا العايش

directives-scopes-angularjs.thumb.png.d1

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

والآن بعد أن اعتدنا على التّوجيهات المخصّصة، فقد حان الوقت لنتعلّم كيفيّة إدارة حالة التّوجيه (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. 

ترجمة -وبتصرّف- للفصل الحادي عشر (Directive Scopes) من كتاب: Angular Basics لصاحبه: Chris Smith.





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


لا توجد أيّة تعليقات بعد



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن