サイトトップ

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

HTML5テクニカルノート

Vue.js + CLI入門 02: リスト項目の削除とスタイル変更


Vue.js + CLI入門 01: リスト項目の表示と追加」に引き続き、Vue.js公式サイトの「TodoMVC の例」を単一ファイルコンポーネント(.vue)でつくってゆきましょう。今回は、リストの各項目を削除できるようにするとともに、チェックした項目はスタイルを変更します。

01 日本語変換を確定する[enter]キーイベントの問題

新たな作業にとりかかる前に、前回の項目追加にあった不具合を直します。実は、項目のテキストを日本語で入力しているとき、[enter]キーで変換が確定されるだけでなく、そのままリストに追加されてしまうことがあるのです(図001)。筆者の環境では、Safari 12.0.3/macOSで確認されています。

図001■日本語変換確定の[enter]キーで項目が追加されてしまう

図001

Vue.js v2.5.14より前は、keydownイベントは日本語変換確定の[enter]キーを拾いませんでした。v2.5.14でその仕様が改められたようです。調べたところkeypressであれば、変換確定の[enter]キーをイベントとして受け取らないことがわかりました(詳しくは、「Vue.js: TodoMVCの例で日本語の項目が正しく入力できるようにする」参照) 。そこで、src/components/TodoInput.vue<template>のキーイベントを、つぎのように書き替えます。呼び出すリスナーメソッド(addTodo())は、そのまま触らなくて構いません。

src/components/TodoInput.vue: <template>

<input

	@keypress.enter="addTodo">
	<!-- @keydown.enter="addTodo"> -->

02 項目をボタンで削除する

項目のテンプレートには、すでに削除のための<button>要素が加えてあります。それぞれの項目にロールオーバーすると、CSSで右端に[×]ボタンが表れるはずです(図002)。

図002■項目にロールオーバーすると表れる削除の[×]ボタン

図002

項目のコンポーネントsrc/components/TodoItem.vueは、つぎのように@clickイベントでリスナーメソッド(removeTodo())から、親のコンポーネントに$emit()メソッドでイベント(remove-todo)と自身のインスタンス(todo)を送ります。v-on(省略記法@)ディレクティブでイベントを受け取った親のリストコンボーネント(src/components/TodoList.vue)は、さらにその親のアプリケーションにイベントとインスタンスを渡すバケツリレーです。

src/components/TodoList.vue

<template>
	<section class="main" v-show="todos.length" v-cloak>

		<ul class="todo-list">
			<li v-for="todo in filteredTodos"

				>
				<todo-item

					@remove-todo="removeTodo">
				</todo-item>
			</li>
		</ul>
	</section>
</template>

<script>

export default {

	methods: {
		removeTodo(todo) {
			this.$emit('remove-todo', todo);
		}
	}
}
</script>

src/components/TodoItem.vue

<template>
	<div class="view">

		<button

			@click="removeTodo">
		</button>
	</div>
</template>

<script>
export default {

	methods: {
		removeTodo() {
			this.$emit('remove-todo', this.todo);
		}
	}
}
</script>

そして、バケツ(remove-todo)を受け取ったアプリケーションsrc/App.vueは、イベントリスナー(removeTodo())で引数のインスタンス(todo)をリストデータの配列(todos)から除けばよいのです。配列のメソッドは、比較的新しいArray.prototype.filter()を用いました(ECMAScript 5.1)。お題の「TodoMVC の例」と同じようにArray.prototype.splice()を使っても構いません。

src/App.vue

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

		<todo-list

			@remove-todo="removeTodo">
		</todo-list>
	</section>
</template>

<script>

export default {

	methods: {

		removeTodo(todo) {
			this.todos = this.todos.filter((item) => item !== todo);
		}
	}
}
</script>

ここまでの3つのコンポーネント(VUE)ファイルの中身を、つぎのコード001にまとめました。<style>要素は書き替えていませんので、省いてあります。

コード001■ボタンで項目が削除できる

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"
			@remove-todo="removeTodo">
		</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
			});
		},
		removeTodo(todo) {
			this.todos = this.todos.filter((item) => item !== todo);
		}
	}
}
</script>

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"
					@remove-todo="removeTodo">
				</todo-item>
			</li>
		</ul>
	</section>
</template>

<script>
import TodoItem from './TodoItem.vue';
export default {
	name: 'TodoList',
	components: {
		TodoItem
	},
	props: {
		todos: Array,
		filteredTodos: Array
	},
	methods: {
		removeTodo(todo) {
			this.$emit('remove-todo', todo);
		}
	}
}
</script>

src/components/TodoItem.vue

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

<script>
export default {
	name: 'TodoItem',
	props: {
		todo: Object
	},
	methods: {
		removeTodo() {
			this.$emit('remove-todo', this.todo);
		}
	}
}
</script>

03 チェックした項目にスタイルを割り当てる

