初めてのVue⑨composable APIを使ってみよう

初めてのVue⑨composable APIを使ってみよう

Viteの仕組みに慣れてきたところでcomposable APIを使ってみましょう。composable APIとはコードを部品のようにまとめたもので、必要な時にcomposable APIをインポートすることで面倒なコードを書くことなく簡単に自分の思った設定をすることができます。

この記事の対象者
  • composable APIって何?
  • Viteでcomposable APIを使ってみたい
  • オリジナルのcomposable APIを作りたい

この記事では3つの例を使ってcomposable APIを説明していきます。

sweatalert

まずはsweatalertを使って説明します。

ターミナルから下記のコードでsweatalertをインストールしていきます。

npm install --save sweetalert

次にコードを書きます。

前回も使ったHoneViewを使ってsetupなしで書いていきます。

<script>
  import swal from 'sweetalert';

//Clickを押すとflash関数が発火してswalを使ったメッセージが出ます
  export default{
    methods:{
      flash(message){
        swal(message);
      }
    }
  }
</script>

<template>
  <main>
    <p>
      <button @click="flash('It works')">Click!</button>
    </p>
  </main>
</template>

ブラウザで確認します。

うまくできてます。

このコードを他のページにも反映させようとするとscriptタグの中身を重複させなければなりません。

それは面倒です。

mixinsの使用

下のような構成にして必要な時に取り出せるようにしていきます。

コードです。

import swal from 'sweetalert';
export default{
  methods:{
    flash(message){
      return swal(message);
    }
  }
}

HomeViewのscriptタグの中はこうなります。

<script>
  import flash from "../components/mixins/flash";
  export default{
    mixins: [flash]
  };
</script>

これでブラウザを確認するとさっきと同じ画面になると思います。

ただし、これはコードが長くなってくると元のコードがどこにあるのか分かりにくくなるため、もう一つの方法が推奨されています。

composable化の方法

さっきと同じようにフォルダとファイルを作ります。

useFlashのコードを書いていきます。

import swal from 'sweetalert';

export function useFlash(){
  function flash(message){
    return swal(message);
  }

  return{ flash };
}

useFlash関数を作ってflashとして使えるようにしています。

では、HomeViewを見ていきます。

<script>
  import { useFlash } from "../components/composables/useFlash";
  export default{
    setup(){
      let { flash } = useFlash();

      return{ flash };
    }

  };
</script>

useFlashのオブジェクトキーであるflashを受け取って使えるようにしています。

プロパティはswalで設定したものが入っています。

returnでflashキーで受け取ったオブジェクトが使えるようになっています。

ブラウザはさっきと変わりません。

setupを使って綺麗にしていきましょう。

<script setup>
  import { useFlash } from "../components/composables/useFlash";
  let { flash } = useFlash();
</script>

こうですね。

別のページで使うときもこれだけのコピペで良くなりました。

では、ページごとに表示を変えたい場合を見ていきましょう。

import swal from 'sweetalert';

export function useFlash(){
  function flash(title, message, level = 'success'){
    return swal(title, message, level);
  }

  return{ flash };
}

flashに色んな変数を持たせています。levelはデフォルトでsuccessになるようにしています。

<template>
  <main>
    <p>
      <button @click="flash('test', 'It works', 'success')">Click!</button>
    </p>
  </main>
</template>

HomeViewではflashを使うときにそれぞれの変数に入力する値を入れてやります。

ブラウザで確認します。

はい、うまくいってますね。

ページごとに変えるときは変数をいじれば完成です。

ローカルストレージの使用

続いて、ローカルストレージを使用するコードをみていきましょう。

まずはローカルストレージを使わない例を見ていきましょう。

<script setup>
  import { ref } from "vue";
  let food = ref('');
</script>

<template>
  <main>
    <p>
      Waht is your favorite food? <input type="text" v-model="food">
    </p>
  </main>
</template>

この状態では何かの拍子にページが再読み込みされたらそれまで入力していた情報が消えてしまいます。

<script setup>
  import { ref } from "vue";

//ポイント:ローカルストレージの設定(inputに書き込まれた時にwriteが発火してfoodにfood.valueが入ります)
  let food = ref(localStorage.getItem('food'));
  function write(){
    localStorage.setItem('food', food.value);
  }

