サイトトップ

Director Flash 書籍 業務内容 プロフィール

HTML5テクニカルノート

Vue.js + CLI入門 01: リスト項目の表示と追加


Vue.js公式サイトには、学習のためのサンプルが「」というページに13個掲げられています。もっとも、コードが開いて見られるだけで、説明はありません。その中の「TodoMVC の例」(図001)については、テクニカルノートの連載「Vue.js: TodoMVCをつくる」01-05で5回に分けて解説しました。JavaScript初心者の方は、こちらを読まれるとよいでしょう。

本稿のお題は、同じTodoMVCのアプリケーションです。ただし、Vue CLI 3を用いて、単一ファイルコンポーネント(.vue)で新たにつくってみます。JavaScripの基礎を学んだ方が対象です。Vue.jsについては、基本的な事項も補っていきます。

図001■TodoMVCの初期画面

図001

01 Vue CLIプロジェクトをつくる

まずは、Vue CLIでプロジェクトのひな形をつくります(「Vue CLI 3入門 04: プロジェクトをつくる」参照)。コマンドラインツールで、vue createコマンドをつぎのように入力してください。


vue create vue-todo-list

vue createコマンドに渡したプロジェクト名のフォルダに、つぎの図002のようなファイルがつくられます。このうちsrc/components/HelloWorld.vueは、あとで別のコンポーネントに差し替えます。

図002■プロジェクトのフォルダにつくられたファイル構成

図002

ブロジェクトのひな形のページは、ローカルサーバーで開くことができます(「Vue CLI 3入門 04: プロジェクトをつくる」01「新しいプロジェクトをつくる」参照)。プロジェクトのディレクトリ(vue-todo-list)に切り替えたら、コマンドラインツールからnpm runコマンドで実行するのはスクリプトserveです。ページはhttp://localhost:8080/で開きます(図003)。


$ npm run serve

図003■ローカルサーバーで開いたプロジェクトのひな形ページ

図003

02 データバインディングを定める

プロジェクトにつくられたファイルのうち、おおもとのコンポーネントになるのがsrc/App.vueです。そこに子のコンポーネントを差し込んでいきます。ひな形の子コンポーネント(src/components/HelloWorld.vue)を、テンプレート(<template>)とJavaScriptコード(<script>)から除くと、大枠がつぎのように残ります。


<template>
	<section id="app" class="todoapp">
		<!-- ...[あとで新たに追加]... -->
	</section>
</template>

<script>
export default {
	name: 'app',
	components: {
		// HelloWorld  // あとで新たに追加
	}
}
</script>

先に、子コンポーネントは使わず、src/App.vueに直接書き込んだコードで、Vue.jsのデータバインディングを試しましょう(コード001)。コンポーネントにもたせたデータを、ページの要素に差し込みます。JavaScriptコードでVueインスタンス(export default {})に、データを定めるのがdataです。プロパティではなくメソッドのかたちで、つぎの抜き書きのようにデータのオブジェクトを返すことにご注意ください(「コンポーネントのデータ」参照)。

Vueインスタンスのデータは、テンプレートから参照できます。要素のテキストで参照するときに用いるのは二重中かっこ{{}}です。これで、データの値がテキストとして示されます(単方向のデータバインデング)。


<template>
	<section id="app" class="todoapp">

		<section class="main">

			<div class="view">
				<label>{{todo.title}}</label>
			</div>

		</section>
	</section>
</template>

<script>
export default {

	data() {
		return {
			todo: {
				id: 0,
				title: 'test todo',
				completed: false
			}
		}
	}
}
</script>

書き直したsrc/App.vueのコード全体が、以下のコード001です。スタイルシートについては「TodoMVC の例」のCSSファイルを@importで使わせてもらいました。アプリケーションを組み立てるプロジェクトのファイルには、ほかにpublic/index.htmlsrc/main.jsがあります。TodoMVCの作例ではこれらには触りません。後者について気になる方は、「Vue CLI 3プロジェクトにおけるVueインスタンスの初期化を理解する」をお読みください。

図004■データバインデングされたページの項目

図004

コード001■データバインディングを定めたアプリケーション

src/App.vue

<template>
	<section id="app" class="todoapp">
		<header class="header">
			<h1>todos</h1>
		</header>
		<section class="main">
			<ul class="todo-list">
				<li class="todo">
					<div class="view">
						<label>{{todo.title}}</label>
					</div>
				</li>
			</ul>
		</section>
	</section>
</template>

