العمل مع JavaScript في ريلز

من موسوعة حسوب

يغطي هذا الدليل وظيفة Ajax/JavaScript المضمّنة في ريلز (والمزيد)؛ وسوف يمكنك من إنشاء تطبيقات Ajax غنية وديناميكية بكل سهولة!

ستتعلم بعد قراءة هذا الدليل:

  • أساسيات Ajax.
  • جافاسكربت الواضحة (Unobtrusive JavaScript).
  • كيف يساعدك مساعدو ريلز المدمجون (built-in helpers).
  • كيفية التعامل مع Ajax من طرف الخادم.
  • الجوهرة Turbolinks.

مقدمة لفهم Ajax

لفهم أجاكس، يجب أولًا فهم ما يفعله متصفّح الويب بشكل طبيعي.

عندما تكتب http://localhost:3000 في شريط عنوان المتصفّح وتضغط على "Go" ، يقدّم المتصفح ("العميل") طلبًا للخادم، ويفرز الاستجابة ثم يجلب جميع الأصول المرتبطة، مثل ملفّات JavaScript وأوراق الأنماط والصور ثم يُجمّع الصفحة. ستحصل نفس العملية إذا نقرت على أحد الروابط: جلب الصفحة، وجلب الأصول، ووضعها معًا ثم عرض النتائج. وهذا ما يسمى "دورة استجابة الطلب" (request response cycle).

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

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

إليك الشيفرة التالية المكتوبة بلغة CoffeeScript التي تقدّم طلب Ajax باستخدام مكتبة jQuery:

$.ajax(url: "/test").done (html) ->
  $("#results").append html

تجلب هذه الشيفرة البيانات من "test/" ثم تضيف النتيجة إلى div ذي المعرِّف results.

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

جافاسكربت الواضحة

تستخدم ريلز تقنية تسمى "جافاسكربت الواضحة" (Unobtrusive JavaScript) للتعامل مع ربط جافاسكربت بـ DOM. يعتبر ذلك عادةً أفضل الممارسات داخل مجتمع الواجهات الأماميّة، ولكن قد تقرأ أحيانًا دروسًا تعرض طرقًا أخرى.

إليك أبسط طريقة لكتابة جافاسكربت. قد تراه يشار إليه باسم "جافاسكربت السطرية" (inline JavaScript):

<a href="#" onclick="this.style.backgroundColor='#990000'">Paint it red</a>

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

<a href="#" onclick="this.style.backgroundColor='#009900';this.style.color='#FFFFFF';">Paint it green</a>

غريب، أليس كذلك؟ يمكننا سحب تعريف الدالّة (function) من معالج النقر وتحويله إلى CoffeeScript:

@paintIt = (element, backgroundColor, textColor) ->
  element.style.backgroundColor = backgroundColor
  if textColor?
    element.style.color = textColor

ثم على صفحتنا:

<a href="#" onclick="paintIt(this, '#990000')">Paint it red</a>

هذا أفضل قليلًا، ولكن ماذا عن الروابط المتعددة التي لها نفس التأثير؟

<a href="#" onclick="paintIt(this, '#990000')">Paint it red</a>
<a href="#" onclick="paintIt(this, '#009900', '#FFFFFF')">Paint it green</a>
<a href="#" onclick="paintIt(this, '#000099', '#FFFFFF')">Paint it blue</a>

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

@paintIt = (element, backgroundColor, textColor) ->
  element.style.backgroundColor = backgroundColor
  if textColor?
    element.style.color = textColor
 
$ ->
  $("a[data-background-color]").click (e) ->
    e.preventDefault()
 
    backgroundColor = $(this).data("background-color")
    textColor = $(this).data("text-color")
    paintIt(this, backgroundColor, textColor)
<a href="#" data-background-color="#990000">Paint it red</a>
<a href="#" data-background-color="#009900" data-text-color="#FFFFFF">Paint it green</a>
<a href="#" data-background-color="#000099" data-text-color="#FFFFFF">Paint it blue</a>

