Chapter 08

petite-vueの諸々

OJK
OJK
2021.11.30に更新

petite-vue 最後の難関である「コンポーネント」に進む前に、これまで紹介できていなかった小ネタ(というには重要なことも含まれていますが)を紹介しておきます。

雛形コード
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>petite-vue入門</title>
</head>
<body>

  <p>Petite Vue!!</p>

  <script src="https://unpkg.com/petite-vue"></script>
  <script src="script.js"></script>
</body>
</html>
script.js
'use strict';

PetiteVue.createApp({

}).mount();

一瞬表示される {{ }} の消去

ここまで petite-vue に触れてきて、最初に一瞬だけ表示される展開前の口ひげ構文が気持ち悪いと感じていた人がいるかもしれません。これを表示させないおまじないがあります。v-cloak ディレクティブ を使います。

まず、HTML ファイルの head 要素内で、style 要素を以下のように記述します。

HTML(head要素内)
<style>
  [v-cloak] { display: none; }
</style>

そして、body 要素に論理属性として v-cloak を追加します。

HTML
<body v-cloak>

こうすることで、{{message}} といった展開前の口ひげ構文が表示されなくなったかと思います。かといって表示が早くなるというわけではなく、代わりに白い画面が表示されるだけなのですが、わずらわしさはなくなったかと思います。

仕組みですが、petite-vue の初期化処理が完了するまでの間、v-cloak が設定された HTML 要素には [v-cloak] セレクタに対する CSS が適用されます。上記のサンプルでは [v-cloak] セレクタに「display: none」が設定されているので、画面に何も表示されない…というわけです。

なお、createApp メソッド末尾の mount() で特定の HTML 要素を指定している場合は、その HTML 要素に v-cloak を付けてください。そうすれば、petite-vue を使用しない HTML 要素は、petite-vue の初期化完了を待つことなく描画されます。

アプリケーション起動時のメソッド実行

body 要素への @mouted ディレクティブにメソッドを指定します。

HTML
<body @mounted="メソッド名">
JS
PetiteVue.createApp({
  メソッド() {
    // アプリケーション起動時に行いたい処理
  }
}).mount();

こちら]で紹介しましたが、検索性が悪かったので本チャプターからもリンクします。

v-for での key 属性の付与

v-for を使用する場合は同時に key 属性 を付けておくことが推奨されています。key 属性を付けておかないと、リストを動的に変更したときに問題が生じる場合があるからです。「動的な変更」とは、一旦リスト表示したあとでボタンを押してリストの順番を変えるといった処理です。

実際に問題が起こるのは、リストの動的な変更が生じて、かつ、v-for を付けた HTML 要素の子孫要素に “一時的な状態” が生じる場合です。一時的な状態が生じうる HTML 要素の代表格は input 要素などのフォーム入力要素です。フォーム入力要素を子孫に含んだリストを動的に変更することなんてあるのかな…?と思うかもしれませんが、初心者の定番課題である TODO リストがまさにそれです。

以下の例は、リストの子孫要素に input 要素(テキストボックス)を持ちます。テキストボックスに何か入力してから[入れ替え]ボタンを押してみてください。

HTML
<ul>
  <li v-for="item in list">{{item}} <input></li>
</ul>
<button @click="list.push(list.shift())">入れ替え</button>
JS
PetiteVue.createApp({
  list: ['A', 'B', 'C']
}).mount();

A ~ C の文字は入れ替わっていきますが、テキストボックスは移動しないことがわかりますね。petite-vue では効率化のために「変更のない要素の DOM は変更せずに再利用する」という方針がとられているのですが、フォーム入力など一時的な状態の変更に petite-vue が気づけないのでおかしなことになるのです。

では、v-for を付けた HTML 要素に key 属性を付けてみましょう。key 属性は各 HTML 要素に対して固有の値(数値か文字列)でなくてはならないので、v-bind でループ変数に紐付けします。

<li v-for="item in list" :key="item">

今回はたまたま配列要素に重複がなかったので、ループ変数 item をそのまま key 属性値としました。ちょうどよい固有の値がない場合には、データにあらかじめ固有の ID を振っておいて、それを参照するようにします。

JS
PetiteVue.createApp({
  list: [
    { id: '01', fruit: 'りんご', price: 200 },
    { id: '02', fruit: 'みかん', price: 130 },
    { id: '03', fruit: 'みかん', price: 200 }
  ]
}).mount();
HTML
<li v-for="item in list" :key="item.id">

勉強がてら、もうひとつありがちなパターンとして、v-for 配下の HTML 要素に動的にスタイルが割り当てられる場合を見てみましょう。個別の HTML 要素へのスタイル適用も petite-vue は検出できないので、スタイルの適用されている位置が変わりません。

HTML
<ul>
  <li v-for="item in list">
    {{item}}
    <button @click="addStyle">装飾</button>
  </li>
</ul>
<button @click="list.push(list.shift())">入れ替え</button>
JS
PetiteVue.createApp({
  list: ['A', 'B', 'C'],
  addStyle(ev) {
    ev.currentTarget.parentElement.style.color = 'coral';
  }
}).mount();

addStyle メソッドの部分は理解できたでしょうか。Event オブジェクトを引数 ev に受け取り、ev.currentTarget でクリックされた button 要素にアクセスしています。さらに、スタイルを適用したいのは button 要素ではなくその親の li 要素なので、parentElement プロパティでアクセスしています。

key 属性の扱いについては旧バージョンである Vue2.x から変更があります(こちらを参照)。petite-vue は Vue3.x から派生したものですので、まだまだネット上に残っている Vue2.x の情報に惑わされないようにしてください。

v-text ディレクティブ

