サイトトップ

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

HTML5テクニカルノート

Vue.js + CLI入門 06: 項目のテキストをダブルクリックで再編集する


Vue.js + CLI入門 05: チェックをまとめてオン/オフしたり削除する」(以下「Vue.js + CLI入門 05」)では、項目にまとめてチェックをつけたり、チェック済みの項目すべてを削除できるようにしました。今回は、ひとつの項目を削除してから改めて追加することなく、すでに入力した項目のテキストを書き替えられるようにします。

01 項目のダブルクリックで編集のテキスト入力フィールドを出す

項目を書き替えるときもボタンは使わず、項目テキスト(<label>要素)のダブルクリックで進めます。リスナーを定めるイベントv-on:dblclick(省略記法@dblclick)です[*1]。リスナーメソッド(editTodo())は、つぎのように$emit()で項目のオブジェクト(todo)をイベント(edit-todo)ともに親コンポーネントに送ります。

src/components/TodoItem.vue

<template>
	<div class="view">

		<label @dblclick="editTodo">
			{{todo.title}}
		</label>

	</div>
</template>

<script>
export default {

	methods: {

		editTodo() {
			this.$emit('edit-todo', this.todo);
		}
	}
}
</script>

ダブルクリックのイベント(edit-todo)が親コンポーネント(TodoList.vue)に渡されると、受け取った項目オブジェクトを以下のようにリスナーメソッド(editTodo())でプロパティ(editedTodo)に定めます。すると、その項目の親要素(<li>)にはv-bind:class(省略記法:class)構文で新たなクラス(editing)がバインディングされるのです(「HTMLクラスのバインディング」参照)。なお、新たに加えた項目編集用の子コンポーネント(TodoEdit.vue)は、のちほどつくります。todoをバインディングしたのは、項目データを編集するためです。

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, editing: todo == editedTodo}">
				<todo-item

					@edit-todo="editTodo">
				</todo-item>
				<todo-edit
					:todo="todo">
				</todo-edit>
			</li>
		</ul>
	</section>
</template>

<script>

import TodoEdit from './TodoEdit.vue';
export default {

	components: {

		TodoEdit
	},

	data() {
		return {
			editedTodo: null
		};
	},
	methods: {

		editTodo(todo) {
			this.editedTodo = todo;
		}
	}
}
</script>

親アプリケーション(App.vue)の<style>@importしたindex.cssには、editingクラスの中の項目の子コンポーネント(TodoItem.vue)に与えたviewクラスに対して、つぎのように表示を隠す定めが加えてあります。つまり、ダブルクリックした項目は消えるということです。

https://unpkg.com/todomvc-app-css@2.2.0/index.css

.todo-list li.editing .view {
	display: none;
}

src/components/TodoItem.vue

<template>
	<div class="view">

	</div>
</template>

項目を編集するコンポーネントの定めはつぎのとおりです。項目テキストが<input>要素のvalue属性にバインディングされて表示されます。クラスeditの要素は、index.cssにより以下のようにはじめは表示していません。けれど、項目がダブルクリックされて親要素にeditingクラスがバインディングされると表れるのです(図001)。

src/components/TodoEdit.vue

<template>
	<input id="edit" class="edit" type="text"
		:value="todo.title">
</template>

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

https://unpkg.com/todomvc-app-css@2.2.0/index.css

.todo-list li.editing .edit {
	display: block;

}

.todo-list li .edit {
	display: none;
}

図001■項目をダブルクリックすると編集用のテキスト入力フィールドが表れる

図001

[*1] Vue.jsサイトの「イベントハンドリング」には、dblclickイベントが載っていません。けれど、JavaScriptネイティブのイベントはサポートされます(「Why not add v-on:doubleClick」参照)。

02 [enter]キーで編集を確定する

編集した項目テキストはモジュールsrc/components/TodoEdit.vueのテンプレートに、以下のように@inputイベントのリスナーメソッド(onInput())を加え、プロパティ(editedTitle)に反映します。項目の追加と同じく、編集の確定は[enter]キーです。@keypress.enterイベントで、リスナーメソッド(doneEdit())から親のコンポーネントに$emit()でイベントと新たな項目テキストを送ります。また、編集する項目のテキストはmounted()で、プロパティに与えました。

イベント(done-edit)を受け取ったモジュールsrc/components/TodoList.vueは、リスナーメソッド(doneEdit())でその項目(editedTodo)のテキスト(title)を改めます。これで、編集したテキストが[enter]キーで差し替わるようになりました(図002)。なお、受け取ったテキストが空または空白スペースのみのときは、項目がデータから除かれます。

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-edit
					:todo="todo"
					@done-edit="doneEdit">
				</todo-edit>

			</li>
		</ul>
	</section>
</template>

<script>

export default {

	methods: {
		doneEdit(todoTitle) {
			if (!this.editedTodo) {
				return;
			}
			const title = todoTitle.trim();
			if (title) {
				this.editedTodo.title = title;
			} else {
				this.removeTodo(this.editedTodo);
			}
			this.editedTodo = null;
		}
	}
}
</script>
src/components/TodoEdit.vue

<template>
	<input id="edit" class="edit" type="text"
		:value="todo.title"
		@input="onInput"
		@keypress.enter="doneEdit">
</template>