<script>
export default {
	name: 'app',
	components: {
	},
	data() {
		return {
			todo: {
				id: 0,
				title: 'test todo',
				completed: false
			}
		}
	}
}
</script>

<style>
@import url("https://unpkg.com/todomvc-app-css@2.2.0/index.css");
</style>

03 データバインディングを双方向にする

要素にv-modelを定めると、Vueのデータと双方向にバインディングできます。確かめてみるために、ページに<input>要素で、テキスト入力フィールドをつぎのように加えましょう。そして、v-modelに属性値のかたちで与えるのが、バインディングするデータです。フィールドに入力したテキストで値が書き替えられますので、<label>要素に示されるテキストも変わります(図005)。


<template>
	<section id="app" class="todoapp">
		<header class="header">

			<input

				v-model="todo.title">
		</header>
		<section class="main">

			<input
				type="checkbox" class="toggle">
			<label>{{todo.title}}</label>

		</section>
	</section>
</template>

<script>
export default {

	data() {
		return {
			todo: {
				id: 0,
				title: 'test todo',
				completed: false
			}
		}
	}
}
</script>

図005■双方向にデータバインディングを定めたページ

図005

双方向にデータバインディングするように書き直したApp.vueは、つぎのコード002にまとめたとおりです。

コード002■双方向にデータバインディングを定めたアプリケーション

src/App.vue

<template>
	<section id="app" class="todoapp">
		<header class="header">
			<h1>todos</h1>
			<input
				class="new-todo" autofocus autocomplete="off"
				placeholder="What needs to be done?"
				v-model="todo.title">
		</header>
		<section class="main">
			<ul class="todo-list">
				<li class="todo">
					<div class="view">
						<input
							type="checkbox" class="toggle">
						<label>{{todo.title}}</label>
					</div>
				</li>
			</ul>
		</section>
	</section>
</template>

<script>
export default {
	name: 'app',
	components: {
	},
	data() {
		return {
			todo: {
				id: 0,
				title: 'test todo',
				completed: false
			}
		}
	}
}
</script>

<style>
@import url("https://unpkg.com/todomvc-app-css@2.2.0/index.css");
</style>

04 テキスト入力の要素をコンポーネント化する

はじめにつくったひな形のプロジェクトには、コンポーネント)(HelloWorld.vue)がひとつページに加えられていました。テキスト入力の要素を新たにコンポーネントにして差し替えましょう。親と子コンポーネントの間では、双方向まとめてデータバインディングはできません。親から子、そして子から親と、ふたつのデータバインディングをそれぞれ分けて定めなければならないのです。

まず、親から子へのデータの流れです。子コンポーネントはファイルsrc/components/TodoInput.vueとしてつくりましょう。親のアプリケーション(src/App.vue)は、JavaScriptコードで子コンポーネントのモジュールを以下のようにimportし、componentsに加えなければなりません。テンプレートでは、単語を小文字にしてハイフンで結んだ名前(todo-input)がコンポーネントを表します(「複数単語コンポーネント名」参照)。親のアプリケーション(src/App.vue)が子コンポーネントに渡したいデータ(todo)は、v-bind(省略記法:)でテンプレートに与えてください。

そして、子コンポーネントのJavaScriptコードで、propsに加えることにより自らのプロパティとして参照できるのです。propsに加えたプロパティには型を定めることが必須とされています(「プロパティの定義」参照)。そのうえで、テンプレートで要素のvaluev-bind(:)すれば、親から子へのデータバインディングのでき上がりです。

src/App.vue

<template>
	<section id="app" class="todoapp">

		<todo-input
			:todo="todo" />

	</section>
</template>

<script>
import TodoInput from './components/TodoInput.vue';

export default {

	components: {
		TodoInput
	},
	data() {
		return {
			todo: {
				// ...[中略]...
			}
		}
	}
}
</script>

src/components/TodoInput.vue

<template>
	<input

		:value="todo.title">
</template>

<script>
export default {
	name: 'TodoInput',
	props: {
		todo: Object
	}
}
</script>

つぎに、子から親へはデータをイベントで渡します。テンプレートの要素にイベントリスナーを定めるディレクティブはv-on(省略記法@)です。子コンポーネントのinputイベントで、以下の抜書きのようにJavaScriptコードのmethodsに定めたメソッド(onInput())を呼び出します。その本体から、さらに親にイベントを送るメソッドが$emitです。第1引数は親に送る文字列のイベント、第2引数には渡すデータを与えます。

