DepDrop Widget DepDrop.php

Thankful to Krajee!
to get more out of us.

NOTE: This extension depends on the yiisoft/yii2-bootstrap extension. Check the composer.json for this extension's requirements and dependencies that may be updated by composer.

The DepDrop widget is a Yii 2 wrapper for the dependent-dropdown jQuery plugin by Krajee. This plugin allows multi level dependent dropdown with nested dependencies. The plugin thus enables to convert normal select inputs to a dependent input field, whose options are derived based on value selected in another input/or a group of inputs. It works both with normal select options and select with optgroups as well.

The widget also offers an alternative method of rendering the dropdowns using \kartik\widgets\Select2 widget.

This widget uses the dependent-dropdown JQuery plugin by Krajee. Refer the plugin documentation for understanding the usage of the dependent dropdown plugin. The widget supports all parameters that one would pass for any Yii Input Widget. The additional parameter settings specially available for the DepDrop widget configuration are:

  • type: int the type of dropdown list to be generated. Should be one of the following values:

    • 1 or DepDrop::TYPE_DEFAULT: render a default HTML select using \yii\helpers\Html::dropDownList.

    • 2 or DepDrop::TYPE_SELECT2: render advanced Select2 using \kartik\widgets\Select2 widget.

    If not set the type will default to 1 or DepDrop::TYPE_DEFAULT.

  • data: array the list of option data just like a standard Yii dropDownList, which can be generated using \yii\helpers\ArrayHelper::map(). However, a simple and efficient way to display a record for update, is that you set this to the last selected value and name as a single row array. This is because, the dropdown by default is disabled for select and this list will be overwritten by ajax operation based on dependent parent value. For example, if the last saved record for the dropdown field was id = 4, desc = 'Product 4', you can set the initial data for this field as:

    'data'=>[4=>'Product 4']
    
  • select2Options: array the additional widget configuration options for the Select2. This is applicable only if type is set to DepDrop::TYPE_SELECT2.

  • options: array the HTML attributes for the select element/ dropdown list.

  • pluginOptions: array the options for the depdrop.js plugin. The following options can be configured:

    • url: string The url string for the controller action that will return the Json encoded dependent dropdown data via ajax.

    • depends: array The list of dependent parent dropdown element IDs (HTML ID attributes without any leading #).

    • loading: boolean whether to show a loading progress spin indicator in the dependent select when server is processing the ajax response. Defaults to true.

    • placeholder: string whether the dependent select has a default placeholder (with an empty value), when no records are found. You can set this to a label which will be displayed as an empty value. For optgroups this will be a disabled option. If you set this to null or empty string , it will not be displayed. Defaults to Select ....

    • placeholder: string the message to display when the ajax response returned from the server is null or an empty array. Defaults to No data found.

  • pluginEvents: array the DepDrop JQuery events. You must define events in event-name=>event-function format. All events will be stacked in the sequence. Refer the plugin events documentation for details. For example:

    pluginEvents = [
        "depdrop:init"=>"function() { log('depdrop:init'); }",
        "depdrop:ready"=>"function() { log('depdrop:ready'); }",
        "depdrop:change"=>"function(event, id, value, count) { log(id); log(value); log(count); }",
        "depdrop:beforeChange"=>"function(event, id, value) { log('depdrop:beforeChange'); }",
        "depdrop:afterChange"=>"function(event, id, value) { log('depdrop:afterChange'); }",
        "depdrop:error"=>"function(event, id, value) { log('depdrop:error'); }",
    ];
    

Important

The plugin triggers an ajax call to the server to fetch the dependent dropdown list. You must return the data in a specified Json encoded format. Refer the usage examples in this page for understanding the format in which the server should return data to the ajax response.

Scenario 1

A 3-level nested dependency example.

/*
 * SCENARIO 1: A 3-level nested dependency example
 */
// THE VIEW
use kartik\widgets\DepDrop;

// Parent 
echo $form->field($model, 'cat')->dropDownList($catList, ['id'=>'cat-id']);

// Child # 1
echo $form->field($model, 'subcat')->widget(DepDrop::classname(), [
    'options'=>['id'=>'subcat-id'],
    'pluginOptions'=>[
        'depends'=>['cat-id'],
        'placeholder'=>'Select...',
        'url'=>Url::to(['/site/subcat'])
    ]
]);

// Child # 2
echo $form->field($model, 'prod')->widget(DepDrop::classname(), [
    'pluginOptions'=>[
        'depends'=>['cat-id', 'subcat-id'],
        'placeholder'=>'Select...',
        'url'=>Url::to(['/site/prod'])
    ]
]);

// THE CONTROLLER
public function actionSubcat() {
    $out = [];
    if (isset($_POST['depdrop_parents'])) {
        $parents = $_POST['depdrop_parents'];
        if ($parents != null) {
            $cat_id = $parents[0];
            $out = self::getSubCatList($cat_id); 
            // the getSubCatList function will query the database based on the
            // cat_id and return an array like below:
            // [
            //    ['id'=>'<sub-cat-id-1>', 'name'=>'<sub-cat-name1>'],
            //    ['id'=>'<sub-cat_id_2>', 'name'=>'<sub-cat-name2>']
            // ]
            echo Json::encode(['output'=>$out, 'selected'=>'']);
            return;
        }
    }
    echo Json::encode(['output'=>'', 'selected'=>'']);
}

public function actionProd() {
    $out = [];
    if (isset($_POST['depdrop_parents'])) {
        $ids = $_POST['depdrop_parents'];
        $cat_id = empty($ids[0]) ? null : $ids[0];
        $subcat_id = empty($ids[1]) ? null : $ids[1];
        if ($cat_id != null) {
           $data = self::getProdList($cat_id, $subcat_id);
            /**
             * the getProdList function will query the database based on the
             * cat_id and sub_cat_id and return an array like below:
             *  [
             *      'out'=>[
             *          ['id'=>'<prod-id-1>', 'name'=>'<prod-name1>'],
             *          ['id'=>'<prod_id_2>', 'name'=>'<prod-name2>']
             *       ],
             *       'selected'=>'<prod-id-1>'
             *  ]
             */
           
           echo Json::encode(['output'=>$data['out'], 'selected'=>$data['selected']]);
           return;
        }
    }
    echo Json::encode(['output'=>'', 'selected'=>'']);
}

Important

When you set the placeholder property, it creates an option with empty value, which is displayed as the first option in the list. For a dropdown with optgroups as seen in this example, the placeholder prompt will be disabled.

Scenario 2

A 2-level dependency with optgroups generated in dependent dropdown list. In addition, it includes preselected data which you may have in a record update scenario (initial category is set to 'Category 1' and subcategory is set to 'Tablets'). Note: the data property must be set for displaying the preselected value on initialization and must contain the key for the initial value. The subcategory is rendered using the \kartik\widgets\Select2 widget.

This example also considers an option where you pass additional params to the ajax call by setting other form input identifiers.

 

Scenario 3

Four level dropdown dependency using \kartik\widgets\Select2 widget. Custom loading progress text is set for each dependent dropdown. In addition, it includes preselected values for dropdowns. NOTE: For a nested list scenario where the last child has the initialize plugin property set to true and depends on all 3 dropdowns before. This ensures each dependent list is loaded on initial page load in the right sequence and with the right preset values.

Also you can see in this example, how after every change of a parent value, a preselected default value is possible to be set for each child.

/*
 * SCENARIO 2: A 2-level dependency with optgroups with subcategory to be 
 * rendered by `\kartik\widgets\Select2` widget.
 * In addition, it includes preselected data which you may have in a record 
 * update scenario (initial category is set to 'Category 1' and 
 * subcategory is set to 'Tablets'). Note: the `data` property must 
 * must be set for displaying the preselected value on initialization 
 * and must contain the key for the initial value.
 *
 * This example also considers an option where you pass additional `params`
 * to the ajax call by setting other form input identifiers.
 */
// THE VIEW
use kartik\widgets\DepDrop;

// Parent 
echo $form->field($model, 'cat1')->dropDownList($majorCatList, ['id'=>'cat-id']);

// Additional input fields passed as params to the child dropdown's pluginOptions
echo Html::hiddenInput('input-type-1', 'Additional value 1', ['id'=>'input-type-1']);
echo Html::hiddenInput('input-type-2', 'Additional value 2', ['id'=>'input-type-2']);

// Child # 1
echo $form->field($model, 'subcat1')->widget(DepDrop::classname(), [
    'type'=>DepDrop::TYPE_SELECT2,
    'data'=>[2 => 'Tablets'],
    'options'=>['id'=>'subcat1-id', 'placeholder'=>'Select ...'],
    'select2Options'=>['pluginOptions'=>['allowClear'=>true]],
    'pluginOptions'=>[
        'depends'=>['cat1-id'],
        'url'=>Url::to(['/site/subcat1']),
        'params'=>['input-type-1', 'input-type-2']
    ]
]);

// THE CONTROLLER
public function actionSubcat1() {
    $out = [];
    if (isset($_POST['depdrop_parents'])) {
        $parents = $_POST['depdrop_parents'];
        if ($parents != null) {
            $cat_id = $parents[0];
            $param1 = null;
            $param2 = null;
            if (!empty($_POST['depdrop_params'])) {
                $params = $_POST['depdrop_params'];
                $param1 = $params[0]; // get the value of input-type-1
                $param2 = $params[1]; // get the value of input-type-2
            }

            $out = self::getSubCatList1($cat_id, $param1, $param2); 
            // the getSubCatList1 function will query the database based on the
            // cat_id, param1, param2 and return an array like below:
            // [
            //    'group1'=>[
            //        ['id'=>'<sub-cat-id-1>', 'name'=>'<sub-cat-name1>'],
            //        ['id'=>'<sub-cat_id_2>', 'name'=>'<sub-cat-name2>']
            //    ],
            //    'group2'=>[
            //        ['id'=>'<sub-cat-id-3>', 'name'=>'<sub-cat-name3>'], 
            //        ['id'=>'<sub-cat-id-4>', 'name'=>'<sub-cat-name4>']
            //    ]            
            // ]
            
            
            $selected = self::getDefaultSubCat($cat_id);
            // the getDefaultSubCat function will query the database
            // and return the default sub cat for the cat_id
            
            echo Json::encode(['output'=>$out, 'selected'=>$selected]);
            return;
        }
    }
    echo Json::encode(['output'=>'', 'selected'=>'']);
}

/*
 * SCENARIO 3: Four level dropdown dependency using `\kartik\widgets\Select2` widget. Custom loading progress 
 * text is set for each dependent dropdown. In addition, it includes preselected values for dropdowns. 
 * 
 * NOTE: For a nested list scenario where the last child has the `initialize` plugin property set to 
 * `true` and depends on all 3 dropdowns before. This ensures each dependent list is loaded on initial page 
 * load in the right sequence and with the right preset values. Also you can see in this example, how after 
 * every change of a parent value, a preselected default value is possible to be set for each child.
 */
// THE VIEW
use kartik\widgets\DepDrop;

// Top most parent
echo $form->field($account, 'lev0')->widget(Select2::classname(), [
    'data' => ArrayHelper::map(Account::find()->where('parent IS NULL')->asArray()->all(), 'id', 'name')
]);

// Child level 1
echo $form->field($account, 'lev1')->widget(DepDrop::classname(), [
    'data'=> [6=>'Bank'],
    'options' => ['placeholder' => 'Select ...'],
    'type' => DepDrop::TYPE_SELECT2,
    'select2Options'=>['pluginOptions'=>['allowClear'=>true]],
    'pluginOptions'=>[
        'depends'=>['account-lev0'],
        'url' => Url::to(['/account/child-account']),
        'loadingText' => 'Loading child level 1 ...',
    ]
]);

// Child level 2
echo $form->field($account, 'lev2')->widget(DepDrop::classname(), [
    'data'=> [9=>'Savings'],
    'options' => ['placeholder' => 'Select ...'],
    'type' => DepDrop::TYPE_SELECT2,
    'select2Options'=>['pluginOptions'=>['allowClear'=>true]],
    'pluginOptions'=>[
        'depends'=>['account-lev0', 'account-lev1'],
        'url' => Url::to(['/account/child-account']),
        'loadingText' => 'Loading child level 2 ...',
    ]
]);

// Child level 3
echo $form->field($account, 'lev3')->widget(DepDrop::classname(), [
    'data'=> [12=>'Savings A/C 2'],
    'options' => ['placeholder' => 'Select ...'],
    'type' => DepDrop::TYPE_SELECT2,
    'select2Options'=>['pluginOptions'=>['allowClear'=>true]],
    'pluginOptions'=>[
        'depends'=>['account-lev0', 'account-lev1', 'account-lev2'],
        'url' => Url::to(['/account/child-account']),
        'loadingText' => 'Loading child level 3 ...',
        'initialize' => true
    ]
]);


// CONTROLLER
public function actionChildAccount() {
    $out = [];
    if (isset($_POST['depdrop_parents'])) {
        $id = end($_POST['depdrop_parents']);
        $list = Account::find()->andWhere(['parent'=>$id])->asArray()->all();
        $selected  = null;
        if ($id != null && count($list) > 0) {
            $selected = '';
            foreach ($list as $i => $account) {
                $out[] = ['id' => $account['id'], 'name' => $account['name']];
                if ($i == 0) {
                    $selected = $account['id'];
                }
            }
            // Shows how you can preselect a value
            echo Json::encode(['output' => $out, 'selected'=>$selected]);
            return;
        }
    }
    echo Json::encode(['output' => '', 'selected'=>'']);
}