Ajax-Submitting an Entire Form

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

When we click the "Save" button, we want to submit the form via Ajax. Take a look at the structure of the modal. The save button is actually outside of the form: the form lives inside of modal-body. This means that we can't add a Stimulus submit action to the form... because clicking save won't actually submit the form! The button does nothing! Instead, over in _modal.html.twig, we're going to add an action to the button itself.

Add data-action= - we can just use the default action for a button, which is click - then our controller name - modal-form - a # sign and a new method name. How about submitForm.

<div
... lines 2 - 5
>
<div class="modal-dialog">
<div class="modal-content">
... lines 9 - 17
<div class="modal-footer">
... lines 19 - 21
<button type="button" class="btn btn-primary" data-action="modal-form#submitForm">
Save
</button>
</div>
</div>
</div>
</div>

Copy that name and go add it to our stimulus controller submitForm().

... lines 1 - 4
export default class extends Controller {
... lines 6 - 18
submitForm() {
... lines 20 - 21
}
}

Finding the form Without a Target

Let's see: we need to find the form element so we can read the data from all of its fields. Normally, when we need to find something, we add a target. Should we do that in this situation?

We could. But I'd like to make it as easy as possible to reuse our modal-form controller on other forms. If we can avoid needing to add extra attributes to the form element, which is rendered by this form_start() function, that will make reusing all of this on other forms much easier.

Instead, in our controller, let's leverage the modalBodyTarget - which is going to be this element right here - and look inside of it for a form element. With jQuery, we can do that with: const $form = $(this.modalBodyTarget) then .find('form').

... lines 1 - 4
export default class extends Controller {
... lines 6 - 18
submitForm() {
const $form = $(this.modalBodyTarget).find('form');
... line 21
}
}

If you use jQuery in Stimulus, you will always use $() and then some element so that we're looking inside that element... instead of inside the whole page. If we wanted to look for something inside of our entire controller element, we would use $(this.element). The point is: we always want our selecting to be looking inside our controller.

I also, as a convention, prefix my variable names with a $ when they are jQuery objects. There's nothing special about that variable name.

If you wanted to do this without jQuery, it would be really similar: this.modalBodyTarget then .getElementsByTagName(), pass it form... and use the 0 index to get the first and only match.

Making the Ajax Call

Anyways, to make the Ajax call, we're going to need the data from all of the fields in the form. Without jQuery, if you look at our submit-confirm_controller, and scroll down to submitForm(), we learned that you can do this with a combination of URLSearchParams and FormData... passing it the form element, which in this case was this.element.

We could do that same thing here. But since we're using jQuery, there's a shortcut: console.log($form.serialize()).

... lines 1 - 4
export default class extends Controller {
... lines 6 - 18
submitForm() {
const $form = $(this.modalBodyTarget).find('form');
console.log($form.serialize());
}
}

Let's try that. Move over, refresh the page, open the modal and fill in at least one of the fields. Hit save. Nothing visually happened... but look at the log.

There it is! We get a long string, which is the format we can use in the Ajax call. If you look closely, the product name does contain "shoelaces".

To make the Ajax call, we need three things... and we already have the first. We need the form field data - we have that with $form.serialize() and also the URL to submit to & method to use. We can get those last two directly from the form element.

Say $.ajax() and I'll pass it the options format where even the URL is an option. Set that to $form.. Now you might expect me to read the action attribute off of the form. But instead, say .prop('action').

... lines 1 - 4
export default class extends Controller {
... lines 6 - 18
async submitForm() {
const $form = $(this.modalBodyTarget).find('form');
this.modalBodyTarget.innerHTML = await $.ajax({
url: $form.prop('action'),
... lines 23 - 24
});
}
}

That's slightly different... and bit smarter: this will return the correct action URL even if there is no action attribute... which means that a form should submit back to the current URL.

If you look back at submit-confirm_controller, that time we used this.element.action... which is a property that exists on all form elements. In jQuery, we're asking it to give us that same property.

Repeat this for method set to $form.prop('method').

... lines 1 - 4
export default class extends Controller {
... lines 6 - 18
async submitForm() {
const $form = $(this.modalBodyTarget).find('form');
this.modalBodyTarget.innerHTML = await $.ajax({
url: $form.prop('action'),
method: $form.prop('method'),
... line 24
});
}
}

Finally, for the data, we can say data set to $form.serialize().