親に送られたイベント(input)は、親のテンプレートでやはりv-on(@)ディレクティブが受け取ります。そして、呼び出されるリスナーメソッド(onInput())により、子から渡されたデータを扱えばよいのです(「コンポーネントで v-model を使う」参照)。

src/App.vue

<template>
	<section id="app" class="todoapp">

		<todo-input

			@input="onInput" />

	</section>
</template>

<script>

export default {

	methods: {
		onInput(value) {
			this.todo.title = value;
		}
	}
}
</script>

src/components/TodoInput.vue

<template>
	<input

		@input="onInput">
</template>

<script>
export default {
	name: 'TodoInput',
	props: {
		todo: Object
	},
	methods: {
		onInput(event) {
			this.$emit('input', event.target.value);
		}
	}
}
</script>

このようにして双方向にデータバインデングを定めたアプリケーションと子コンポーネントができました。それぞれのVUEファイルの中身は、つぎのコード003のとおりです。

コード003■子コンポーネントと双方向にデータバインディングを定めた

src/App.vue

<template>
	<section id="app" class="todoapp">
		<header class="header">
			<h1>todos</h1>
			<todo-input
				:todo="todo"
				@input="onInput" />
		</header>
		<section class="main">
			<ul class="todo-list">
				<li class="todo">
					<div class="view">
						<input
							type="checkbox" class="toggle">
						<label>{{todo.title}}</label>
					</div>
				</li>
			</ul>
		</section>
	</section>
</template>

<script>
import TodoInput from './components/TodoInput.vue';

export default {
	name: 'app',
	components: {
		TodoInput
	},
	data() {
		return {
			todo: {
				id: 0,
				title: 'test todo',
				completed: false
			}
		}
	},
	methods: {
		onInput(value) {
			this.todo.title = value;
		}
	}
}
</script>

<style>
@import url("https://unpkg.com/todomvc-app-css@2.2.0/index.css");
</style>

src/components/TodoInput.vue

<template>
	<input
		class="new-todo" autofocus autocomplete="off"
		placeholder="What needs to be done?"
		:value="todo.title"
		@input="onInput">
</template>

<script>
export default {
	name: 'TodoInput',
	props: {
		todo: Object
	},
	methods: {
		onInput(event) {
			this.$emit('input', event.target.value);
		}
	}
}
</script>

05 [enter]キーで項目を入力する

テキスト入力フィールドは、Todoリストに項目を加えるために使うつもりです。ですから、アプリケーション(App)のデータと直にバインディングせず、[enter]キーで反映されるようにしましょう。[enter]キーの押下を捉えるディレクティブ@keydown.enterです(「キー修飾子」参照)。そのリスナーメソッドからアプリケーションに値を送ります。

テキスト入力フィールドのテキストは、子コンポーネント(TodoInput)の中だけでデータとバインディングすればよいので、つぎの抜き書きのようにv-modelを用いることにしました。その値は、dataに新たに加えます(propsは要らなくなります)。[enter]キーで送られるテキストは、親のアプリケーション(App)がv-on(@)ディレクティブのリスナーメソッド(addTodo())で受け取って、dataのプロパティ値を改めるという流れです。なお、テキストが空かスペースのみのときは、値を変えません。

src/App.vue

<template>
	<section id="app" class="todoapp">
		<header class="header">

			<todo-input

				@add-todo="addTodo" />
				<!-- :todo="todo"
				@input="onInput"> -->
		</header>

	</section>
</template>

<script>

export default {

	methods: {
		/* onInput(value) {
			this.todo.title = value;
		} */
		addTodo(todoTitle) {
			const newTodo = todoTitle && todoTitle.trim();
			if (!newTodo) {
				return;
			}
			this.todo.title = newTodo;
		}
	}
}
</script>

src/components/TodoInput.vue

<template>
	<input

		v-model="newTodo"
		@keydown.enter="addTodo">
		<!-- :value="todo.title"
		@input="onInput"> -->
</template>

<script>
export default {

	/* props: {
		todo: Object
	}, */
	data() {
		return {
			newTodo: ''
		};
	},
	methods: {
		/* onInput(event) {
			this.$emit('input', event.target.value);
		} */
		addTodo() {
			this.$emit('add-todo', this.newTodo);
			this.newTodo = '';
		}
	}
}
</script>

図006■[enter]キーで入力が確定する

図006

ここまでのふたつのVUEファイルの記述を、つぎのコード004にまとめます。

コード004■[enter]キーで項目を入力する

src/App.vue

