HTML dialog for Modals
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 SubscribeWelcome to day 19. Today we have the luck to play around with a little-known HTML element that absolutely rocks when it comes to building modals. The <dialog> element. If you're in a hurry for modal magnificence, you can skip ahead to snag the final markup and Stimulus controller. But I promise that today's journey is going to be fun.
Open up templates/voyage/index.html.twig. For the h1, I'm going to paste some new content:
| // ... lines 1 - 4 | |
| {% block body %} | |
| <div class="m-4 p-4 bg-gray-800 rounded-lg"> | |
| <div | |
| class="flex justify-between" | |
| > | |
| <h1 class="text-xl font-semibold text-white mb-4">Voyages</h1> | |
| <button | |
| class="flex items-center space-x-1 bg-blue-500 hover:bg-blue-700 text-white text-sm font-bold px-4 rounded" | |
| > | |
| <span>New Voyage</span> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="w-4 inline" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M3 12a9 9 0 1 0 18 0a9 9 0 0 0 -18 0" /><path d="M9 12h6" /><path d="M12 9v6" /></svg> | |
| </button> | |
| </div> | |
| // ... lines 18 - 45 | |
| </div> | |
| {% endblock %} |
This adds a "New voyage" button.
At the bottom, I'll remove the old button. There's nothing special with this new code: it's just... a button. And when we go to the right page... there it is! But it doesn't do anything yet.
Hello <dialog>
Back in the template, right after the button, add a <dialog> element. Inside I'll proclaim "I am a dialog". Also add an open attribute:
| // ... lines 1 - 4 | |
| {% block body %} | |
| <div class="m-4 p-4 bg-gray-800 rounded-lg"> | |
| <div | |
| class="flex justify-between" | |
| > | |
| // ... lines 10 - 17 | |
| <dialog open> | |
| I am a dialog! | |
| </dialog> | |
| </div> | |
| // ... lines 22 - 49 | |
| </div> | |
| {% endblock %} |
Hit refresh and behold the dialog element. It's... interesting. The dialog is absolutely positioned on the page, centered horizontally and near, but not at the top vertically. That's because the <dialog> element is designed for modals... or really any dialog, like a dismissable alert or any sub window. It's a normal HTML element, but with a bunch of superpowers that we're going to experience.
Making a Pretty dialog
But first, we gotta make it prettier. Back in the template, I'll paste over that dialog:
| // ... lines 1 - 4 | |
| {% block body %} | |
| <div class="m-4 p-4 bg-gray-800 rounded-lg"> | |
| <div | |
| class="flex justify-between" | |
| > | |
| // ... lines 10 - 18 | |
| <dialog | |
| open | |
| class="open:flex bg-gray-800 rounded-lg shadow-xl inset-0 w-full md:w-fit md:max-w-[50%] md:min-w-[50%]" | |
| > | |
| <div class="flex grow p-5"> | |
| <div class="grow overflow-auto p-1"> | |
| <div class="text-white space-y-4"> | |
| <div class="flex justify-between items-center"> | |
| <h2 class="text-xl font-bold">Create new Voyage</h2> | |
| <button class="text-lg absolute top-5 right-5"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="w-4" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M18 6l-12 12"/><path d="M6 6l12 12"/></svg> | |
| </button> | |
| </div> | |
| <p class="text-gray-400"> | |
| Join us on an exciting journey through the cosmos! Discover the | |
| mysteries of the universe and explore distant galaxies. | |
| </p> | |
| <div class="flex justify-end"> | |
| <button | |
| class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"> | |
| Let's Go! | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </dialog> | |
| </div> | |
| // ... lines 47 - 74 | |
| </div> | |
| {% endblock %} |
This is adapted from Flowbite with some AI help. And a designer could create this no problem. Because, there's nothing special: we still have a dialog, it's still open... and even the Tailwind classes are pretty boring. I set a width... and round the corners. But most of the positioning details are already built into the element. And most of the code is dummy modal content to get us started.
The result... is awesome. Though... the close button doesn't do its job yet! No worries: this is a great opportunity to show off one of dialog's superpowers!
Find the close button. Around it, add a <form method="dialog">:
| // ... lines 1 - 4 | |
| {% block body %} | |
| <div class="m-4 p-4 bg-gray-800 rounded-lg"> | |
| <div | |
| class="flex justify-between" | |
| > | |
| // ... lines 10 - 18 | |
| <dialog | |
| open | |
| class="open:flex bg-gray-800 rounded-lg shadow-xl inset-0 w-full md:w-fit md:max-w-[50%] md:min-w-[50%]" | |
| > | |
| <div class="flex grow p-5"> | |
| <div class="grow overflow-auto p-1"> | |
| <div class="text-white space-y-4"> | |
| <div class="flex justify-between items-center"> | |
| // ... line 27 | |
| <form method="dialog"> | |
| <button class="text-lg absolute top-5 right-5"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="w-4" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M18 6l-12 12"/><path d="M6 6l12 12"/></svg> | |
| </button> | |
| </form> | |
| </div> | |
| // ... lines 34 - 43 | |
| </div> | |
| </div> | |
| </div> | |
| </dialog> | |
| </div> | |
| // ... lines 49 - 76 | |
| </div> | |
| {% endblock %} |
This is a normal button: it will naturally submit the form when we click it, but the button doesn't have anything special on it.
But now when we click X... it closes!
Opening with a modal Stimulus Controller
To really make the <dialog> element shine, we need a bit of JavaScript. Head up to assets/controllers/ and create a new file called modal_controller.js. I'll cheat, steal some content from another controller... and clear it out. This controller will be simple. Start by adding a static targets = ['dialog'] so we can quickly find the <dialog> element. Next add an open method. Here, say this.dialogTarget.show():
| import { Controller } from '@hotwired/stimulus'; | |
| export default class extends Controller { | |
| static targets = ['dialog']; | |
| open() { | |
| this.dialogTarget.show(); | |
| } | |
| } |
This is another superpower of the <dialog> element: it has a show() method! Built into the <dialog> element is this core idea of showing and hiding.
To use the new controller, over in index.html.twig, find the div that holds the button and the dialog and add data-controller="modal". Then, on the button, say data-action="modal#open":
| // ... lines 1 - 4 | |
| {% block body %} | |
| <div class="m-4 p-4 bg-gray-800 rounded-lg"> | |
| <div | |
| data-controller="modal" | |
| class="flex justify-between" | |
| > | |
| // ... lines 11 - 12 | |
| <button | |
| data-action="modal#open" | |
| class="flex items-center space-x-1 bg-blue-500 hover:bg-blue-700 text-white text-sm font-bold px-4 rounded" | |
| > | |
| // ... lines 17 - 18 | |
| </button> | |
| // ... lines 20 - 49 | |
| </div> | |
| // ... lines 51 - 78 | |
| </div> | |
| {% endblock %} |
Finally, we need to set the <dialog> as a target. Remove the open attribute so it starts closed and replace it with data-modal-target="dialog":
| // ... lines 1 - 4 | |
| {% block body %} | |
| <div class="m-4 p-4 bg-gray-800 rounded-lg"> | |
| <div | |
| data-controller="modal" | |
| class="flex justify-between" | |
| > | |
| // ... lines 11 - 20 | |
| <dialog | |
| class="open:flex bg-gray-800 rounded-lg shadow-xl inset-0 w-full md:w-fit md:max-w-[50%] md:min-w-[50%]" | |
| data-modal-target="dialog" | |
| > | |
| // ... lines 25 - 49 | |
| </div> | |
| // ... lines 51 - 78 | |
| </div> | |
| {% endblock %} |
I like it! Over here, it starts closed. And when we click, open! Close, open, close!
Opening as a Modal
A <dialog> element has two modes: the normal mode that we've been using and a modal mode... which is much more useful. To use the modal mode, instead of show(), use showModal():
| // ... lines 1 - 2 | |
| export default class extends Controller { | |
| // ... lines 4 - 5 | |
| open() { | |
| this.dialogTarget.showModal(); | |
| } | |
| } |
Now when we click, it still opens, but there are some subtle differences. The first is that we can close it by hitting Esc. Cool! The second is that it has a backdrop. Watch: when I click, the screen will get just a little bit darker. Did you see that? This also blocks me from interacting with the rest of the page. And we get this for free thanks to <dialog>. That's huge.
Styling the Backdrop
Inspect and find the <dialog> element - there it is. The backdrop is added via a pseudo-element called backdrop. So it takes care of adding that for us... but it's a real element that can style. And I do want to style it!
Back in the template, find the dialog element. Thanks to Tailwind, we can style the backdrop pseudo-element directly. Add backdrop:bg-slate-600 and backdrop:opacity-80:
| // ... lines 1 - 4 | |
| {% block body %} | |
| <div class="m-4 p-4 bg-gray-800 rounded-lg"> | |
| <div | |
| data-controller="modal" | |
| class="flex justify-between" | |
| > | |
| // ... lines 11 - 20 | |
| <dialog | |
| class="open:flex bg-gray-800 rounded-lg shadow-xl inset-0 w-full md:w-fit md:max-w-[50%] md:min-w-[50%] backdrop:bg-slate-600 backdrop:opacity-80" | |
| data-modal-target="dialog" | |
| > | |
| // ... lines 25 - 48 | |
| </dialog> | |
| </div> | |
| // ... lines 51 - 78 | |
| </div> | |
| {% endblock %} |
Watch the effect. That is starting to feel really, really smooth.
Removing Background Page Scroll
One thing the dialog element doesn't handle automatically is... the page in the background still scrolls. It doesn't hurt anything... but it's not the behavior we expect.
To fix this, over in the open() method, say document.body to get the body element, .classList.add('overflow-hidden'):
| // ... lines 1 - 2 | |
| export default class extends Controller { | |
| // ... lines 4 - 5 | |
| open() { | |
| // ... line 7 | |
| document.body.classList.add('overflow-hidden'); | |
| } | |
| } |
And now... that's what we want!
Cleaning up on Close
Though... if we close, I still can't scroll! We need to remove that class.
To do that, copy the open() method, paste and name it close(). To close the dialog, call close()... then remove overflow-hidden:
Tip
To code more defensively (Firefox may need this), use:
if (this.hasDialogTarget) {
this.dialogTarget.close();
}
| // ... lines 1 - 2 | |
| export default class extends Controller { | |
| // ... lines 4 - 10 | |
| close() { | |
| this.dialogTarget.close(); | |
| document.body.classList.remove('overflow-hidden'); | |
| } | |
| } |
I like it! There's just one tiny problem: we're not calling the close() method! If we hit X or press Esc, the dialog is closing, yes, but I still can't scroll because nothing calls this close() method on our controller.
Fortunately, the dialog element has our back. Whenever a dialog element closes - for any reason - it dispatches an event called close. We can listen to that.
On the <dialog> element, add a data-action set to close->modal#close:
| // ... lines 1 - 4 | |
| {% block body %} | |
| <div class="m-4 p-4 bg-gray-800 rounded-lg"> | |
| <div | |
| data-controller="modal" | |
| class="flex justify-between" | |
| > | |
| // ... lines 11 - 20 | |
| <dialog | |
| // ... lines 22 - 23 | |
| data-action="close->modal#close" | |
| > | |
| // ... lines 26 - 49 | |
| </dialog> | |
| </div> | |
| // ... lines 52 - 79 | |
| </div> | |
| {% endblock %} |
So no matter how the dialog closes - I'll press Escape - we can now scroll because the close() method on our controller was called.
Blurring the Background
Tip
Thanks to help from Rob Meijer, you can do this in pure CSS. On the <dialog> element
use backdrop:bg-opacity-80 instead of backdrop:opacity-80 then add backdrop:backdrop-blur-sm.
No JS needed!
Ok, I'm excited. What else can we do? How about blurring the background? You might try to do this by blurring the backdrop. I totally tried that... but couldn't make it work. That's ok. What we can blur is the body. Add one more class: blur-sm and remove the blur-sm in close():
| // ... lines 1 - 2 | |
| export default class extends Controller { | |
| // ... lines 4 - 5 | |
| open() { | |
| // ... line 7 | |
| document.body.classList.add('overflow-hidden', 'blur-sm'); | |
| } | |
| close() { | |
| // ... line 12 | |
| document.body.classList.remove('overflow-hidden', 'blur-sm'); | |
| } | |
| } |
Let's see how this look. That is really cool!
Close on Click Outside
But if I try to click outside the modal, it doesn't close. That's another thing the dialog element doesn't handle. Fortunately, there's a quick one-time fix.
Up on the root element of our controller... Actually, we can put it down here on the dialog. Add a new action: click->modal#clickOutside:
| // ... lines 1 - 4 | |
| {% block body %} | |
| <div class="m-4 p-4 bg-gray-800 rounded-lg"> | |
| <div | |
| data-controller="modal" | |
| class="flex justify-between" | |
| > | |
| // ... lines 11 - 20 | |
| <dialog | |
| // ... lines 22 - 23 | |
| data-action="close->modal#close click->modal#clickOutside" | |
| > | |
| // ... lines 26 - 49 | |
| </dialog> | |
| </div> | |
| // ... lines 52 - 79 | |
| </div> | |
| // ... lines 81 - 82 |
I bet that looks odd - it'll be called whenever we click anywhere in the dialog - so let's go write that method. Say clickOutside(), give it an event argument, then if event.target === this.dialogTarget, this.dialogTarget.close():
| // ... lines 1 - 2 | |
| export default class extends Controller { | |
| // ... lines 4 - 15 | |
| clickOutside(event) { | |
| if (event.target === this.dialogTarget) { | |
| this.dialogTarget.close(); | |
| } | |
| } | |
| } |
Tip
To make the "click outside" work perfectly, instead of adding padding directly
to the dialog, add an element inside and give it the padding. We've done
that already - but it's an important detail.
event.target will be the actual element that received the click. It turns out, the only way to click exactly on the dialog element itself is if you click the backdrop. If you click anywhere else inside, event.target will be one of these elements. So it's a clever three lines of code, but the result is perfect. Click in here, no problem. Click out there, closed.
CSS Animation to Fade In
At this point, I am happy! But this tutorial isn't about making good things, it's about making great things. Next up: I want the dialog element to fade in. We could do this with a CSS transition. But another option is a CSS animation. I know, transitions, animations - CSS has a lot.
An animation is something you apply to an element and... it'll just... do that animation forever. Or you can make it animate just once. Like, we can make this button animate up and down forever. One of the nice things about animations is that you can make an animation only happen once... and it won't start until the element becomes visible on the page. For example, we could create an animation from opacity 0 to opacity 100, which would execute as soon as our dialog becomes visible.
Tailwind does have some built-in animations, but not one for fading in. So, we'll add it. Down in tailwind.config.js, I'll paste over the theme key:
| // ... lines 1 - 3 | |
| module.exports = { | |
| // ... lines 5 - 9 | |
| theme: { | |
| extend: { | |
| animation: { | |
| 'fade-in': 'fadeIn .5s ease-out;', | |
| }, | |
| keyframes: { | |
| fadeIn: { | |
| '0%': { opacity: 0 }, | |
| '100%': { opacity: 1 }, | |
| }, | |
| }, | |
| }, | |
| }, | |
| // ... lines 23 - 27 | |
| } |
This is mostly CSS animation stuff: it adds a new one called fade-in that will go from opacity 0 to 100 in 1/2 a second.
To use this, find the dialog element and add animate-fade-in:
| // ... lines 1 - 4 | |
| {% block body %} | |
| <div class="m-4 p-4 bg-gray-800 rounded-lg"> | |
| <div | |
| data-controller="modal" | |
| class="flex justify-between" | |
| > | |
| // ... lines 11 - 20 | |
| <dialog | |
| class="open:flex bg-gray-800 rounded-lg shadow-xl inset-0 w-full md:w-fit md:max-w-[50%] md:min-w-[50%] animate-fade-in backdrop:bg-slate-600 backdrop:opacity-80" | |
| // ... lines 23 - 24 | |
| > | |
| // ... lines 26 - 49 | |
| </dialog> | |
| </div> | |
| // ... lines 52 - 79 | |
| </div> | |
| {% endblock %} |
Try it out. Gorgeous! Could we fade out? Sure, but I actually like that it closes immediately. So I'm going to skip that.
Modals & Turbo Page Cache
Ok, I have one last detail before I let you go for the day. When we added view transitions, in app.js, we disabled a feature in Turbo called page cache... because it apparently doesn't always play nicely with view transitions. When view transitions become standard in Turbo 8, I'm guessing this won't be a problem.
Anyway, when caching is enabled:
| // ... lines 1 - 20 | |
| document.addEventListener('turbo:load', () => { | |
| // View Transitions don't play nicely with Turbo cache | |
| // if (shouldPerformTransition()) Turbo.cache.exemptPageFromCache(); | |
| }); | |
| // ... lines 25 - 42 |
the moment you click away from a page, Turbo takes a snapshot of the page before navigating away. When we click back, it's instant: boom! Instead of making a network request, it uses the cached version of this page. There's more to it than that, but you get the idea.
With caching enabled, one thing we need to worry about is removing any temporary elements from the page before the snapshot is taken, like toast messages or modals. Because, when you click "Back", you don't want a toast notification to be sitting up here.
The way that we normally solve this, for example in _flashes.html.twig, is to add a data-turbo-temporary attribute:
| {% for message in app.flashes('success') %} | |
| <div | |
| // ... lines 3 - 4 | |
| data-turbo-temporary | |
| // ... lines 6 - 7 | |
| > | |
| // ... lines 9 - 31 | |
| </div> | |
| {% endfor %} |
That tells Turbo to remove this element before it takes the snapshot.
Let's try adding this to our dialog so it's not in the snapshot. To see what happens, open the modal and click back. That just took a snapshot of the previous page. Now click forward. Woh. We're in a strange state. It looks like the dialog is gone... but we can't scroll and the page is blurred.
That's because we need to do more than just hide the dialog: we need to remove these classes from the body. Basically, before Turbo takes the snapshot, we need something to call the close() method!
And we can do that! In index.html.twig, on the root controller element - though this could go anywhere - add a data-action="". Right before Turbo takes its snapshot, it dispatches an event called turbo:before-cache. We can listen to that and then call modal#close. The only detail is that the turbo:before-cache event isn't dispatched on a specific element. So listening to it on this element won't work. It's dispatched above us, on the window. It's a global event.
Fortunately, Turbo gives us a simple way to listen to global events by adding @window:
| // ... lines 1 - 4 | |
| {% block body %} | |
| <div class="m-4 p-4 bg-gray-800 rounded-lg"> | |
| <div | |
| // ... line 8 | |
| data-action="turbo:before-cache@window->modal#close" | |
| // ... line 10 | |
| > | |
| // ... lines 12 - 51 | |
| </div> | |
| // ... lines 53 - 80 | |
| </div> | |
| {% endblock %} |
It's a little technical, but with this one-time fix, we can open the modal, go back, forward, and the page looks beautiful.
Wowza! Today was a huge day, but look what we accomplished! A beautiful modal system that we have total control over. Tomorrow is going to be just as big as we bring this modal to life with real dynamic content and forms. See you then.
32 Comments
As an alternative of hardcoding (Tailwind) classes into the Stimulus controller, you can also use Stimulus CSS Classes.
They use a logical name approach, but can be a bit tricky to understand and write.
For example:
Or alternatively use the Twig helper:
And add to
modal_controller.js:And use the class:
I admit this might not the best use-case to leverage this, because you probably want the overflow-hidden behaviour everytime you're using the
modal_controller.But still wanted to share the solution. Especially when using multiple classes this can help out.
Cool! I was not aware of that feature. Thank you for sharing it!
If anyone has issues getting the modal to close by adding
data-turbo-temporary, check you came to the page via a Turbo navigation. If you've landed on the page without a Turbo transition then going back & forward again won't remove the dialog.This took me a while to work out. I hope I save someone the same.
I think that this might have happened because I initially navigated to the page before I commented out the
if (shouldPerformTransition()) Turbo.cache.exemptPageFromCache();inapp.js.Yo @ToG!
That might make sense. When I read your first comment, I wasn't sure I understood: the element should be removed not matter how you got to the pae. So this explanation, indeed, makes more sense to me :).
Cheers!
Regarding the "blur on backdrop" topic... got that working with CSS:
Check out the navigation menu: https://www.bikesport-schindler.de/
Hey @barbieswimcrew ,
That's cool! Thanks for sharing it with others :)
Cheers!
I use Encore to build the project. And I added the line in app.js.
After running 'npm run build' in the console, I get the following error:
does anyone use Encore? And can someone help me with the problem?
Hey @Bart-V
It is telling you the package
turbo-view-transitionswas not found (for some reason). Can you double-check that it's installed?You can install it by running
php bin/console importmap:require turbo-view-transitionsCheers!
Great tutorial, thanks !
Related to the Turbo Page Cache issue discuss at the end of the video,
data-action="turbo:before-cache@window->modal#close"was not working for me. After reading the documentation (https://turbo.hotwired.dev/reference/events#document) I replacedwindowbydocumentand it's working, sodata-action="turbo:before-cache@document->modal#close". I don't know if it's due to a recent update or something else, just wanted to give the information.I have an other issue however. When I am on the page containing the button and the modal, then I go to another page, then I decide to go back to the previous page using the back button of my browser, nothing happen when I click on the button to open the modal. I must refresh the page to be able to open the modal via the button. In other words, navigate to the page containing the modal via the back navigation deactivate somehow the button. Does anyone know how to solve this ?
Hey @BigBenJr
Thank you for the observation. I'm not sure why "window" is not working for you - I've used it before and it works
About the modal, when you go back, does the
openattribute remains on thedialogelement? I'm guessing theclose()method is missing something to make this work on all scenariosCheers!
Thanks for your reply @MolloKhan !
I just realized that because I added
data-turbo-temporaryto the dialog element, when I navigate to the page containing the modal via the back navigation, because the page I see is from the cache, the dialog element is not anymore on the page. If I don't adddata-turbo-temporaryto the dialog I don't have this bug anymore. Anyway, as I useturbo:before-cache@document->modal#closeI don't needdata-turbo-temporaryanymore.EDIT: I just realized as well that the 2 problems were related. Since I removed
data-turbo-temporarynow I may usewindowinstead ofdocument.Ha! I love it when a single change fixes the whole thing. Cheers!
I'm having inconsistencies in the fadein animation "'fade-in': 'fadeIn .5s ease-out;'," between local and production.
I'm hosting a containerized symfony app on Azure.
The problem is the animation duration. It works fine on my local environment, and the fade in animation is correctly defined on the dialogue element as "fadeIn 0.5s ease-out" in the chrome dev tools.
But when the container is pushed to production, I still see the animate-fade-in class on the dialogue but the definition changed to just "fadeIn ease-out", with the duration missing and so no animation actually occurs.
Hey Nick,
Hm, probably something is missing. It's impossible to know for sure, so I may just give you some tips on where to look.
Make sure it's not a cache problem on production, try to open the website in a different browser, in incognito mode, and try hard reloading in your browser. If you're not using hashes for your CSS files, browsers can cache assets that lead to this behavior. But still, even if you're using hashes for assets, try to use hard reload, incognito mode, and a different browser. Such services like Azure may have some layers of caching for assets, probably it's a good idea to make sure that cache is not the reason, I bet there should be some mechanisms to refresh it.
Also, probably the Symfony prod mode causes this problem, try to clear all the cache and run your project locally in Symfony
prodmode. Do you still have that 0.5s animation? Probably you include some extra CSS files only in prod Symfony mode and that overrides the default behaviour that you have in dev mode.And finally, double-check that nothing in your CSS files can override the default behavior, probably you have the same declaration in a few files that overrides each other, the files' load order may sometimes be different on a different OS causing a weird behavior, like on some it works and on some it does not.
I would also suggest you to double-check your build process locally and on production, probably you have different versions of some tools that are installed, and that version difference may cause different behavior.
I hope that helps!
Cheers!
Hi Ryan,
I'm using the dialog on page that contains a form, and it turns out that when the modal closes, it submit the form. I had to add an event.preventDefault() in the close method.
It seems the <form method="dialog"> conflicts with the main on. I removed it and tied the close button to a close() method in the controller. I'll follow the next course and see what happen when posting the form in the modal... but i fear this will mix up badly.
Hey Denis,
Are you trying to follow this course on your personal project? We do recommend you download the course code and follow this course from the start/ directory to have the exact code the course author has in the videos. Yes, you can follow it on a personal project, or a fresher (latest) Symfony version but it may cause more work from your side. In your case, I suppose you need to improve the business logic that's responsible for sending the form, probably using a more specific CSS selector to send the form that is only in the dialog in case it's trying to send every form on the page, etc.
I hope this helps!
Cheers!
Feeling a bit ashamed for asking, since I haven't paid yet for any course. In future, while hopefully beeing wealthier, I will.
For the form to work within a dialog, the method needs to be set as 'dialog'
Normally I'm using twig helpers for creating forms. But that doesn't seem to work in this case, am I write?
Doing so, according to the docs and as expected, when using something different than 'get' and 'post' as method, 'post' is used and the specified method gets put in a hidden form field like this:
Do I just have to write the starting of the form by hand in that case?
Hey Nexo,
If you have a Symfony form type where you want to add a hidden field - I would recommend you to add that field right in the form type, see the
HiddenTypefor that specific case: https://symfony.com/doc/current/reference/forms/types/hidden.htmlThen you will be able to render your form with Twig helper, e.g.
{{ form(form) }}.But in case you want/need to put that field manually in the form - then you can render your form with
{{ form_start(form) }}and{{ form_end(form) }}methods, between which you will place your custominput. But putting extra fields into your form manually may lead to an error in Symfony forms, so better to do it in the form type and put manually only buttons - those are safe to be added to the form.Cheers!
Hi Victor,
Thank you for the answer. I might have been not clear enough. I was talking about when setting the form type in the controller other than get/put, this hidden field will be generated by Symony. In my case it was 'dialog', because that's what I thought is necessary for using forms in <dialog>. I couldnt make the modal dialog getting automatically closed while using a form created in a controller. So I thought, I need to write the form start by hand, instead of using {{form_star(form) }}, for having 'dialog' as form method instead of a hidden field.
While using method 'dialog' would clearly close the dialog, the form didnt got submitted. Back using 'put' as form method, the form got submitted, but the dialog kept beeing open. Eventually I found my problem.
I messed up the empty <turbo-frame id="modal"> while extending twig templates.
Greetings!
Hey Nexo,
Oh, I see now... Glad you were able to find the problem youself, sometimes it's very simple problem but difficult to be spotted, and you need to double-check everything. Thanks for sharing the evential solution with others btw! It definitely might be useful or someone
Cheers!
What if we would need to fire this dialog by clicking on different elements on the page, e.g. by buttons in header and footer so we couldn't surround and put it into same button container/controller?
Should we avoid then using Stimulus, or make some application controller with global, custom events firing or what would be the pattern?
Thank you!
Hi @Kirill!
Three possibilities on this:
A) If you load what's in the modal via Ajax, you can use our AJAX-loading system that we build in the next few days to load from anywhere.
B) In many cases, if you have multiple button that open the same modal (and it is not an Ajax modal), then it's often simple enough to embed that modal in all 3 places. I don't see a huge problem with that, though I'm sure you will have some cases where you have a big modal and really want to avoid this.
C) You can always create some small system to do what you're saying. I have created a
modal-open-controller.jsbefore that I put in the<button>that should open a modal. I pass in a value calledidto the controller. The controller then finds a the modal on the page with thatidand opens it (it opens it by firing and event on the modal - e.g.modal:open- which I list for from mymodal-controller.js... though even easier might just be to put theidon the<dialog>itself, then calldocument.getElementById(this.idValue).showModal().The point is: we have a few options and they're all pretty great and are fairly simple.
Cheers!
Hi! Thank you!
In
model_controller:clickOutside()I expected you to callthis.close()instead ofthis.dialogTarget.close(). I understand that callingdialogTarget.close()triggersmodel_controller.close()but couldthis.close()have been used instead and had the same result?Yo @kbond!
Yup, definitely. The way we've written things (which I really like), we can call
this.close()directly... orthis.dialogTarget.close()(which will then trigger ourthis.close()). Probablythis.close()would be even a bit simpler, I admit :).Cheers!
I searched for this question in the comments, I had the same reasoning ! Thanks @kbond !
I have mixed feelings about that. In close() method, we called the dialog element close() method.
But, with the same syntax we call the close() method of our controller. Did I understand good ?
Hey @Adn-B!
Let me see if I understand what you're asking. Let's look at the 2 situations of what we could do inside of
clickOutside():this.dialogTarget.close(). In this case, the<dialog>will, of course, close. That will trigger the browser to dispatch thecloseevent on the<dialog>. Because of ourdata-actionon the<dialog>, that will call theclose()method on our controller. This time, because the<dialog>is already closed, it will not close again. But we CAN run our cleanup code.this.close(). This case should have the same result, but it's a bit simpler: ourclose()method closes the dialog and cleans things up. Closing the dialog should also trigger theclosemethod from the browser... and that should causeclose()to be called again. But because the dialog is already closed, we can't close it again. I think our "cleanup clode" inclose()may be called twice, but it's not a big deal.Let me know if that helps... or if I just answered the completely wrong question ;).
Cheers!
I'm seeing an error with the adding of the
turbo:before-cache@window->modal#closeaction.This is what I see in the console…
Anyone else had this & worked out why?
OK. So I've updated the
close()method as follows as it appears that Firefox Developer Edition may remove the dialog before calling theclose()handler…I wonder if this is something to do with the View Transitions support in FF?
Same problem here, thanks @ToG
@julien_bonnier was it also on FF for you? If so, normal version of dev edition?
I'll add a note about this either way - just curious to learn more.
Thanks!
"Houston: no signs of life"
Start the conversation!