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 SubscribeI want to add a CSS transition so that our search preview fades in and out, instead of showing up immediately. This may seem like a small detail, but this kind of stuff can be the difference between a mediocre interface and a truly beautiful experience.
So how can we do this? We learned some CSS transition basics earlier. In assets/styles/app.css
, if you look for cart-item
... we set transition: opacity 500ms
. When that element also has a removing
class, we set opacity
to 0.
The result is that, as soon as we added this removing
class to an element that already has the cart-item
class, that element's opacity
changed from 1 to 0 over 500 milliseconds.
So CSS transitions are simple, right?
Well... not always. If we repeated that trick here, we could transition the opacity
of this element to 0. But then... the element would still technically be there... it would just be invisible. So if the user clicked right here, to them, it would look like they were clicking this photo. But in reality, they would be clicking the invisible dropdown above, which would take them to the disappearing ink pens page.
Ok then! So we just need to fade this element to opacity 0, wait 500 milliseconds for the transition to finish... and then fully hide the element. Right? Yea! That's exactly what we need to do! But sheesh, this is getting complicated! And if, later, we decided to change the transition in CSS from 500 milliseconds to 1 second, we would need to remember to go into our Stimulus controller and also change the delay there to 1 second... so that we don't hide the element before it's finished fading out.
So that is why CSS transitions are trickier than they seem at first. Fortunately, the stimulus-use
library we installed earlier - to get behaviors like clickOutside
and dispatch
- has a solution for us!
Head to their docs and find a behavior called useTransition
. This is a brand new feature that allows us to add CSS transitions when we need to hide or show an element. At the time of recording, this is currently a beta feature... which means it could change a bit - but I'll update the video if that happens.
The feature was introduced in version 0.24.0... and it's so new that I need to upgrade my stimulus-use
to get it!
Find your terminal and run:
yarn upgrade stimulus-use@beta
I'm using @beta
because the feature isn't included yet in a stable release at the time of this recording. The 0.24.0-1
version is a beta release. Once they release 0.24
stable or higher, you won't need that.
Open up the current search-preview_controller
. As a reminder, as we type, this makes an Ajax call to an endpoint that returns the HTML of the search preview, which we then put into resultTarget
. That's this element right here.
To activate the useTransition
behavior, we'll do the same thing as the other behaviors. Import useTransition
... and down in connect()
, initialize it: useTransition(this)
.
... line 1 | |
import { useClickOutside, useDebounce, useTransition } from 'stimulus-use'; | |
... line 3 | |
export default class extends Controller { | |
... lines 5 - 11 | |
connect() { | |
... lines 13 - 14 | |
useTransition(this, { | |
... lines 16 - 23 | |
}); | |
} | |
... lines 26 - 44 | |
} |
But this behavior has several required options. The first is called element
. This is the element that we want to hide or show. For us, that's this.resultTarget
.
I'm going to paste in the next six options.
... lines 1 - 11 | |
connect() { | |
... lines 13 - 14 | |
useTransition(this, { | |
element: this.resultTarget, | |
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', | |
}); | |
} | |
... lines 26 - 46 |
These are CSS classes that we're going to create in our CSS file in a minute. Don't worry about them yet. The last option we need is hiddenClass
set to d-none
.
You'll see how this is used in a second, but d-none
is a Bootstrap class that sets display: none
on an element.
Before we talk about these options, now that we've initialized the useTransition()
behavior, our controller has 3 new methods! Woo! They're called enter()
, leave()
, and toggle()
.
Down after we make the Ajax call, to show the resultTarget
element, say this.enter()
.
Then, later, when we want to hide it, we can say this.leave()
. And we will not clear the HTML anymore: we'll keep the HTML, but hide the element.
... lines 1 - 3 | |
export default class extends Controller { | |
... lines 5 - 30 | |
async search(query) { | |
... lines 32 - 38 | |
this.enter(); | |
} | |
... line 41 | |
clickOutside(event) { | |
this.leave(); | |
} | |
} |
Okay! That's all we need in the controller. If we do nothing else, the new behavior already gives us an easy way to hide or show an element without CSS transitions.
Let me show you. Reload the page, inspect element on the input and look at the result
target element. Woh! It now has a d-none
class! useTransition
added that!
This happens thanks to the hiddenClass
option that we passed. This tells useTransition
which class to use to fully hide an element. And, by default, useTransition
assumes that your element starts hidden. So as soon as our controller is initialized, it adds that class.
If you have a situation where your element starts visible instead of hidden, you'll need to add a transitioned
option set to true
... though that may be renamed to an option called initialState
that you would set to enter
. Check the docs to be sure.
Anyways, watch the element as I start typing. Boom! The d-none
class is gone and the element is visible! But there was no transition yet: it all happened instantly. When we click off the element, the d-none
is instantly added back. Like I said, if we do nothing else, this behavior gives us an easy way to hide or show an element.
So... how can we make this actually transition? That's up to us! Next, let's add the CSS needed to make the element fade in and fade out. We'll explore the lifecycle of how useTransition
intelligently adds and removes classes at just the right time to make this all possible.
"Houston: no signs of life"
Start the conversation!
// 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
}
}