<template>
	<section id="app" class="todoapp">
		<header class="header">
			<h1>todos</h1>
			<todo-input
				@add-todo="addTodo" />
		</header>
		<section class="main">
			<ul class="todo-list">
				<li class="todo">
					<div class="view">
						<input
							type="checkbox" class="toggle">
						<label>{{todo.title}}</label>
					</div>
				</li>
			</ul>
		</section>
	</section>
</template>

<script>
import TodoInput from './components/TodoInput.vue';

export default {
	name: 'app',
	components: {
		TodoInput
	},
	data() {
		return {
			todo: {
				id: 0,
				title: 'test todo',
				completed: false
			}
		}
	},
	methods: {
		addTodo(todoTitle) {
			const newTodo = todoTitle && todoTitle.trim();
			if (!newTodo) {
				return;
			}
			this.todo.title = newTodo;
		}
	}
}
</script>

<style>
@import url("https://unpkg.com/todomvc-app-css@2.2.0/index.css");
</style>

src/components/TodoInput.vue

<template>
	<input
		class="new-todo" autofocus autocomplete="off"
		placeholder="What needs to be done?"
		v-model="newTodo"
		@keydown.enter="addTodo">
</template>

<script>
export default {
	name: 'TodoInput',
	data() {
		return {
			newTodo: ''
		};
	},
	methods: {
		addTodo() {
			this.$emit('add-todo', this.newTodo);
			this.newTodo = '';
		}
	}
}
</script>

06 項目をリスト表示する

表示する項目を複数にするため、リスト表示のコンポーネント(TodoList)をアプリケーション(App)に加えましょう。さらに、項目ひとつのコンポーネント(TodoItem)をつくって差し込みます。このコンポーネントは項目の数だけ使い回すことになるのです。

アプリケーション
App

リスト表示
TodoList

単独項目
TodoItem

×項目数

複数項目のオブジェクトは配列に入れて、つぎのようにアプリケーション(App)のdataに加えます。そのデータ(todos)は、テンプレートでリスト表示のコンポーネントにバインディングしましょう。もうひとつバインディングされているのは、算出プロパティ(filteredTodos)です。computedにメソッドのかたちで定めて、プロパティのように参照できます(「算出プロパティ」「基本的な例」参照)。標準JavaScriptのget構文のような役割です。

src/App.vue

<template>
	<section id="app" class="todoapp">

		<!-- <section class="main">
			...[中略]...
		</section> -->
		<todo-list
			:todos="todos"
			:filtered-todos="filteredTodos">
		</todo-list>
	</section>
</template>

<script>

import TodoList from './components/TodoList.vue';

export default {

	components: {

		TodoList
	},
	data() {
		return {
			/* todo: {
				...[中略]...
			} */
			todos: [
				{
					id: 0,
					title: '項目0',
					completed: false
				},
				{
					id: 1,
					title: '項目1',
					completed: false
				}
			]
		}
	},
	computed: {
		filteredTodos() {
			return this.todos;
		}
	},
	methods: {
		addTodo(todoTitle) {

			// this.todo.title = newTodo;
			this.todos[0].title = newTodo;
		}
	}
}
</script>

算出プロパティfilteredTodos()は、今のところ項目の配列データ(todos)をそのまま返しているだけです。後のちプロパティの名前のとおり、フィルタ機能をもたせます。つまり、項目データそのものは変えずに、リスト表示される項目を切り替えようということです。なお、テキストフィールドから入力して呼ばれるメソッド(addTodo())は、とりあえずリストの先頭の項目を書き替えるようにしてあります。

リスト表示のsrc/components/TodoList.vueと単独項目のsrc/components/TodoItem.vueは、それぞれ以下のコード005のように定めます。v-forは、それが加えられたテンプレートにもとづいて、配列からすべての項目を差し込むディレクティブです。このディレクティブを使うときは、key属性に一意の値を与えることが必須とされています(後述「v-forで差し込む要素には一意のkey属性を与える」参照)。

なお、v-showディレクティブは、ブール値で評価した値がtrueのときに要素を表示します。また、v-cloakは、Vueインスタンスのコンパイルが終わるまで存在するディレクティブで、ここではそれまでCSSで表示を隠す(display: none)ために使いました。また、VUEファイルの<style>要素にscoped属性を与えると、CSSの定めはそのコンポーネントにのみ適用されます。

コード005■リスト表示と項目のコンポーネントを追加

src/components/TodoList.vue