</script>
<template>
  <main>
    <p>
      Waht is your favorite food? <input type="text" v-model="food" @input="write">
    </p>
  </main>
</template>

うまくローカルストレージに保存されてますね。

これで再読み込みしても入力した値が消えません。

しかし、コードが綺麗でないので綺麗にしていきましょう。

コードの整理

コードの整理のためにWatchを使います。コードも追加します。

<script setup>
  import { ref, watch } from "vue";
  let food = ref(localStorage.getItem('food'));
  let age = ref(localStorage.getItem('age'));

//ポイント:watchは第一引数に紐づいた値が変更されたらwriteを発火させてという意味です
  watch(food, (val) =>{
    write('food', val);
  });
  watch(age, (val) =>{
    write('age', val);
  });

  function write(key, val){
    localStorage.setItem(key, val);
  }
</script>
<template>
  <main>
    <p>
      Waht is your favorite food? <input type="text" v-model="food">
    </p>
    <p>
      How old are you? <input type="text" v-model="age">
    </p>
  </main>
</template>

ageを追加しましたが、うまく機能してます。

これをさらに綺麗にしていきます。

composable化

下記のように新しいファイルを作成し、コードを書きます。

import { ref, watch } from "vue";

//ポイント①:使いまわせるようにkeyに反応するようにする
export function useStorage(key){
  let storeVal = localStorage.getItem(key);
  let val = ref(storeVal);

//ポイント②:valが変更されたときにwrite発火(valはv-modelに紐づいたkeyによって変更される)
  watch(val, () =>{
    write(key, val);
  })

  function write(key, val){
    localStorage.setItem(key, val.value);
  }

  return val;
}

useFlashの時とほぼ同じですね。

HomeViewに書いてたコードとほぼ一緒です。

<script setup>
  import { ref, watch} from "vue";
  import { useStorage } from "../components/composables/useStorage";

  let food = useStorage('food');
  let age = useStorage('age');
</script>

HomeViewはスッキリしましたね。

ブラウザで確認します。

ストレージもうまく動いているのが分かります。

デフォルト値を設定

最後にデフォルトの設定や何もない時の処理を追加します。

import { ref, watch } from "vue";

//ポイント①:デフォルトでvalにnullを入れて、valに変更があったらリアクティブに更新
export function useStorage(key,val = null){
  let storeVal = localStorage.getItem(key);
  if(storeVal){
    val = ref(storeVal);
  }else{
    val = ref(val);
    write(key, val);
  }
  

  watch(val, () =>{
    write(key, val);
  })

//ポイント②:valがnullもしくは空ならキーも削除
  function write(key, val){
    if(val.value == null || val.value == ''){
      localStorage.removeItem(key);
    }else{
      localStorage.setItem(key, val.value);
    }
  }

  return val;
}

第二引数にデフォルト値を入れれるようにして、第二引数がnullならキーも削除という形にしました。

HomeViewでは下記のように設定すればデフォルトでsushiが入るようになります。

let food = useStorage('food', 'sushi');

続いて別の例も見ていきましょう。

textareaの入力方法変更

ますはコードです。

<script setup>
  import { ref, onMounted } from "vue";

  let textarea = ref("null");

//ここは複雑ですがtabを押した時に次のタブに移動するのではなく、スペースが入るようにしてます。
  onMounted(()=>{
  //console.log(textarea.value)
    textarea.value.addEventListener("keydown", (e)=>{
      let t = textarea.value;
      if(e.keyCode == 9){
        let val = t.value,
        start = t.selectionStart,
        end = t.selectionEnd;
        t.value = val.substring(0, start) + "\t" + val.substring(end);
        t.selectionStart = t.selectionEnd = start + 1;
        e.preventDefault();
      }
    })
  })

<template>
  <main>
    <form>
      <textarea ref="textarea" style="width:100%; height:300px;">Hi!</textarea>
    </form>
  </main>
</template>

ブラウザで確認します。

テキストエリアを使って入力するスペースを作りました。

通常はタブを押すと別の入力エリアへと移動するのですが、プログラミングする時のようにスペースが入るようにしています。

下記の属性を加えることで特定の DOM 要素や子コンポーネントのインスタンスがマウントされた後に、そのインスタンスへの更新直接の参照を取得することができます。

ref="textarea"