<script>
export default {

	data() {
		return {
			editedTitle: null
		};
	},
	mounted() {
		this.editedTitle = this.todo.title;
	},
	methods: {
		onInput(event) {
			this.editedTitle = event.target.value;
		},
		doneEdit(event) {
			this.editedTitle = event.target.value;
			this.$emit('done-edit', this.editedTitle);
		}
	}
}
</script>

図002■編集したテキストが[enter]キーで差し替わる

図002

03 [esc]キーで編集を中止する

編集したテキストを[enter]で確定せず、[esc]キーを押したときはとり止めることにしましょう。モジュールsrc/components/TodoEdit.vueのテンプレートに加えるのは、以下のような@keyup.escイベントのリスナーメソッド(cancelEdit())です。<input type="text">要素のテキストは項目オブジェクト(todo)のタイトルに戻し、親コンポーネントに取り消しのイベント(cancel-edit)を送ります。

イベントを受け取ったモジュールsrc/components/TodoList.vueがリスナーメソッド(cancelEdit())で行うのは、編集項目のプロパティ(editedTodo)をnullにすることだけです。データはそのまま変えなくてよく、表示する子コンポーネント(TodoItem.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-edit

					@cancel-edit="cancelEdit">
				</todo-edit>
			</li>
		</ul>
	</section>
</template>

<script>

export default {

	methods: {

		cancelEdit() {
			this.editedTodo = null;
		}
	}
}
</script>
src/components/TodoEdit.vue

<template>
	<input id="edit" class="edit" type="text"

		@keyup.esc="cancelEdit">
</template>

<script>
export default {

	methods: {

		cancelEdit(event) {
			event.target.value = this.todo.title;
			this.$emit('cancel-edit');
		}
	}
}
</script>

ここまで書き加えたリスト表示(src/components/TodoList.vue)とリスト項目(src/components/TodoController.vue)および項目編集の各モジュールファイルの中身は、つぎのコード001にまとめたとおりです。他のモジュールも含めたアプリケーションのファイル全体は、GitHub「vue-todo-list-06」をご参照ください。

コード001■項目テキストをダブルクリックで編集する

src/components/TodoList.vue

<template>
	<section class="main" v-show="todos.length" v-cloak>
		<input id="toggle-all" class="toggle-all" type="checkbox"
			:value="allDone"
			:checked="allDone"
			@change="onInput">
		<label for="toggle-all"></label>
		<ul class="todo-list">
			<li v-for="todo in filteredTodos"
				class="todo"
				:key="todo.id"
				:class="{completed: todo.completed, editing: todo == editedTodo}">
				<todo-item
					:todo="todo"
					@remove-todo="removeTodo"
					@done="done"
					@edit-todo="editTodo">
				</todo-item>
				<todo-edit
					:todo="todo"
					@done-edit="doneEdit"
					@cancel-edit="cancelEdit">
				</todo-edit>
			</li>
		</ul>
	</section>
</template>

<script>
import TodoItem from './TodoItem.vue';
import TodoEdit from './TodoEdit.vue';
export default {
	name: 'TodoList',
	components: {
		TodoItem,
		TodoEdit
	},
	props: {
		todos: Array,
		filteredTodos: Array,
		allDone: Boolean
	},
	data() {
		return {
			editedTodo: null
		};
	},
	methods: {
		removeTodo(todo) {
			this.$emit('remove-todo', todo);
		},
		done(todo, completed) {
			this.$emit('done', todo, completed);
		},
		onInput() {
			this.$emit('allDone', !this.allDone);
		},
		editTodo(todo) {
			this.editedTodo = todo;
		},
		doneEdit(todoTitle) {
			if (!this.editedTodo) {
				return;
			}
			const title = todoTitle.trim();
			if (title) {
				this.editedTodo.title = title;
			} else {
				this.removeTodo(this.editedTodo);
			}
			this.editedTodo = null;
		},
		cancelEdit() {
			this.editedTodo = null;
		}
	}
}
</script>

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

src/components/TodoItem.vue

<template>
	<div class="view">
		<input
			type="checkbox" class="toggle"
			:value="todo.completed"
			:checked="todo.completed"
			@change="onInput">
		<label @dblclick="editTodo">
			{{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);
		},
		editTodo() {
			this.$emit('edit-todo', this.todo);
		}
	}
}
</script>

src/components/TodoEdit.vue

<template>
	<input id="edit" class="edit" type="text"
		:value="todo.title"
		@input="onInput"
		@keypress.enter="doneEdit"
		@keyup.esc="cancelEdit">
</template>

<script>
export default {
	name: 'TodoEdit',
	props: {
		todo: Object
	},
	data() {
		return {
			editedTitle: null
		};
	},
	mounted() {
		this.editedTitle = this.todo.title;
	},
	methods: {
		onInput(event) {
			this.editedTitle = event.target.value;
		},
		doneEdit(event) {
			this.editedTitle = event.target.value;
			this.$emit('done-edit', this.editedTitle);
		},
		cancelEdit(event) {
			event.target.value = this.todo.title;
			this.$emit('cancel-edit');
		}
	}
}
</script>

Vue.js + CLI入門


作成者: 野中文雄
更新日: 2019年3月24日 ブラウザの違いへの対応も含めて、本文とコードを修正。
更新日: 2019年3月18日 04「ブラウザの違いによる問題に対応する」を追加。
作成日: 2019年2月28日


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