Skip to content

Tabular Steps: Working with 3rd Party Widgets

Coackroach edited this page Jan 11, 2020 · 2 revisions

Working with 3rd Party Widgets

You would have noticed that when using any widget with the tabular steps when adding a new row does not enable or initialize the plugin to the input again like for instance using Select2 it isnt clickable after the row is cloned and you need to reinitialize it.

The approach used to cater with all such plugins is that tabularEvents are introduced for the fields inside the tabular steps, which can have any of the following events defined as name=>callback

'tabularEvents'=>[
    "beforeClone"=>"function(event){}",
    "afterClone"=>"function(event){}",
    "afterInsert"=>"function(event,params){}"
]

Yes, beforeClone and afterClone might sound a bit different if you have used other Yii2 plugins like dynamic-forms as they have afterInsert and beforeInsert. The reason is that it won't be possible to control it correctly inside the plugin as there could be any widget a user wants to use and going to keep adding the code for every other widget isn't what I would vote for. So a better approach would be to provide the event triggers for the specific actions which require a pre or post-processing of an element, where according to the needs the user can adjust his code.

Consider the most commonly used Select2 plugin, when cloned the extra HTML that is generated by the plugin is also copied along, and then when the new cloned select is re-initialized it shows either 2 selects with one disabled or shows garbled display. So it is recommended that before you clone the actual/source element you should call .destroy() on the element, which will remove all the extra HTML of the plugin and leave behind the default elements so after cloning you need to attach it again back to the original element before moving forward to the new select element and applying the select2 on it.

With that said beforeClone and afterClone are basically for processing the source element and not the newly generated element, and both of these events will always use the first row as the source to clone and add a new row. using $(this) inside the callback will refer to the element in the first row.

Similarly the afterInsert will have the newly generated row object and the rowIndex inside the params json passed as the second parameter of the function params.rowIndex. Using $(this) inside the callback will refer to the new insert row.

Usage with kartik\Select2 widget

So using the Address book example if I have an Address model

+---------+--------------+------+-----+---------+----------------+
| Field   | Type         | Null | Key | Default | Extra          |
+---------+--------------+------+-----+---------+----------------+
| id      | int(11)      | NO   | PRI | NULL    | auto_increment |
| address | varchar(255) | NO   |     | NULL    |                |
| city    | int(11)      | NO   | MUL | NULL    |                |
| country | varchar(255) | NO   | MUL | NULL    |                |
+---------+--------------+------+-----+---------+----------------+

and i want to use it inside the tabular step with the city field to be populated with select2 showing all the city names, so i can use the following code to initialize the tabular step for the formwizard.

echo FormWizard::widget([
    'steps' => [
        [
            'model' => [new app\models\Address()],
            'title' => 'Address Book',
            'type' => FormWizard::STEP_TYPE_TABULAR,
            'description' => 'Add addresses',
            'formInfoText' => 'Address Book',
            'fieldConfig' => [
                'city' => [
                    'widget' => Select2::class,
                    'containerOptions' => [
                        'class' => 'form-group'
                    ],
                    'options' => [
                        'data' => ArrayHelper::map(City::find()->limit(50)->all(), 'id', 'name'), //the list can be from the database
                        'options' => [
                            'class' => 'form-control'
                        ],

                        'theme' => Select2::THEME_BOOTSTRAP,
                        'pluginOptions' => [
                            'allowClear' => true,
                            'placeholder' => 'Select City'
                        ]
                    ],
                    'tabularEvents' => [
                        'beforeClone' => "function(event, params){
                                let element = $(this);
                                element.select2('destroy');
                            }",
                        "afterClone" => "function(event, params){
                                let element = $(this);
                                let elementId = element.attr('id');
                                let dataKrajee = eval(element.data('krajee-select2'));
                                let dataSelect2Options = element.data('s2-options');
                                $.when(element.select2(dataKrajee)).done(initS2Loading(elementId, dataSelect2Options));
                            }",
                        "afterInsert" => "function(event,params){
                                let selectElement = $(this).find('.field-address-'+params.rowIndex+'-city > select');
                                let dataKrajee = eval(selectElement.data('krajee-select2'));

                                //update the dataset attribute to the
                                if (typeof selectElement[0].dataset.select2Id !== 'undefined') {
    
                                    //get the old dataset which has the old id of the input the select2 is bind to 
                                    let oldDataSelect2Id = selectElement[0].dataset.select2Id;
    
                                    //delete the old dataset
                                    delete selectElement[0].dataset.select2Id;
    
                                    //add the new dataselect pointing to the new id of the cloned element
                                    let newDataSelect2Id = oldDataSelect2Id.replace(
                                          /\-([\d]+)\-/,
                                          '-' + parseInt(params.rowIndex) + '-'
                                    );

                                    //add the new dataset with the new cloned input id 
                                    selectElement[0].dataset.select2Id= newDataSelect2Id;
                                }
                                selectElement.select2(dataKrajee);
                            }"
                    ]
                ]
            ]
        ],
    ]
]);

Usage with yii\jui\DatePicker widget

Similarly if you are using \yii\jui\DatePicker for a date field created_on you don't need to use the beforeClone and afterClone as Datepicker does not require any such pre or post processing on the source element, it requires to remove the hasDatepicker class from the element before initializing datepicker on a newly created element, see the code below

'created_at' => [
    'containerOptions' => [
        'class' => 'form-group'
    ],
    'widget' => DatePicker::class,
    'labelOptions' => [
        'label' => false
    ],
    'options' => [
        'options' => [
            'class' => 'form-control'
        ]
    ],
    'tabularEvents' => [
        'afterInsert' => "function(event,params){
            let element=$(this).find('.field-address-'+params.rowIndex+'-created_at > .hasDatepicker');
            element.removeClass(\"hasDatepicker\");
            element.datepicker({\"dateFormat\":\"M d, yy\"});
        }"
    ]
],
Clone this wiki locally