Yii2 - 在表单中使用bootstrap选项卡,其他选项卡中的必填字段卡在表单保存中

I have a form in Yii2 and I have organized this using bootstrap Tabs.

The problem I am facing is like in the active tab, all fields are filled and some field in other tab are empty which is required. When I click on save, it just stuck only to find out by navigating to other tabs that some required fields are not filled. This is awkward. How I can show some message on top that some required fields are not filled.

Here is my form code.

<?php
use yii\bootstrap\ActiveForm;
use yii\helpers\Html;
use yii\helpers\ArrayHelper;
use app\models\State;
use kartik\depdrop\DepDrop;
use wbraganca\dynamicform\DynamicFormWidget;
/* @var $this yii\web\View */
/* @var $model app\models\User */
/* @var $form yii\widgets\ActiveForm */
?>

<?php 

$this->registerJsFile("@web/views/site/js/jquery.geocomplete.js",[
    'depends' => [
        \yii\web\JqueryAsset::className()
    ]
]);

$catList=ArrayHelper::map(app\models\State::find()->all(), 'id', 'state_name' );  

?>
<div class="row">
    <div class="col-md-10 col-lg-offset-1">

        <?php $form = ActiveForm::begin(['id' => 'dynamic-form', 'layout' => 'horizontal', 'enableClientValidation' => true, 'enableAjaxValidation' => true]);?>
  <div class="panel panel-default">
      <div class="panel-body nav-tabs-animate nav-tabs-horizontal">
      <ul class="nav nav-tabs nav-tabs-line" data-plugin="nav-tabs" role="tablist">
          <li class="active" role="presentation"><a data-toggle="tab" href="#profile" aria-controls="profile" role="tab">
          <?=Yii::t('app', 'Profile');?>
        </a></li>

        <li role="presentation"><a data-toggle="tab" href="#user_kids" aria-controls="user_kids" role="tab">
          <?=Yii::t('app', 'User Kids');?>
        </a></li>
        <li role="presentation" class="pull-right">
          <div class="form-group">
           <?=Html::submitButton('Save', ['class' => 'btn btn-primary'])?>
          </div>
        </li>
      </ul>
           <div class="tab-content"> <br clear="all">
               <div class="tab-pane active animation-slide-left" id="profile" role="tabpanel">
        <?php // $form = ActiveForm::begin(['id' => 'my_form', 'layout' => 'horizontal']);?>                   
        <div class="form-group form-material col-lg-6">
            <?=$form->field($model, 'first_name')->textInput(['placeholder' => 'First Name'])?>
        </div>
        <div class="form-group form-material col-lg-6">
            <?=$form->field($model, 'last_name')->textInput(['placeholder' => 'Last Name'])?>
        </div>        
        <div class="form-group form-material col-lg-6">
            <?=$form->field($model, 'phone')->textInput(['placeholder' => 'Mobile'])?>
        </div>
        <div class="form-group form-material col-lg-6">
            <?=$form->field($model, 'email')->input('email', ['placeholder' => 'Email', 'readonly' => true])?>
        </div>        
        <div class="form-group form-material col-lg-6">
            <?= $form->field($profile, 'street1')->input('street1', ['placeholder' => 'Address Street1'])?>
        </div>
        <div class="form-group form-material col-lg-6">
            <?= $form->field($profile, 'street2')->input('street1', ['placeholder' => 'Address Street2'])?>
        </div>
        <div class="form-group form-material col-lg-6">
            <?= $form->field($profile, 'state')->dropDownList($catList, ['id'=>'state_name']) ?>

        </div>

        <div class="form-group form-material col-lg-6">
           <?=  $form->field($profile, 'city')->input('city', ['placeholder' => 'City'])
           /*
           echo $form->field($profile, 'city')->widget(DepDrop::classname(), [
            'data'=>ArrayHelper::map(\app\models\City::find()->all(), 'id', 'city_name' ),

            'pluginOptions'=>[
            'depends'=>['state_name'], // the id for cat attribute
            'placeholder'=>'Select...',
            'url'=>  \yii\helpers\Url::to(['subcat'])
            ]
            ]);
          */
              ?>
        </div>
        <div class="form-group form-material col-lg-6">
            <?= $form->field($profile, 'zipcode')->input('zipcode', ['placeholder' => 'Zip code'])?>
        </div>        
        <div class="form-group form-material col-lg-12" style="text-align: center"><h3>Billing Address</h3></div>

        <div class="form-group form-material col-lg-6">
            <?= $form->field($billinginfo, 'street1')->textInput(['maxlength' => true, 'data-geo' => 'street1', 'class' => 'form-control geocomplete'])?>
        </div>
         <div class="form-group form-material col-lg-6">
            <?= $form->field($billinginfo, 'street2')->textInput(['maxlength' => true, 'data-geo' => 'street2', 'class' => 'form-control geocomplete'])?>
        </div> 
        <div class="form-group form-material col-lg-6">
            <?=$form->field($billinginfo, 'city')->textInput(['maxlength' => true, 'data-geo' => "administrative_area_level_2", 'class' => 'form-control geocomplete'])?>
        </div>        
         <div class="form-group form-material col-lg-6">
            <?=$form->field($billinginfo, 'state')->textInput(['maxlength' => true, 'data-geo' => "administrative_area_level_1", 'class' => 'form-control geocomplete'])?>
        </div>        
         <div class="form-group form-material col-lg-6">
            <?= $form->field($billinginfo, 'zipcode')->textInput(['maxlength' => true, 'data-geo' => 'postal_code', 'class' => 'form-control geocomplete'])?>
        </div>     
            </div>