<template>
	<section class="main" v-show="todos.length" v-cloak>
		<input class="toggle-all" type="checkbox">
		<ul class="todo-list">
			<li v-for="todo in filteredTodos"
				class="todo"
				:key="todo.id">
				<todo-item
					:todo="todo">
				</todo-item>
			</li>
		</ul>
	</section>
</template>

<script>
import TodoItem from './TodoItem.vue';
export default {
	name: 'TodoList',
	components: {
		TodoItem
	},
	props: {
		todos: Array,
		filteredTodos: Array
	}
}
</script>

<style scoped>
[v-cloak] {
	display: none;
}
</style>

src/components/TodoItem.vue

<template>
	<div class="view">
		<input
			type="checkbox" class="toggle">
		<label>{{todo.title}}</label>
		<button
			class="destroy">
		</button>
	</div>
</template>

<script>
export default {
	name: 'TodoItem',
	props: {
		todo: Object
	}
}
</script>

図007■複数項目がリスト表示される

図007

これで、複数項目がリスト表示されるようになりました(図007)。今回の仕上げは、テキストフィールドからの入力が、リストに新たな項目として加わるようにします(図008)。前掲コード005のリスト表示と単独項目のコンポーネントはとくに書き替えるところはありません。テキスト入力フィールドから送られてきた値を、アプリケーション(App)が以下のようにリスト項目の配列(todos)に加えればよいのです。項目が追加できるようになりましたので、データの配列の初期値は空にします。

図008■テキスト入力フィールドから項目が加えられる

図008
src/App.vue

<script>
export default {

	data() {
		return {
			todos: [
				/* {
					...[中略]...
				} */
			],
			uid: 0
		}
	},
	computed: {

	methods: {
		addTodo(todoTitle) {

			// this.todos[0].title = newTodo;
			this.todos.push({
				id: this.uid++,
				title: newTodo,
				completed: false
			});
		}
	}
}
</script>

v-forで差し込む要素には一意のkey属性を与える

v-forディレクティブを使うと、データに納められた複数の項目がページに要素として差し込めます。このときv-forkeyを与えるのが、「スタイルガイド」に定められた「必須」のルールです(「キー付き v-for」)。key特別属性は、要素に一意の値を与えます。:keyv-bind:keyの省略構文です(「key」参照)。

ところで、配列からv-forでデータを取り出すとき、配列インデックスも調べられます。インデックスはもちろん一意です。けれど、配列インデックスはkeyの値とすべきではありません。その理由とkey属性の役割については、「Vue.js: v-forで項目インデックスをkey属性にしていいのか」をお読みください。

こうしてでき上がったアプリケーション(App)のVUEファイルの中身は、つぎのコード006に掲げたとおりです。それぞれのファイルについては、CodeSandboxに公開した以下のサンプル001をご覧ください。

コード006■リストに項目を加える

src/App.vue

<template>
	<section id="app" class="todoapp">
		<header class="header">
			<h1>todos</h1>
			<todo-input
				@add-todo="addTodo" />
		</header>
		<todo-list
			:todos="todos"
			:filtered-todos="filteredTodos">
		</todo-list>
	</section>
</template>

<script>
import TodoInput from './components/TodoInput.vue';
import TodoList from './components/TodoList.vue';

export default {
	name: 'app',
	components: {
		TodoInput,
		TodoList
	},
	data() {
		return {
			todos: [],
			uid: 0
		}
	},
	computed: {
		filteredTodos() {
			return this.todos;
		}
	},
	methods: {
		addTodo(todoTitle) {
			const newTodo = todoTitle && todoTitle.trim();
			if (!newTodo) {
				return;
			}
			this.todos.push({
				id: this.uid++,
				title: newTodo,
				completed: false
			});
		}
	}
}
</script>

<style>
@import url("https://unpkg.com/todomvc-app-css@2.2.0/index.css");
</style>

サンプル001■vue-todo-mvc-01

Vue.js + CLI入門


作成者: 野中文雄
更新日: 2019年10月14日 src/App.vueのコードを一部修正。
更新日; 2019年09月04日 07「データバインディングに用いるイベントchangeとinput」は削除し、サンプル001を追加。
更新日; 2019年03月24日 07「データバインディングに用いるイベントchangeとinput」を追加。
更新日: 2019年03月22日 ブラウザの違いによる問題に対応。
更新日: 2018年12月09日 v-showディレクティブの説明を追加。
作成日: 2018年11月30日


Copyright © 2001-2019 Fumio Nonaka.  All rights reserved.