Vue.js explained through Pokemon #2 $refs, Promises & event bus
This is the second article in my tutorial series that tries to explain Vue.js concepts by recreating the battle scene from Pokemon.
Original tutorial
#1 Single file components
#2 Attacks: $refs, Promises & event bus
#3 Vuex: state management
#4 Damage calculations
#5 Transitions & animation
In this second installment we’ll be bringing back the attack functionality.
There are several ways we can do this.
- Hack it together with $refs and Promises
- Use an event bus that does the heavy lifting
- Separate the logic/data further with Vuex, a state management library.
We’ll be looking at the first 2 options in this article since they’re not too hard to explain and implement.
Let’s dive right in.
Attack functions
First thing’s first, let’s define our attack functions.
Open up Pokemon.vue, we’re going to be adding an attack() and a pickRandomAttack() method.
Let’s define them under a newly created methods property:
As you can see at this time the attack function doesn’t really offer a way to notify its caller of a faint or when to continue to the next action. We’ll tackle this next.
A small note We’re accessing the pokemon component’s parent state through this.$parent. As you can guess it returns the Vue Instance of the component that the pokemon component is referenced in. In our scenario we’re sure that App.vue will always be the parent, so it’s possible to use it. In other scenario’s where you’re not sure you’ll have to find a way around this (through events or a state manager).
Promise-based approach
If you don’t know about promises you can quickly read up on the use case and API here.
Basically promises allow us to cleanly define a chain of command.
It starts by defining a function that can either pass or fail, with then() we can define what happens when it gets resolved successfully. With catch() you can define what should happen when it fails.
Let’s describe our chain of command.
When a user selects an attack:
- We reduce the opponent’s HP
- Wait a second for the HP bar to drop
- If the opponent is alive, select a random attack and reduce the player’s HP
- wait a second for the HP bar to drop
After step 1 & 3 we’ll also update the battle text with something like ‘{pokemon} used {attack}‘.
At the beginning of step 3 and at the end of step 4 we’ll check if there’s enough HP left, if not We’ll fail the promise and show that the pokemon has fainted.
After a success we’ll have to reset the battle text to ‘What will {pokemon.player.name} do?’.
Let’s translate this process into a promise resolution:
- Promise: attack opponent pokemon
- Fails on (reject): opponent’s HP drop to 0
- Succeeds on (resolve): opponent has HP left
- On success (then): return a new promise that selects a random attack from the opponent and use it on the player.
- Fails on (reject): player HP drop to 0
- Succeeds on (resolve): player has HP left
- On success (then): reset the battle text
- on fail (catch): change the battle text and show endOptions menu
Let’s pour that into code, open up App.vue and add this code under the resetBattle method:
Constructor
As you can see, you create a promise by assigning a function in its constructor. This function get 2 parameters assigned: a resolve callback and a reject callback. As you guessed correctly, you should call these one success/failure.
Do you remember from last tutorial that we added a ref attribute to the pokemon components?
Because of that we can access its state and methods through this.$refs with the assigned name.
We’ll have to adjust the attack function so it also takes the resolve and reject callbacks as parameters.
then
The first then will be called if the opponent still has HP left.
In which case a random attack is selected and performed on the player’s pokemon.
We do this by returning another promise (since the previous promise has already succeeded).
then
The second then is called if the player’s pokemon still has HP left.
This time we’ll just want to reset the battle text so the player can keep on playing.
catch
Catch will be called when a pokemon faints. This could be either the opponent or the player’s pokemon.
It’s good to know that if the promise is rejected when the player attacks, it won’t reach the opponent’s new promise.
We’ll have to call the reject callback and assign it the name of the pokemon that has fainted.
Let’s go back to Pokemon.vue and adding the resolve/reject callbacks:
That’s it for promises, the attacks are working.
You can find a copy of the code right here:
Event based approach
Taking this approach is a little bit different. It takes a little bit more code, but it’s more verbose.
The main difference with a promise based approach is that each event has its own name, so the code will be easier to read and understand.
We’ll be using vuemit to do the heavy lifting for us. This package allows us to define events globally, while still allowing us to access the state we want.
Let’s start off by pulling it in.
Once that’s done we’ll have to add it to our main.js, right after loading Vue.js.
Events
Let’s take a look at a more traditional Javascript approach.
We’ll want to make use of 4 events:
- player.attack(attackName): performs the player’s pokemon attack.
- opponent.attack(): performs the opponent’s pokemon attack.
- attack.completed(): resets the battle text.
- fainted(pokemonName): changes the battle text and show endOptions
Let’s register these once the App.vue has been mounted, this means we’ll register the events once the app has been loaded and is ready to use.
You can define it on the Vue instance definition, I tend to put it right after any data definitions (after the data method & computed property).
As you can see it’s nearly the same logic we used when defining the promises.
However it reads more cleanly, it’s been logically named and ordered.
Implementation
Let’s switch out the promise-based implementation with our event-based implementation by changing the processAttack method to the following:
It’s as simple as that, we don’t need more than that one line of code, all that’s left to do is rewrite the attack function.
We’ll just have to replace the places where we used the resolve and reject callbacks previously.
Let’s open up Pokemon.vue and change the attack method accordingly:
There we have it, that’s how you would use events instead of promises.
You can find the code for this approach right here:
Next time we’ll be looking into using Vuex to make it easier to manage state, so we can start preparing for implementing some more heavier logic that doesn’t really belong inside the components.
As always you can get the most recent code on github:
https://github.com/happyDemon/learning-vue-through-pokemon