Chapters
-
Course Code
Subscribe to download the code!
Subscribe to download the code!
-
This Video
Subscribe to download the video!
Subscribe to download the video!
-
Subtitles
Subscribe to download the subtitles!
Subscribe to download the subtitles!
-
Course Script
Subscribe to download the script!
Subscribe to download the script!
Correcting the Form Action & Preventing Default
Scroll down to the script below, click on any sentence (including terminal blocks) to jump to that spot in the video!
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.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeSubmitting 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.
Show Lines
|
// ... lines 1 - 4 |
export default class extends Controller { | |
Show Lines
|
// ... lines 6 - 18 |
async submitForm() { | |
const $form = $(this.modalBodyTarget).find('form'); | |
this.modalBodyTarget.innerHTML = await $.ajax({ | |
url: this.formUrlValue, | |
Show Lines
|
// ... 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 | |
Show Lines
|
// ... lines 2 - 5 |
> | |
<div class="modal-dialog"> | |
<div class="modal-content"> | |
Show Lines
|
// ... lines 9 - 14 |
<div | |
class="modal-body" | |
data-modal-form-target="modalBody" | |
data-action="submit->modal-form#submitForm" | |
> | |
{{ modalContent|default('Loading...') }} | |
</div> | |
Show Lines
|
// ... 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()
.
Show Lines
|
// ... lines 1 - 4 |
export default class extends Controller { | |
Show Lines
|
// ... lines 6 - 18 |
async submitForm(event) { | |
event.preventDefault(); | |
Show Lines
|
// ... 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.
17 Comments
Hey @Amine!
Are you referring to the Stimulus action submitForm
? Or the Symfony action that handles the form submit? That word "action" is one we like to overuse :)
Cheers!
Hey @weaverryan
I'm talking about action (stimulus term) and exactly the relance action
Thank you man :)
Hey @Amine!
Sorry for dropping this! I believe the answer is simply that I wanted to submit the form via Ajax instead of a full, traditional, page reload submit. So I added the submitForm
Stimulus action that handles the form via Ajax.
These days, I would almost definitely solve this with Turbo (using the native Turbo Ajax form submit or a <turbo-frame>
if needed.
Cheers!
Hello!
I have the same problem that Macarena and Martín.
I try to use this modal example on my own project but error message are not displayed in the modal.
I have a 422 response from AJAX with the correct error message.
Can you help me please ?
I'm using Symfony 6.2 and Flowbite for the modal.
Hey @cctaborin!
Hmm. Let's see if we can figure this out! If you're using the jQuery ajax like we are in this tutorial (btw, you can also just use fetch()
these days - it's built into the browser and works great) - then, irrc, if the server returns a 422 status code, then the Ajax call is seen as "failed". When I recorded, the Ajax call was returning a 200 status code when the validation errors happened.
Why the difference? Because starting in Symfony 6.2, when you use $this->render()
in your controller and pass in a form
value, Symfony now (correctly) changes the status code to 422 if the form has validation errors. This is GOOD, but it changes the behavior of the tutorial :). Sorry about that!
This what the fixed code should look like:
async submitForm() {
const $form = $(this.modalBodyTarget).find('form');
try {
this.modalBodyTarget.innerHTML = await $.ajax({
url: this.formUrlValue,
method: $form.prop('method'),
data: $form.serialize(),
});
} catch (error) {
this.modalBodyTarget.innerHTML = error.message;
}
}
That last error.message
part I'm not 100% sure about. If that's wrong, console.log(error)
to see what that is and where the HTML response might be. And please, if you get this working, let me know to help others :).
Cheers and thanks for bringing this up again!
Hey,
I just tried this and it work, you just need to use error.responseText
.
Hey @weaverryan!
Thank you for your explanations!
It works very well with this.modalBodyTarget.innerHTML = error.message;
(as @Djamina suggests).
Besides, your tutorials are great, keep it up!
Cheers!
Awesome! Thanks for the verifications! I'll get a note added to the tutorial to help others ❤️
I have put the code below inside _form.html.twig
but when I click the button, it returns with the error:
Uncaught ReferenceError: test is not defined.
Is there a reason why scripts don't work?
<button onclick="test()">Try it</button>
<script>
function test() {
alert("Hello! I am an alert box!");
}
</script>
I'd appreciate any help.
Hi!!, sorry me again....
I try this modal example in my own proyect but it not appear the error message in the modal, just an error in the console.
But if I execute the form normaly it have no problem with the validation.
Pleaseeeee helpp
Hello. The same thing happens to me too.
I have unsuccessfully tried to fire a checkValid() on the form before submitting via Ajax. I don't have much knowledge of Javascript so maybe it didn't work because I didn't know how to do it.
What I haven't finished understanding is that in some cases the invalid form is processed as expected, returning from the Ajax request with a 422 error, the HTML5 validation errors are shown in the form. In other cases, it returns with a 500 error. There, the form does not show HTML5 errors. Shows a symfony exception error. Ugly! That's why I've tried to validate the form before making the Ajax request. I think doing so would solve the problem.
Not a minor fact. If I submit the form from the submit button that contains the original Symfony form, everything works perfectly. Validations, automatic focus on required fields, etc.
The modal's submit button does not have the same behavior.
I appreciate any hint or solution.
Hey Martin,
Make sure your submit button is inside of the form, and submit that form via JS using that submit button. Also, take a look at some tips here: https://stackoverflow.com/a... - I believe they might be useful for you.
Otherwise, you can turn off HTML validation and base your validation on server side re-rendering the form with errors if there're any.
Cheers!
Hey Macarena I.
Which error is in console? Can you provide more information about issue =)
Cheers!
Hi team, great track! As there is no validation set in the source code, I guess you enabled auto mapping.
For those passing by, see https://symfonycasts.com/sc...
Hey Steven J.
Sorry for the confusion but if you open up the Product.php
class you will see that we have some validation on the properties. If you have any other doubt, please let us know.
Cheers!
sorry, I was using an old code version, thank you for this course, really love it
"Houston: no signs of life"
Start the conversation!
What PHP libraries does this tutorial use?
// composer.json
{
"require": {
"php": ">=8.1",
"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.21.6
"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
}
}
Hi guys,
Thank you for this tutorial :)
I don't understand why we add the second action. We can do everything with just one action.