Dynamic Form Validation in Symfony using Ajax

Symfony has a great form validation system. The only downside is that it requires the form to be submitted in order to check for any constraint violations. And sometimes, that means that user experience will suffer. And other times, a form submission only for error detection is just not possible. For example, I like to open small forms in modal windows. In this case, submitting the form means closing the modal. And detecting the error and re-opening the same modal with the new information is a painful process. In order to handle all these cases elegantly, I have a small Ajax routine which dynamically checks form constraints while the user completes the form. I still have server-side Symfony validation in place, because one can easily get around JavaScript verification.

Aims of this tutorial :

  • Create a simple, dynamic, Ajax form validation system

Resources :

  • none 😉

Symfony version : 3.4 (LTS), but should work with all 3+ versions

Let’s get started!

Part 1: Context

In this tutorial, I talked about how we can easily upload and remove multiple files using the CollectionType Field. The example used was that of folders in which we are storing different documents. In this tutorial, I will be using the same Entities, but I will focus on the creation of the Folder itself, rather than the documents inside it.

Part 2: Form

For this example, I only need a simple form, containing the folder name only.

<?php

namespace Playground\CookiejarBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

use Symfony\Component\Form\Extension\Core\Type\TextType;

class FolderType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', TextType::class, array(
        	    'required'	    => true,
        	    'label'	    => 'folder_name'
            ));

    }
    
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'Playground\CookiejarBundle\Entity\Folder'
        ));
    }

    public function getBlockPrefix()
    {
        return 'playground_cookiejarbundle_folder';
    }
}

Part 3: Controller

The add/edit methods are quite basic. They contain all the standard form validation and the usual custom redirections in case of success/failure.

<?php

class FolderController extends Controller
{

    public function addAction(Request $request)
    {
    	// New object
    	$folder = new Folder();
    
    	// Build the form
    	$form = $this->get('form.factory')->create(FolderType::class, $folder);
    
    	if ($request->isMethod('POST'))
    	{
    		$form->handleRequest($request);
    		// Check form data is valid
    		if ($form->isValid())
    		{
    			// Save data to database
    			$this->entityManager->persist($folder);
    			$this->entityManager->flush();
    
    			// Redirect to view page
    			return $this->redirectToRoute('cookiejar_folder_view', array(
    				'id'	=>	$folder->getId(),
    			));
    		}
    	}
    	// If we are here it means that either
    	//	- request is GET (user has just landed on the page and has not filled the form)
    	//	- request is POST (form has invalid data)
    	return $this->render(
    		'@CookiejarBundle/Folder/add.html.twig',
    		array (
    			'form'	=>	$form->createView(),
    		)
    	);
    }
    
    public function editAction(Request $request, Folder $folder)
    {
        // Build the form
        $form = $this->get('form.factory')->create(FolderType::class, $folder);
        
        // All the rest is identical to the addAction method
        
        return $this->render(
			'@CookiejarBundle/Folder/edit.html.twig',
			array (
				'form'		=>	$form->createView(),
				'folder'	=>	$folder
			)
		);
    }
}

Part 4: Form template

Inside the template file is where all the magic happens. While the user fills the above input box, an Ajax call will send a request to the Controller with the user input value. If the data already exists in the database, the template will display a warning message and prevent the actual form submission.

 First of all, the error message needs to be added to the template and hidden. It will only be made visible if needed.

<div class="row form-group">
    {{ form_label(form.name, null, {'label_attr': {'class': 'col-md-2'}}) }}
    <div class="col col-md-10">{{ form_widget(form.name) }}</div>
    <div class="form_errors col-md-12">
	    {{ form_errors(form.name) }}
    </div>
</div>

<div class="row" id="folder_exists_error" style="display: none;">
    <div class="col col-xs-12">
	    <div class="form_errors col-lg-12">
		    <span class="help-block">
			    <ul class="list-unstyled">
				    <li><span class="glyphicon glyphicon-exclamation-sign"></span> {{ 'folder_exists' | trans }}</li>
			    </ul>
		    </span>
	    </div>
    </div>
