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

الدليل الكامل لتعلم Angular JS في يوم واحد


محمد فوّاز عرابي

ما هو AngularJS؟

AngularJS هو إطار عمل JavaScript لطرف العميل يتبع بنية Model-View-Controller/Model-View-View-Model، ويعتبر اليوم مُهمًا لبناء تطبيقات ويب وحيدة الصفحة (SAP) أو حتى المواقع العادية. يُعتبر Angular JS قفزة كبيرة نحو مستقبل HTML وما يجلبه الإصدار الخامس منها (مع التطورات على صعيد JavaScript بالطبع!)، ويبعث الحياة من جديد في تعاملنا مع الويب الحديث. هذا المقال جولة شاملة في Angular JS مستخلصة من تجاربي ونصائح وممارسات تعلمتها خلال استخدامي.

angularjs-logo.thumb.png.e1c62a999f100ed

المُصطلحات

ليس عليك أن تبذل جهداً كبيراً لتعلم Angular، وأهم ما يجب معرفته هو معاني المُصطلحات وتبني نمط MVC، وMVC هو اختصار لـModel-View-Controller، أي نموذج-طريقة عرض-مُتحكِّم. فيما يلي بعض المصطلحات والواجهات البرمجية الأساسية التي تزوّدنا بها Angular.

MVC

ربما سمعت بهذا الاختصار من قبل، وهو مستخدم في لغات برمجة عديدة كوسيلة لبناء هيكل التطبيقات أو البرامج. وهاك تلخيص سريع لمعناه:

  • النموذج (Model): بنية البيانات التي تقوم عليها أجزاء التطبيقات، غالبًا ما تُمثل بصيغة JSON. يفضل أن تكون لديك معرفة مسبقة بـJSON قبل تعلم Angular، لأنها ضرورية للتواصل بين الخادوم وطريقة العرض (سنأتي على شرحها في النقطة التالية). على سبيل المثال، لنفترض أنه لدينا مجموعة من المستخدمين، يمكن تمثيل بيانات تعريفهم كما يلي:
{
"users" : [{
    "name": "أحمد",
    "id": "82047392"
  },{
    "name": "سامر",
    "id": "65198013"
  }]
}

عادة ما تُجلب هذه المعلومات من خادوم بطلب XMLHttpRequest، ويقابله في jQuery الإجراء $.ajax، وفي Angular الكائن $http. وقد تكون هذه المعلومات مكتوبة ضمن النص البرمجي أثناء تفسير الصفحة (من قاعدة بيانات أو مخزن بيانات). بعد ذلك يكون بإمكانك تعديل هذه المعلومات وإعادة إرسالها.

  •  طريقة العرض (View): وهو أمر سهل التفسير، فهي ببساطة المُخرج النهائي أو صفحة HTML التي تعرض البيانات (النموذج) على المستخدم مثلاً. باستخدام إطار عمل MVC، تُجلب البيانات من النموذج وتُعرض المعلومات المناسبة في صفحة HTML.
  • المُتحكِّم (Controller): وله من اسمه نصيب، فهو يتحكم بالأشياء، ولكن أية أشياء؟ البيانات. المُتحكمات هي الطريقة التي تصل من خلالها بين الخادوم وطريقة العرض، فتسمح بتحديث البيانات سريعًا من خلال التواصل مع كلا الخادوم والعميل.

إعداد مشروع Angular JS (الأساسيات)

يجب أولاً تهيئة الأساسيات التي يقوم عليها مشروعنا. يجب أن نبدأ بتطبيق (ng-app) الذي يُعرِّف التطبيق (وng هي بادئة تعني Angular وتسبق عادة كل مكونات Angular JS)، ثم متحكم (Controller) ليتواصل مع طريقة العرض، ثم ربط DOM ولا ننسى تضمين Angular بالطبع. إليك الأساسيات:

نص HTML مع تصريحات ng-*:

<div ng-app="myApp">
  <div ng-controller="MainCtrl">
    <!-- محتويات المتحكم -->
  </div>
</div>

وحدة Angular مع متحكم:

var myApp = angular.module('myApp', []);

myApp.controller('MainCtrl', ['$scope', function ($scope) {
  // أوامر المتحكم
}]);

قبل أن نستبق الأمور، نحتاج لإنشاء وحدة Angular (أو Angular module)، التي ستتضمن كل النص البرمجي المُتعلق بالمشروع. هناك أكثر من طريقة للتصريح عن الوحدات، إحداها سَلسَلة كل النص البرمجي معًا (لا أفضل هذه الطريقة):

angular.module('myApp', [])
       .controller('MainCtrl', ['$scope', function ($scope) {...}])
       .controller('NavCtrl', ['$scope', function ($scope) {...}])
       .controller('UserCtrl', ['$scope', function ($scope) {...}]);

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

var myApp = angular.module('myApp', []);
myApp.controller('MainCtrl', ['$scope', function ($scope) {...}]);
myApp.controller('NavCtrl', ['$scope', function ($scope) {...}]);
myApp.controller('UserCtrl', ['$scope', function ($scope) {...}]);

