Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ajax form submission in symfony2 with graceful degradation for users without javascript

I am building a web application that involves forms. The framework being used is symfony2. I would like to have everything working for users without javascript, and then progressively enhance the experience for people with javascript enabled. I am planning to use JQuery as my javascript library.

I would like to have forms where it submits and displays a message using the FlashMessage component if the user does not have javascript and the request is not an ajax request.

For users with javascript support, I would like to return the message to them if submission is successful. The javascript should then display that message in a div immediately.

At the same time, I need to deal with error messages. For users without javascript, the whole form will be rerendered with the error messages in place.

For users with javascript, the ajax response should be an array containing the id of the input component and the error message. Javascript should then insert these into the form without a refresh.

Looking at the symfony2 book, I can see the following for submissions, I then modified it to check if the request is an ajax request:

public function newAction(Request $request)
{
    // just setup a fresh $task object (remove the dummy data)
    $task = new Task();

    $form = $this->createFormBuilder($task)
        ->add('task', 'text')
        ->add('dueDate', 'date')
        ->getForm();

    if ($request->getMethod() == 'POST') {
        $form->bindRequest($request);

        if ($form->isValid()) {
            // perform some action, such as saving the task to the database
            if ($this->request->isXmlHttpRequest(){
                   //return data ajax requires.
            } 
            return $this->redirect($this->generateUrl('task_success'));
        }
    }

    // ...
}

The problem I find with this approach is that is isn't very elegant. I need add that extra check every single form. Further more, this method would not work if say the request comes in via Zend_AMF.

Are there any better ways to do this?

like image 478
F21 Avatar asked Oct 16 '11 08:10

F21


3 Answers

Another option is to have your controller return something other than a response and use the kernel.view event to transform that into what you want. This would be better DRY because the logic is encapsulated in the listener. You just need to decide what you want your controllers to return.

if ($form->isValid()) {
    return new Something($redirectUrl, $ajaxData);
}

And in your listener:

public function onKernelView($event)
{
    $request = $event->getRequest();
    $result = $event->getControllerResult();

    if ($result instanceof Something) {
        if ($request->isXmlHttpRequest()) {
            $event->setResponse(new Response(json_encode($result->getAjaxData())));
        } else {
            $event->setResponse(new RedirectResponse($result->getRedirectUrl()));
        }
    }
}
like image 80
Kris Wallsmith Avatar answered Sep 28 '22 09:09

Kris Wallsmith


You can return a response like usual and decide on the view if you want to do a partial render or render the whole page by extending different twig layouts.

In your controller:

if ($form->isValid()) {
    // perform some action, such as saving the task to the database
    $this->get('session')->setFlash('notice', 'Success!');
    return $this->render('success.html.twig');
} else {
    $this->get('session')->setFlash('notice', 'Please correct the errors below');
}

And in your views:

{# new.html.twig #}
{% extends app.request.isXmlHttpRequest ? 'ajax.html.twig' : 'layout.html.twig' %}
{% block content %}
    {% if app.session.hasFlash('notice') %}
        <div class="flash-notice">{{ app.session.flash('notice') }}</div>
    {% endif %}
    <form ...>
        {{ form_widget('form') }}
        <input type="submit" />
    </form>
{% endblock %}


{# success.html.twig #}
{% extends app.request.isXmlHttpRequest ? 'ajax.html.twig' : 'layout.html.twig' %}
{% block content %}
    {% if app.session.hasFlash('notice') %}
        <div class="flash-notice">{{ app.session.flash('notice') }}</div>
    {% endif %}
{% endblock %}


{# ajax.html.twig #}
{% block content %}{% endblock %}


{# layout.html.twig #}
<html>
<body> normal page content
    <div id='content'>
    {% block content %}{% endblock %}
    </div>
</body>
</html>

(You might want to put the flash messages part in a macro or an include...)

Now simply render the return from the ajax request into the tag you want to place it:

$.ajax({
  type: "POST",
  url: "tasks/new",
  data: {task: foo, dueDate: bar},
}).done(function( msg ) {
  $('#content').html(msg);
});
like image 41
solarc Avatar answered Sep 28 '22 09:09

solarc


As long as you can generalize the sort of response needed for non-ajax forms and for ajax forms, you can relegate that decision of ajax/non-ajax to another method and then simply have:

return handleResponseType(whateverInfoIsNeededToHandleIt)

You then have full control over what gets passed in to your deciding function.

like image 26
cdeszaq Avatar answered Sep 28 '22 09:09

cdeszaq