نسمي هذا جافاسكربت "الواضحة" لأننا لم نعد نمزج JavaScript في HTML. لقد فصلنا مخاوفنا بشكل صحيح، مما يجعل التغيير المستقبلي سهلًا. يمكننا تشغيل كل شفراتنا المكتوبة بلغة JavaScript من خلال وحدة تصغير (minimizer) وتجميع (concatenator). يمكننا تقديم حزمة JavaScript بالكامل في كل صفحة، مما يعني أنها ستُسلّم على تحميل الصفحة الأولى ثم تُخزّن مؤقتًا في كل صفحة بعد ذلك. الكثير من الفوائد الصغيرة تتراكم حقًّا هناك.

يشجعك فريق ريلز بشدّة على كتابة الـ CoffeeScript (و JavaScript) في هذا النمط ويمكنك أن تتوّقع أن العديد من المكتبات سوف تتبع هذا النمط أيضًا.

مساعدون مدمجون

العناصر البعيدة

توفّر ريلز مجموعة من توابع مساعدي العرض (view helper) مكتوبة في روبي لمساعدتك في إنشاء HTML. قد ترغب أحيانًا في إضافة القليل من Ajax لتلك العناصر؛ ريلز موجود لمساعدتك في تلك الحالات.

بسبب جافاسكربت الواضحة،  "مساعدات Ajax " في ريلز هنَّ في الواقع جزءان: نصف JavaScript

ونصف روبي.

ما لم تعطّل "أنابيب الأصول"، توفّر rails-ujs نصف JavaScript، وتضيف مساعدات واجهة روبي العاديّة الوسوم المناسبة إلى DOM.

يمكنك أن تقرأ أدناه حول الأحداث المختلفة التي تُطلق للتعامل مع العناصر البعيدة داخل تطبيقك.

form_with

المساعد form_with يساعد بكتابة النماذج. يفترض form_with أن نموذجك سيستخدم Ajax افتراضيًّا. يمكنك إلغاء هذا السلوك عبر تمرير الخيار local: إلى form_with.

<%= form_with(model: @article) do |f| %>
  ...
<% end %>

سيؤدي هذا لإنشاء شيفرة HTML التالي:

<form action="/articles" accept-charset="UTF-8" method="post" data-remote="true">
  ...
</form>

لاحظ وجود الخاصية "data-remote="true. سيُرسل النموذج الآن من قبل Ajax بدلاً من آليّة الإرسال العادية للمتصفّح. لكن قد لا تريد أن تجلس بخمول مع <form> مملوءة. قد تريد أن تفعل شيئًا ما عند نجاح التسليم. لفعل هذا، اربط تابعًا بالحدث ajax:success. عند الفشل، استخدم ajax:error. ألق نظرة هنا:

$(document).ready ->
  $("#new_article").on("ajax:success", (event) ->
    [data, status, xhr] = event.detail
    $("#new_article").append xhr.responseText
  ).on "ajax:error", (event) ->
    $("#new_article").append "<p>ERROR</p>"

من البديهي أنك سترغب بشيء أكثر تطوّرًا من هذا ولكنها مجرد بداية.

اعتبارًا من الإصدار 5.1 لريلز و rails-ujs الجديدة، جُمعت المعاملات data, و status, و xhr في event.detail. لمزيد من المعلومات حول jquery-ujs المستخدمة سابقًا في الإصدار 5 من ريلز والإصدارات الأقدم، اطلع على هذه الصفحة.

link_to

المساعد link_to يساعد في توليد الروابط. يحتوي على الخيار remote: يمكنك استخدامه على النحو التالي:

<%= link_to "an article", @article, remote: true %>

الذي يولّد:

<a href="/articles/1" data-remote="true">an article</a>