بهذه الطريقة أُقسّم النص البرمجي على عدة ملفات، وفي كل ملف أربط مكوّناً من المكونات مع فضاء الأسماء myApp فيصبح تلقائيًا جزءًا من تطبيقي. نعم، الأمر كما فهمت، أفضل أن أنشئ ملفًا مستقلًا لكل متحكم ومُرشِد (directive) ومعمل (factory) وأي شيء آخر (ستشكرني على هذا). فيما بعد يمكنك دمجها معًا وتقليصها لتصبح ملفًا واحدًا (مستخدمًا مُدير مهام مثل Grunt أو Gulp) فتدفعَه إلى DOM.

المُتحكّمات (Controllers)

أصبحت تعرف الآن مفهوم MVC وتعلمت طريقة إعداد مشروع جديد، فلنطّلع الآن على الكيفية التي يُطبِّق فيها Angular JS العمل بالمتحكّمات.
بناء على المثال السابق، بإمكاننا الآن أن نخطو خطوة بسيطة نحو عرض بعض البيانات ضمن طريقة العرض مستخدمين مُتحكّمًا. يستخدم Angular تركيب " handlebars" لقولبة HTML. ببساطة يعني هذا أنه بإمكان المتحكمات أن تعرض البيانات في صفحة HTML بأن تستبدل كل عبارة فيها مكتوبة ضمن الأقواس المزدوجة هكذا: {{ data }} قيمة يُعيّنها المُتحكم. في الحالة المثالية يجب أن لا تحوي صفحة HTML نصًا حقيقيًا أو قيمًا مُدرجة مسبقًا، ويجب أن تترك هذه المهمة لمتحكمات Angular. فيما يلي مثال يُبيّن كيف يمكن عرض نص أو سلسلة حروف String بسيطة ضمن الصفحة:

<div ng-app="myApp">
  <div ng-controller="MainCtrl">
    {{ text }}
  </div>
</div>

في ملف JavaScript:

var myApp = angular.module('myApp', []);

myApp.controller('MainCtrl', ['$scope', function ($scope) {

  $scope.text = 'مرحباً بمعجبي Angular!';

}]);

والناتج النهائي: رابط المثال

أهم مفهوم هنا هو مفهوم النطاق ($scope) والذي ستربطه بكل الوظائف ضمن متُحكّم مُعيّن. يُشير $scope إلى العُنصر أو المنطقة الحالية في DOM (فهو لا يُساوي this ضمن النص البرمجي) وبهذا يخلق نطاقًا يُحيط بكل البيانات والوظائف ضمن العناصر (DOM elements)، ويعزلها عن العناصر الأخرى، فيبدو وكأنه ينقل مجالات JavaScript العامة/الخاصة إلى DOM، وهذا شيء رائع.
قد يبدو مفهوم النطاق مخيفًا للوهلة الأولى، لكنه طريقك الواصل بين الخادوم (أو حتى البيانات المحلية) من جهة وDOM من الجهة الأخرى. يعطيك هذا المثال فكرة عن الطريقة التي "تُدفع" بها البيانات إلى DOM.

لنٌلقِ نظرة على بيانات حقيقية نفترض أننا جلبناها من خادوم لنعرض تفاصيل تسجيل دخول المستخدم، سنكتفي في هذه المرحلة باستخدام بيانات جاهزة، وسنتعلم كيفية جلبها من الخادوم على هيئة JSON لاحقًا.

أولاً سنكتب شفرة JavaScript:

var myApp = angular.module('myApp', []);

myApp.controller('UserCtrl', ['$scope', function ($scope) {

  // لنجعل معلومات المستخدم ضمن عنصر فرعي
  $scope.user = {};
  $scope.user.details = {
    "username": "Todd Motto",
    "id": "89101112"
  };

}]);

ثم ننتقل إلى DOM لعرض هذه البيانات

<div ng-app="myApp">
    <div ng-controller="UserCtrl">
        <p class="username">Welcome, {{ user.details.username }}</p>
        <p class="id">User ID: {{ user.details.id }}</p>
    </div>
</div>

النتيجة

من المهمّ أن تتذكر أن المتحكمات تستخدم فقط للبيانات ولإنشاء وظائف تتواصل مع الخادوم وتجلب أو ترسل بيانات JSON. لا تستخدم المتحكمات لمعالجة DOM (كأن تنقل عنصرًا ضمن الصفحة أو تخفيه أو تظهره...)، فمعالجة DOM مهمة المُرشِدات (directives)، وهي ما سنشرحه لاحقًا، المهم أن تتذكر أن موضع jQuery وغيرها من المكتبات التي تتعامل مع DOM ليس ضمن المتحكّمات.

تنويه: خلال اطلاعك على وثائق Angular الرسمية، ستلاحظ أن الأمثلة المقدمة تعتمد الأسلوب التالي لإنشاء المتحكمات:

var myApp = angular.module('myApp', []);

function MainCtrl ($scope) {
  //...
};

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

المُرشِدات (Directives)

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