... lines 1 - 4
export default class extends Controller {
... lines 6 - 18
async submitForm() {
const $form = $(this.modalBodyTarget).find('form');
this.modalBodyTarget.innerHTML = await $.ajax({
url: $form.prop('action'),
method: $form.prop('method'),
data: $form.serialize(),
});
}
}

So there you have it: a jQuery version of how you can submit a form via Ajax.

Now, this will submit the form... but we want to also do something with the response.

Look at ProductAdminController::new(). If we submit to this URL and the form fails validation, it will automatically re-render our _form.html.twig template. But this time the form will render with errors.

So... great! This means that after the Ajax call is finished, we want to put the returned HTML back onto this.modalBodyTarget. Copy that and say this.modalBodyTarget.innerHTML equals await $.ajax(). Then make submitForm() async.

Debugging the Ajax Error

Moment of truth! Spin over, refresh the page, click the button, but leave the form blank this time. Hit save and... ah! There's an error!

What happened? Let's find out next... and make sure that our form - no matter how or where it's submitted - will always work.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=7.4.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "1.11.99.1", // 1.11.99.1
        "doctrine/annotations": "^1.0", // 1.11.1
        "doctrine/doctrine-bundle": "^2.2", // 2.2.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
        "doctrine/orm": "^2.8", // 2.8.1
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "sensio/framework-extra-bundle": "^5.6", // v5.6.1
        "symfony/asset": "5.2.*", // v5.2.3
        "symfony/console": "5.2.*", // v5.2.3
        "symfony/dotenv": "5.2.*", // v5.2.3
        "symfony/flex": "^1.3.1", // v1.12.1
        "symfony/form": "5.2.*", // v5.2.3
        "symfony/framework-bundle": "5.2.*", // v5.2.3
        "symfony/property-access": "5.2.*", // v5.2.3
        "symfony/property-info": "5.2.*", // v5.2.3
        "symfony/proxy-manager-bridge": "5.2.*", // v5.2.3
        "symfony/security-bundle": "5.2.*", // v5.2.3
        "symfony/serializer": "5.2.*", // v5.2.3
        "symfony/twig-bundle": "5.2.*", // v5.2.3
        "symfony/ux-chartjs": "^1.1", // v1.2.0
        "symfony/validator": "5.2.*", // v5.2.3
        "symfony/webpack-encore-bundle": "^1.9", // v1.11.1
        "symfony/yaml": "5.2.*", // v5.2.3
        "twig/extra-bundle": "^2.12|^3.0", // v3.2.1
        "twig/intl-extra": "^3.2", // v3.2.1
        "twig/twig": "^2.12|^3.0" // v3.2.1
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.0
        "symfony/debug-bundle": "^5.2", // v5.2.3
        "symfony/maker-bundle": "^1.27", // v1.30.0
        "symfony/monolog-bundle": "^3.0", // v3.6.0
        "symfony/stopwatch": "^5.2", // v5.2.3
        "symfony/var-dumper": "^5.2", // v5.2.3
        "symfony/web-profiler-bundle": "^5.2" // v5.2.3
    }
}

What JavaScript libraries does this tutorial use?

// package.json
{
    "devDependencies": {
        "@babel/preset-react": "^7.0.0", // 7.12.13
        "@popperjs/core": "^2.9.1", // 2.9.1
        "@symfony/stimulus-bridge": "^2.0.0", // 2.1.0
        "@symfony/ux-chartjs": "file:vendor/symfony/ux-chartjs/Resources/assets", // 1.1.0
        "@symfony/webpack-encore": "^1.0.0", // 1.0.4
        "bootstrap": "^5.0.0-beta2", // 5.0.0-beta2
        "core-js": "^3.0.0", // 3.8.3
        "jquery": "^3.6.0", // 3.6.0
        "react": "^17.0.1", // 17.0.1
        "react-dom": "^17.0.1", // 17.0.1
        "regenerator-runtime": "^0.13.2", // 0.13.7
        "stimulus": "^2.0.0", // 2.0.0
        "stimulus-autocomplete": "^2.0.1-phylor-6095f2a9", // 2.0.1-phylor-6095f2a9
        "stimulus-use": "^0.24.0-1", // 0.24.0-1
        "sweetalert2": "^10.13.0", // 10.14.0
        "webpack-bundle-analyzer": "^4.4.0", // 4.4.0
        "webpack-notifier": "^1.6.0" // 1.13.0
    }
}