ユニファ開発者ブログ

ユニファ株式会社プロダクトデベロップメント本部メンバーによるブログです。

Test Driving the Proposed Vue.js Function API

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>

f:id:unifa_tech:20190814123651p:plain

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')