إذن، ما فائدة المُرشدات؟ إنها مفيدة في عدة أمور، منها إنشاء عناصر DOM، مثل علامات التبويب (tabs) وقوائم التصفح - ففائدتها تعتمد على ما يفعله تطبيقك في الواجهة. لتسهيل الشرح، سأقول ببساطة: إن كنت استعملت ng-show وng-hide من قبل، فقد استعملت المرشدات (حتى وإن كان هذان لا يُدرجان أية عناصر DOM).
على سبيل التمرين، سنُنشئ نوعًا خاصًّا من الأزرار ونُسميه customButton، يُدرج هذا العنصر بعض العناصر الفرعية التي لا نريد كتابتها في كل مرة. تتنوع طرق التصريح عن المرشدات في DOM، وهي مبينة في النص البرمجي التالي:

<!-- 1: تصريح عن مُرشد كخاصّة (attribute) -->
<a custom-button>انقرني</a>

<!-- 2: كعنصر مخصص (custom elements) -->
<custom-button>انقرني</custom-button>

<!-- 3: كصنف (class) (للتوافق مع النسخ القديمة من IE) -->
<a class="custom-button">انقرني</a>

<!-- 4: كتعليق (comment) (ليس ملائماً لهذا التمرين) -->
<!-- directive: custom-button -->

 

أفضّل استخدام المرشدات كخواصّ (attributes)، أما العناصر المُخصصة (custom elements) فقادمة في النسخ المستقبلية من HTML باسم Web Components، يوفر Angular ما يشبهها، ولكنها قد تنطوي على بعض العيوب والعلل في المتصفحات القديمة.
الآن نعرف كيف نصرح عن المرشدات ضمن الصفحة، سننتقل إلى إنشائها ضمن JavaScript. لاحظ أنني سأربطه مع فضاء الأسماء العام myApp؛ في صيغته الأبسط يُكتب المرشد كما يلي:

myApp.directive('customButton', function () {
  return {
    link: function (scope, element, attrs) {
      // هنا اكتب التعليمات التي تعالج DOM أو تتعامل مع أحداثه
    }
  };
});

عرّفنا المرشد باستخدام الدالة .directive()، ومررنا إليها اسم المرشد 'customButton'. عندما تكتب حرفًا كبيرًا بالإنجليزية في اسم المُرشد، فإنه ينبغي استخدام اسم المرشد ضمن DOM بصيغته التي يُفصل بها باستخدام الشرطة (-) بين الحروف الكبيرة (كما في المثال السابق: استخدمنا 'customElement' في JavaScript و"custom-button" في HTML).
يُرجع المُرشد كائنًا (Object) له عدد من الخصائص. أهم ما يجب تعلّمه منها: restrict وreplace وtransclude وtemplate وtemplateUrl وأخيرًا link. لنُضف بعضها إلى شفرتنا البرمجية:

myApp.directive('customButton', function () {
  return {
    restrict: 'A',
    replace: true,
    transclude: true,
    template: '<a href="" class="myawesomebutton" ng-transclude>' +
                '<i class="icon-ok-sign"></i>' +
              '</a>',
    link: function (scope, element, attrs) {
      // هنا اكتب التعليمات التي تعالج DOM أو تتعامل مع أحداثه
    }
  };
});

النتيجة

تأكد من فحص العنصر (من الأمر Inspect element في المُتصفح) لرؤية العناصر الجديدة التي أُدخلت في الصفحة. اعلم أن الرمز لم يظهر ضمن العنصر الجديد، ببساطة لأنني لم أُضمّن Font Awesome ضمن المشروع، ولكن يمكنك فهم كيف تعمل المرشدات. لنتعرف الآن ما تعنيه كل واحدة من خصائص المرشد السابقة الذكر:
الخاصية restrict: تُقيّد هذه الخاصة كيفية استخدام المُرشد، كيف نريد أن نستخدمه؟ إن كنت تبني مشروعًا يتطلب دعم النسخ القديمة من IE، فعليك استخدامه كخاصّة (attribute) أو صنف (class). القيمة 'A' تعني حصر استخدام المرشد بالخواص (attributes) فقط. 'E' تعني Element و'C' صنف و'M' تعليق. القيمة الافتراضية هي 'EA' (أي عنصر وخاصة).
الخاصية replace: تعني استبدال HTML العنصر المصرّح عن المُرشد ضمن الصفحة بالقالب (template) الذي يُحدد في الخاصة template (مشروحة أدناه).
الخاصةtransclude: تسمح بنسخ المحتوى الأصلي للعنصر المُصرّح عن المُرشد في الصفحة ودمجه ضمن المرشد (عند التنفيذ، ستلاحظ أن العبارة "انقرني" انتقلت إلى المُرشد).
الخاصية template: قالب (كذلك المستخدم في المثال) يُدخل إلى الصفحة. يفضّل استخدام القوالب الصغيرة فقط. تُعالج القوالب وتبنى من قبل Angular مما يسمح باستخدام صيغة handlebars ضمنها.
الخاصة templateUrl: مشابهة للسابقة، ولكنها تُجلب من ملف أو من وسم <script> بدل كتابتها ضمن تعريف المُرشد. كل ما عليك هو تعيين مسار الملف الذي يحوي القالب. يكون هذا الخيار مناسبًا عندما تريد الاحتفاظ بالقوالب خارج النص البرمجي لملفات JavaScript:

 myApp.directive('customButton', function () {
     return {
       templateUrl: 'templates/customButton.html'
       // directive stuff...

   });

وضمن الملف، نكتب:

<!-- inside customButton.html -->
   <a href="" class="myawesomebutton" ng-transclude>
     <i class="icon-ok-sign"></i>
   </a>

ملاحظة: لا يخضع اسم الملف إلى أية قاعدة، وليس من الضروري أن يوافق اسمَ المُرشد.
عند استخدام الأسلوب السابق، سيحتفظ المتصفح بنسخة مُخبأة (cached) من ملف HTML، وهو أمر رائع، الخيار البديل هو استخدام قالب ضمن وسم <script> وهنا لا تُخبأ نسخة منه في المتصفح:

 <script type="text/ng-template" id="customButton.html">
  <a href="" class="myawesomebutton" ng-transclude>
    <i class="icon-ok-sign"></i>
  </a>
  </script>

هنا أخبرنا Angular بأن وسم <script> هذا هو قالب (ng-template) وأعطيناه المُعرف. سيبحث Angular عن القالب أو عن ملف html، فاستخدم ما تراه مناسبًا. شخصيًا، أفضّل إنشاء ملفات html لسهولة تنظيمها ولتحسين الأداء وإبقاء DOM نظيفًا، فقد يستخدم مشروعك مع الوقت عشرات المُرشدات، وترتيبها في ملفات مستقلة يجعل مراجعتها أسهل.

الخدمات (Services)

كثيرًا ما تثير الخدمات في Angular ارتباك المطورين، ومن خبرتي وأبحاثي، أعتقد أن الخدمات وُضعت كنمط وأسلوب للتصميم أكثر من اختلافها بالوظيفة التي تؤديها. بعد التنقيب في مصدر Angular، وجدت أنها تُعالج وتبنى باستخدام المُجمّع (compiler) ذاته، وكذلك فهي تقدم العديد من الوظائف المشابهة. أنصح باستخدام الخدمات لبناء الكائنات المُتفرِّدة (singletons)، واستخدام المعامل (Factories) لبناء وظائف أكثر تعقيدًا كالكائنات الحرفيّة (Object Literals).
فيما يلي مثال لاستخدام خدمة توجد ناتج الضرب لعددين:

myApp.service('Math', function () {
  this.multiply = function (x, y) {
    return x * y;
  };
});

يمكنك بعد هذا استخدامها ضمن مُتحكم كما يلي:

myApp.controller('MainCtrl', ['$scope', function ($scope) {
    var a = 12;
    var b = 24;

    // الناتج: 288
    var result = Math.multiply(a, b);
}]);

نعم بالطبع إيجاد ناتج الضرب سهل ولا يحتاج خدمة، لكننا نستخدمه لإيصال الفكرة فحسب.
عندما ننشئ خدمة (أو معملاً) نحتاج إلى إخبار Angular عن متطلبات هذه الخدمة، وهو ما يسمى "حقن المتطلبات Dependency Injection" - إن لم تُصرّح عن المتطلبات فلن يعمل المتحكم المعتمد على الخدمة، وسيحدث خطأ عند التجميع. ربما لاحظت الجزء function ($scope) ضمن التصريح عن المتحكم أعلاه، وهذا هو ببساطة حقن المتطلبات. ستُلاحظ أيضًا [$scope] قبل الجزء function ($scope)، وهو ما سأشرحه لاحقًا. فيما يلي طريقة استخدام حقن المتطلبات لإخبار Angular أنك تحتاج إلى الخدمة التي أنشأتها للتو:

// مرر الخدمة Math
myApp.controller('MainCtrl', ['$scope', 'Math', function ($scope, Math) {
    var a = 12;
    var b = 24;

    // يُعطي 288
    var result = Math.multiply(a, b);
}]);

المعامل (Factories)

إيضاح فكرة المعامل سهل إذا كنت قد استوعبت فكرة الخدمات، بإمكاننا إنشاء كائن حرفي (Object Literal) ضمن المعمل أو عبر طرق أكثر تعقيدًا:

function ($http) {
  return {
    get: function(url) {
      return $http.get(url);
    },
    post: function(url) {
      return $http.post(url);
    },
  };
}]);

هنا أنشأنا مُغلفات (wrappers) مخصصة لخدمة $http في Angular المسؤولة عن طلبات XHR. بعد حقن المتطلبات ضمن المتحكم يمكننا استخدام هذا المعمل بسهولة:

myApp.controller('MainCtrl', ['$scope', 'Server', function ($scope, Server) {
    var jsonGet = 'http://myserver/getURL';
    var jsonPost = 'http://myserver/postURL';
    Server.get(jsonGet);
    Server.post(jsonPost);
}]);

