By Robin Dickson, software engineer at UniFa.
An RFC (request for comments) for Vue.js was published that explains the plan for a new Function API. Following that, a plugin was created that allows the proposed Function API to be used in current Vue applications: vue-function-api.
I thought I would experiment with the Function API by building a mini app.
Function API Installation
The base app was created using vue cli, and the vue-function-api plugin installed using yarn:
$ vue create janken
$ yarn add vue-function-api
Then the plugin installed explicitly:
import Vue from 'vue'
import { plugin } from 'vue-function-api'
Vue.use(plugin)
The current API (Standard API) still works as usual, and it is even possible to use a hybrid approach (Function API + Standard API).
The app I decided to build was Janken, or in English: Rock, Scissors, Paper.
In this app there are 4 features:
- The player can choose their hand
- The computer chooses their hand and a winner is calculated
- The total amount of points (wins) for each player are shown
- The player can change their name
The initial design was:
<template>
<div>
<div class="score">
<div>Player</div>
<div>0 - 0</div>
<div>Computer</div>
</div>
<div class="player-hands">
<div>✊</div>
<div>✊</div>
</div>
<ul class="hand-choices">
<li>✊</li>
<li>✌️</li>
<li>🖐️</li>
</ul>
</div>
</template>
Vue Implementation
Setup, Data and Value
The first change from the Standard API is that a setup
option is used to set up
the component logic. If you need to use props they are passed to setup
as an argument (more
info).
The score data would have previously be stored in the data
option, which is not used in the Function API.
Instead the data is stored by using the value
API. Data and functions
that are used in the template are returned from the setup
option.
import { value } from "vue-function-api";
export default {
setup() {
const playerScore = value(0);
const computerScore = value(0);
return {
playerScore,
computerScore
};
}
};
<div class="score">
<div>Player</div>
<div>{{ playerScore }} - {{ computerScore }}</div>
<div>Computer</div>
</div>
Methods
The next task was to enable the player to choose their hand. Using the Standard API this can be done using the
methods
option. In the Function API the same can be done using a function.
export default {
setup() {
// ...
const playerHand = value(null);
function submitHand(hand) {
playerHand.value = hand;
}
return {
playerScore,
computerScore,
playerHand,
submitHand
};
}
};
To set (and also get) the value of playerHand
within setup
playerHand.value
must be used.
Computed
The hands are displayed in the UI using emoji. The hand data stored as a string ('rock'
) is converted
to an emoji ('✊'
) with the computed
API (similar to the Standard API's computed
option). Again this is stored to a variable and returned from setup
to be used in the template.
import { value, computed } from "vue-function-api";
export default {
setup() {
const handsToEmoji = {
rock: "✊",
scissors: "✌️",
paper: "🖐️"
};
const isShowGameHands = value(false);
// ...
const playerHand = value(null);
// ...
const playerDisplayEmoji = computed(() =>
isShowGameHands.value
? handsToEmoji[playerHand.value]
: handsToEmoji["rock"]
);
// ...
return {
// ...
playerDisplayEmoji,
computerDisplayEmoji
};
}
};
If there are many methods or computed values in the returned object, these can be grouped into a single object and
destructured in the object returned from setup
.
// example code (not from to the Janken app)
const methods = {
methodA() {
// ...
},
methodB(arg) {
// ...
}
}
const computeds = {
computedA: computed(() => 'a'),
computedB: computed(() => 'b')
};
return {
...computeds,
...methods
};
(see in this example)
Composition Functions
After adding the logic for the game and editing the player name I tried refactoring using a technique made possible in the Function API. Using a composition
function the logic (variables and methods) could be extracted to a separate function, and then included in the
object returned from the main setup
option.
function useName() {
const playerName = value("Player");
const isEditingName = value(false);
function editName() {
isEditingName.value = true;
}
function submitName() {
isEditingName.value = false;
}
return { playerName, isEditingName, editName, submitName };
}
export default {
setup() {
// ...
return {
isShowGameHands,
playerScore,
computerScore,
playerHand,
computerHand,
submitHand,
...computeds,
...useName()
};
}
}
By doing this the code can be organised more clearly, collecting related code together rather than it being separated
between different options(data
, computed
, methods
, etc) which can happen in the
Standard API. It is also possible to reuse the logic in other components.
Although not used in this app lifecycle
hooks and watchers are
used in a similar way to value
and computed
and can also be extracted.
The Janken app and code can be seen and used below. It only took a few steps to get started with the Function API, and there are various features I did not use that I'm looking forward to trying. For more information check out the RFC and try it yourself!
<html> <head> <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script> <script src="https://unpkg.com/vue-function-api@1.0.4/dist/vue-function-api.umd.js"></script> <title></title> </head> <body> <div id="app"> <main> <div> <div v-if="isEditingName" class="emoji-button" @click="submitName">🆗</div> <div v-else class="emoji-button" @click="editName">✏️</div> <div class="score"> <div class="input-wrapper" v-if="isEditingName"> <input v-model="playerName" @keyup.enter="submitName" type="text" maxlength="7" /> </div> <div v-else>{{ playerName }}</div> <div>{{ playerScore }} - {{ computerScore }}</div> <div>Computer</div> </div> <div class="player-hands"> <div>{{ playerDisplayEmoji }}</div> <div>{{ computerDisplayEmoji }}</div> </div> <ul class="hand-choices"> <li :class="{ selected: playerHand === 'rock' }" @click="sumbmitHand('rock')">✊</li> <li :class="{ selected: playerHand === 'scissors' }" @click="sumbmitHand('scissors')">✌️</li> <li :class="{ selected: playerHand === 'paper' }" @click="sumbmitHand('paper')">🖐️</li> </ul> </div> </main> </div> </body> </html>
main { font-family: "Courier New", Courier, monospace; width: 280px; margin: auto; } .score { display: flex; justify-content: space-between; margin-top: 40px; font-size: 18px; font-weight: bold; } .score div:nth-child(1) { width: 35%; text-align: right; } .score div:nth-child(2) { width: 30%; text-align: center; } .score div:nth-child(3) { width: 35%; text-align: left; } .player-hands { display: flex; justify-content: space-between; margin-top: 80px; font-size: 20vh; } .hand-choices { display: flex; justify-content: space-between; margin-top: 60px; padding: 0; } .hand-choices li { font-size: 15vh; list-style: none; cursor: pointer; } .hand-choices li.selected { text-decoration: lightblue underline; transition: font-size 0.2s; } .score div { padding: 5px 0; } .score .input-wrapper { padding: 0; } input[type="text"] { width: 80px; padding: 5px; margin: 0 0 0 15px; border: 2px solid #ccc; border-radius: 5px; font-family: "Courier New", Courier, monospace; } .emoji-button { position: fixed; top: 10px; left: 10px; cursor: pointer; }
const { plugin, value, computed } = vueFunctionApi; Vue.config.productionTip = false; Vue.use(plugin); function useName() { const playerName = value("Player"); const isEditingName = value(false); function editName() { isEditingName.value = true; } function submitName() { isEditingName.value = false; } return { playerName, isEditingName, editName, submitName }; } var app = new Vue({ el: '#app', setup() { const handsToEmoji = { rock: "✊", scissors: "✌️", paper: "🖐️" }; const isShowGameHands = value(false); const playerScore = value(0); const computerScore = value(0); const playerHand = value(null); const computerHand = value(null); const computeds = { playerDisplayEmoji: computed(() => isShowGameHands.value ? handsToEmoji[playerHand.value] : handsToEmoji["rock"] ), computerDisplayEmoji: computed(() => isShowGameHands.value ? handsToEmoji[computerHand.value] : handsToEmoji["rock"] ) }; function sumbmitHand(hand) { playerHand.value = hand; runGame(); } function randomHand() { const hands = Object.keys(handsToEmoji); return hands[Math.floor(Math.random() * hands.length)]; } function addPointToWinner() { const handToWeakness = { rock: "paper", scissors: "rock", paper: "scissors" }; if (handToWeakness[computerHand.value] === playerHand.value) { playerScore.value++; } else if (handToWeakness[playerHand.value] === computerHand.value) { computerScore.value++; } } function runGame() { computerHand.value = randomHand(); isShowGameHands.value = true; addPointToWinner(); } return { isShowGameHands, playerScore, computerScore, playerHand, computerHand, sumbmitHand, ...computeds, ...useName() }; } }).$mount('#app')