口ひげ構文と同じ機能をディレクティブで提供するものです。正直なところ、使いどころがわかりません。

v-textディレクティブ
<HTML要素名 v-text="message">
 ↕ 同じ
<HTML要素名>{{message}}</HTML要素名>

v-html ディレクティブ

口ひげ構文に HTML タグを含む文字列を指定するとどうなるでしょうか。

HTML
{{tag}}
JS
PetiteVue.createApp({
  tag: '<h1>h1要素です</h1>'
}).mount();

<h1>タグが HTML として解釈されず、文字列としてそのまま表示されています。セキュリティのためにこのようになっているのですが、その解説は後回しにして、まずは HTML タグとして解釈させる方法をお伝えします。

v-html ディレクティブ を使うと、口ひげ構文内に記述された HTML タグを HTML として解釈させることができます。ディレクティブを追加するための HTML 要素が必要です(template 要素は使えません)ので、h1 要素を子要素に含むことのできる div 要素に v-html を追加してみます。

HTML
<div v-html="tag"></div>

こうすることで、データプロパティ tag に含められた<h1>タグが HTML として解釈されます。Chrome の Elements タブ(Firefox のインスペクタータブ)を確認してみてください。きちんと h1 要素が追加されてると思います。

ただし、v-html は慎重に使用する必要があります。以下のコードでは、悪意のあるユーザがデータプロパティ tag に外部からスクリプトを仕込む例を模倣しています。

HTML
<div v-html="tag"></div>
<button @click="crack">ユーザ入力</button>
JS
PetiteVue.createApp({
  tag: '<h1>h1要素です</h1>',
  crack() {
    this.tag = '<h1 onclick="window.alert(\'侵入成功\')">クリックしてみて</h1>';
  },
}).mount();

[ユーザ入力]というボタンを押すと「クリックしてみて」という h1 要素が表示されます。これをクリックすると window.alert() が実行されます。

外部からスクリプトが埋め込めるということは、JavaScript で可能なことは何でもできてしまうということです。これは簡単な例ですが、実際にはクロスサイトスクリプティング(XSS)攻撃という危険にアプリケーションを晒すことになります。

それでも v-html ディレクティブが用意されているのは、アプリケーションによっては HTML コードが動的に読み込めると便利なことがあるからです。例えば、ブログの記事を HTML コードの形でファイルやデータベースに記録しておき、それを読み込むような処理が考えられます。そうした明確な目的がないときは v-html は使わないようにしましょう。

セッター

ゲッターは petite-vue に欠かせない便利な機能ですが、代入によってあとから値を変更できません。getter という名前のとおり、あくまで取得(読み取り)専用です。

例えば、以下のサンプルコードで[変更]ボタンを押すとエラーが出ます。

HTML
<p>氏名:{{fullName}}</p>
<button @click="setName">更新</button>
JS
PetiteVue.createApp({
  // データプロパティ
  lastName: 'OJK',
  firstName: 'Alexander',

  // ゲッター
  get fullName() {
    return this.firstName + ' ' + this.lastName;
  },

  // メソッド
  setName() {
    this.fullName = 'Alexandros PetiteOJK';
  }
}).mount();

そもそもゲッターはデータプロパティを元に“生成”される値です。このサンプルコードでも、ゲッター fullName の値を直接変更したいのではなく、その構成要素であるデータプロパティの firstName と lastName を変更したいわけです。

実際、データプロパティを直接変更する以下のコードはエラーなく動きますし、ゲッター fullName の値もちゃんと自動的に更新されます。

JS
setName() {
  this.firstName = 'Alexandros';
  this.lastName= 'PetiteOJK';
}

しかし、表向きのデータプロパティは fullName のほうなので、fullName への代入を介して(裏方の)firstName と lastName を更新したいという要望もあるでしょう。
そのときには セッター/setter を定義します。

set構文
set セッター名(引数) {
  // データプロパティの更新
}

構文上は get が set に変わっただけですが、必ず 1 個の引数を持たなくてはなりません。ゲッターと対応させるには、セッター名とゲッター名は同じにします。ちなみに、ゲッターとセッターによって定義されたこの「表向きはプロパティとして扱える何か」のことを アクセサプロパティ と呼びます。

具体例を見てみましょう。v-on でのメソッド呼び出しを止めて、アクセサプロパティ fullName に値を代入しています。

HTML
<p>氏名:{{fullName}}</p>
<button @click="fullName='Alexandros PetiteOJK'">更新</button>
JS
PetiteVue.createApp({
  // データプロパティ
  lastName: 'OJK',
  firstName: 'Alexander',

  // アクセサプロパティ
  get fullName() {
    return this.firstName + ' ' + this.lastName;
  },
  set fullName(newName) {
    const [fN, lN] = this.newName.split(' ');
    this.firstName = fN;
    this.lastName = lN;
  }
}).mount();

引数は "firstName lastName" という文字列ですが、split メソッドを介して分割代入するのは定番の方法なので覚えてください。

const [変数, 変数, ...] = 文字列.split(区切り文字);

なお、セッターの引数は 1 個という縛りですが、配列やオブジェクトも 1 個の引数です。ですので、次のような値の渡し方をしても構いません。

// v-on
fullName = ['Alexandros', 'PetiteOJK']

set fullName(newName) {
  this.firstName = newName[0];
  this.lastName = newName[1];
}

// もしくは仮引数の受け取りのところで分割代入して…
set fullName([fN, lN]) {
  this.firstName = fN;
  this.lastName = lN
}
// v-on
fullName = { fN: 'Alexandros', lN: 'PetiteOJK' }

set fullName(newName) {
  this.firstName = newName.fN;
  this.lastName = newName.lN;
}