إذا أرت طلب التحديثات من الخادوم، بإمكانك إنشاء دالة Server.poll أو إن كنت تستخدم مقبسًا  (ٍSocket)، فربما سترغب في إنشاء دالة Server.socket وهكذا... المعامل تسمح لك بتنظيم نصك البرمجي ضمن وحدات يمكن إدراجها ضمن المتحكمات منعًا لتكرار النص البرمجي فيها والحاجة المتكررة لصيانته.

المُرشّحات

تستخدم المرشحات مع مصفوفات (arrays) من البيانات وخارج الحلقات (loops). إن احتجت للمرور على عناصر من مصفوفة بيانات والحصول على بعض منها فقط، فأنت في المكان الصحيح، يمكنك أيضًا استخدام المرشحات لتصفية ما يكتبه المستخدم ضمن حقل إدخال <input> مثلًا. هناك عدة طرق لاستخدام المُرشحات: ضمن متحكم أو دالة مُعرفة. فيما يلي الطريقة الأخيرة:

myApp.filter('reverse', function () {
    return function (input, uppercase) {
        var out = '';
        for (var i = 0; i < input.length; i++) {
            out = input.charAt(i) + out;
        }
        if (uppercase) {
            out = out.toUpperCase();
        }
        return out;
    }
});

// Controller included to supply data
myApp.controller('MainCtrl', ['$scope', function ($scope) {
    $scope.greeting = 'Todd Motto';
}]);

وفي HTML:

<div ng-app="myApp">
    <div ng-controller="MainCtrl">
        <p>No filter: {{ greeting }}</p>
        <p>Reverse: {{ greeting | reverse }}</p>
    </div>
</div>

رابط المثال

وهنا نستخدم المُرشح ضمن حلقة ng-repeat:

<ul>
  <li ng-repeat="number in myNumbers |filter:oddNumbers">{{ number }}</li>
</ul>

مثال عن مُرشح ضمن متحكم:

myApp.controller('MainCtrl', ['$scope', function ($scope) {

    $scope.numbers = [10, 25, 35, 45, 60, 80, 100];

    $scope.lowerBound = 42;

    // Does the Filters
    $scope.greaterThanNum = function (item) {
        return item > $scope.lowerBound;
    };

}]);

واستخدامه حلقة ng-repeat:

<li ng-repeat="number in numbers | filter:greaterThanNum">
  {{ number }}
</li>

رابط المثال

كان هذا القسم الأكبر مما تحتاج لمعرفته عن AngularJS وواجهاتها البرمجية، ومع أن ما تعلمناه كافٍ لبناء تطبيق Angular، ولكننا إلى الآن لم نسكتشف أغوراها بعد.

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

عندما سمعت للمرة الأولى عن ربط البيانات ثنائي الاتجاه لم أفهم ما يعنيه. باختصار يمكن القول إنه حلقة متصلة من البيانات المُزامنة: حدّث النموذج (Model) لتُحدَّث طريقة العرض (View)، أو حدّث طريقة العرض ليُحدَّث النموذج (Model). هذا يعني أن البيانات تبقى محدثة دومًا دون عناء. إن ربطت نموذج ng-model مع حقل إدخال <input> وكتبت فيه، فهذا يُنشئ (أو يُحدِّث) نموذجاً في الوقت ذاته.
فيما يلي نقوم بإنشاء حقل <input> ونربطه بنموذج نسميه myModel، يمكنني الآن استخدام صياغة handlebars لعكس هذا النموذج وما يطرأ عليه من تحديثات في طريقة العرض في الوقت ذاته:

<div ng-app="myApp">
    <div ng-controller="MainCtrl">
        <input type="text" ng-model="myModel" placeholder="Start typing..." />
        <p>My model data: {{ myModel }}</p>
    </div>
</div>

 

myApp.controller('MainCtrl', ['$scope', function ($scope) {
    // Capture the model data
    // and/or initialise it with an existing string
    $scope.myModel = '';
}]);

 

النتيجة

طلبات XHR/Ajax/$http وربط JSON

نعرف الآن كيف نرسل بيانات بسيطة ضمن المجال ($scope)، ونعرف ما يكفي عن كيفية عمل النماذج وربط البيانات ثنائي الجانب، والآن حان الوقت لمحاكاة طلبات XHR حقيقية للخادوم. ليس هذا ضروريًا لمواقع الويب العادية، لكنه مناسب جدًا لجلب البيانات في تطبيقات الويب.
عندما تطور تطبيقك على جهازك المحلي، فغالبًا ما ستستخدم لغة مثل Java أو ASP.NET أو PHP أو غيرها لتشغيل خادوم محلي. وسواء كنا نتصل بقاعدة بيانات محلية أم بخادوم بعيد كواجهة برمجية، فإننا نتبع نفس الخطوات بالضبط.