</div>

The Ajax call needs to be done on user input. One might say, why not do it when the user validates the form? Since the Ajax call is asynchronous, that would be impossible, because the form submission would not wait for the server’s response and will let the user submit incorrect data. In order to avoid that, we are validating data as it is being typed. On form submission, we only check if the last data entered was valid or not, using a variable that is updated by the Ajax calls.

$(document).ready(function(){

	// Each time the folder name gets modified, do an ajax check
	// Cover all possible user actions: actual typing, but also copy/paste.
	$("#playground_cookiejarbundle_folder_name").bind("input change keyup", checkExists);

});

The checkExists function is quite simple: it sends an Ajax request and updates a flag variable according to the server’s response. The error message is displayed and hidden accordingly.

var inUse = false;

function checkExists()
{
    // Get the input value
    var folderName = $("#playground_cookiejarbundle_folder_name").val();

    // Send an ajax request with the user input data
    $.ajax({
    	type: "POST",
    	url: "{{ path('cookiejar_folder_exists') }}",
    	data: {folderName: folderName }
    })
    .done(function(data){

        if (typeof data.status != "undefined" && data.status != "undefined" && data.status == "OK")
        {
        	if (typeof data.message != "undefined" && data.message != "undefined")
        	{
        		if (parseInt(data.message) == 1)
        		{
                    // Folder name already exists
                    // => raise flag
                    inUse = true;
                    // => display error message
                    $("#folder_exists_error").show();
                    return;
        		}
        	}
        }

	});

    // We suppose all went well.
    // If not, the Ajax call will update the flag and display the error message
    inUse = false;
    $("#folder_exists_error").hide();
}

All that rests to be done is to block the submit in case of incorrect data.

$(document).ready(function(){

    // Each time we modify the folder name, do an ajax check
    $("#playground_cookiejarbundle_folder_name").bind("input change keyup", checkExists);
    
    // On form submission, check data
    $("form[name=playground_cookiejarbundle_folder]").on('submit', function(e){
    
        // Hide errors
        $("#folder_exists_error").hide();
        
        if (inUse === true)
        {
            $("#folder_exists_error").show();
            return false;
        }
    
    });
});

Part 5: The Controller

<?php
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;

use Playground\CookiejarBundle\Entity;
use Playground\CookiejarBundle\Entity\Folder;

class FolderController extends Controller
{
    public function existsAction(Request $request)
	{
        // This is optional.
        // Only include it if the function is reserved for ajax calls only.
        if (!$request->isXmlHttpRequest()) {
            return new JsonResponse(array(
                'status' => 'Error',
                'message' => 'Error'),
            400);
        }

    	if(isset($request->request))
    	{
            // Get data from ajax
            $folderName = $request->request->get('folderName');

            // Check if a Folder with the given name already exists
			$folder = $this
			    ->getDoctrine()
			    ->getManager()
			    ->getRepository('PlaygroundCookiejarBundle:Folder')
			    ->findOneByName($folderName);
			    
            if ($folder === null)
            {
            	// Folder does not exist
            	return new JsonResponse(array(
            		'status' => 'OK',
            		'message' => 0),
            	200);
            }
            else
            {
            	// Folder exists
            	return new JsonResponse(array(
            		'status' => 'OK',
            		'message' => 1),
            	200);
            }
        }

        // If we reach this point, it means that something went wrong
        return new JsonResponse(array(
            'status' => 'Error',
            'message' => 'Error'),
        400);
    }
}

Part 6: The Routing

cookiejar_folder_exists:
    path:     /cookieja/folder/exists
    defaults:
        _controller: PlaygroundCookiejarBundle:Folder:exists

And that is all folks! Have fun and have a  !

5 thoughts to “Dynamic Form Validation in Symfony using Ajax”

      1. Hello

        The element is generated by the form : FolderType.php

        public function getBlockPrefix()
        {
        return ‘playground_cookiejarbundle_folder’;
        }

        This block gives the form prefix. So if your bundle has a different name, this element will too have a different syntax.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.