إنشاء قائمتين منسدلتين (Dropdown lists) باستخدام Laravel و jQuery


محمد أحمد العيل

نسعى في هذا الدّرس إلى إنشاء قائمتين منسدلتين Dropdown lists ترتبط إحداهما بالأخرى. تحتوي الأولى مثلا على تصنيف والثانية على تصنيف فرعي من التصنيف الموجود في القائمة الأولى. يعني هذا أنْ تتغيّر التصنيفات الفرعيّة الموجودة في القائمة الثّانيّة عند تغيّر العنصُر المحدّد في القائمة الأولى.

laravel-jquery-dropdown-menu.png

تبدو النتيجة بنهاية الخطوات المشروحة في هذا الدرس على النحو التالي:

01_dependent_dropdown.png

نستخدم Laravel في الجانب الخلفيّ Backend لتولي التعامل مع الطّلبات والتخاطب مع قاعدة البيانات للحصول على التصنيفات والتّصنيفات الفرعيّة منها. يتولّى سكربت jQuery العمل في الواجهة الأماميّة Frontend لتحديث محتويات القائمة الثانيّة عند تعديل محتوى الأولى.

في ما يلي نظرة عامّة على الخطوات التي سنتّبعها:

  • إنشاء النماذج Models والتهجيرات Migrations.
  • تهيئة معمل نماذج Model factory وبذر Seed جداول البيانات.
  • إعداد المسارات Routes والمتحكّمات Controllers.
  • إعداد العروض Views.

الطريقة التي نريد تنفيذها هي كالتالي: عند الدخول إلى المسار categories/ يتلقى ملف المسارات الطّلب ويحوّله إلى الدالّة categories في المتحكّم HomeController. تستقبل الداّلة الطلب وتطلب قائمة بالتّصنيفات من نموذج التّصنيف Category؛ ثم ترسل التّصنيفات إلى العرض categories الذي يعرضها في القائمة المنسدلة الأولى. نستخدم سكربت jQuery في العرض للإنصات لتغييرات القائمة المنسدلة العلويّة، وعند اختيّار أحد عناصرها يأخذ السكربت معرّف العنصر المحدَّد ثم يرسل به طلب Ajax إلى المسار api/category-dropdown/ الذي يجيب بلائحة التصنيفات الفرعيّة للتّصنيف المحدّد في القائمة الأولى. يستقبل السكربت اللائحة ويعرضها في القائمة الثانية.

تمكنك مراجعة المقالات التاليّة لتفصيلات أكثرعن إنشاء النماذج، استخدام معمل النماذج و العروض.

نبدأ بتثبيت Laravel بالأمر التالي:
 

composer create-project --prefer-dist laravel/laravel laradropdown "5.3.*"

انتظر اكتمال التثبيت ثم انتقل لمجلد المشروع laradropdown لمتابعة بقيّة الخطوات. سنفترض في ما يلي أن لاتصال بقاعدة البيانات مضبوط.

إنشاء النماذج والتهجيرات

نبدأ بإنشاء نموذجيْن Category و SubCategory. الأوّل للتصنيفات والثاني للتصنيفات الفرعيّة. ننفّذ ما يلي في مجلّد المشروع:

php artisan make:model Category -m
php artisan make:model SubCategory -m

استخدمنا خيار m- لإنشاء التهجيرات مع إنشاء النماذج.

نفتح ملفّ التهجير الخاصّ بالتصنيف ونعدّله:

public function up()
{
  Schema::create('categories', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name')->unique();
    $table->timestamps();
  });
}

public function down()
{
  Schema::dropIfExists('categories')
}

يحوي الملفّ كما هو ظاهر أربعة حقول: معرّفا، اسما للتّصنيف وحقليْ الأختام الزمنية.

ننتقل لملفّ التهجير الخاصّ بالتصنيف الفرعي:

public function up()
{
  Schema::create('sub_categories', function (Blueprint $table) {
    $table->increments('id');
    $table->string('name')->unique();
    $table->integer('category_id')->unsigned();
    $table->timestamps();
  });
  Schema::table('sub_categories', function (Blueprint $table) {
    $table->foreign('category_id')
          ->references('id')
          ->on('categories')
          ->onDelete('cascade');
  });
}