هنا يأتي دور $http، صديقك المخلص من اليوم فصاعدًا. الدالة $http هي مُغلّف wrapper تقدمه Angular للوصول إلى البيانات من الخادوم، وهو سهل الاستخدام للغاية ولا يحتاج لأية خبرة. فيما يلي مثال عن طلب GET لجلب البيانات من الخادوم. الصياغة مشابهة جدًا لصياغة jQuery، وهذا يُسهل الانتقال من الأخيرة إلى Angular:

myApp.controller('MainCtrl', ['$scope', '$http', function ($scope, $http) {
  $http({
    method: 'GET',
    url: '//localhost:9000/someUrl'
  });
}]);

يُعيد Angular إلينا أمرًا يُصطلح على تسميته الوعد (Promise) ، وهو بديل أسهل استخدامًا من الاستدعاءات الراجعة (callbacks). يمكن تركيب الوعود في سلسلة باستخدام النقطة، ويمكننا ربطها مع مستقبلات النجاح والفشل:

myApp.controller('MainCtrl', ['$scope', function ($scope) {
  $http({
    method: 'GET',
    url: '//localhost:9000/someUrl'
  })
  .success(function (data, status, headers, config) {
    // successful data retrieval
  })
  .error(function (data, status, headers, config) {
    // something went wrong :(
  });
}]);

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

علينا --لو كنا حريصين على المثالية-- أن نصمم بيانات JSON التي نريدها أولاً. دعونا الآن نُبسط الأمور، ولندع هذا الأمر ليتولاه من يفهم في أمور النهاية الخلفية (backend)، ولكن لنقل أننا تفترض أن نستقبل بيانات مثل هذه:

{
  "user": {
    "name": "Todd Motto",
    "id": "80138731"
  }
}

هذا يعني أننا سنحصل على كائن Object من الخادوم (سنسميه data، وسترى أنه يُمرر إلى مستقبلات الوعد الذي أنشأناه).
علينا الآن أن نربط هذا الكائن بالخاصة data.user، وضمنها لدينا name وid.
يمكن ببساطة الوصول إلى هذه القيم باستخدام data.user.name للحصول على "Todd Motto" مثلاً.

فيما يلي الشفرة البرمجية (اطلع على التعليقات المضمنة):

myApp.controller('UserCtrl', ['$scope', '$http', function ($scope, $http) {

  // create a user Object
  $scope.user = {};

  // Initiate a model as an empty string
  $scope.user.username = '';

  // نريد أن نرسل طلباً ونحصل على اسم المستخدم
  $http({
    method: 'GET',
    url: '//localhost:9000/someUrlForGettingUsername'
  })
  .success(function (data, status, headers, config) {
    // عند نجاح الطلب، نُسنِد الاسم إلى النموذج الذي أنشأناه
    $scope.user.username = data.user.name;
  })
  .error(function (data, status, headers, config) {
    // وقع خطأ ما! :(
  });
}]);

الآن ضمن الصفحة يمكننا ببساطة كتابة:

<div ng-controller="UserCtrl">
  <p>{{ user.username }}</p>
</div>

هذا سيعرض اسم المستخدم. لننتقل الآن إلى خطوة أبعد بفهم ربط البيانات التصريحي (Declarative data-binding) حيث تصبح الأمور أكثر إثارة.

ربط البيانات التصريحي

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

yApp.controller('EmailsCtrl', ['$scope', function ($scope) {

  // نُنشئ كائناً يدعاً `emails`
  $scope.emails = {};

  // لنفترض أننا حصلنا على هذه البيانات من الخادوم
  // هذه **مصفوفة** من **الكائنات**
  $scope.emails.messages = [{
        "from": "Steve Jobs",
        "subject": "I think I'm holding my phone wrong :/",
        "sent": "2013-10-01T08:05:59Z"
    },{
        "from": "Ellie Goulding",
        "subject": "I've got Starry Eyes, lulz",
        "sent": "2013-09-21T19:45:00Z"
    },{
        "from": "Michael Stipe",
        "subject": "Everybody hurts, sometimes.",
        "sent": "2013-09-12T11:38:30Z"
    },{
        "from": "Jeremy Clarkson",
        "subject": "Think I've found the best car... In the world",
        "sent": "2013-09-03T13:15:11Z"
    }];

}]);

علينا الآن دفعها ضمن الصفحة. هنا نستخدم الربط التصريحي لنُعلن عما سيفعله تطبيقنا: إنشاء أول جزء من عناصر HTML الحيوية. سنستخدم مُرشد ng-repeat المبني ضمن Angular، والذي سوف يمر على البيانات ويعرض الناتج دون عناء الاستدعاءات الرجعية أو تغيير الحالة، بهذه السهولة:

<ul>
  <li ng-repeat="message in emails.messages">
    <p>From: {{ message.from }}</p>
    <p>Subject: {{ message.subject }}</p>
    <p>{{ message.sent | date:'MMM d, y h:mm:ss a' }}</p>
  </li>
</ul>

النتيجة

قمت أيضًا باستخدام مُرشّح التاريخ لأبين لك كيف يمكن أن تعرض تواريخ UTC.

اطلع على المُرشدات التي توفرها Angular لتتعرف على القدرات الكاملة للربط التصريحي. بهذا نكون قد عرفنا كيف نصل البيانات بين الخادوم وطريقة العرض.

