zen of coding

Helpful jQuery modals

This post is actually mostly about jQuery (with some cake sprinkles), but why not…

Let’s examine a typical situation:

We have a Product model, which belongsTo a Category model.

Now we need to add some products…

An admin interface would have a category select input (or drop-down) and a few form fields to fill-in some product details. Happily, we venture to add our first product, only to realize that we have not yet created any categories… backtracking to create a category would be a shame and with a little help from jQuery we can easily solve this problem.

One state that developers rarely consider during (and after) development, is the “blank” state of the application, which in turn can lead to some unexpected behavior…

The goal is to do the following:

  1. Get to the www.example.com/products/add page
  2. Using jQuery, “realize” that our Category drop-down is empty
  3. Prompt the user through… “lightwindow-modal-jquery-ui-dialog”… To create at least one category, so we could get moving
  4. Refresh the product/add page with the newly created category neatly in place within the select input

Since I use jQuery extensively, I am going to pick jQuery UI’s dialog widget to accomplish my needs. (It may not be the most robust or feature-rich, but it plays nice with the jQuery core… and does what I need).

(It goes without saying that you should include jQuery core and jQuery UI libraries… load them from Google, if you’d like).

Anyway…
First, let’s take a look at the products/add view:

<?php $this->Html->script('jquery.form.js', array('inline' => FALSE)); ?>
<?php $this->Html->script('parent_list.js', array('inline' => FALSE)); ?>
<?php $this->Html->css(array('smoothness/jquery'), NULL, array('inline' => FALSE)); ?>

<div>
  <?php
    echo $this->Form->create('Product');
    echo $this->Form->input('category_id', array('class' => 'parent-list'));
    echo $this->Form->input('name');
    echo $this->Form->end('Add product');
  ?>
</div>

<div id=&quot;category-form&quot;>
  <?php
    echo $this->Form->create('Category', array('action' => 'add'));
    echo $this->Form->input('name');
    echo $this->Form->end('Create category');
  ?>
</div>

Stepping through the code, the first thing we see is the jquery.form plugin. This will be used for submitting the “category add” form via AJAX, from the modal. (Will show that later on).
Next, we load parent_list.js, which is going to handle most of the magic.
Finally, in the view we have a div category-form (which will be turned by jQuery UI into a dialog/modal).

As you can see both forms are very simple, for purpose of this example.

Now, let’s see the jQuery file, which does all the heavy lifting:

(function ($) {
  var getModal = function () {
    $('#category-form').dialog({
      modal: true,
      autoOpen: false,
      title: 'Create a category'
    });

    if($('.parent-list').children().size() === 0) {
      $('#category-form').dialog('open');
    }
  };

  var showResponse = function (response) {
    if(response == 'saved') {
      $('#category-form').dialog('close');
      window.location.reload();
    }
  };

  $(document).ready(function() {
    getModal();

    var submitOptions = {
      success: showResponse,
      dataType: 'json'
    };

    $('#CategoryAddForm').ajaxForm(submitOptions);
  });
}(jQuery));

This is more JavaScript/jQuery than I usually show, so let’s take a look at some interesting things.
First, you’ll notice that the entire code is wrapped into a “self-calling anonymous function”. To keep things simple, the two benefits of using such pattern is to avoid conflicts and global vars (although it is not evident from this example, but a little googlin’ will explain things in more detail).

Next, notice the function declarations: var getModal = function () … by using such assignment we avoid a couple of problems in everybody’s favorite IE (the above method will define the function at parse-time, rather than run-time). It also kind of “forces” the developer to declare and call things in proper sequence without hoping that the browser might pick up some of the slack, where developer was careless. At any rate, run your JavaScript through JSLint before going to production ;)

Now that the general stuff is out of the way, let’s see the “most important” line of code:
if($(‘.parent-list’).children().size() === 0)
The above will check the category select input (i.e. parent of the product) to see if it has any options (options are children of the category select in this case). If not, the jQuery goodness kicks in and we get a modal/dialog which displays the category add form, as you saw from the products/add view above.

Once the name is entered and submitted via AJAX, I send back “saved” from the server as a response (assuming that all goes well). At which point we close the dialog and refresh the page. Now, we have our newly added category as one of the options… and can continue to add the product without backtracking to create a category in some other UI.

