├── .gitignore ├── DualListbox.php ├── DualListboxAsset.php ├── LICENSE ├── README.md ├── composer.json └── sample-code.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ -------------------------------------------------------------------------------- /DualListbox.php: -------------------------------------------------------------------------------- 1 | 13 | * @since 1.0 14 | */ 15 | class DualListbox extends InputWidget 16 | { 17 | /** 18 | * @var array listbox items 19 | */ 20 | public $items = []; 21 | 22 | /** 23 | * @var string|array selected items 24 | */ 25 | public $selection; 26 | 27 | /** 28 | * @var array listbox options 29 | */ 30 | public $options = []; 31 | 32 | /** 33 | * @var array dual listbox options 34 | */ 35 | public $clientOptions = []; 36 | 37 | /** 38 | * @inheritdoc 39 | */ 40 | public function run() 41 | { 42 | $this->registerClientScript(); 43 | 44 | Html::addCssClass($this->options, 'form-control'); 45 | $this->options['multiple'] = true; 46 | 47 | if ($this->hasModel()) { 48 | return Html::activeListBox($this->model, $this->attribute, $this->items, $this->options); 49 | } else { 50 | return Html::listBox($this->name, $this->selection, $this->items, $this->options); 51 | } 52 | } 53 | 54 | /** 55 | * Registers the required JavaScript. 56 | */ 57 | public function registerClientScript() 58 | { 59 | $view = $this->getView(); 60 | DualListboxAsset::register($view); 61 | 62 | $id = (array_key_exists('id', $this->options)) ? $this->options['id'] : Html::getInputId($this->model, $this->attribute); 63 | $options = empty($this->clientOptions) ? '' : Json::encode($this->clientOptions); 64 | $view->registerJs("jQuery('#$id').bootstrapDualListbox($options);"); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /DualListboxAsset.php: -------------------------------------------------------------------------------- 1 | 11 | * @since 1.0 12 | */ 13 | class DualListboxAsset extends AssetBundle 14 | { 15 | /** 16 | * @inheritdoc 17 | */ 18 | public $sourcePath = '@vendor/istvan-ujjmeszaros/bootstrap-duallistbox/dist'; 19 | 20 | /** 21 | * @inheritdoc 22 | */ 23 | public $depends = [ 24 | 'yii\web\JqueryAsset', 25 | ]; 26 | 27 | /** 28 | * @inheritdoc 29 | */ 30 | public function init() 31 | { 32 | if (YII_DEBUG) { 33 | $this->js = [ 34 | 'jquery.bootstrap-duallistbox.js', 35 | ]; 36 | $this->css = [ 37 | 'bootstrap-duallistbox.css', 38 | ]; 39 | } else { 40 | $this->js = [ 41 | 'jquery.bootstrap-duallistbox.min.js', 42 | ]; 43 | $this->css = [ 44 | 'bootstrap-duallistbox.min.css', 45 | ]; 46 | } 47 | parent::init(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Nobuo Kihara @softark.net 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | * Neither the name of softark.net nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | yii2-dual-listbox 2 | ================= 3 | 4 | Dual Listboxt for Yii framework 2.0. 5 | 6 | Description 7 | ----------- 8 | 9 | **softark\duallistbox\DualListbox** widget is a Yii 2 wrapper for [Bootstrap Dual Listbox](https://github.com/istvan-ujjmeszaros/bootstrap-duallistbox). 10 | 11 | It works with bootstrap 3, 4, or 5 12 | 13 | Requirements 14 | ------------ 15 | + Yii Version 2.0.0 or later 16 | + yii2-bootstrap, yii2-bootstrap4 or yii2-bootstrap5 17 | + istvan-ujjmeszaros/bootstrap-duallistbox v.3.0.x or v.4.0.x 18 | 19 | Usage 20 | ----- 21 | 1. Add `softark/yii2-dual-listbox` and `istvan-ujjmeszaros/bootstrap-duallistbox` in your project's `composer.json`, and let Composer configure your project. 22 | 23 | + You have to use a different version of `istvan-ujjmeszaros/bootstrap-duallistbox` depending on the bootstrap version. 24 | + For bootstrap 3, use `~3.0.0` : 25 | ```php 26 | "require": { 27 | "php": ">=7.0.0", 28 | "yiisoft/yii2": "*", 29 | "yiisoft/yii2-bootstrap": "*", 30 | "istvan-ujjmeszaros/bootstrap-duallistbox": "~3.0.0", 31 | "softark/yii2-dual-listbox": "dev-master" 32 | }, 33 | ``` 34 | * For bootstrap 4 and 5, use `~4.0.0`: 35 | ```php 36 | "require": { 37 | "php": ">=7.0.0", 38 | "yiisoft/yii2": "*", 39 | "yiisoft/yii2-bootstrap4": "*", // OR 40 | "yiisoft/yii2-bootstrap5": "*", 41 | "istvan-ujjmeszaros/bootstrap-duallistbox": "~4.0.0", 42 | "softark/yii2-dual-listbox": "dev-master" 43 | }, 44 | ``` 45 | 46 | 4. Use `softark\duallistbox\DualListbox::widget()` in place of `yii\helpers\Html::listBox()`, `yii\helpers\Html::activeListBox()`, or `yii\widgets\ActiveField::listBox()` in your view. 47 | 48 | 1. Replacing **Html::listBox()** using **name and selection** 49 | 50 | ```php 51 | use softark\duallistbox\DualListbox; 52 | ... 53 | true, 56 | 'size' => 20, 57 | ]; 58 | // echo Html::listBox($name, $selection, $items, $options); 59 | echo DualListbox::widget([ 60 | 'name' => $name, 61 | 'selection' => $selection, 62 | 'items' => $items, 63 | 'options' => $options, 64 | 'clientOptions' => [ 65 | 'moveOnSelect' => false, 66 | 'selectedListLabel' => 'Selected Items', 67 | 'nonSelectedListLabel' => 'Available Items', 68 | ], 69 | ]); 70 | ?> 71 | ``` 72 | 73 | 2. Replacing **Html::activeListBox()** using **model and attribute** 74 | 75 | ```php 76 | use softark\duallistbox\DualListbox; 77 | ... 78 | true, 81 | 'size' => 20, 82 | ]; 83 | // echo Html::activeListBox($model, $attribute, $items, $options); 84 | echo DualListbox::widget([ 85 | 'model' => $model, 86 | 'attribute' => $attribute, 87 | 'items' => $items, 88 | 'options' => $options, 89 | 'clientOptions' => [ 90 | 'moveOnSelect' => false, 91 | 'selectedListLabel' => 'Selected Items', 92 | 'nonSelectedListLabel' => 'Available Items', 93 | ], 94 | ]); 95 | ?> 96 | ``` 97 | 98 | 3. Replacing **ActiveField::listBox()** using **model and attribute** 99 | 100 | ```php 101 | use softark\duallistbox\DualListbox; 102 | ... 103 | true, 106 | 'size' => 20, 107 | ]; 108 | // echo $form->field($model, $attribute)->listBox($items, $options); 109 | echo $form->field($model, $attribute)->widget(DualListbox::className(),[ 110 | 'items' => $items, 111 | 'options' => $options, 112 | 'clientOptions' => [ 113 | 'moveOnSelect' => false, 114 | 'selectedListLabel' => 'Selected Items', 115 | 'nonSelectedListLabel' => 'Available Items', 116 | ], 117 | ]); 118 | ?> 119 | ``` 120 | 121 | 3. Collect the user input in the server side, just as you do with a single Listbox with multiple selection. Note that the input value will be an array. 122 | 123 | If you find difficulty in handling the user input, please read [A Sample Code](sample-code.md) which demonstrates how to use a dual listbox to the data in array format. 124 | 125 | 126 | Properties of softark\duallistbox\DualListbox 127 | --------------------------------------------- 128 | 129 | 1. **name** @var string 130 | 131 | The input name. 132 | 133 | 2. **selection** @var array 134 | 135 | The selected values. 136 | 137 | 3. **model** @var yii\base\Model 138 | 139 | The model object. 140 | 141 | 4. **attribute** @var string 142 | 143 | The attribute name. 144 | 145 | 5. **items** @var array 146 | 147 | The option data items. The array keys are option values, and the array values are the corresponding option labels. 148 | 149 | 6. **options** @var array 150 | 151 | The tag options for the listbox in terms of name-value pairs. 152 | 153 | 7. **clientOptions** @var array 154 | 155 | The options for the Bootstrap Dual Listbox in terms of name-value pairs. 156 | See [Initialzation parameters object](https://github.com/istvan-ujjmeszaros/bootstrap-duallistbox/blob/master/README.md#initialization-parameters-object) section of the official documentation of [Bootstrap Dual Listbox](https://github.com/istvan-ujjmeszaros/bootstrap-duallistbox) for details. 157 | 158 | The first 6 properties correspond to the parameters used in `Html::listBox()`, `Html::activeListBox()` and `ActiveField::listBox()`. 159 | 160 | Note that you have to use either **name-selection** pair or **model-attribute** pair. The former is for replacing `Html::listBox()` and the latter is for `Html::activeListBox()` and `ActiveField::listBox()`. 161 | 162 | Notice 163 | ------ 164 | 165 | For some reason, [Bootstrap Dual Listbox](https://github.com/istvan-ujjmeszaros/bootstrap-duallistbox) 166 | **doesn't work in mobile device browsers**, and so you can not 167 | use this widget for them. 168 | 169 | Consider using checkbox list instead. 170 | 171 | History 172 | ------- 173 | 174 | + Version 1.0.0 (2016-01-12) 175 | + Tested on Yii 2.0.6 176 | + Version 1.0.1 (2020-09-18) 177 | + Supports both bootstrap3 and bootstrap4 178 | + Version 1.0.2 (2022-09-08) 179 | + Supports also bootstrap5 180 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "softark/yii2-dual-listbox", 3 | "description": "Bootstrap Dual Listbox Widget for Yii 2", 4 | "type": "yii2-extension", 5 | "keywords": ["yii2", "yii 2", "dual-listbox", "duallistbox", "widget"], 6 | "license": "BSD-3-Clause", 7 | "authors": [ 8 | { 9 | "name": "Nobuo Kihara", 10 | "email": "softark@gmail.com", 11 | "homepage": "https://softark.net" 12 | } 13 | ], 14 | "minimum-stability": "dev", 15 | "support": { 16 | "issues": "https://github.com/softark/yii2-dual-listbox/issues?state=open", 17 | "source": "https://github.com/softark/yii2-dual-listbox" 18 | }, 19 | "require": { 20 | "yiisoft/yii2": "~2.0", 21 | "istvan-ujjmeszaros/bootstrap-duallistbox": "^3.0|^4.0" 22 | }, 23 | "suggests": { 24 | "yiisoft/yii2-bootstrap": "^2.0", 25 | "yiisoft/yii2-bootstrap4": "^2.0", 26 | "yiisoft/yii2-bootstrap5": "*" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "softark\\duallistbox\\": "" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /sample-code.md: -------------------------------------------------------------------------------- 1 | A Sample Code 2 | ============= 3 | 4 | The following is the code that constructs a page that gathers a user's favorite foods. 5 | 6 | Here are 4 models involved in this page. 7 | 8 | 1. User extends ActiveRecord 9 | 10 | - id 11 | - name 12 | - ... etc. 13 | 14 | 2. Food extends ActiveRecord 15 | 16 | - id 17 | - name 18 | - ... etc. 19 | 20 | 3. FavoriteFood extends ActiveRecord 21 | 22 | - user_id 23 | - food_id 24 | 25 | 4. UserFovorites extends Model 26 | 27 | - user_id 28 | - food_ids 29 | 30 | Note that the last one is **not** an ActiveRecord, and **'food_ids' attribute is an array** of food ids. We use this model for the form of the page. 31 | 32 | Model 33 | ----- 34 | 35 | Please take note that **`EachValidator`** is used to validate the array of `food_ids` attribute. 36 | 37 | ```php 38 | class UserFavorites extends Model 39 | { 40 | /** 41 | * @var integer 42 | */ 43 | $user_id; 44 | 45 | /** 46 | * @var array IDs of the favorite foods 47 | */ 48 | $food_ids = []; 49 | 50 | /** 51 | * @return array the validation rules. 52 | */ 53 | public function rules() 54 | { 55 | return [ 56 | ['user_id', 'required'], 57 | ['user_id', 'exist', 'targetClass' => User::className(), 'targetAttribute' => 'id'], 58 | // each food_id must exist in food table 59 | ['food_ids', 'each', 'rule' => [ 60 | 'exist', 'targetClass' => Food::className(), 'targetAttribute' => 'id' 61 | ]], 62 | ]; 63 | } 64 | 65 | /** 66 | * @return array customized attribute labels 67 | */ 68 | public function attributeLabels() 69 | { 70 | return [ 71 | 'user_id' => 'User', 72 | 'food_ids' => 'Favorite Foods', 73 | ]; 74 | } 75 | 76 | /** 77 | * load the user's favorite foods 78 | */ 79 | public function loadFavorites() 80 | { 81 | $this->food_ids = []; 82 | $favfoods = FavoriteFood::find()->where(['user_id' => $this->user_id])->all(); 83 | foreach($favfoods as $ff) { 84 | $this->food_ids[] = $ff->food_id; 85 | } 86 | } 87 | 88 | /** 89 | * save the user's favorite foods 90 | */ 91 | public function saveFavorites() 92 | { 93 | FavoriteFood::deleteAll(['user_id' => $this->user_id]); 94 | if (is_array($this->food_ids)) { 95 | foreach($this->food_ids as $food_id) { 96 | $ff = new FavoriteFood(); 97 | $ff->user_id = $this->user_id; 98 | $ff->food_id = $food_id; 99 | $ff->save(); 100 | } 101 | } 102 | /* Be careful, $this->food_ids can be empty */ 103 | } 104 | 105 | /** 106 | * @return array available foods 107 | */ 108 | public static function getAvailableFoods() 109 | { 110 | $foods = Food::find()->order('name')->asArray()->all(); 111 | $items = ArrayHelper::map($foods, 'id', 'name'); 112 | return $items; 113 | } 114 | } 115 | ``` 116 | 117 | Controller 118 | ---------- 119 | 120 | ```php 121 | public function actionFavoriteFood($user_id) 122 | { 123 | $model = new UserFavorites(); 124 | $model->user_id = $user_id; 125 | 126 | if ($model->load(Yii::$app->request->post()) { 127 | if ($model->validate()) { 128 | $model->saveFavorites(); 129 | return $this->redirect(['index']); 130 | } 131 | } 132 | $model->loadFavorites(); 133 | $items = UserFavorites::getAvailableFoods(); 134 | return $this->render('favorite', [ 135 | 'model' => $model, 136 | 'items' => $items, 137 | ]); 138 | } 139 | ``` 140 | 141 | View 142 | ---- 143 | 144 | ```php 145 | 'favorite-form', 147 | 'enableAjaxValidation' => false, 148 | ]); ?> 149 | 150 | 151 | 152 | field($model, 'food_ids')->widget(DualListbox::className(),[ 153 | 'items' => $items, 154 | 'options' => [ 155 | 'multiple' => true, 156 | 'size' => 15, 157 | ], 158 | 'clientOptions' => [ 159 | 'nonSelectedListLabel' => 'Available Foods', 160 | 'selectedListLabel' => 'Favorite Foods', 161 | 'moveOnSelect' => false, 162 | ], 163 | ]) 164 | ->hint('Select the favorite foods.'); 165 | ?> 166 | 167 |
168 | 'btn btn-primary' 170 | ]) ?> 171 |
172 | 173 | 174 | ``` 175 | --------------------------------------------------------------------------------