Working on the User Interface

In this article by Fabrizio Caldarelli, author of the book Yii By Example, will cover the following topics related to the user interface in this article:

  • Customizing JavaScript and CSS
  • Using AJAX
  • Using the Bootstrap widget
  • Viewing multiple models in the same view
  • Saving linked models in the same view

It is now time for you to learn what Yii2 supports in order to customize the JavaScript and CSS parts of web pages. A recurrent use of JavaScript is to handle AJAX calls, that is, to manage widgets and compound controls (such as a dependent drop-down list) from jQuery and Bootstrap.

Finally, we will employ jQuery to dynamically create more models from the same class in the form, which will be passed to the controller in order to be validated and saved.

(For more resources related to this topic, see here.)

Customize JavaScript and CSS

Using JavaScript and CSS is fundamental to customize frontend output.

Differently from Yii1, where calling JavaScript and CSS scripts and files was done using the Yii::app() singleton, in the new framework version, Yii2, this task is part of the yii\web\View class.

There are two ways to call JavaScript or CSS: either directly passing the code to be executed or passing the path file.

The registerJs() function allows us to execute the JavaScript code with three parameters:

  • The first parameter is the JavaScript code block to be registered
  • The second parameter is the position where the JavaScript tag should be inserted (the header, the beginning of the body section, the end of the body section, enclosed within the jQuery load() method, or enclosed within the jQuery document.ready() method, which is the default)
  • The third and last parameter is a key that identifies the JavaScript code block (if it is not provided, the content of the first parameter will be used as the key)

