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 SubscribeWe need to initialize the useTransition()
behavior on this controller so we can call this.enter()
or this.leave()
inside toggle to show or hide the results.
This time, instead of putting all the code right here, let's create a reusable function so that we can quickly add our fade transition behavior to other controllers in the future.
In the assets/
directory, add a new directory called, how about, util/
. And inside that a new file called add-transition.js
. I'll paste in the code: you can get this from the code block on this page.
import { useTransition } from 'stimulus-use'; | |
export function addFadeTransition(controller, element) { | |
useTransition(controller, { | |
element, | |
enterActive: 'fade-enter-active', | |
enterFrom: 'fade-enter-from', | |
enterTo: 'fade-enter-to', | |
leaveActive: 'fade-leave-active', | |
leaveFrom: 'fade-leave-from', | |
leaveTo: 'fade-leave-to', | |
hiddenClass: 'd-none', | |
}); | |
} |
This exports a named function called addFadeTransition()
which adds the useTransition
behavior to the passed controller. Most of what you see here is identical to what we had when we originally leveraged useTransition
.
Cool! Back in our controller, in the connect()
method, which is normally where we add behaviors, say addFadeTransition
and hit tab to autocomplete this... so that PhpStorm adds the import
for us! Nice! Pass the controller - which is this
- and then we need to pass the element that is going to be hidden or shown... which we don't actually have access to yet. Let's add a target for that. Pass this.resultsTarget
... then initialize that target above: static targets =
an array with results
inside.
... line 1 | |
import { addFadeTransition } from '../util/add-transition'; | |
... line 3 | |
export default class extends Controller { | |
static targets = ['results']; | |
connect() { | |
addFadeTransition(this, this.resultsTarget); | |
} | |
... lines 10 - 13 | |
} |
In the template, we need to add the target to the results div
. Hmm, this already has a target for the autocomplete
controller. Copy that, paste, and add an identical target for our autocomplete-transition
controller.
... lines 1 - 2 | |
{% block body %} | |
... lines 4 - 37 | |
<form> | |
... lines 39 - 57 | |
<div | |
... lines 59 - 60 | |
data-autocomplete-transition-target="results" | |
></div> | |
... line 63 | |
</form> | |
... lines 65 - 114 | |
{% endblock %} |
It is a bit weird to have two different targets on the same element... but this is totally allowed. If you really didn't like this, you could actually find this element manually in your controller by using the this.element.querySelector()
to find this element using the data-autocomplete-target
attribute.
Anyways, back in our toggle method, because we've initialized the useTransition
behavior, we now have enter()
and leave()
methods. And so, if event.detail.action
equals open
, call this.enter()
. Else, call this.leave()
.
... lines 1 - 3 | |
export default class extends Controller { | |
... lines 5 - 10 | |
toggle(event) { | |
if (event.detail.action === 'open') { | |
this.enter(); | |
} else { | |
this.leave(); | |
} | |
} | |
} |
Let's try it! Move over, refresh, type "de" and... yes! It transitioned!
As a reminder, the details of this transition - like the fact that it fades for 2 seconds - live in the app.css
file. Search for "fade": here they are.
By the way, that 2000 milliseconds is way too long: I'm only using that so the transition is obvious.
Anyways, back at the browser, type to re-open the suggestions then click off of to close it. That happened instantly! Where was our transition?
Inspect the element. Ah: see that little hidden
attribute on the results div
? That was added by the stimulus-autocomplete
controller as soon as we clicked off. Thanks that, the element became hidden instantly instead of waiting for our transition.
Normally, that's great! It's how stimulus-autocomplete
hides the results. But now that we are controlling the hiding and showing with our transition behavior, we do not want this hidden
attribute to be added. Fortunately, assuming my PR is merged, we can pass a value to disable that behavior.
In the template, on the autocomplete
controller, pass a new value called skipHiddenProperty
set to true
.
... lines 1 - 2 | |
{% block body %} | |
... lines 4 - 37 | |
<form> | |
<div | |
... line 40 | |
{{ stimulus_controller({ | |
'autocomplete': { | |
... line 43 | |
skipHiddenProperty: true | |
}, | |
... line 46 | |
}) }} | |
... line 48 | |
> | |
... lines 50 - 63 | |
</div> | |
</form> | |
... lines 66 - 115 | |
{% endblock %} |
That literally says: please do not set that hidden
property: we are handling the hiding and showing ourselves.
Let's try it out again. I'll type... we still get the nice fade in... and when I click off. Yes! It fades out!
And.... we're done! I mean, the whole tutorial is done! I hope you found this journey through Stimulus as refreshing as I did. I love coding with Stimulus.
In the next tutorial in the series - about Turbo - I hope to show prove that we can have an even more dynamic and speedy app while writing even less custom JavaScript.
Let us know what cool stuff your building. And, as always, if you have any questions, we're here for you in the comments section.
Okay, friends, seeya next time!
Hey Julien,
Fairly speaking, me too! But yeah, when you give it a try - you'll start to like it :) Thank you for your feedback and interest in SymfonyCasts tutorials! ;)
Cheers!
Hello,
I have a problem that I can't solve. I am using dropzone UX for adding and editing media on my project.
For when editing your media, I would like to have the media preview by default instead of drag and drop. I can't find a way to use the targets generated by dropzone UX in my controller. Do you have a method to do this?
thanking you in advance.
Hey Jimmy!
Hmm. Is this "media preview" something you're building by hand? Or are you referring to the little preview that you normally see after dragging/dropping a file onto the dropzone? And, do you still want the dropzone - but also the preview? Or just the preview?
Anyways, to your real question:
I can't find a way to use the targets generated by dropzone UX in my controller. Do you have a method to do this?
Hmm. Indeed, there's not an way to access those targets, as we don't expose them in the events and we also don't give you access to the underlying Dropzone controller instance from the event. However, there are some strategies on this page for accessing one Stimulus controller from another - https://www.betterstimulus.com/interaction/controller-dom-mapper.html
Assuming you can get access to the dropzone controller, then, in theory, you could say dropzoneController.previewImageTarget
.
Let me know how it goes - this makes me feel like there is something missing that could make this easier to do.
Cheers!
Hello Weaverryan,
Indeed, I am referring to the small preview when dragging and dropping.
I find it unfortunate when editing a post not to have a preview of the image already in place using ux_dropzone. I'll try to see with the leads you provided me.
Cordially.
Hello all. And thank you very much for this tutorial.
I am French speaking and I use google translate to translate.
I followed this training until the end and it's very cool for someone like me who is a backend coder.
However, I have a concern with UX-Dropzone. In fact I am on a Symfony 5.4 project and for a form in my app, I loaded the 2.6.1 version of ux-dropzone. I have followed the documentation on the subject end to end but, when I post my form after dragging and dropping a file, I get a 422 error. Need help.
Thanks in advance.
`
My formType
<?php
namespace App\Form;
use App\Entity\SourceReference;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\UX\Dropzone\Form\DropzoneType;
class SourceReferenceType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('filename', DropzoneType::class, [
'attr' => [
'data-controller' => 'dropzone-form',
'placeholder' => 'Drag and drop a file or click to browse',
],
'label' => false,
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => SourceReference::class,
]);
}
}
My controller
/**
* @Route("_{id}/reference", name="app_source_reference", methods={"GET", "POST"})
*/
public function sourceReferenceUpload(
SourceCategory $category,
Request $request
): Response {
$source = new SourceReference($category);
$form = $this->createForm(SourceReferenceType::class, $source);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
if ($request->isXmlHttpRequest()) {
return new Response("I'm in!", 204);
}
return $this->redirectToRoute('app_source_reference', [
'id' => $category->getId()
]);
}
$template = $request->isXmlHttpRequest() ? '_form.html.twig' : 'index.html.twig';
return $this->render('source_reference/' . $template, [
'category' => $category,
'form' => $form->createView()
], new Response(
null,
$form->isSubmitted() ? 422 : 200
));
}
My dropzone-form_controller.js file
import {Controller} from "@hotwired/stimulus";
import $ from "jquery";
import {useDispatch} from "stimulus-use";
export default class extends Controller {
static targets = ['dropzoneBody'];
static values = {
formUrl : String
}
connect()
{
useDispatch(this, {debug: true});
}
async submitForm(event)
{
event.preventDefault();
const $form = $(this.dropzoneBodyTarget).find('form');
try {
await $.ajax({
url: this.formUrlValue,
method: $form.prop('method'),
data: $form.serialize()
})
this.dispatch('success')
} catch (e) {
this.dropzoneBodyTarget.innerHTML = e.responseText
}
}
}
`
Hey Diarill,
The 422 error means that you got a validation error, could you show it to me, please? It's hard to spot what's wrong in your code, apparently, it looks fine
Cheers!
Hello MolloKhan and thank you for answering me.
Below the error I am getting
`
Symfony\Component\Validator\ConstraintViolation {#1471 ▼
-message: "This value should not be blank."
-messageTemplate: "This value should not be blank."
-parameters: [▼
"{{ value }}" => "null"
]
-plural: null
-root: Symfony\Component\Form\Form {#1180 ▶}
-propertyPath: "data.filename"
-invalidValue: null
-constraint: Symfony\Component\Validator\Constraints\NotBlank {#930 …}
-code: "c1051bb4-d103-4f74-8988-acbcafc7fdc3"
-cause: null
}
`
Ohh, the file field it's empty. You'll need to tweak your JavaScript code because you're using Dropzone (I overlooked that fact). Here you can see how Ryan implements file uploading via Dropzone https://symfonycasts.com/screencast/symfony-uploads/dropzone
I recommend watching the next chapter as well.
I hope it helps. Cheers!
Hello MolloKhan. Sorry to respond late. I was traveling.
The suggestion you made for me to look at how Ryan implements file upload with dropzone, I understand it perfectly. However, I encountered small problems with this way of doing things, in particular, doing the pagination, and the localization for the download URLs of the uploaded files. These are the reasons that lead me to change and stimulus seems interesting to me as an approach. Except that I'm not very comfortable with front-end-oriented languages yet. I had time to read some jquery articles and I could see that there was a difference between Serialize() and FormData(). the Serialize() function allows you to post forms that do not contain file type fields, but FormData() allows it. I think that's where my problem lies. Now the difficulty lies in how to redo my code in the 'dropzone-form_controller.js' file by integrating FormData() rather than Serialize(). Thank you for your support.
Hi,
Could you please advise what's the best practice to use nested/child targets of parent target?
For example i have menu items, each item (wrapper) contain link and list.
I could add data-main-menu-target="menuItem" to each of parent items and then iterate over them in controller loop using this.menuItemTargets.forEach(...)
But what's the best practice to find menu-item-link and menu-item-list for each menu-item target per each loop iteration?
In general i could add add targets also for those elements, e.g. menuItemLink & menuItemList, but how then i could select them from parent menuItem target, Is it possible to do something like "this.menuTarget.find(this.this.menuListTarget)"?
To visualise the structure is the following:
data-controller="main-menu"
data-main-menu-target="menuItem"
data-main-menu-target="menuLink"
data-main-menu-target="menuList"
....
data-main-menu-target="menuItem"
data-main-menu-target="menuLink"
data-main-menu-target="menuList"
....
How then select "menuLink" for certain "menuItem" target on each loop?
Thanks!
Hi again @Kirill!
Hmm, that's an interesting question!
Is it possible to do something like "this.menuTarget.find(this.this.menuListTarget)"?
I totally get what you're saying here. Unfortunately, I don't think that's possible. So I don't think there's a silver bullet here. I see two options:
A) For menuLink and menuList, you handle it by yourself: use CSS classes, and then use normal selectors. So, once you've used the menuItem target to find the menuItem you want, you would then do menuItem.querySelector('.menu-link')
. Not a Stimulus solution, but it's pretty simple and it's nice to be able to "back out" and do things manually if you need to.
B) I'm not sure what your overall Stimulus controller is meant to do, but it's possible that there should be a menu-item
controller that lives on the menuItem
target. Depending on what you're trying to accomplish, that could replace the main-menu
controller or, more likely (because I'm assuming you are doing some "work" on the top level main-menu where you want to be aware of all of the "items"), in addition to the main-menu
controller. With this setup, your main-menu
controller could loop over the menuItem
targets and, in each one, directly use its underlying controller instance - even calling methods on it. This is not something I showed on the tutorial, but it's not an uncommon pattern: you would expose the "controller instance" of the "menu-item" controller on its element - e.g. https://www.betterstimulus.com/interaction/controller-dom-mapper.html (the big difference in that example is that both of the controllers are on the same element - so adjust accordingly).
Let me know if either of these sound good. The best answer really depends on what you want to accomplish.
Cheers!
Hi,
A) Yeap, seems that using simple CSS class inside the target element is not too bad and really simple.
We could even add sub-targets (CSS classes) to controller values to not hardcode them.
B) That's interesting, I will hold this in mind for more complex controllers, thank you for reference!
Thanks for reply!
I really appreciate your time for supporting as well as for high quality tutorials!
Hi,
Thanks for awesome tutorial, stimulus is really amazing and you tutorials the best as always!
I found that your PR was removed later in this commit: https://github.com/afcapel/...
They had replaced "skipHiddenProperty" with set hide getter and setter (that were later renamed to get/set resultsShown)
So what's now the correct way to avoid hiding results element?
It's to Extend 3rd part "autocomplete" controller and override this setter (leave it empty) to avoid setting hide on the element at all and then use your custom/overrided controller name in HTML data-controller attr?
Is that the right way to use their idea why they removed property and created methods?
Thanks!
Hey @Kirill!
Thanks - I'm happy it was useful!
They had replaced "skipHiddenProperty" with set hide getter and setter (that were later renamed to get/set resultsShown)
Hmm, yes, I thought this might happen - thank you for pointing that out.
It's to Extend 3rd part "autocomplete" controller and override this setter (leave it empty) to avoid setting hide on the element at all and then use your custom/overrided controller name in HTML data-controller attr?
I need to play with the new code (so we can add a proper note to the tutorial), but yes, this is generally the idea. The code should actually look just like what the author suggested: https://github.com/afcapel/stimulus-autocomplete/pull/82#issuecomment-968332706 - you DO need to keep track of if the element is hidden or not, but you do it by setting a new _isHidden
property, instead of actually hiding the element.
and then use your custom/overrided controller name in HTML data-controller attr?
Exactly. You should be able to point to YOUR custom controller ONLY. Then, due to inheritance, you'll get all the autocomplete magic, but with the overridden methods :).
If you get something working, I'd love to know!
Cheers!
For other's looking, I finally got a chance to look at this in more detail. IF you're using the latest version of stimulus-autocomplete (which requires @hotwired/stimulus 3 and the ^0.50.0-2
of stimulus-use or higher), then here is how to get our example working. It's actually quite a bit simpler now: https://gist.github.com/weaverryan/b5e24b429a4c129f336dbb7b70b353a0 - or check out the "diff" from our code in the tutorial: https://gist.github.com/weaverryan/b5e24b429a4c129f336dbb7b70b353a0/revisions#diff-d20275d811dfd8cb0706be57a23f4888912d34fe11a762f5ab9e22228ec8f1dd
Cheers!
Hi Ryan, Thank you for the code gist. Would be good to update/mention in video this one.
Personally I like previous custom code more. As now with mixed inheritance and configuration how is now extended this Autocomplete package, this also merged together functionality and design decisions in one controller. UX and UI together throwing away all the benefits of events and separate code for design decisions.
Frankly without using events, listeners (data-action) and just extending that controller, I am feeling like throwing everything away for what the Stimulus is here for.
I was very sceptical about Stiumulus at first.
I am coming from the alpine.js side and I`ve done a lot even complex stuff with it in the last year.
But you totally convinced me. I love stimulus ;) and I love your tutorials. I can totally see, why you decided to use that hotwire ecosystem.
I can't wait for the turbo one.
Good job Ryan! Thanks for your efforts.
Hey Michael B.!
Ah!!! This is awesome - and I'm so honored. Alpine is a very powerful system - so I'm even happier that you like Stimulus (I also much prefer its approach).
And Turbo is... starting today!
Cheers!
Hi, great tutorial ! Is there a way to create a height animation with stimulus use ? The only way I see it is to create a max-height animation
Hey upmwqn!
I haven't tried this yet, but since we're using CSS transitions, the trick to this is to find the CSS needed for such a transition. For example, here's an answer - https://stackoverflow.com/q... - and corresponding Fiddle: http://jsfiddle.net/b62soqdv/
You would need to adjust the styles to fit into the classes used for the transitions, of course :). Try it out and let me know if you hit any problems.
Cheers!
Hey Tien!
Thank you for interest in SymfonyCasts tutorials and your feedback, we're really happy to hear it!
Cheers!
Hi Ryan, I have a project in Symfony 4.3 and I can't update at the moment. Is there any possibility of including stimuli in that project?
I have updated the version of the webpack encore to 1.1.2
but it gives me an error when compiling.
Thanks for a nice tuto!
Hey Alberto,
In theory it should work, you just need to upgrade your webpack encore bundle to the latest. But it requires at least Symfony 4.4. If you're on Symfony 4.3 - it should be an easy upgrade to 4.4 for you, most probably you won't need to do anything special with your code as no BC breaks should be introduced there.
Sorry, can't say more without the seeing the error you have.
Cheers!
// 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.18.5
"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
}
}
// 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
}
}
Once again, GRrrrreat tutorial! I was very sceptical about Stimulus at first, but it's kinda fun and allows developing reactive applications faster.
Thanks for that great tuto!
Cheers!