public function down()
{
  Schema::table('sub_categories', function(Blueprint $table) {
    $table->dropForeign('sub_categories_category_id_foreign');
  });
  Schema::dropIfExists('sub_categories');
}

يختلف ملفّ التهجير هذا قليلا عن الملفّ السّابق؛ إذ يحوي إلى جانب حقول المعرّف، الاسم والأختام الزمنيّة معرفَ التصنيف الذي يتفرّع منه. هذا المعرف هو مفتاح خارجي Foreign key يحيل إلى جدول التّصنيفات. نعرّف الحقول أولا ثم نحدّد الحقل category_id على أنه مفتاح خارجي. ينبغي أن يكون الحقل مطابقا تماما من حيث النوع للحقل الذي يحيل إليه. في الدالة down نبدأ بحذف القيد من على الحقل حتى يمكننا حذف الجدول.

أنهينا إعداد التهجيرات. ننتقل لإعداد النماذج.

نغيّر ملف النموذج Category على النحو التالي:

class Category extends Model
{
protected $fillable = ['id', 'name'];

public function sub_categories()
    {
        return $this->hasMany('SubCategory');
    }
}

نحدّد أولا الحقول التي يمكن إسنادها (خاصيّة fillable) ثم نضيف الدالة sub_categories التي تعرّف العلاقة بين التّصنيف Category والتّصنيف الفرعي SubCategory. هذه العلاقة هي من النوع hasMany بمعنى أنه توجد بالتّصنيف تصنيفات فرعية تابعة له.

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

نفس الشيء تقريبا بالنسبة للنموذج SubCategory:

class SubCategory extends Model
{
protected $fillable = ['id', 'name', 'category_id'];

public function category()
    {
        return $this->belongsTo('Category');
    }
}

نعرّف في النموذج SubCategory العلاقة العكسيّة لتلك المعرّفة في النموذج Category؛ وهي belongsTo التي تعني أن هذا الصّنف يتبع للصّنف Category.

النماذج والتهجيرات جاهزة؛ يمكننا الآن تنفيذ التهجيرات: 

php artisan migrate

تهيئة معامل النماذج وبذر جداول البيانات

سنستفيد من الخبرى التي تحصّلنا عليها في درس استخدام معمل النماذج (Model factory) في Laravel لتوليد بيانات الاختبار لتهيئة معملَيْ نماذج نستخدمهما لبذر البيانات في الجدوليْن categories و sub_categories.

ننشئ ملفيْن في المجلّد /database/factories؛ واحدا باسم CategoryFactory.php والآخر SubCategoryFactory.php. ثم نضيف المحتوى التالي إلى CategoryFactory.php :

<?php
    $factory->define(App\Category::class, function (Faker\Generator $faker){
        return [
        'name' => $faker->unique()->word
        ];
});

نفس المبدأ المستخدَم في الدرس المُشار إليه أعلاه. نعرّف النموذج الذي نريد توليد بيانات له ثم نستخدم مكتبة Faker لملْء الحقل المحدَّد (name). طلبنا توليد كلمات فريدة Unique حتى نوافق الشّرط المعرَّف في تهجير قاعدة البيانات الخاصّ بالجدول categories.

الأمر مختلف قليلا مع الملف SubCategoryFactory.php الذي نعدّله كالتالي:

<?php

    $factory->define(App\SubCategory::class, function (Faker\Generator $faker){
        $categories = App\Category::get()->pluck('id')->all();
        return [
        'name' => $faker->unique()->word,
        'category_id' =>  $faker->randomElement($categories),
        ];
});

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

$categories = App\Category::get()->pluck('id')->all();

ثم عند توليد بيانات للحقل category_id نطلب منه أن يختار واحدا عشوائيا من المعرّفات التي تحصّلنا عليها سابقا:

'category_id' =>  $faker->randomElement($categories),

هذا كلّ شيء بالنسبة لمعمل النماذج. ننتقل للبذر. ننشئ صنفا سنستخدمه لبذر النموذجيْن Category و SubCategory:

php artisan make:seeder SubCategoryTableSeeder

