Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

useTransition in a Neat, Reusable Module

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

We 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.

Creating addFadeTransition()

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.

Setting up the Transition Behavior

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.

The skipHiddenProperty Value

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!

Leave a comment!

19
Login or Register to join the conversation

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!

1 Reply

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!

Reply
Intexsys I. Avatar
Intexsys I. Avatar Intexsys I. | posted 7 months ago

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!

Reply

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.... (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!

1 Reply
Intexsys I. Avatar

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!

Reply
Intexsys I. Avatar
Intexsys I. Avatar Intexsys I. | posted 7 months ago

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!

Reply

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/... - 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!

1 Reply
Intexsys I. Avatar

Hi again! Got it, thanks for reply! :)

1 Reply

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/wea... - or check out the "diff" from our code in the tutorial: https://gist.github.com/wea...

Cheers!

Reply
Peter L. Avatar

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.

Reply
Michael B. Avatar
Michael B. Avatar Michael B. | posted 1 year ago

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.

Reply

Hey Michael Brauner!

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!

Reply
upmwqn Avatar

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

Reply

Hey Mike Devresse!

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!

Reply
Michael B. Avatar

Hooray!

Reply
Default user avatar
Default user avatar Tien Vo Xuan | posted 1 year ago

Thanks for great tutorial!

Reply

Hey Tien!

Thank you for interest in SymfonyCasts tutorials and your feedback, we're really happy to hear it!

Cheers!

Reply
It O. Avatar

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!

Reply

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!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial works perfectly with Stimulus 3!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=7.4.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "1.11.99.1", // 1.11.99.1
        "doctrine/annotations": "^1.0", // 1.11.1
        "doctrine/doctrine-bundle": "^2.2", // 2.2.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.0.2
        "doctrine/orm": "^2.8", // 2.8.1
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "sensio/framework-extra-bundle": "^5.6", // v5.6.1
        "symfony/asset": "5.2.*", // v5.2.3
        "symfony/console": "5.2.*", // v5.2.3
        "symfony/dotenv": "5.2.*", // v5.2.3
        "symfony/flex": "^1.3.1", // v1.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
    }
}

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