Hopefully this serves as a decent example on how to build a much more intelligent UI and improve user experience, with the help from the lovely jQuery and CakePHP.

  • In your products/add view example, I think that you forgot to echo the first three lines that used HtmlHelper to include the scripts. I would also love to see some sort of demo since my brain recently started suffering from tldr;

  • teknoid

    @jblotus

    No need to echo out in this case, because we are not including the files in-line. They will be included in the layout where you have $scripts_for_layout var.

    p.s. Demo would be nice, but I don’t really have the time. Sorry ;)

  • Very nice example, thanks!

  • teknoid

    @marius

    No problem. Glad you’ve found it helpful.

  • In your function statements, you’re doing:

    var foo = function foo () { … };

    What you’ve actually done is mix the two different types of function declarations. It might work, but it’s not what you intended. You’re missing out on the benefits of function assignment statements.

    What you really want is:

    var foo = function () { … };

  • teknoid

    @Eric Leads

    Duh. Thanks! :)

  • Ponch

    Hey, thanks for your great post, as always.

    Just a quick question: you are not currently sending the data to any cake function, are you? What I’m trying to do is input the form on the modal screen and actually save to the database, using what I’m guessing should be an action on the controller.

    Any advice on how to do that?

    Thanks in advance!

  • teknoid

    @Ponch

    I am sending the form data, with this line of code:
    $(‘#CategoryAddForm’).ajaxForm(submitOptions);

  • Ponch

    Oh ok, but which action receives it? The one in which you’re in?

    Thanks!

  • teknoid

    @Ponch

    Using this example the data is posted to the form which has id=”CategoryAddForm”, so the action would be “add”, in the Categories controller.

  • Jens

    Nice article!

    One question, can you please show me how you sent back the response from the server (categories/add)? I’m not sure how my controller has to look like.

    Thanks and greatings from Germany

  • teknoid

    @Jens

    Something like this should do:

    public function add() {
    if (!empty($this->data)) {
    if ($this->Category->save($this->data)) {
    $response = ‘saved’;
    } else {
    $response = $this->Category->invalidFields();
    }
    $this->set(compact(‘response’));
    }
    }

  • Jens

    Thank you for your answer. That’s working!
    In my application I have to insert the following two lines before $this->set(compact(‘response’));:
    $this->autoRender = false;
    $this->redirect(‘/projects’);
    I use your sample to insert a new project on the index-page in my application.

  • teknoid

    @Jens

    Glad it worked out, but you do need the view to actually render the response. Otherwise you might be braking MVC.

    It could be as simple as echo json_encode($response);

    I have another post which shows how to create a custom JSON view, to speed up your app in general, if you deal with a decent amount of AJAX requests.

    Well, either way… best of luck ;)

  • Jens

    @teknoid
    I think I made a mistake and I’m confused.
    Instead of showing the dialog automatically I show it on activating a link. Therefore I changed your code to the following:
    $(document).ready(function() {
    $(‘.add_project’).click(function(event){
    event.preventDefault();
    getModal();

    var submitOptions = {
    success: showResponse,
    dataType: ‘json’
    };

    $(‘#ProjectAddForm’).ajaxForm(submitOptions);
    });
    });

    Changes in getModal:
    //if($(‘.parent-list’).children().size() === 0) {
    $(‘#project-add’).dialog(‘open’);
    //}

    My controller looks like yours:
    function add() {
    if (!empty($this->data)) {
    if ($this->Project->save($this->data)) {
    $response = ‘saved’;
    } else {
    $response = $this->Project->invalidFields();
    }
    $this->set(compact(‘response’));
    }
    }

    In my add view I wrote the echo json_encode($response); to render the response.

    My link looks like the following:
    echo $html->link(
    null,
    ”,
    array(
    ‘escape’ => false,
    ‘class’ => ‘menu_add add_project’,
    ‘title’ => ‘Projekt hinzufügen’
    ),
    null,
    false
    );

    But when I’m using this code I’m always redirecting to the add view. Is there a mistake in my code? Did I understand something wrong?

    I hope you can help me once more.

    I will read the other post about json now.

%d bloggers like this: