Modal Twig Component
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 SubscribeToday is a good day. Today we get to combine our modal system with Twig components to achieve a goal! I want to be able to quickly add a modal anywhere in our app.
Creating the Modal Component
Start in base.html.twig. All the way at the bottom, copy the modal markup. You can see... it's quite a bit: not something we want to copy and paste somewhere else:
| <html> | |
| // ... lines 3 - 15 | |
| <body class="bg-black text-white font-mono"> | |
| // ... lines 17 - 51 | |
| <div | |
| data-controller="modal" | |
| data-action="turbo:before-cache@window->modal#close" | |
| > | |
| <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" | |
| data-modal-target="dialog" | |
| data-action="close->modal#close click->modal#clickOutside" | |
| > | |
| <div class="flex grow p-5"> | |
| <div class="grow overflow-auto p-1"> | |
| <turbo-frame | |
| id="modal" | |
| data-modal-target="dynamicContent" | |
| data-action="turbo:before-fetch-request->modal#showLoading" | |
| class="aria-busy:opacity-50 transition-opacity" | |
| > | |
| {{ include('_frameSuccessStreams.html.twig', { frame: 'modal' }) }} | |
| </turbo-frame> | |
| </div> | |
| </div> | |
| </dialog> | |
| <template data-modal-target="loadingTemplate"> | |
| <div class="bg-space-pattern bg-cover rounded-lg p-8"> | |
| <div class="space-y-2"> | |
| <div class="h-4 bg-gray-700 rounded w-3/4 animate-pulse"></div> | |
| <div class="h-4 bg-gray-700 rounded animate-pulse"></div> | |
| <div class="h-4 bg-gray-700 rounded animate-pulse"></div> | |
| <div class="h-4"></div> | |
| <div class="h-4 bg-gray-700 rounded animate-pulse"></div> | |
| <div class="h-4 bg-gray-700 rounded w-1/2 animate-pulse"></div> | |
| <div class="h-4 bg-gray-700 rounded w-3/4 animate-pulse"></div> | |
| <div class="h-4"></div> | |
| <div class="h-4 bg-gray-700 rounded w-1/2 animate-pulse"></div> | |
| </div> | |
| </div> | |
| </template> | |
| </div> | |
| // ... lines 91 - 94 | |
| </body> | |
| </html> |
Copy, then delete it. Let's craft a Modal component, this time by hand. Create a new file in templates/components/ called Modal.html.twig, and paste:
| <div | |
| data-controller="modal" | |
| data-action="turbo:before-cache@window->modal#close" | |
| > | |
| <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" | |
| data-modal-target="dialog" | |
| data-action="close->modal#close click->modal#clickOutside" | |
| > | |
| <div class="flex grow p-5"> | |
| <div class="grow overflow-auto p-1"> | |
| <turbo-frame | |
| id="modal" | |
| data-modal-target="dynamicContent" | |
| data-action="turbo:before-fetch-request->modal#showLoading" | |
| class="aria-busy:opacity-50 transition-opacity" | |
| > | |
| {{ include('_frameSuccessStreams.html.twig', { frame: 'modal' }) }} | |
| </turbo-frame> | |
| </div> | |
| </div> | |
| </dialog> | |
| <template data-modal-target="loadingTemplate"> | |
| <div class="bg-space-pattern bg-cover rounded-lg p-8"> | |
| <div class="space-y-2"> | |
| <div class="h-4 bg-gray-700 rounded w-3/4 animate-pulse"></div> | |
| <div class="h-4 bg-gray-700 rounded animate-pulse"></div> | |
| <div class="h-4 bg-gray-700 rounded animate-pulse"></div> | |
| <div class="h-4"></div> | |
| <div class="h-4 bg-gray-700 rounded animate-pulse"></div> | |
| <div class="h-4 bg-gray-700 rounded w-1/2 animate-pulse"></div> | |
| <div class="h-4 bg-gray-700 rounded w-3/4 animate-pulse"></div> | |
| <div class="h-4"></div> | |
| <div class="h-4 bg-gray-700 rounded w-1/2 animate-pulse"></div> | |
| </div> | |
| </div> | |
| </template> | |
| </div> |
Like I said with the Button, we don't need a PHP class for a component. Because we don't have one, we call this an "anonymous component".
On top, render attributes... then add .defaults() so we can move these two attributes into that. Paste... then each of these needs a makeover to fit as Twig keys and values instead of HTML attributes:
| <div | |
| {{ attributes.defaults({ | |
| 'data-controller': 'modal', | |
| 'data-action': 'turbo:before-cache@window->modal#close', | |
| }) }} | |
| > | |
| // ... lines 7 - 40 | |
| </div> |
I like it! Over in base.html.twig, render this with <twig:Modal>. Easy!
Adding Blocks to the Component
However, look closer at Modal.html.twig: there are some things that shouldn't be here. For example, the <turbo-frame>! Not every modal needs a frame. A lot of times, we'll render a modal with simple, hardcoded content inside.
Copy this, and replace it with, of course, {% block content %} and {% endblock %}:
| <div | |
| // ... lines 2 - 5 | |
| > | |
| <dialog | |
| // ... lines 8 - 10 | |
| > | |
| <div class="flex grow p-5"> | |
| <div class="grow overflow-auto p-1"> | |
| {% block content %}{% endblock %} | |
| </div> | |
| </div> | |
| </dialog> | |
| // ... lines 18 - 33 | |
| </div> |
In base.html.twig, paste the frame... and add a closing tag:
| <html> | |
| // ... lines 3 - 15 | |
| <body class="bg-black text-white font-mono"> | |
| // ... lines 17 - 54 | |
| <twig:Modal> | |
| <turbo-frame | |
| id="modal" | |
| data-modal-target="dynamicContent" | |
| data-action="turbo:before-fetch-request->modal#showLoading" | |
| class="aria-busy:opacity-50 transition-opacity" | |
| > | |
| {{ include('_frameSuccessStreams.html.twig', { frame: 'modal' }) }} | |
| </turbo-frame> | |
| </twig:Modal> | |
| </body> | |
| </html> |
Let's keep going! The loading template down here? Yea, that's also something that specific to this one modal. If our modal is a hardcoded message, it won't need this at all!
Copy the loading div, delete, then around the <template> add: if block('loading_template'):
| <div | |
| // ... lines 2 - 5 | |
| > | |
| // ... lines 7 - 18 | |
| {% if block('loading_template') %} | |
| <template data-modal-target="loadingTemplate"> | |
| {% block loading_template %}{% endblock %} | |
| </template> | |
| {% endif %} | |
| </div> |
So if we pass the block, render it inside the <template>.
Back in base.html.twig, anywhere, define that block. But instead of the normal {% block %} tag - which would work - when you're inside a Twig component, you can use a special <twig:block> syntax with name="loading_template". Then, paste:
| <html> | |
| // ... lines 3 - 15 | |
| <body class="bg-black text-white font-mono"> | |
| // ... lines 17 - 54 | |
| <twig:Modal> | |
| <turbo-frame | |
| // ... lines 57 - 62 | |
| </turbo-frame> | |
| <twig:block name="loading_template"> | |
| <div class="bg-space-pattern bg-cover rounded-lg p-8"> | |
| <div class="space-y-2"> | |
| <div class="h-4 bg-gray-700 rounded w-3/4 animate-pulse"></div> | |
| <div class="h-4 bg-gray-700 rounded animate-pulse"></div> | |
| <div class="h-4 bg-gray-700 rounded animate-pulse"></div> | |
| <div class="h-4"></div> | |
| <div class="h-4 bg-gray-700 rounded animate-pulse"></div> | |
| <div class="h-4 bg-gray-700 rounded w-1/2 animate-pulse"></div> | |
| <div class="h-4 bg-gray-700 rounded w-3/4 animate-pulse"></div> | |
| <div class="h-4"></div> | |
| <div class="h-4 bg-gray-700 rounded w-1/2 animate-pulse"></div> | |
| </div> | |
| </div> | |
| </twig:block> | |
| </twig:Modal> | |
| </body> | |
| </html> |
We just moved around a lot of stuff. And yet... the existing modal still works! And now, we have a leaner, meaner modal component that we can reuse in other places.
Delete Modal with Custom Content
Let's do exactly that. I want to add a delete link on each row that, on click, opens a modal with a confirmation. Open _row.html.twig. Copy the edit link, paste, and call it delete:
| <tr class="even:bg-gray-700 odd:bg-gray-600" id="voyage-list-item-{{ voyage.id }}"> | |
| // ... lines 2 - 4 | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| // ... lines 6 - 11 | |
| <a | |
| href="{{ path('app_voyage_edit', {'id': voyage.id}) }}" | |
| class="ml-4 text-yellow-400 hover:text-yellow-600" | |
| data-turbo-frame="modal" | |
| >edit</a> | |
| </td> | |
| </tr> |
To get this to work, one option is to create a new, standalone delete confirmation page, point to that and... done! The data-turbo-frame="modal" would load that page into the modal.
But since we've done that before, let's try something different. Delete the href, change this to a button, remove the data-turbo-frame attribute... and change the yellow colors to red:
| <tr class="even:bg-gray-700 odd:bg-gray-600" id="voyage-list-item-{{ voyage.id }}"> | |
| // ... lines 2 - 4 | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| // ... lines 6 - 11 | |
| <button | |
| class="ml-4 text-red-400 hover:text-red-600" | |
| >delete</button> | |
| </td> | |
| </tr> |
Let's go check out the look. Nice!
Back over, I'll paste in a modal:
| <tr class="even:bg-gray-700 odd:bg-gray-600" id="voyage-list-item-{{ voyage.id }}"> | |
| // ... lines 2 - 4 | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| // ... lines 6 - 14 | |
| <twig:Modal> | |
| <svg class="mx-auto mb-4 text-gray-400 w-12 h-12 dark:text-gray-200" xmlns="http://www.w3.org/2000/svg" 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="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 9v4" /><path d="M12 16v.01" /></svg> | |
| <h3 class="mb-5 text-lg font-normal text-gray-500 dark:text-gray-400"> | |
| Delete this thrilling voyage??? | |
| </h3> | |
| </twig:Modal> | |
| </td> | |
| </tr> |
There's nothing special here. The big difference is, instead of a <turbo-frame>, the content we need is right here. When we refresh, every row now has a delete dialog inside of it. But that's totally okay! It's not open, so it's invisible.
Opening the Modal
Now for the tricky part. When we click this button, we need to open this modal. To make this happen, we need the button to physically live inside the data-controller="modal" element so that it can call the open action on the modal Stimulus controller.
To allow that, inside the modal template, add a new block called trigger, endblock:
| <div | |
| // ... lines 2 - 5 | |
| > | |
| {% block trigger %}{% endblock %} | |
| // ... lines 8 - 25 | |
| </div> |
Now, if you have a button that triggers the modal to open, you can put that right here! Over in _row.html.twig, copy the button, remove it, say <twig:block name="trigger"> and paste.
And because we're inside the modal, add data-action="modal#open":
| <tr class="even:bg-gray-700 odd:bg-gray-600" id="voyage-list-item-{{ voyage.id }}"> | |
| // ... lines 2 - 4 | |
| <td class="px-6 py-4 whitespace-nowrap flex"> | |
| // ... lines 6 - 12 | |
| <twig:Modal> | |
| <twig:block name="trigger"> | |
| <button | |
| class="ml-4 text-red-400 hover:text-red-600" | |
| data-action="modal#open" | |
| >delete</button> | |
| </twig:block> | |
| // ... lines 20 - 25 | |
| </twig:Modal> | |
| </td> | |
| </tr> |
Let's try this! So excited! Refresh! The styling got weird. Before, we had three a tags, which are inline elements. Now we have two inline elements and a block element. So that is something that changes slightly, but it's an easy fix. Up on the <td>, add a flex class:
| <tr class="even:bg-gray-700 odd:bg-gray-600" id="voyage-list-item-{{ voyage.id }}"> | |
| // ... lines 2 - 4 | |
| <td class="px-6 py-4 whitespace-nowrap flex"> | |
| // ... lines 6 - 26 | |
| </td> | |
| </tr> |
Modal Conditional Size & the props Tag
And now... much better. Most importantly, when we hit Delete, the modal opens! However, you know me, I want this to be perfect. And I'm not happy with how big this is. When I open the edit form, it makes sense to be half the screen width. But when I open the delete, we should let it shrink down to the size of the content inside.
To do that, in this one case, I want to be pass a new flag called allowSmallWidth set to true:
| <tr class="even:bg-gray-700 odd:bg-gray-600" id="voyage-list-item-{{ voyage.id }}"> | |
| // ... lines 2 - 4 | |
| <td class="px-6 py-4 whitespace-nowrap flex"> | |
| // ... lines 6 - 12 | |
| <twig:Modal :allowSmallWidth="true"> | |
| // ... lines 14 - 25 | |
| </twig:Modal> | |
| </td> | |
| </tr> |
I added this : because, if I pass allowSmallWidth="true", that will pass the string true. By adding a colon, this becomes Twig code, so that will pass the Boolean true. They would both work... but I like being stricter.
With the Button, we learned that if you want this to become a variable instead of an attribute, you can add a public property with that same name. And we could create a new Modal.php file.
But there's another way to convert from an attribute into a variable when using an anonymous component. At the top of Modal.html.twig, add a props tag that's special to Twig components. Add allowSmallWidth and default it to false:
| {% props allowSmallWidth=false %} | |
| // ... lines 2 - 28 |
Cool, huh? Below, we want to make this min-width conditional. Say {{ allowSmallWidth }} - if that is true, render nothing, else render the md:min-w-[50%]:
| {% props allowSmallWidth=false %} | |
| <div | |
| // ... lines 3 - 6 | |
| > | |
| // ... lines 8 - 9 | |
| <dialog | |
| class="open:flex bg-gray-800 rounded-lg shadow-xl inset-0 w-full md:w-fit md:max-w-[50%] {{ allowSmallWidth ? '' : 'md:min-w-[50%] ' }}animate-fade-in backdrop:bg-slate-600 backdrop:opacity-80" | |
| // ... lines 12 - 13 | |
| > | |
| // ... lines 15 - 19 | |
| </dialog> | |
| // ... lines 21 - 26 | |
| </div> |
Back on the page, the edit link still opens with half width... but that delete link, ah, it's nice and small! Now it deserves some real content! In _row.html.twig, after the <h3>, I'll add some styling... then I want a cancel button that closes the modal. For that, we can go old-school. Add a <form method="dialog">, and inside a <twig:Button> that says Cancel. And I want the button to look like a link, so add variant="link":
| <tr class="even:bg-gray-700 odd:bg-gray-600" id="voyage-list-item-{{ voyage.id }}"> | |
| // ... lines 2 - 4 | |
| <td class="px-6 py-4 whitespace-nowrap flex"> | |
| // ... lines 6 - 12 | |
| <twig:Modal :allowSmallWidth="true"> | |
| // ... lines 14 - 26 | |
| <div class="flex justify-between"> | |
| <form method="dialog"> | |
| <twig:Button variant="link">Cancel</twig:Button> | |
| </form> | |
| // ... line 31 | |
| </div> | |
| </twig:Modal> | |
| </td> | |
| </tr> |
That doesn't exist yet, so in the Button class, add it: variant and it just needs text-white:
| // ... lines 1 - 7 | |
| class Button | |
| { | |
| // ... lines 10 - 12 | |
| public function getVariantClasses(): string | |
| { | |
| return match ($this->variant) { | |
| // ... lines 16 - 18 | |
| 'link' => 'text-white', | |
| // ... line 20 | |
| }; | |
| } | |
| } |
After the form, to render the delete button, include voyage/_delete_form.html.twig:
| <tr class="even:bg-gray-700 odd:bg-gray-600" id="voyage-list-item-{{ voyage.id }}"> | |
| // ... lines 2 - 4 | |
| <td class="px-6 py-4 whitespace-nowrap flex"> | |
| // ... lines 6 - 12 | |
| <twig:Modal :allowSmallWidth="true"> | |
| // ... lines 14 - 26 | |
| <div class="flex justify-between"> | |
| <form method="dialog"> | |
| <twig:Button variant="link">Cancel</twig:Button> | |
| </form> | |
| {{ include('voyage/_delete_form.html.twig') }} | |
| </div> | |
| </twig:Modal> | |
| </td> | |
| </tr> |
Oh, and that template has a built-in confirm. Delete that because we have something way nicer now.
Moment of truth! Refresh and delete. It looks great! Cancel closes the modal... and deleting works. And it shouldn't be a surprise that it works. The delete form is not inside a <turbo-frame>. So when we click delete, that triggers a normal form submit that redirects and causes a normal full page navigation.
Hiding Search Options in a Modal
Ok, I know this is already a full day, but I really want to use the modal in one more spot. And it's a cool use-case.
On the homepage, in my PHP & Symfony code, I won't show it, but I already added logic to filter this list by the planets. I only didn't add any planet checkboxes to the page because... we don't really have space for them.
So here's my idea: add a link here that opens a modal that holds the extra filtering options.
Open up main/homepage.html.twig and find that input. Start by adding a <div class="w-1/3 flex">... add the closing on the other side of the input... then remove w-1/3 from the input. We're making space for that link:
| // ... lines 1 - 27 | |
| {% block body %} | |
| <div class="flex"> | |
| // ... lines 30 - 36 | |
| <section class="flex-1 ml-10"> | |
| <form | |
| method="GET" | |
| action="{{ path('app_homepage') }}" | |
| class="mb-6 flex justify-between" | |
| data-controller="autosubmit" | |
| data-turbo-frame="voyage-list" | |
| > | |
| <div class="w-1/3 flex"> | |
| <input | |
| type="search" | |
| name="query" | |
| value="{{ app.request.query.get('query') }}" | |
| aria-label="Search voyages" | |
| placeholder="Search voyages" | |
| class="px-4 py-2 rounded bg-gray-800 text-white placeholder-gray-400" | |
| data-action="autosubmit#debouncedSubmit" | |
| autofocus | |
| > | |
| </div> | |
| // ... lines 57 - 59 | |
| </form> | |
| // ... lines 61 - 145 | |
| </section> | |
| </div> | |
| {% endblock %} |
But I'll paste in a full modal:
| // ... lines 1 - 27 | |
| {% block body %} | |
| <div class="flex"> | |
| // ... lines 30 - 36 | |
| <section class="flex-1 ml-10"> | |
| <form | |
| method="GET" | |
| action="{{ path('app_homepage') }}" | |
| class="mb-6 flex justify-between" | |
| data-controller="autosubmit" | |
| data-turbo-frame="voyage-list" | |
| > | |
| <div class="w-1/3 flex"> | |
| <input | |
| type="search" | |
| name="query" | |
| value="{{ app.request.query.get('query') }}" | |
| aria-label="Search voyages" | |
| placeholder="Search voyages" | |
| class="px-4 py-2 rounded bg-gray-800 text-white placeholder-gray-400" | |
| data-action="autosubmit#debouncedSubmit" | |
| autofocus | |
| > | |
| <twig:Modal> | |
| <twig:block name="trigger"> | |
| <twig:Button | |
| variant="link" | |
| type="button" | |
| data-action="modal#open" | |
| >Options</twig:Button> | |
| </twig:block> | |
| <h3 class="text-white text-lg font-semibold mb-2">Search Options</h3> | |
| <hr class="mb-4"> | |
| <div class="flex justify-end"> | |
| <twig:Button | |
| variant="success" | |
| data-action="modal#close" | |
| >See Results</twig:Button> | |
| </div> | |
| </twig:Modal> | |
| </div> | |
| // ... lines 77 - 79 | |
| </form> | |
| // ... lines 81 - 165 | |
| </section> | |
| </div> | |
| {% endblock %} |
This will be invisible except for the trigger. So we basically just added a button that says "options". But it's already set up to open the modal. Inside, to start, we have an h3 and a <twig:Button> that closes the modal.
Adding a Modal Close Button
But the result when I click options... is nice! Though, it needs a close button on the upper right. We could add it to just this modal... but it might be nice if it were an option in the modal component.
Let's do it! In Modal.html.twig, add one more prop called closeButton defaulting to false:
| {% props allowSmallWidth=false, closeButton=false %} | |
| // ... lines 2 - 37 |
If that's true, at the end of the dialog, I'll paste in a close button:
| {% props allowSmallWidth=false, closeButton=false %} | |
| <div | |
| // ... lines 3 - 6 | |
| > | |
| // ... lines 8 - 9 | |
| <dialog | |
| // ... lines 11 - 13 | |
| > | |
| // ... lines 15 - 19 | |
| {% if closeButton %} | |
| <button | |
| class="absolute right-4 top-3 text-white flex items-center opacity-70 transition-opacity hover:opacity-100" | |
| data-action="modal#close" | |
| type="button" | |
| > | |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 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> | |
| {% endif %} | |
| </dialog> | |
| // ... lines 30 - 35 | |
| </div> |
Again, nothing special here: some absolute styling, an icon... and the important part: it calls modal#close.
In homepage.html.twig find that modal and add closeButton="true"... but with the : like last time:
| // ... lines 1 - 27 | |
| {% block body %} | |
| <div class="flex"> | |
| // ... lines 30 - 36 | |
| <section class="flex-1 ml-10"> | |
| <form | |
| // ... lines 39 - 43 | |
| > | |
| <div class="w-1/3 flex"> | |
| // ... lines 46 - 56 | |
| <twig:Modal :closeButton="true"> | |
| // ... lines 58 - 74 | |
| </twig:Modal> | |
| </div> | |
| // ... lines 77 - 79 | |
| </form> | |
| // ... lines 81 - 165 | |
| </section> | |
| </div> | |
| {% endblock %} |
Let's check it out! I love that!
Finally, let's frost this cake. Near the bottom of the content, I'll paste in the planet checkboxes:
| // ... lines 1 - 27 | |
| {% block body %} | |
| <div class="flex"> | |
| // ... lines 30 - 36 | |
| <section class="flex-1 ml-10"> | |
| <form | |
| // ... lines 39 - 43 | |
| > | |
| <div class="w-1/3 flex"> | |
| // ... lines 46 - 56 | |
| <twig:Modal :closeButton="true"> | |
| // ... lines 58 - 65 | |
| <h3 class="text-white text-lg font-semibold mb-2">Search Options</h3> | |
| <hr class="mb-4"> | |
| <h4 class="text-white text-sm font-semibold mb-2"> | |
| Planets | |
| </h4> | |
| {% for planet in planets %} | |
| <div class="flex items-center mb-4"> | |
| <input | |
| type="checkbox" | |
| class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" | |
| name="planets[]" | |
| value="{{ planet.id }}" | |
| id="planet-search-{{ planet.id }}" | |
| {{ planet.id in searchPlanets ? 'checked' : '' }} | |
| > | |
| <label for="planet-search-{{ planet.id }}" class="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">{{ planet.name }}</label> | |
| </div> | |
| {% endfor %} | |
| // ... lines 84 - 89 | |
| </twig:Modal> | |
| </div> | |
| // ... lines 92 - 94 | |
| </form> | |
| // ... lines 96 - 180 | |
| </section> | |
| </div> | |
| {% endblock %} |
This is more boring code! I loop over the planets and render input check boxes. My Symfony controller is already set up to read the planets parameter and filter the query.
Final test. Open it up. Lovely! Now watch: click a few. When I press "See Results", the table should update. Boom. It did!
But the coolest part is... how this worked! Think about it: I click this button... and the table reloads. That means the form is submitting. But... what caused that? Look at the button: there's no code to submit the form. So what's going on?
Remember: this button, the planet checkboxes and this modal physically live inside the <form> element. And what happens when you press a button that lives inside a form? It submits the form! We run the modal#close, but we also allow the browser to do the default behavior: submitting the form. This is ancient alien technology at work!
On the close button, I was a bit sneaky. When I added that, I included a type="button". That tells the browser to not submit any form that it might be inside. That's why when we click "X", nothing updates. But when we click "see results", the form submits.
Woh! Best day ever! Tomorrow, it's time to look at Live components, where we take Twig components and let them re-render on the page via Ajax as the user interacts when them.
12 Comments
Hello guys, thanks for the fabulous tutorials :)
I get the following error message in the console when opening the modal
Error: Missing target element "loadingContent" for "modal" controllerWhat is missing here? I also see that comes in your finished example as well. Can I ignore this?
Cheers!
Hey @creativx007
That's a little mistake coming from the course code (I'm going to fix it soon). For the moment, replace
loadingTemplatewithloadingContentintemplates/components/Modal.html.twigat line 37Cheers!
hey, thank you very much. That works.
Can I ask another question? I have also installed Flowbite for the design blocks. I only create the modals using the dialog element. But now I always get the following error in the console every time I call it:
Modal with id dialog does not exist. Are you sure that the data-modal-target attribute points to the correct modal id?Modal with id dynamicContent does not exist. Are you sure that the data-modal-target attribute points to the correct modal id?<br />Modal with id loadingContent does not exist. Are you sure that the data-modal-target attribute points to the correct modal id?How can I prevent this? Thanks and cheers :)
Great! I'm glad to know that it worked. About your other question, I'm not familiar with Flowbite, but unless those error messages are misleading, I think you need to double-check your modal id value, and also check that you're correctly setting the value of
data-modal-targetCheers!
Good morning :) Thank you very much, it is really a pleasure to work here - you get competent help and it works. I was able to fix the error with the ID dialog. I still can't solve the other two errors (Modal with id dynamicContent and Modal with id loadingContent).
In the base.html.twig template I have the following block:
Here I have the target dynamicContent - but I don't know what's happening here. The error complains that a modal with the ID dynamicContent doesn't exist. In the entire code, "dynamicContent" only appears in connection with data-modal-target. Do you have any ideas?
Cheers :)
I'm a bit confused about the error messages. It says that the "Modal with id dynamicContent" does not exist. So, it makes me think that you're creating 3 modal elements, but you should have only one. Please, double check that you're rendering the Modal component only once.
Now, the modal controller requires 3 target elements, dialog, dynamicContent, and loadingTemplate. Double-check that these 3 elements are set up correctly. You can check this code block or download the course code and look at the
finishdirectoryhttps://symfonycasts.com/screencast/last-stack/modal-component#codeblock-54eed16182
I hope it helps!
Hi guys,
I've come accross the comments, and I can say that I've got the same issue here.
3 same errors saying that the
data-modal-targetare not set, but it is...The modal works just fine, but still got the errors on page loading and after every action
Hi everyone !
Thanks again for that great tutorial !
I need to load an
<turbo-frame>modal (exactly like you do) but triggered from an stimulus controller (action).To do that I tried by putting an empty a tag in my html like
<a href="my_url" id="myIdLinkToLoadTurboFrame" data-turbo-frame="turbo_frame_id"></a>.And in my stimulus controller I do
this.element.querySelector('#myIdLinkToLoadTurboFrame').click();Work fine with Firefox, but doesn't work at all with Chrome and Safari.
Is it possible to trigger the same action of clicking the data-turbo-frame link but from an stimulus controller ?
Cheers!
Hey @ThierryGTH!
Really glad you enjoyed it!
Here's how I would probably handle this:
A) Pass the url -
my_url- into your Stimulus controller as a value so you have access to the URLB) Instead of trying to click the link, find the frame and set its
src:That should be all you need. The frame will see that the
srcjust changed and start doing its thing. In fact, I think when you havedata-turbo-frame="turbo_frame_id", all that does internally is take thehrefand put it onto thesrcof theturbo-frame. Then the frame handles things from there.Cheers!
Thank you very much !
That's work perfectly and it's much prettier setting src than virtually clicking a tag.
The issue with Chrome and Safari was just because I was using
data-action=click->inside an select option tag :<select><option data-action="click->myController#myFunctionToClickVirtualy>Add item</></select>The solution is to handle at the select level :
<select data-action="myController#myOnChangeFunctionToManageWhatToDoDependingValue"><option value="addItem">Add item</option></select>Cheers!
Hi!
Before, I was always using
stimulus_controller('modal')instead ofdata-controller="modal"in twig... something I learned from your other Stimulus tutorial and that I grew used to. I was using stimulus_target() and stimulus_action() the same way. This does not work inside the twig component's default attributes though.If i use this in Modal.html.twig
it complains: A hash key must be followed by a colon (:). Unexpected token "punctuation" of value "(" ("punctuation" expected with value ":").
Looking forward to finishing this great course!
Hey @escobarcampos!
Yea, I created these helpers and get used to them... then recently got accustomed to the attribute names. Now those feel better to me. But, both are fine!
You can do this - here's the reference in general about this (there's a note in this section) - https://symfony.com/bundles/ux-twig-component/current/index.html#component-attributes - I think you could combine them in this way, but I could be totally borking the syntax ðŸ«
The
.defaults()functions accepts a "hash" (i.e. associative array) andstimulus_controller('modal')returns an iterable object. So, iirc, using...to spread that will work. But let me know. It's not super smooth, and it may even have some escaping differences... I can't quite remember. This definitely helped push me towards just using the attributes.Cheers!
"Houston: no signs of life"
Start the conversation!