<?php
if (!$model->isNewRecord) {
    ?>
        <div class="tab-pane animation-slide-left" id="user_kids" role="tabpanel">
          <?php DynamicFormWidget::begin([
        'widgetContainer' => 'dynamicform_wrapper', // required: only alphanumeric characters plus "_" [A-Za-z0-9_]
        'widgetBody' => '.container-items', // required: css class selector
        'widgetItem' => '.item', // required: css class
        'limit' => 4, // the maximum times, an element can be cloned (default 999)
        'min' => 1, // 0 or 1 (default 1)
        'insertButton' => '.add-item', // css class
        'deleteButton' => '.remove-item', // css class
        'model' => $modelsKids[0],
        'formId' => 'dynamic-form',
        'formFields' => [
            'id',
            'child_name',
            'child_birth_date',
                        'child_gender',
        ],
    ]);?>

 <div class="container-items"><!-- widgetContainer -->
          <?php foreach ($modelsKids as $i => $modelsKid): ?>
          <div class="item panel panel-success" ><!-- widgetBody -->
          <div class="panel-heading">
            <h3 class="panel-title pull-left">Kids:  <?=($i + 1)?></h3>
            <div class="pull-right">
              <button type="button" class="add-item btn btn-sm btn-icon btn-success btn-round waves-effect waves-light waves-round" style="margin-left:-20px;margin-top:5px;"><i class="glyphicon glyphicon-plus"></i></button>
              <button type="button" class="remove-item btn btn-sm btn-icon btn-danger btn-round waves-effect waves-light waves-round" style="margin-right:10px;;margin-top:5px;"><i class="glyphicon glyphicon-minus"></i></button>
            </div>
            <div class="clearfix"></div>
          </div>
          <div class="panel-body">

        <?php
// necessary for update action.
    if (!$modelsKid->isNewRecord) {
        echo Html::activeHiddenInput($modelsKid, "[{$i}]id");
    }
    ?>
            <div>
              <div class="row userkids" id="userkids<?=$i?>">
                <div class="col-md-6">
                  <div class="form-group form-material">
                    <?php
if ($modelsKid->isNewRecord) {
        $modelsKid->child_name = '';
        $modelsKid->child_birth_date = '';
                $modelsKid->child_gender = '';

    }?>
                    <?=$form->field($modelsKid, "[{$i}]child_name")->input('text')?>

                  </div>
                  <br>
                  <div class="form-material end_time">
                    <?=$form->field($modelsKid, "[{$i}]child_birth_date")->input('date')?>
                  </div>
                   <?=$form->field($modelsKid, "[{$i}]child_gender")->dropDownList(['Male' =>'Male','Female'=>'Female'])?>

                </div>
              </div>
            </div>
          </div>
        </div>
        <?php endforeach;?>
      </div>

      <?php DynamicFormWidget::end();?>

                </div>
                <!-- end user_kids tab -->
    <?php
}
?>
<?php ActiveForm::end();?>      
    </div>
</div>

You need to fix this before you move ahead towards the solution. Move the <?php ActiveForm::end (); ?> after the <div class="panel panel-default"> closing tag as currently it is wrong.

Now, You need to do the following to accomplish this

  • Make the Submit button global.
  • Control the navigation of the tabs with the submit button of the form

To make the submit button global means move it out of the third tab, so that it shows up with both of the tabs, you can place it next to the <div class="tab-content"> div or inside it on the top. In short just place it somewhere we can keep clicking and moving forward to the next tab.

Now the second thing is to navigate between the tabs. First, we will add some attributes to the anchors inside the nav-tabs with name data-next.

Your html will look like the following

