Render Form in popup via AJAX (Create and Update) with ajax validation & Also load any page via ajax Yii 2.0 (2.3)

You are viewing revision #15 of this wiki article.
This is the latest version of this article.
You may want to see the changes made in this revision.

« previous (#14)

  1. How to use a modal with ajax (below is any item via ajax)
  2. How to load any view via ajax

There are a few issues with the other solutions I originally used that I found from other wikis. I address the issues I had in this much simpler and shorter way. I am also going to explain what is going into way more detail than others to help people understand what's going on.

This will allow you to create, update, or load ANY page in your project using the same modal so you aren't creating a lot of modals in all of your pages; just in the layout main.php.

This will also let you open a modal and reload the modal with other content without being closed and reopened.

You could technically use this to load any div not just a modal. The only difference is the js load div classes and you could make a whole site load via ajax. I will show you at the end of this how to load anything via ajax.

You must have bootstrap and jQuery or the will not work.

How to use a modal with ajax (below is any item via ajax)

So we are going to do 5-6 steps depending on what route you choose.

  1. Include JS

  2. Add Modal to your main.php layout

  3. Update controller actions to use Ajax

  4. Update form to use Ajax

  5. Add buttons to views to load content.

First we will create the JS you can either add it to your main layout as script tags or add it to your App Assets

To add it to App Assets you will create a file in your web directory ie. frontend/web/js/ajax-modal-popup.js. If you want to include your script inline the skip two code blocks.

$(function(){
    //get the click of modal button to create / update item
    //we get the button by class not by ID because you can only have one id on a page and you can
    //have multiple classes therefore you can have multiple open modal buttons on a page all with or without
    //the same link.
//we use on so the dom element can be called again if they are nested, otherwise when we load the content once it kills the dom element and wont let you load anther modal on click without a page refresh
      $(document).on('click', '.showModalButton', function(){
         //check if the modal is open. if it's open just reload content not whole modal
        //also this allows you to nest buttons inside of modals to reload the content it is in
        //the if else are intentionally separated instead of put into a function to get the 
        //button since it is using a class not an #id so there are many of them and we need
        //to ensure we get the right button and content. 
        if ($('#modal').data('bs.modal').isShown) {
            $('#modal').find('#modalContent')
                    .load($(this).attr('value'));
            //dynamiclly set the header for the modal
            document.getElementById('modalHeader').innerHTML = '<h4>' + $(this).attr('title') + '</h4>';
        } else {
            //if modal isn't open; open it and load content
            $('#modal').modal('show')
                    .find('#modalContent')
                    .load($(this).attr('value'));
             //dynamiclly set the header for the modal
            document.getElementById('modalHeader').innerHTML = '<h4>' + $(this).attr('title') + '</h4>';
        }
    });
});

register the file in your appassets i.e. frontend/assets/AppAsset.php

class AppAsset extends AssetBundle {

    public $basePath = '@webroot';
    public $baseUrl = '@web';
    public $css = [
    ];
    public $js = [
        'js/ajax-modal-popup.js',
    ];
    public $depends = [
        'yii\web\YiiAsset',
    ];

}

To include the script without using app assets you can do the following however, it is not recommenced.

It is a little bit different and would be placed inline with your other js files at the end of your layout main.php

<script type="text/javascript">
    //get the click of modal button to create / update item
    //we get the button by class not by ID because you can only have one id on a page and you can
    //have multiple classes therefore you can have multiple open modal buttons on a page all with or without
    //the same link.
//we use on so the dom element can be called again if they are nested, otherwise when we load the content once it kills the dom element and wont let you load anther modal on click without a page refresh
      $(document).on('click', '.showModalButton', function(){
        //check if the modal is open. if it's open just reload content not whole modal
        //also this allows you to nest buttons inside of modals to reload the content it is in
        //the if else are intentionally separated instead of put into a function to get the 
        //button since it is using a class not an #id so there are many of them and we need
        //to ensure we get the right button and content. 
        if ($('#modal').data('bs.modal').isShown) {
            $('#modal').find('#modalContent')
                    .load($(this).attr('value'));
            //dynamiclly set the header for the modal via title tag
            document.getElementById('modalHeader').innerHTML = '<h4>' + $(this).attr('title') + '</h4>';
        } else {
            //if modal isn't open; open it and load content
            $('#modal').modal('show')
                    .find('#modalContent')
                    .load($(this).attr('value'));
             //dynamiclly set the header for the modal via title tag
            document.getElementById('modalHeader').innerHTML = '<h4>' + $(this).attr('title') + '</h4>';
        }
    });
</script>

Add the bootstrap modal to your layouts main.php file. I put it at the very end after all of my scripts.

<?php
yii\bootstrap\Modal::begin([
    'headerOptions' => ['id' => 'modalHeader'],
    'id' => 'modal',
    'size' => 'modal-lg',
    //keeps from closing modal with esc key or by clicking out of the modal.
    // user must click cancel or X to close
    'clientOptions' => ['backdrop' => 'static', 'keyboard' => FALSE]
]);
echo "<div id='modalContent'></div>";
yii\bootstrap\Modal::end();
?>

You can also put a loading Gif in the content show until the content is loaded

<?php
yii\bootstrap\Modal::begin([
    'headerOptions' => ['id' => 'modalHeader'],
    'id' => 'modal',
    'size' => 'modal-lg',
    //keeps from closing modal with esc key or by clicking out of the modal.
    // user must click cancel or X to close
    'clientOptions' => ['backdrop' => 'static', 'keyboard' => FALSE]
]);
echo "<div id='modalContent'><div style="text-align:center"><img src="my/path/to/loader.gif"></div></div>";
yii\bootstrap\Modal::end();
?>

set up the each and every actions to you want to use ajax on to handel the ajax by using renderAjax() instead of render(). The only difference is that renderajax will only load the page content and render will load the page content and the layout (i.e. menus, sidebars, footers etc.).

If you want be able to view a page with either ajax or direct URL (going to yoursite.com/youraction in the url) you must put a check in the controller to use either ajax or standard render.

public function actionSomeAction($id) {
        $model = $this->findModel($id);
        if ($model->load(Yii::$app->request->post()) && $model->save()) {
            return $this->redirect(['view', 'id' => (string) $model->_id]);
        }elseif (Yii::$app->request->isAjax) {
            return $this->renderAjax('_form', [
                        'model' => $model
            ]);
        } else {
            return $this->render('_form', [
                        'model' => $model
            ]);
        }
    }

The same action as above with just ajax and would never use direct access via url would be

public function actionSomeAction($id) {
        $model = $this->findModel($id);
        if ($model->load(Yii::$app->request->post()) && $model->save()) {
            return $this->redirect(['view', 'id' => (string) $model->_id]);
        }else (Yii::$app->request->isAjax) {
            return $this->renderAjax('_form', [
                        'model' => $model
            ]);
        }
    }

Next you need to tell each and every form to still use inline validation. Things you must do here!

1. All forms MUST have an ID

if both of those are not set the form will NOT do client/ajax validation.

<?php
    $form = ActiveForm::begin([
                'options' => [
                    'id' => 'create-product-form'
                ]
    ]);
    ?>

Now just create your buttons to load into the modal!

Things you must have on the button is one class showModalButton because that is what we told our JS function in the begining of this wiki to look out for.

Title tag because we told the JS to use the title tag for the modal title. If you want an empty title use 'title'=>'' but don't leave it out our you will have a js error.

Don't use href because we told our JS to look for attribute 'value' and use it to load the content so you must use the 'value' attribute.

here are examples of both loading content with or without variables

use yii\helpers\Html;
use yii\helpers\Url;

    <?= Html::button('Create New Company', ['value' => Url::to(['companies/create']), 'title' => 'Creating New Company', 'class' => 'showModalButton btn btn-success']); ?>

//load existing content from db or pass varibles


    <?= Html::button('Update Company', ['value' => Url::to(['companies/update', 'id'=>1]), 'title' => 'Updating Company', 'class' => 'showModalButton btn btn-success']); ?>

//could even load a view in it

    <?= Html::button('Create New Company', ['value' => Url::to(['companies/view']), 'title' => 'Viewing Company', 'class' => 'showModalButton btn btn-success']); ?>

You can nest them too. with other solutions I found on here you couldn't. I.e.

If you have a create page in a modal you can also put another modal button in that modal and it will reload the modal.

Note: if you modal opens and it has no content that means you have an error on your page/form that you are tying to render in the modal. it will not show an error it will just be empty!!! Also, if its not working use firebug and inspect the console errors.

How to load any view via ajax

Add the js you can use the same way I listed above.

$(function(){
//load the current page with the conten indicated by 'value' attribute for a given button.
   $(document).on('click', '.loadMainContent', function(){
            $('#main-content').load($(this).attr('value'));
    });
});

in your layout main.js or whatever layout you use wrap your content in a div and give it the id of main-content (if you change this show change it in the js script too). This won't effect anything that's not being used via ajax.

<div class="content" id="main-content">;
<?= $content ?>
</div>

use the buttons just like i did above just change the class to loadMainContent

<?= Html::button('Create New Company', ['value' => Url::to(['companies/create']), 'title' => 'Creating New Company', 'class' => 'loadMainContent btn btn-success']); ?>

Some more examples of buttons are above.

That's it... If you are using a form you must set your controllers to validate via ajax and forms like i showed in the bootstrap modal portion.

Also, you can switch any button to "a" link and use "FALSE" as the url.

<?= Html::a('title', FALSE, ['value' => Url::to(['create/company', 'type' => 'company']), 'class' => 'loadMainContent']); ?>

You can also alter the js to use the url instead of value like

$('#main-content').load($(this).attr('href'));

but I'd recommend using pjax for page content. I'll write one of these using pjax instead of ajax. The main benefit is that pjax will keep intact back buttons / forward buttons/ allows for links to be properly booked marked etc.

if you use this please up vote or if you have questions/ comments/ improvements please post a comment! that's it and i hope you find this useful!