各項目の先頭にはチェックボックスがついていて、それぞれオン・オフできます。けれど、データバインディングはされていません。アプリケーションデータのリスト項目のプロパティ(completed)と双方向にバインディングしましょう。リスト項目のコンポーネントsrc/components/TodoItem.vueに、ディレクティブ:value@inputをつぎのように定めます。そして、リスナーメソッド(onInput())から親コンポーネント(src/components/TodoList.vue)にイベント(done)を送り、さらにその親のアプリケーションにバケツリレーするお約束の流れです。

src/components/TodoList.vue

<template>
	<section class="main" v-show="todos.length" v-cloak>

		<ul class="todo-list">
			<li v-for="todo in filteredTodos"

				>
				<todo-item

					@done="done">
				</todo-item>
			</li>
		</ul>
	</section>
</template>

<script>

export default {

	methods: {

		done(todo, completed) {
			this.$emit('done', todo, completed);
		}
	}
}
</script>

src/components/TodoItem.vue

<template>
	<div class="view">
		<input
			type="checkbox" class="toggle"
			:value="todo.completed"
			@input="onInput">

	</div>
</template>

<script>
export default {

	methods: {

		onInput() {
			this.$emit('done', this.todo, !this.todo.completed);
		}
	}
}
</script>

イベント(done)を受け取ったアプリケーションは、リスナーメソッド(done())で受け取った項目インスタンス(todo)のプロパティ(completed)の値を書き替えます。これで、双方向のデータバインディングができ上がりました。アプリケーションにもたせたデータのプロパティで項目にチェックがつくかどうか決まり、チェックボックスを操作すればプロパティの値が変わるということです。もっとも、見た目からはわかりません。

src/App.vue

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

		<todo-list

			@done="done">
		</todo-list>
	</section>
</template>

<script>

export default {

	methods: {

		done(todo, completed) {
			todo.completed = completed;
		}
	}
}
</script>

プロパティの値に応じてクラスをバインディングすることができます。用いるディレクティブは、v-bind:class(省略記法:class)です(「クラスとスタイルのバインディング」参照)。与えるのはオブジェクトで、プロパティをクラス名にします。値をブール値評価して、trueならそのクラスが割り当てられる仕組みです。

src/components/TodoList.vue

<template>
	<section class="main" v-show="todos.length" v-cloak>

		<ul class="todo-list">
			<li v-for="todo in filteredTodos"

				:class="{completed: todo.completed}">
				<todo-item

					>
				</todo-item>
			</li>
		</ul>
	</section>
</template>

これで項目にチェックを入れると、そのスタイルが変わります(図003)。今回はここまでにしましょう。書き上げた3つのコンポーネントファイルの中身は、以下のコード002に掲げたとおりです。前述01「日本語変換を確定する[enter]キーイベントの問題」でsrc/components/TodoInput.vueに加えた修正は、テンプレートのイベント1箇所だけですので省きます。他のコンポーネントも含めたアプリケーションのファイル全体は、CodeSandboxに公開した以下のサンプル001でお確かめください。

図003■チェックした項目のスタイルが変わる

図003

コード002■項目チェックの双方向データバインディングとスタイル変更

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"
			@remove-todo="removeTodo"
			@done="done">
		</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
			});
		},
		removeTodo(todo) {
			this.todos = this.todos.filter((item) => item !== todo);
		},
		done(todo, completed) {
			todo.completed = completed;
		}
	}
}
</script>

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

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"
				:class="{completed: todo.completed}">
				<todo-item
					:todo="todo"
					@remove-todo="removeTodo"
					@done="done">
				</todo-item>
			</li>
		</ul>
	</section>
</template>

<script>
import TodoItem from './TodoItem.vue';
export default {
	name: 'TodoList',
	components: {
		TodoItem
	},
	props: {
		todos: Array,
		filteredTodos: Array
	},
	methods: {
		removeTodo(todo) {
			this.$emit('remove-todo', todo);
		},
		done(todo, completed) {
			this.$emit('done', todo, completed);
		}
	}
}
</script>

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

src/components/TodoItem.vue

<template>
	<div class="view">
		<input
			type="checkbox" class="toggle"
			:value="todo.completed"
			@input="onInput">
		<label>{{todo.title}}</label>
		<button
			class="destroy"
			@click="removeTodo">
		</button>
	</div>
</template>

<script>
export default {
	name: 'TodoItem',
	props: {
		todo: Object
	},
	methods: {
		removeTodo() {
			this.$emit('remove-todo', this.todo);
		},
		onInput() {
			this.$emit('done', this.todo, !this.todo.completed);
		}
	}
}
</script>

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

Vue.js + CLI入門


作成者: 野中文雄
更新日: 2019年10月14日 src/App.vueのコードを一部修正。
更新日; 2019年09月04日 サンプル001を追加。
更新日: 2019年3月22日 ブラウザの違いによる問題に対応。
作成日: 2018年12月16日


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