يمكنك الربط بنفس أحداث Ajax كـ form_with. وهنا مثال على ذلك. فلنفترض أن لدينا قائمة بالمقالات التي يمكن حذفها بنقرة واحدة فقط. سنولّد بعض HTML بهذا الشكل:

<%= link_to "an article", @article, remote: true %>

واكتب بعض شفرات CoffeeScript مثل:

$ ->
  $("a[data-remote]").on "ajax:success", (event) ->
    alert "The article was deleted."

button_to

المساعد button_to يساعدك على إنشاء الأزرار. يحتوي على الخيار remote: الذي يمكنك استدعائه على النحو التالي:

<%= button_to "An article", @article, remote: true %>

هذا يولّد:

<form action="/articles/1" class="button_to" data-remote="true" method="post">
  <input type="submit" value="An article" />
</form>

جميع المعلومات الواردة في form_with تنطبق أيضًا نظرًا لأنه مجرد <form>.

تخصيص العناصر عن بعد

من الممكن تخصيص سلوك العناصر ذات الخاصية data-remote بدون كتابة سطر JavaScript واحد. يمكنك تحديد خاصيات إضافية من الشكل -data لتحقيق ذلك.

data-method

يؤدي تنشيط الارتباطات التشعبية (hyperlinks) دائمًا في طلب HTTP GET. لكن إن كان تطبيقك ‎،RESTful بعض الروابط هي حقيقة إجراءات تغيّر البيانات على الخادم ويجب تنفيذها بطلبات غير GET. تسمح هذه الخاصية بوضع علامات على هذه الروابط بطريقة واضحة مثل "post" أو "put" أو "delete".

ينشأ الرابط عند تنشيطه نموذجًا مخفيًّا في المستند مع الخاصية "action" المقابلة لقيمة "href" للرابط، والتابع المطابق لقيمة data-method، ثم يرسل ذلك النموذج.

نظرًا لعدم دعم إرسال نماذج باستخدام توابع HTTP عدا GET و POST على نطاق واسع عبر المتصفّحات، تُرسَل كل توابع HTTP الأخرى عبر POST باستخدام التابع المناسب المذكور في المعامل method_.  تكتشف ريلز هذا تلقائيًّا وتعوّض عنه.

data-url و data-params

لا تشير عناصر معينة من صفحتك فعليًا إلى أي عنوان URL ولكن قد ترغب أن يشغّلوا استدعاءات Ajax. سيؤدي تحديد الخاصية data-url مع الخاصية data-remote إلى اطلاق استدعاء Ajax إلى العنوان URL المحدّد. يمكنك أيضًا تحديد معاملات إضافية من خلال الخاصية data-params.

يمكن أن يفيدك هذا في بدء إجراء على مربعات اختيار مثلًا:

<input type="checkbox" data-remote="true"
    data-url="/update" data-params="id=10" data-method="put">

data-type

ومن الممكن أيضا لتحديد dataType في Ajax بشكل صريح أثناء أداء طلبات للعناصر data-remote عبر الخاصية data-type.

التأكيدات

يمكنك طلب تأكيد إضافي من المستخدم عبر إضافة الخاصية data-confirm على الروابط والنماذج. سيظهر للمستخدم مربّع حوار‏ ()‏confirm الذي يخص‏‏ JavaScript يحتوي على نص الخاصية. إن اختار المستخدم الإلغاء، فلن يقع الإجراء.

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

<%= link_to "Dangerous zone", dangerous_zone_path,

يولّد هذا:

<a href="..." data-confirm="Are you sure?">Dangerous zone</a>

تُسمَح بهذه الخاصية أيضًا في أزرار إرسال النموذج. يتيح لك ذلك تخصيص رسالة التحذير بناءً على الزر الذي فُعّل. في هذه الحالة يجب ألّا يكون لديك data-confirm في النموذج ذاته.

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

التعطيل التلقائي