نفتح الملف ونعدّله كما يلي:

public function run()
{
  App\SubCategory::truncate();
  factory(App\Category::class, 10)->create();
  factory(App\SubCategory::class, 50)->create();
}

نطلُب في الملفّ توليد 10 تصنيفات و50 تصنيفا فرعيا.

نعدّل ملف البذر DatabaseSeeder كما يلي:

public function run()
{
    Model::unguard();
    $this->call(SubCategoryTableSeeder::class);
    Model::reguard();
}

لا ننسى استدعاء النموذج Model في الملف DatabaseSeeder:

use Illuminate\Database\Eloquent\Model;

نستدعي ملفّ البذر SubCategoryTableSeeder الذي يستخدم معمليْ النماذج لتوليد البيانات. يمكننا الآن تنفيذ البذر:

php artisan db:seed

إعداد المسارات والمتحكمات

نريد أن نصل إلى صفحة القائمتين المنسدلتين عبر الرابط categories/؛ لذا سنعرّف مسارا له في ملف مسارات الوِب  routes/web.php:

Route::get('/categories', 'HomeController@categories');

لا خصوصيةَ هنا؛ المسار والمتحكّم والدالة اللذان يعالجان الطّلب.

إن لم يكن المتحكّم HomeController معرّفا لديك فاستخدم الأمر php artisan make:controller HomeController وأضف إليه دالة باسم categories:

public function categories() {
  $categories = Category::orderBy('name', 'asc')->get();
  return view('layouts.categories', compact('categories'));
}

لا تنس استيراد النموذج Category:

use App\Category;

نستخدم النموذج Category في الدالة للحصول على لائحة التصنيفات مرتبة تصاعديا حسب الاسم؛ ثم نرسل النتيجة إلى العرض categories الموجود في المجلّد layouts ضمن مجلّد العروض resources/views. هذا العرض غير موجود لحدّ الساعة لذا يجب أن ننشئه. لكن قبل ذلك يجب أن ننهي إعداد المتحكّمات.

سنضع في العرض سكربت jQuery -كما أشرنا سابقا- للحصول على لائحة بالتصنيفات الفرعيّة للتّصنيف المحدّد في القائمة المنسدلة العلوية. يرسل السكربت طلب Ajax إلى المسار api/category-dropdown/. ننشئ هذا المسار في ملفّ المسارات الخاصّ بواجهة التطبيقات البرمجيّة routes/api.php على النحو التالي:

Route::get('/category-dropdown', 'ApiController@categoryDropDownData');

يمكن أن نستخدم نفس المتحكّم السابق (HomeController) ونعرّف دالة فيه كما يمكن أن ننشئ متحكّما جديدا. اخترنا الحلّ الأخير حتى نفصل بين التحكّم في المسارات العادية والمسارات المعدّة لتكون واجهة برمجية Application programming interface, API.

نستخدم الأمر التالي لإنشاء متحكّم باسم ApiController

php artisan make:controller ApiController

ثم نعدّل عليه:

public function categoryDropDownData() {

    $category_id = Input::get('category_id');
    $subcategories = App\Category::find($category_id)->sub_categories;

    return Response::json($subcategories);
}

يتلقى المسار api/category-dropdown/ معرّف التّصنيف المحدّد في القائمة ضمن متغيّر category_id. نستقبل المتغيّر المُرسَل من المستخدم بالدّالة get من الصّنف Input.

بما أننا عرّفنا علاقات بين التصنيف والتصنيف الفرعي في النموذجين المقابليْن فيمكننا بسهولة العثور على التّصنيفات الفرعيّة لتصنيف؛ كلّ ما علينا فعله هو استخدام الدّالة sub_categories التي عرّفناها في الصّنف Category.

تعثُر التعليمة التالية على جميع التّصنيفات الفرعيّة للتّصنيف ذي المعرّف category_id. 

$subcategories = Category::find($category_id)->sub_categories;

في الأخير نرمّز التصنيفات الفرعيّة بصيغة Json ثم نرسلها في الإجابة.

لا تنس استدعاء الأصناف التي استخدمناها وإضافتها إلى بداية الملف:

use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Input;
use App\Category;