وظائف المجال (Scope functions)

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

myApp.controller('MainCtrl', ['$scope', function ($scope) {

  $scope.deleteEmail = function (index) {
    $scope.emails.messages.splice(index, 1)
  };

}]);

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

ربط الوظائف مع طريقة العرض يمر عبر المُرشدات، هذه المرة نستخدم مُرشد ng-click:

<a ng-click="deleteEmail($index)">Delete email</a>

هذا يختلف تمامًا عن مستقبلات النقر التقليدية في JavaScript، لأسباب عديدة سنشرحها لاحقًا.
لاحظ أنني أيضًا أمرر فهرس العنصر $index، إذ يعرف Angualr ما العنصر المُراد حذفه (كم يوفر هذا من العناء؟)

النتيجة

دوال DOM التصريحية

ننتقل الآن لدوال DOM، وهي أيضًا مُرشدات تؤدي وظيفة ضمن الصفحة بدونها كنا سنكتب شفرات برمجية طويلة. أحد الأمثلة المناسبة لإيضاح الفكرة هنا هو إظهار أو إخفاء قسم التنقل ضمن الصفحة باستخدام ng-show وng-click، لنرَ بساطة هذا:

<a href="" ng-click="toggle = !toggle">Toggle nav</a>
  <ul ng-show="toggle">
    <li>Link 1</li>
    <li>Link 2</li>
    <li>Link 3</li>
</ul>

هنا ندخل عالم MVVM (اختصار Model-View-View-Model)، لاحظ أننا لم نستخدم متحكمًا، وسنشرح فكرة MVVM بعد قليل.

الناتج (جرب الإظهار والإخفاء):

النتيجة

التعابير (Expressions)

من أفضل ما يقدمه Angular، يقدم بديلاً عن الحاجة لكتابة الكثير من JavaScript والنصوص المكررة.

هل قمت يومًا ما بكتابة شيء كهذا؟

elem.onclick = function (data) {
  if (data.length === 0) {
    otherElem.innerHTML = 'No data';
  } else {
    otherElem.innerHTML = 'My data';
  }
};

ربما يكون هذا استدعاءً راجعًا عن طلب GET، ونحتاج لنغير محتوى في الصفحة بناء على البيانات، يقدم Angular هذا بدون الحاجة لكتابة حرف JavaScript!

<p>{{ data.length > 0 && 'My data' || 'No data' }}</p>

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

النتيجة

طرق العرض الديناميكية والتوجيه (Routing)

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

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

myApp.config(['$routeProvider', function ($routeProvider) {

  /**
   * $routeProvider
   */
  $routeProvider
  .when('/', {
    templateUrl: 'views/main.html'
  })
  .otherwise({
    redirectTo: '/'
  });

}]);

لاحظ أنه عندما (when) يكون الرابط / فقط (الصفحة الرئيسية)، ستعرض الصفحة main.html. من المفيد تسمية طريقة العرض الأساسية main.html وليس index.html لأنه سيكون لدينا مسبقاً الصفحة index.html وهي الصفحة التي تحوي طرق العرض الديناميكية وبقية الأجزاء.
يمكن ببساطة إضافة المزيد من طرق العرض:

myApp.config(['$routeProvider', function ($routeProvider) {

  /**
   * $routeProvider
   */
  $routeProvider
  .when('/', {
    templateUrl: 'views/main.html'
  })
  .when('/emails', {
    templateUrl: 'views/emails.html'
  })
  .otherwise({
    redirectTo: '/'
  });

}]);

بإمكاننا الآن تحميل الصفحة emails.html ببساطة لتقوم بعرض قائمة الرسائل الإلكترونية. الخلاصة أننا استطعنا أن نبني تطبيقًا معقدًا للغاية بجهد ضئيل جدًا.
توفر الخدمة $routerProvider المزيد من الخيارات، لكن ما تعلمناه عنها كافٍ في البداية. هناك أيضًا أمور أخرى مثل مُعترِضات $http التي تبدأ أحداثًا خلال مسير طلب AJAX، فتتيح لنا عرض مقدار التقدم على سبيل المثال أثناء جلب البيانات.

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

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

عندما أطور تطبيقات مؤسستنا، تُدرج وسوم Java ضمن الصفحة وعندما تُعرض، تُرسل البيانات من الخادوم (لا خبرة لدي في Java لذا سأكتب فيما يلي تصريحات وهمية، يمكنك استخدام أي لغة على الخادوم إن أحببت). النصوص التالية توضح كيف يمكنك كتابة JSON ضمن الصفحة ثم تمريرها إلى المتحكم لربطها مباشرة:

<!-- ضمن index.html (في نهاية الصفحة بالطبع) -->
<script>
window.globalData = {};
globalData.emails = <javaTagHereToGenerateMessages>;
</script>

سيُنشئ وسم Java الذي اختلقته البينات بينما سيعالج Angular الرسائل فورًا. كل ما عليك هو إعطاؤه البيانات عبر المتحكم:

myApp.controller('EmailsCtrl', ['$scope', function ($scope) {

    $scope.emails = {};

    // Assign the initial data!
    $scope.emails.messages = globalData.emails;

}]);

تقليص الملفات (Minification)

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

myApp.controller('MainCtrl',
['$scope', 'Dependency', 'Service', 'Factory',
function ($scope, Dependency, Service, Factory) {

  // code

}]);

بعد التقليص:

myApp.controller('MainCtrl',
['$scope', 'Dependency', 'Service', 'Factory',
function (a,b,c,d) {

  // a = $scope
  // b = Dependency
  // c = Service
  // d = Factory

  // $scope alias usage
  a.someFunction = function () {...};

}]);

عليك أن تحافظ على ترتيب المتطلبات المحقونة في المصفوفة ['_one', '_two'] وضمن مُعاملات الدالة function(_one, _two)، وإلا فإنك ستسبب لنفسك ولفريق العمل معك مشاكل كثيرة!

الاختلافات بين MVC وMVVM

سننهي مقالتنا العملاقة الآن بشرح سريع يشمل الفروق بين MVC وMVVM:

  • MVC: التواصل يعتمد على المتحكم (Controller)، لذلك نقول Model-View-Controller
  • MVVM: يشمل ربط البيانات التصريحي الذي يتواصل، بالمفهوم التقني، مع نفسه؛ أي Model-View-View-Model. يتواصل النموذج مع طريقة العرض، وتتواصل هذه الأخيرة مع النموذج ثانية. يسمح هذا للبيانات بأن تبقى محدّثة على الجانبين دون الحاجة لفعل أي شيء. لا داعي هنا للمتحكم.
    مثال على ذلك: إنشاء حلقة ng-repeat دون أن نعتمد على بيانات يُرسلها متحكم:
<li ng-repeat="number in [1,2,3,4,5,6,7,8,9]">
  {{ number }}
</li>

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

مثال

مكونات الويب في HTML5

قد تبدو هذه الفكرة مُكررّة، ولكنني سأعيدها هنا لنتحدث عن مكونات الويب.
يسمح Angular بإنشاء عناصر (elements) مخصصة مثل: <myCustomElement></myCustomElement>

في الحقيقة هذا أشبه ما يكون بمستقبل HTML5 التي تقدم فكرة جديدة تُسمى مكونّات الويب (Web Components)، والتي تتركب من عناصر مخصصة ضمن HTML مترافقة مع نص JavaScript ديناميكي، وهذا أمر مثيرٌ للغاية - والأكثر إثارة أنه أمر ممكن اليوم باستخدام Angular. فريق Angular بعيد النظر - شكرًا لكم!

تعليقات المجال (Scope)

أعتقد أن تعليقات المجال إضافة جميلة لسياق العمل، فبدل الحاجة لكتابة التعليقات ضمن HTML بالطريقة التالية:

<!-- header -->
<header>
  Stuff.
</header>
<!-- /header -->

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

!-- scope: MainCtrl -->
<div class="content" ng-controller="MainCtrl">

</div>
<!-- /scope: MainCtrl -->

تصحيح العلل في Angular

تتوفر إضافة جميلة للغاية لمتصفح Chrome ينصح بها فريق Angular، وتسمى Batarang.

برمجة سعيدة!

ترجمة -وبتصرف- للمقال:  Ultimate guide to learning AngularJS in one day  لصاحبه Todd Motto (الذي -بطبيعة الحال- يعود عليه ضمير المُتكلم في هذا المقال).


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

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

يعني بصراحة انا مبرمج AngularJS وقمت بتطوير أكتر من مشروع 

ولكني لما دخلت على هذه المقالة ضعت صراحة لانو المصطلحات والشرح بالعربي كتير غريب 

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

رابط هذا التعليق
شارك على الشبكات الإجتماعية

بصراحة انا ذاكرت الانجولار من عدة مواقع اجنبية لانى اثق فى المحتوى الاجنبى اكثر لكنى عندما قرات هذا المقال وجدت اشياء لم اقرئها من قبل وبامانة استفدت منها كثيرا جدا جزاكم الله خير 

رابط هذا التعليق
شارك على الشبكات الإجتماعية

المقالة ما شاء الله جميلة جدا واستفدت منها كتير
فقط المصطلحات نرجوا ذكرها باللغة الانجليزية لعدم الدخول في دوامة تعريب المصطلح والتوهان اثناء قراءة المقالة (بانك تقصد ايه مثلاً بكلمة كذا)

رابط هذا التعليق
شارك على الشبكات الإجتماعية

المقال جميل و عملي وشكرا ل الكاتب والمترجم العربي مع التنبيه لكل كاتبي المقالات العربية يجب وضع اسم المصطلح بااللغة الانكليزية جانب اللفظ العربي كيما نتجنب الخلط في المفاهيم لان الناس تترجم المصطلحات بشكل مختلف 

رابط هذا التعليق
شارك على الشبكات الإجتماعية



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

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

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

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   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.


×
×
  • أضف...