من الممكن أيضًا تعطيل أحد المدخلات تلقائيًا أثناء إرسال النموذج باستخدام الخاصية data-disable-with. وذلك لمنع النقرات المزدوجة العرضيّة من المستخدم مما قد ينتج طلبات HTTP مكرّرة قد لا تكتشفها الواجهة الخلفية على هذا النحو. قيمة الخاصية هي النص الذي سيصبح القيمة الجديدة للزر في حالته المعطّلة.

ينطبق هذا أيضًا على الروابط ذات الخاصية data-method.

على سبيل المثال:

<%= form_with(model: @article.new) do |f| %>
  <%= f.submit data: { "disable-with": "Saving..." } %>
<%= end %>

يولّد هذا النموذج مع:

<input data-disable-with="Saving..." type="submit">

معالجات الحدث Rails-ujs

وفرَّت ريلز في الإصدار 5.1 المعالجات rails-ujs وأهملت jQuery كإعتماديّة (dependency). أعيدت كتابة برنامج JavaScript الواضحة (UJS) نتيجة لذلك ليعمل بدون jQuery. تؤدّي هذه التقديمات إلى تغييرات صغيرة في الأحداث المخصّصة (custom events) المطلقة أثناء الطلب:

غُيّر التوقيع على النداءات إلى معالجات الأحداث في UJS. ترد كل الأحداث المخصّصة معاملة واحدة فقط: event بخلاف الإصدار مع jQuery. في هذه المعاملة هناك الخاصية الإضافيّة detail التي تحتوي على مصفوفة من المعاملات الإضافية.

اسم الحدث المعاملات الإضافية (event.detail) يُطلَق
ajax:before قبل العمل مع ajax
ajax:beforeSend [xhr, options] قبل إرسال الطلب
ajax:send [xhr] عند إرسال الطلب
ajax:stopped عند ايقاف الطلب
ajax:success [response, status, xhr] بعد الانتهاء إن كانت الاستجابة ناجحة.
ajax:error [response, status, xhr] بعد الانتهاء إن كانت الاستجابة خطأ.
ajax:complete [xhr, status] بعد الانتهاء من الطلب بغض النظر عن النتيجة.

مثال عملي يشرح ما سبق:

document.body.addEventListener('ajax:success', function(event) {
  var detail = event.detail;
  var data = detail[0], status = detail[1], xhr = detail[2];
})

اعتبارًا من الإصدار 5.1 في ريلز و rails-ujs الجديدة، جُمعت المعاملات data، و status، و xhr في event.detail. لمزيد من المعلومات حول jquery-ujs المستخدمة سابقًا في الإصدار 5 والإصدارات الأقدم، اطلع على هذه الصفحة.

الأحداث القابلة للإيقاف

يمكنك إيقاف تنفيذ طلب Ajax بتشغيل event.preventDefault()‎ من توابع المعالجات ajax:before أو ajax:beforeSend. يستطيع الحدث ajax:before معالجة بيانات النموذج قبل التسلسل، والحدث ajax:beforeSend مفيد بإضافة ترويسات طلبات مخصصّة.

إن أوقفت حدث ajax:aborted:file، سيُلغى السلوك الافتراضي الذي يسمح للمتصفّح بإرسال الاستمارة عبر الوسائل العادية (أي إرسال غير Ajax) ولن تُقدّم الاستمارة على الإطلاق. الفائدة من هذا السلوك هي تعريف استخدام حل شخصي لتحميل ملف Ajax.

ملاحظة: عليك استخدام return false لمنع الحدث jquery-ujs و e.preventDefault()‎ لـ rails-ujs.

مخاوف من طرف الخادم

Ajax ليس فقط من طرف العميل، بل تحتاج للعمل من طرف الخادم أيضًا لدعمه. كثيرًا ما يفضّل الأشخاص أن ترد طلباتهم Ajax ملف JSON بدل HTML. فلنناقش ما يلزم لتحقيق ذلك.

مثال بسيط