On the other hand, the registerJsFile() function allows us to execute a JavaScript file with three parameters:

  • The first parameter is the path file of the JavaScript file
  • The second parameter is the HTML attribute for the script tag, with particular attention given to the depends and position values, which are not treated as tag attributes
  • The third parameter is a key that identifies the JavaScript code block (if it's not provided, the content of the first parameter will be used as the key)

CSS, similar to JavaScript, can be executed using the code or by passing the path file.

Generally, JavaScript or CSS files are published in the basic/web folder, which is accessible without restrictions.

So, when we have to use custom JavaScript or CSS files, it is recommended to put them in a subfolder of the basic/web folder, which can be named as css or js.

In some circumstances, we might be required to add a new CSS or JavaScript file for all web application pages. The most appropriate place to put these entries is AppAsset.php, a file located in basic/assets/AppAsset.php. In it, we can add CSS and JavaScript entries required in web applications, even using dependencies if we need to.

Using AJAX

Yii2 provides appropriate attributes for some widgets to make AJAX calls; sometimes, however, writing a JavaScript code in these attributes will make code hard to read, especially if we are dealing with complex codes.

Consequently, to make an AJAX call, we will use external JavaScript code executed by registerJs().

This is a template of the AJAX class using the GET or POST method:

<?php
$this->registerJs( <<< EOT_JS
     
     // using GET method
$.get({
  url: url,
  data: data,
  success: success,
  dataType: dataType
});

     // using POST method
$.post({
  url: url,
  data: data,
  success: success,
  dataType: dataType
});

EOT_JS
);
?>

An AJAX call is usually the effect of a user interface event (such as a click on a button, a link, and so on). So, it is directly connected to jQuery .on() event on element. For this reason, it is important to remember how Yii2 renders the name and id attributes of input fields.

When we call Html::activeTextInput($model, $attribute) or in the same way use <?= $form->field($model, $attribute)->textInput() ?>.

The name and id attributes of the input text field will be rendered as follows:

  • id : The model class name separated with a dash by the attribute name in lowercase; for example, if the model class name is Room and the attribute is floor, the id attribute will be room-floor
  • name: The model class name that encloses the attribute name, for example, if the model class name is Reservation and the attribute is price_per_day, the name attribute will be Reservation[price_per_day]; so every field owned by the Reservation model will be enclosed all in a single array

In this example, there are two drop-down lists and a detail box. The two drop-down lists refer to customers and reservations; when user clicks on a customer list item, the second drop-down list of reservations will be filled out according to their choice.

Finally, when a user clicks on a reservation list item, a details box will be filled out with data about the selected reservation.

In an action named actionDetailDependentDropdown():

 

public function actionDetailDependentDropdown()
    {
        $showDetail = false;
        
        $model = new Reservation();
        
        if(isset($_POST['Reservation']))
        {
          $model->load( Yii::$app->request->post() );
        
          if(isset($_POST['Reservation']['id'])&&
          ($_POST['Reservation']['id']!=null))
            {
               $model = 
               Reservation::findOne($_POST['Reservation']['id']);
               $showDetail = true;
            }
        }
        
        
        return $this->render('detailDependentDropdown', [ 'model' 
        => $model, 'showDetail' => $showDetail ]);
    }

In this action, we will get the customer_id and id parameters from a form based on the Reservation model data and if it are filled out, the data will be used to search for the correct reservation model to be passed to the view.

There is a flag called $showDetail that displays the reservation details content if the id attribute of the model is received.

In Controller, there is also an action that will be called using AJAX when the user changes the customer selection in the drop-down list:

 

public function actionAjaxDropDownListByCustomerId($customer_id)
    {
        $output = '';
        
        $items = Reservation::findAll(['customer_id' => 
        $customer_id]);
        foreach($items as $item)
        {
            $content = sprintf('reservation #%s at %s', $item->id, 
            date('Y-m-d H:i:s', strtotime($item-
            >reservation_date)));
            $output .= \yii\helpers\Html::tag('option', $content, 
            ['value' => $item->id]);
        }
        
        return $output;
    }

This action will return the <option> HTML tags filled out with reservations data filtered by the customer ID passed as a parameter.

If the customer drop-down list is changed, the detail div will be hidden, an AJAX call will get all the reservations filtered by customer_id, and the result will be passed as content to the reservations drop-down list. If the reservations drop-down list is changed, a form will be submitted.

Next in the form declaration, we can find the first of all the customer drop-down list and then the reservations list, which use a closure to get the value from the ArrayHelper::map() methods. We could add a new property in the Reservation model by creating a function starting with the prefix get, such as getDescription(), and put in it the content of the closure, or rather:

public function getDescription()
{
$content = sprintf('reservation #%s at %s', $this>id, date('Y-m-d 
H:i:s', strtotime($this>reservation_date)));
            return $content;
}

Or we could use a short syntax to get data from ArrayHelper::map() in this way:

<?= $form->field($model, 'id')->dropDownList(ArrayHelper::map( 
    $reservations, 'id', 'description'), [ 'prompt' => '--- 
    choose' ]); ?>

Finally, if $showDetail is flagged, a simple details box with only the price per day of the reservation will be displayed.

Using the Bootstrap widget

Yii2 supports Bootstrap as a core feature. Bootstrap framework CSS and JavaScript files are injected by default in all pages, and we could use this feature even to only apply CSS classes or call our own JavaScript function provided by Bootstrap.

However, Yii2 embeds Bootstrap as a widget, and we can access this framework's capabilities like any other widget.

The most used are:

Class name

Description

yii\bootstrap\Alert

This class renders an alert Bootstrap component

yii\bootstrap\Button

This class renders a Bootstrap button

yii\bootstrap\Dropdown

This class renders a Bootstrap drop-down menu component

yii\bootstrap\Nav

This class renders a nav HTML component

yii\bootstrap\NavBar

This class renders a navbar HTML component

For example, yii\bootstrap\Nav and yii\bootstrap\NavBar are used in the default main template.

 

<?php
            NavBar::begin([
                'brandLabel' => 'My Company',
                'brandUrl' => Yii::$app->homeUrl,
                'options' => [
                    'class' => 'navbar-inverse navbar-fixed-top',
                ],
            ]);
            echo Nav::widget([
                'options' => ['class' => 'navbar-nav navbar-
                right'],
                'items' => [
                    ['label' => 'Home', 'url' => ['/site/index']],
                    ['label' => 'About', 'url' => 
                    ['/site/about']],
                    ['label' => 'Contact', 'url' => 
                    ['/site/contact']],
                    Yii::$app->user->isGuest ?
                        ['label' => 'Login', 'url' => 
                        ['/site/login']] :
                        ['label' => 'Logout (' . Yii::$app->user-
                        >identity->username . ')',
                            'url' => ['/site/logout'],
                            'linkOptions' => ['data-method' => 
                            'post']],
                ],
            ]);
            NavBar::end();
        ?>

Yii2 also supports, by itself, many jQuery UI widgets through the JUI extension for Yii 2, yii2-jui.

If we do not have the yii2-jui extension in the vendor folder, we can get it from Composer using this command:

php composer.phar require --prefer-dist yiisoft/yii2-jui

In this example, we will discuss the two most used widgets: datepicker and autocomplete. Let's have a look at the datepicker widget. This widget can be initialized using a model attribute or by filling out a value property. The following is an example made using a model instance and one of its attributes:

echo DatePicker::widget([
    'model' => $model,
    'attribute' => 'from_date',
    //'language' => 'it',
    //'dateFormat' => 'yyyy-MM-dd',
]);

And, here is a sample of the value property's use:

echo DatePicker::widget([
    'name'  => 'from_date',
    'value'  => $value,
    //'language' => 'it',
    //'dateFormat' => 'yyyy-MM-dd',
]);

When data is sent via POST, the date_from and date_to fields will be converted from the d/m/y to the y-m-d format to make it possible for the database to save data. Then, the model object is updated through the save() method. Using the Bootstrap widget, an alert box will be displayed in the view after updating the model.

Create the datePicker view:

<?php

use yii\helpers\Html;
use yii\widgets\ActiveForm;
use yii\jui\DatePicker;

?>

<div class="row">
    <div class="col-lg-6">
        <h3>Date Picker from Value<br />(using MM/dd/yyyy format 
        and English language)</h3>
        <?php
            $value = date('Y-m-d');
            
        echo DatePicker::widget([
            'name'  => 'from_date',
            'value'  => $value,
            'language' => 'en',
            'dateFormat' => 'MM/dd/yyyy',
        ]);
        ?>
    </div>
    <div class="col-lg-6">
        
        <?php if($reservationUpdated) { ?>
            <?php
            echo yii\bootstrap\Alert::widget([
                'options' => [
                    'class' => 'alert-success',
                ],
                'body' => 'Reservation successfully updated',
            ]);   
            ?>         
        <?php } ?>
        
        <?php $form = ActiveForm::begin(); ?>
        
        <h3>Date Picker from Model<br />(using dd/MM/yyyy format 
        and italian language)</h3>
        
        <br />
        
        <label>Date from</label>
        <?php
        // First implementation of DatePicker Widget
        echo DatePicker::widget([
            'model'  => $reservation,
            'attribute' => 'date_from',
            'language' => 'it',
            'dateFormat' => 'dd/MM/yyyy',
        ]);
        ?>
   
        <br />
        <br />
        
        <?php
        // Second implementation of DatePicker Widget
        echo $form->field($reservation, 'date_to')-
        >widget(\yii\jui\DatePicker::classname(), [
                'language' => 'it',
                'dateFormat' => 'dd/MM/yyyy',
        ]) ?>        
        
        <?php     
            echo Html::submitButton('Send', ['class' => 'btn btn-
            primary'])
        ?>

        <?php $form = ActiveForm::end(); ?>
        
    </div>
</div>

The view is split into two columns, left and right. The left column simply displays a DataPicker example from the value (fixed to the current date). The right column displays an alert box if the $reservation model has been updated, and the next two kinds of widget declaration too; the first one without using $form and the second one using $form, both outputting the same HTML code.

In either case, the DatePicker date output format is set to dd/MM/yyyy through the dateFormat property and the language is set to Italian through the language property.

Multiple models in the same view

Often, we can find many models of same or different class in a single view. First of all, remember that Yii2 encapsulates all the views' form attributes in the same container, named the same as the model class name. Therefore, when the controller receives the data, these will all be organized in a key of the $_POST array named the same as the model class name.

If the model class name is Customer, every form input name attribute will be Customer[attributeA_of_model] This is built with: $form->field($model, 'attributeA_of_model')->textInput().

In the case of multiple models of the same class, the container will again be named as the model class name, but every attribute of each model will be inserted in an array, such as:

Customer[0][attributeA_of_model_0]
Customer[0][attributeB_of_model_0]
…
…
…
Customer[n][attributeA_of_model_n]
Customer[n][attributeB_of_model_n]

These are built with:

$form->field($model, '[0]attributeA_of_model')->textInput();
$form->field($model, '[0]attributeB_of_model')->textInput();
…
…
…
$form->field($model, '[n]attributeA_of_model')->textInput();
$form->field($model, '[n]attributeB_of_model')->textInput();

Notice that the array key information is inserted in the attribute name!

So when data is passed to the controller, $_POST['Customer'] will be an array composed by the Customer models and every key of this array, for example, $_POST['Customer'][0] is a model of the Customer class.

Let's see now how to save three customers at once. We will create three containers, one for each model class that will contain some fields of the Customer model.

Create a view containing a block of input fields repeated for every model passed from the controller:

<?php

use yii\helpers\Html;
use yii\widgets\ActiveForm;

/* @var $this yii\web\View */
/* @var $model app\models\Room */
/* @var $form yii\widgets\ActiveForm */
?>

<div class="room-form">

    <?php $form = ActiveForm::begin(); ?>

    <div class="model">
      
      <?php for($k=0;$k<sizeof($models);$k++) { ?>
          <?php $model = $models[$k]; ?>
          <hr />
          <label>Model #<?php echo $k+1 ?></label>
          <?= $form->field($model, "[$k]name")->textInput() ?>
          <?= $form->field($model, "[$k]surname")->textInput() ?>
          <?= $form->field($model, "[$k]phone_number")-
          >textInput() ?>
      <?php } ?>
        
    </div>

<hr />

    <div class="form-group">
      <?= Html::submitButton('Save', ['class' => 'btn btn-
      primary']) ?>
    </div>

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

</div>

For each model, all the fields will have the same validator rules of the Customer class, and every single model object will be validated separately.

Saving linked models in the same view

It could be convenient to save different kind of models in the same view. This approach allows us to save time and to navigate from every single detail until a final item that merges all data is created. Handling different kind of models linked to each other it is not so different from what we have seen so far. The only point to take care of is the link (foreign keys) between models, which we must ensure is valid.

Therefore, the controller action will receive the $_POST data encapsulated in the model's class name container; if we are thinking, for example, of the customer and reservation models, we will have two arrays in the $_POST variable, $_POST['Customer'] and $_POST['Reservation'], containing all the fields about the customer and reservation models.

Then, all data must be saved together. It is advisable to use a database transaction while saving data because the action can be considered as ended only when all the data has been saved.

Using database transactions in Yii2 is incredibly simple! A database transaction starts with calling beginTransaction() on the database connection object and finishes with calling the commit() or rollback() method on the database transaction object created by beginTransaction().

To start a transaction:

$dbTransaction = Yii::$app->db->beginTransaction();

Commit transaction, to save all the database activities:

$dbTransaction->commit();

Rollback transaction, to clear all the database activities:

$dbTransaction->rollback();

So, if a customer was saved and the reservation was not (for any possible reason), our data would be partial and incomplete. Using a database transaction, we will avoid this danger.

We now want to create both the customer and reservation models in the same view in a single step. In this way, we will have a box containing the customer model fields and a box with the reservation model fields in the view.

Create a view the fields from the customer and reservation models:

<?php

use yii\helpers\Html;
use yii\widgets\ActiveForm;
use yii\helpers\ArrayHelper;
use \app\models\Room;
?>

<div class="room-form">

    <?php $form = ActiveForm::begin(); ?>

    <div class="model">
        
      <?php echo $form->errorSummary([$customer, $reservation]); ?>

      <h2>Customer</h2>        
      <?= $form->field($customer, "name")->textInput() ?>
      <?= $form->field($customer, "surname")->textInput() ?>
      <?= $form->field($customer, "phone_number")->textInput() ?>

      <h2>Reservation</h2>        
      <?= $form->field($reservation, "room_id")-
      >dropDownList(ArrayHelper::map(Room::find()->all(), 'id', 
      function($room, $defaultValue) {
          return sprintf('Room n.%d at floor %d', $room-
          >room_number, $room->floor);
      })); ?>
      <?= $form->field($reservation, "price_per_day")->textInput() 
      ?>
      <?= $form->field($reservation, "date_from")->textInput() ?>
      <?= $form->field($reservation, "date_to")->textInput() ?>
      
    </div>

    <div class="form-group">
        <?= Html::submitButton('Save customer and room', ['class' 
        => 'btn btn-primary']) ?>
    </div>

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

</div>

We have created two blocks in the form to fill out the fields for the customer and the reservation.

Now, create a new action named actionCreateCustomerAndReservation in ReservationsController in basic/controllers/ReservationsController.php:

 

public function actionCreateCustomerAndReservation()
    {
        $customer = new \app\models\Customer();
        $reservation = new \app\models\Reservation();

        // It is useful to set fake customer_id to reservation 
        model to avoid validation error (because customer_id is 
        mandatory)
        $reservation->customer_id = 0;
        
        if(
            $customer->load(Yii::$app->request->post())
            &&
            $reservation->load(Yii::$app->request->post())
            &&
            $customer->validate()
            &&
            $reservation->validate()
        )
        {
        
            $dbTrans = Yii::$app->db->beginTransaction();
            
            $customerSaved = $customer->save();
            
            if($customerSaved)
            {
                $reservation->customer_id = $customer->id;
                $reservationSaved = $reservation->save();
                
                if($reservationSaved)
                {
                    $dbTrans->commit();
                }
                else {
                    $dbTrans->rollback();
                }                
            }
            else {
                $dbTrans->rollback();
            }
        }
        
        
        return $this->render('createCustomerAndReservation', [ 'customer' => $customer, 'reservation' => $reservation ]);
    }

Yii By Example

Summary

In this article, we saw how to embed JavaScript and CSS in a layout and views, with file content or an inline block. This was applied to an example that showed you how to change the number of columns displayed based on the browser's available width; this is a typically task for websites or web apps that display advertising columns.

Again on the subject of JavaScript, you learned how implement direct AJAX calls, taking an example where the reservation detail was dynamically loaded from the customers dropdown list.

Next we looked at Yii's core user interface library, which is built on Bootstrap and we illustrated how to use the main Bootstrap widgets natively, together with DatePicker, probably the most commonly used jQuery UI widget.

Finally, the last topics covered were multiple models of the same and different classes. We looked at two examples on these topics: the first one to save multiple customers at the same time and the second to create a customer and reservation in the same view.

Resources for Article:


Further resources on this subject:


You've been reading an excerpt of:

Yii2 By Example

Explore Title