マウントとはDOMを作ってDOMにアクセスできるようにすることを言います。

下記の記載があるのはまだtextareaがマウントされておらず、参照できないからです。

let textarea = ref("null");

DOMってなんだ?という人は下記の記事が参考になります。

https://ja.javascript.info/dom-nodes

イベントリスナーの中身は下記の通りです。

//keydownは何かキーを押した時のイベント  
   textarea.value.addEventListener("keydown", (e)=>{
      let t = textarea.value;
//keycodeはkeydownに付随するプロパティでタブキーには9が割り当てられています
      if(e.keyCode == 9){
        let val = t.value,
        start = t.selectionStart, //選択した部分の初め
        end = t.selectionEnd; //選択した部分の終わり
        t.value = val.substring(0, start) + "\t" + val.substring(end); //選択した部分をタブと置き換える
        t.selectionStart = t.selectionEnd = start + 1; //カーソルを選択した部分から+1したところに移動する
        e.preventDefault(); //デフォルトで設定されている動きを削除
      }
    })

onMountedの中のconsole.log(textarea.value)を実行すると、ブラウザでもあるようにtextareaタブが表示されます。

textareaに入力されてた文字ではないことに注意です。

入力されたものにアクセスするためにはさらに.valueをつけます。

composable化

では、綺麗にしていきましょう。

TabbableTextarea.vueにコードを移動していきます。

<script setup>
  // import { ref, onMounted } from "vue";

  // let textarea = ref("null");

  function onTabPress(e){
    let textarea = e.target;
    let val = textarea.value,
    start = textarea.selectionStart,
    end = textarea.selectionEnd;
    textarea.value = val.substring(0, start) + "\t" + val.substring(end);
    textarea.selectionStart = textarea.selectionEnd = start + 1;
     // e.preventDefault();
  }

<template>
  <main>
    <form>
      <textarea @keydown.tab.prevent="onTabPress" style="width:100%; height:300px;">Hi!</textarea>
    </form>
  </main>
</template>

@keydownにより、EventListenerのkeydownを指定しなくても良くなり、onTabPress関数で処理を書けば良くなりました。

.tab.preventにより下記の2つも必要なくなりました。

keyCode == 9
e.preventDefault();

refでtextareaを取得してマウントしてましたが、下記のコードで取得してます。

let textarea = e.target;

選択したものをtextareaに入れてアクセスできるようにしています。

ブラウザの動きは変わらないです。

v-modelをつけてみよう

さらに拡張性の高いものにするためv-modelをつけてみましょう。

<script setup>
  import { ref } from "vue";
  import TabbableTextarea from "../components/TabbableTextarea.vue";
//追加
  let comment = ref('test value');
  
</script>
<template>
  <main>  
    <form>
      <TabbableTextarea id="1" v-model="comment" style="width:100%; height:300px;"/>
    </form>
  </main>
</template>

v-modelでcommentを更新できるようにするだけではうまくいきません。

子からのアップデートが受け取れないからです。子からのemitが必要です。

<script setup>
//追加
  defineProps({
    modelValue:String
  });
//追加
  let emit = defineEmits(['update:modelValue']);

  function onTabPress(e){
    let textarea = e.target;

    let val = textarea.value,
    start = textarea.selectionStart,
    end = textarea.selectionEnd;

    textarea.value = val.substring(0, start) + "\t" + val.substring(end);

    textarea.selectionStart = textarea.selectionEnd = start + 1;
  }
//ここを動かすために他を追加してます。update:modelValueにより親のv-modelが使えるようになります。
  function update(e){
    emit('update:modelValue', e.target.value);
  }
</script>

<template>
//追加:@keyupはキーが離された時に発火します。
  <textarea 
    @keydown.tab.prevent="onTabPress"
    @keyup="update"
    v-text="modelValue"
  />
</template>

update関数を作ってその中でemitしてmodelValueを更新してます。

この機能を使うためにpropsやemitを定義してやります。

ブラウザでもcommentが更新されているのが確認できました。

以前、v-modelはv-bindとv-onの合わせ技という話をしましたが、合わせ技は下記のような感じです。

<input :value="name" @input="name = $event.target.name">

emitの中身とちょっと似てますよね。

emit('update:modelValue', e.target.value);

update:modelValueが親のv-modelを機能させてます。

関連記事