Correcting the Form Action & Preventing Default

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

Submitting the form via Ajax just exploded! We can see the error down here: the POST request failed with a 405 error. Open that link in a new tab so we can see what happened.

Ah:

No route found for POST /admin/product: method not allowed, allowed get

This is not the URL that we expected: we expected this to submit to /admin/product/new

What happened? I was trying to be really responsible by reading the action and the method directly from the form. The problem is, if you inspect the form... there is no action attribute! That's normally fine: it just means that the form should submit back to the current URL.

When we render this form on the actual new product page - /admin/product/new - the empty action is ok because it will submit to this URL. But over here on the list page, the missing action attribute makes it look like it should submit right back to this URL. That's not what we want!

To fix this, instead of $form.prop('action'), use the formUrl value: this.formUrlValue. This assumes that the same URL that you use to fetch the form is also the URL that should be used to submit the form.

... 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: this.formUrlValue,
... lines 23 - 24
});
}
}

Let's try that again. Refresh the page, hit add, and Save. Yes! Look at that! We instantly see validation errors! If I fill out one field and submit again, it goes away!

Preventing the Full Form Submit

But there is one tiny problem. If I focus on a field and hit enter... oh... actually let me fill in a price. Now hit enter. Woh! The form submitted! But not via Ajax.

No problem. In addition to executing the submitForm() method when we click the "Save" button, we also need to execute that method if the form is submitted.

But wait: does this mean that we need to add a Stimulus data-action attribute to the form element... inside this form_start() function? I thought we were trying to avoid needing to add stuff to this template!

Fortunately, we do not need to do that. In _modal.html.twig, break the modal-body element onto multiple lines for readability. Now add data-action= the name of the event - submit, ->, the name of our controller - modal-form - # sign and then submitForm.

<div
... lines 2 - 5
>
<div class="modal-dialog">
<div class="modal-content">
... lines 9 - 14
<div
class="modal-body"
data-modal-form-target="modalBody"
data-action="submit->modal-form#submitForm"
>
{{ modalContent|default('Loading...') }}
</div>
... lines 22 - 29
</div>
</div>
</div>

Remember: events bubble up. The submit event is first dispatched on this form element. But then it bubbles up to the modal-body div. That means that this element will also receive the submit event.

To prevent the form from actually submitting... so that we can make the Ajax call, add an event argument to submitForm() and say event.preventDefault().

... lines 1 - 4
export default class extends Controller {
... lines 6 - 18
async submitForm(event) {
event.preventDefault();
... lines 21 - 25
});
}
}

When we hit enter on the form, that will prevent the submit. When we click the save button, this will have no effect... because clicking a button outside of a form doesn't do anything by default. So there's nothing to prevent.

Try it out. Reload, open up the modal and hit enter. Bah! HTML 5 validation is keeping me honest. Fill in the two required fields... but put a negative price so we hit a validation error. Hit enter now. Gorgeous!

Next: this is working awesome when there's a validation error. But if the submit is successful, we need to do something different: close the modal.

To do that, we need to create a systematic way for our controller to communicate whether or not the form submit was successful. This new idea will serve us super well in the next tutorial when we Ajaxify more form submits with Turbo.

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
    }
}