تخيّل أن لديك سلسلة من المستخدمين الراغبين في عرض وتقديم نموذج بنفس الصفحة لإنشاء مستخدم جديد. يشبه إجراء (action) فهرس وحدة تحكّمك ما يلي:

class UsersController < ApplicationController
  def index
    @users = User.all
    @user = User.new
  end
  # ...

يحتوي عرض الفهرس (app/views/users/index.html.erb) على:

<b>Users</b>
 
<ul id="users">
<%= render @users %>
</ul>
 
<br>
 
<%= form_with(model: @user) do |f| %>
  <%= f.label :name %><br>
  <%= f.text_field :name %>
  <%= f.submit %>
<% end %>

يحتوي app/views/users/_user.html.erb الجزئي على ما يلي:

<li><%= user.name %></li>

يعرض الجزء العلوي من صفحة الفهرس المستخدمين. يوفّر الجزء السفلي نموذجًا لإنشاء مستخدم جديد. سيستدعي النموذج السفلي الإجراء create على UsersController. بما أن خيار النموذج البعيد مضبوط إلى "true"، سيُنشر الطلب إلى UsersController كطلب Ajax يبحث عن JavaScript. سيبدو إجراء وحدة التحكم create كما يلي لتلبية هذا الطلب:

# app/controllers/users_controller.rb
# ......
def create
  @user = User.new(params[:user])
 
  respond_to do |format|
    if @user.save
      format.html { redirect_to @user, notice: 'User was successfully created.' }
      format.js
      format.json { render json: @user, status: :created, location: @user }
    else
      format.html { render action: "new" }
      format.json { render json: @user.errors, status: :unprocessable_entity }
    end
  end
end

لاحظ format.js في البنية respond_to block: يسمح هذا لوحدة التحكّم بالرد على طلبك Ajax. يصبح لديك عندئذٍ ملف واجهة app/views/users/create.js.erb يُولّد شفرة JavaScript فعليّة ستُرسل وتُنفّذ على جانب العميل.

$("<%= escape_javascript(render @user) %>").appendTo("#users");

المكتبة Turbolinks

تشحن ريلز مع المكتبة Turbolinks التي تستخدم Ajax لتسريع تقديم الصفحة في معظم التطبيقات.

كيف تعمل Turbolinks

ترفق Turbolinks معالج نقر بجميع الوسوم <a> على الصفحة. ستطلب Turbolinks طلب Ajax من الصفحة ثم يحلّل الاستجابة ويستبدل <body> الصفحة مع <body> للاستجابة إن دعم متصفّحك PushState. سيستخدم بعدها PushState لتغيير عنوان URL إلى العنوان الصحيح مع الحفاظ على دلالات التحديث (refresh semantics) وإعطائك عناوين URL جميلة.

كل ما عليك فعله لتفعيل Turbolinks هو وجوده في ملفك Gemfile ووضع ‎//= require turbolinks في بيانك JavaScript والذي عادة ما يكون في app/assets/javascripts/application.js.

إن أردت تعطيل Turbolinks لروابط معينة أضف الخاصية "data-turbolinks="false إلى الوسم:

<a href="..." data-turbolinks="false">No turbolinks here</a>.

أحداث تغيير الصفحة

ستحتاج عند كتابة CoffeeScript لإجراء نوع من المعالجة عند تحميل الصفحة. ستكتب شيئًا مشابهًا لما يلي مع jQuery:

$(document).ready ->
  alert "page has loaded!"

ولكن لن يشتغل الحدث الذي تعتمد عليه لأن Turbolinks تستبدل (overrides) عملية تحميل الصفحة العادية. عليك تغيير شيفرتك إن شابهت المثال أعلاه لما يلي تقريبًا:

$(document).on "turbolinks:load", ->
  alert "page has loaded!"

لمزيد من التفاصيل بما في ذلك الأحداث الأخرى التي يمكنك ربطها، اطلع على Turbolinks README.

مصادر أخرى

إليك بعض الروابط المفيدة لمساعدتك على معرفة المزيد:

مصادر