<ul class="nav nav-tabs nav-tabs-line" data-plugin="nav-tabs" role="tablist">
    <li class="active" role="presentation">
        <a data-toggle="tab" href="#profile" data-next="#user_kids" aria-controls="profile" role="tab">
            <?= Yii::t ( 'app' , 'Profile' ); ?>
        </a>
    </li>
    <li role="presentation">
        <a data-toggle="tab" href="#user_kids" data-next="#submit" aria-controls="user_kids" role="tab">
            <?= Yii::t ( 'app' , 'User Kids' ); ?>
        </a>
    </li>
</ul>

Navigation

We will use the afterValidate event of the ActiveForm which is raised as soon as we hit the submit button and the fields are validated so that the fields that are required have the class .has-error added to it.

Detecting tabs

Here we add the next tabs id in the data-next attribute ,and for the last tab we will simply provide the value submit and check in our script if the data-next for the current tab has the value submit then submit the form otherwise move to the next tab.

Restrict User if Validation Complete

We will get the current tab and the next tab and then check if the fields inside the active tab has the class .has-error

Add the following javascript on top of your view file

$this->registerJs ( '
   function shakeModal(tabpane){
      $(""+tabpane+"").addClass("shake");
       setTimeout( function(){ $(""+tabpane+"").removeClass("shake"); }, 1000 ); 
   }

    $("#dynamic-form").on("afterValidate",function(e){

        var currentTabID  =   $("#dynamic-form .nav-tabs").find("li.active>a").attr("href");
        var nextTab     =   $("#dynamic-form .nav-tabs").find("li.active>a").data("next");

        if($(currentTabId).find("div.has-error").length){
            e.preventDefault();
            shakeModal(currentTabID);
            return false;
        }else{
            $(nextTab).tab("show");
            $("#dynamic-form .nav-tabs li").removeClass("active");
            $("#dynamic-form .nav-tabs li > a[href="\'+nextTab+\'"]").parent().addClass("active").tab("show");
            $(".tab-content").children().each(function(){
                $(this).find("div.has-error").removeClass("has-error");
                $(this).find("div.has-success").removeClass("has-success");
            });
        }

        if(nextTab=="#submit"){
            return true;
        }else{
            e.preventDefault();
            return false;
        }
    });
    ' , yii\web\View::POS_END );

The above is required to do the basic thing i have added a shake effect function shakeModal(tabpane) which requires some Css to be added and make a nice effect, if you do not want to use it you can comment out the call shakeModal(currentTabID); inside the if block.

Css for shaking the tab, you can add it in a separate file with name shake.css and include in your page

/*  Shake animation  */
@charset "UTF-8";

.animated {
  -webkit-animation-duration: 1s;
       -moz-animation-duration: 1s;
         -o-animation-duration: 1s;
            animation-duration: 1s;
    -webkit-animation-fill-mode: both;
       -moz-animation-fill-mode: both;
         -o-animation-fill-mode: both;
            animation-fill-mode: both;
}

.animated.hinges {
    -webkit-animation-duration: 2s;
       -moz-animation-duration: 2s;
         -o-animation-duration: 2s;
            animation-duration: 2s;
}

.animated.slow {
    -webkit-animation-duration: 3s;
       -moz-animation-duration: 3s;
         -o-animation-duration: 3s;
            animation-duration: 3s;
}

.animated.snail {
    -webkit-animation-duration: 4s;
       -moz-animation-duration: 4s;
         -o-animation-duration: 4s;
            animation-duration: 4s;
}

@-webkit-keyframes shake {
    0%, 100% {-webkit-transform: translateX(0);}
    10%, 30%, 50%, 70%, 90% {-webkit-transform: translateX(-10px);}
    20%, 40%, 60%, 80% {-webkit-transform: translateX(10px);}
}

@-moz-keyframes shake {
    0%, 100% {-moz-transform: translateX(0);}
    10%, 30%, 50%, 70%, 90% {-moz-transform: translateX(-10px);}
    20%, 40%, 60%, 80% {-moz-transform: translateX(10px);}
}

@-o-keyframes shake {
    0%, 100% {-o-transform: translateX(0);}
    10%, 30%, 50%, 70%, 90% {-o-transform: translateX(-10px);}
    20%, 40%, 60%, 80% {-o-transform: translateX(10px);}
}

@keyframes shake {
    0%, 100% {transform: translateX(0);}
    10%, 30%, 50%, 70%, 90% {transform: translateX(-10px);}
    20%, 40%, 60%, 80% {transform: translateX(10px);}
}

.shake {
    -webkit-animation-name: shake;
    -moz-animation-name: shake;
    -o-animation-name: shake;
    animation-name: shake;
}

You may face some errors as i did not tested it with your structure but hope it will work