المحصّلة:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Input;
use App\Category;

class ApiController extends Controller
{
    public function categoryDropDownData() {

      $category_id = Input::get('category_id');
      $subcategories = Category::find($category_id)->sub_categories;

      return Response::json($subcategories);
    }
}

إعداد العروض

أكملنا الإعدادات من جهة النهاية الخلفيّة؛ تبقى فقط إعداد العرض لاستقبال البيانات وتقديمها للزائر.

ننشئ لهذا الغرض عرضا باسم categories.blade.php في المجلّد layouts. يمدّد هذا العرض عرضا رئيسا Master view اسمُه app.blade.php، يوجد في نفس المجلّد. ينفّذ categories.blade.php مقطعًا باسم content معرّفًا في العرض الرّئيس ويوجد به محتوى الصفحة.

@extends('layouts.app')
@section('content')
<div class="container">
    <div class="row">
        <div class="col-md-10 col-md-offset-1">
            <div class="panel panel-default">
                <div class="panel-heading">Dashboard</div>
                <div class="panel-body">
                <div class="form-group">
                    <label>Category:</label><br>
                    <select class="form-control input-lg" name="category_id" id="category_id">
                    <option value="">Select Category</option>
                    @foreach($categories as $category)
                        <option value="{{ $category->id }}"> {{$category->name}}</option>
                    @endforeach
                    </select>
                </div>
                <div class="form-group">
                    <label>Subcategory:</label><br>
                    <select class="form-control input-lg" name="subcategory_id" id="subcategory_id">
                    <option value="">First Select Category</option>
                    </select>
                </div>

                </div>
            </div>
        </div>
    </div>
</div>

<script>
$('#category_id').on('change', function(e){
var cat_id = e.target.value;
//ajax
$.get('/api/category-dropdown?category_id=' + cat_id, function(data){
    //success data
    $('#subcategory_id').empty();
    $('#subcategory_id').append('<option value=""> Please choose one</option>');
    $.each(data, function(index, subcatObj){
    $('#subcategory_id').append('<option value="' + subcatObj.id+'">'
    + subcatObj.name + '</option>');
    });
});
});
</script>
@endsection

يستقبل المقطع content التصنيفات في المتغيّر categories ثم يستخدم دالة foreach التكراريّة لعرضها في القائمة المنسدلة:

        @foreach($categories as $category)
            <option value="{{ $category->id }}"> {{$category->name}}</option>
        @endforeach

نضيف في المقطع script سكربت jQuery الذي سيتولى مزامنة محتوى القائمتيْن.

يأخذ الجزء الأول من السكربت العنصر category_id الذي يمثّل القائمة الأولى ويستخدم الحدث on change لمراقبة التغيّرات عليه. نحتفظ بمعرّف التصنيف في المتغيّر cat_id:

$('#category_id').on('change', function(e){
var cat_id = e.target.value;

ثم يأتي دور نداء Ajax الذي يرسل المتغيّر cat_id إلى المسار api/category-dropdown/ ضمن المعطى category_id:

$.get('/api/category-dropdown?category_id=' + cat_id, function(data)

يخزّن النداء النتيجة في المتغيّر data. عند تلقّي الطلب ننفّذ ثلاثة أمور:

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

    $('#subcategory_id').empty();

    ثم نضيف تعليمة جديدة تطلب من الزائر الاختيّار:

    $('#subcategory_id').append(' Please choose one');

    ثم في الأخير نستخدم دالة each التكرارية في jQuery للمرور على البيانات (التصنيفات الفرعيّة) الواحدة تلو الأخرى وإضافتها إلى القائمة المنسدلة الثانيّة:

    $.each(data, function(index, subcatObj){
        $('#subcategory_id').append(''
        + subcatObj.subcategory_name + '</option');
    });

     

بالنسبة للعرض الرّئيس app.blade.php فقد استخدمنا هذا الملف.

لم يتبق سوى زيارة الرابط categories/ لمعاينة النتيجة.

ترجمة -وبتصرّف- للمقال Dependent Dropdown List with jquery in Laravel 5.1 لصاحبه  Bill Keck.





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


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



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

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

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


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

تسجيل الدخول

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


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