サイトトップ

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

HTML5テクニカルノート

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


単一ファイルコンポーネントにVuexのStoreを加えてつくるTodoMVCアプリケーションのチュートリアルシリーズ「Vue.js + Vuex入門」も第7回になりました。これまでのところ、追加した項目を書き替えるには、一旦削除して入力し直すしかありません。これを、項目のダブルクリックで編集できるようにしましょう。

01 項目のダブルクリックで編集ボックスに切り替える

ダブルクリックで表れる編集ボックスは、新たなコンポーネントsrc/components/TodoEdit.vueとしてつぎのように定めます。そして、リスト表示のコンポーネントsrc/components/TodoList.vueのテンプレートで、項目のコンポーネント(TodoItem)の下に重ねてください。親要素(<li>)にバインディングを加えたクラス(editing)により、編集ボックスとの表示・非表示が切り替えられます(index.css参照)。

src/components/TodoEdit.vue

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

<script>
export default {
	name: 'TodoEdit',
	props: {
		todo: Object
	},
	data() {
		return {
			editedTitle: null
		};
	},
	methods: {
		onInput(event) {
			this.editedTitle = event.target.value;
		},
	}
};
</script>

src/components/TodoList.vue

<template>

		<ul class="todo-list">
			<li
				v-for="todo in filteredTodos"
				:class="{completed: todo.completed, editing: todo === editedTodo}"

			>
				<!-- :class="['todo', {completed: todo.completed}]" -->

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

<script>

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

	components: {

		TodoEdit
	},
	computed: {

		editedTodo() {
			return this.$store.state.editedTodo;
		},

	},

};
</script>

編集ボックスに切り替えるダブルクリックイベント(dblclick)のハンドラメソッドを定めるのは、リスト項目のコンポーネントsrc/components/TodoItem.vueです。モジュールsrc/store.jsmutationsに加えるメソッド(editTodo())が呼び出され、編集する項目オブジェクト(todo)をstateのプロパティ(editedTodo)に定めます。すると、前掲のクラスバインディングにより、その項目が編集ボックスと切り替わるのです(図001)。

src/components/TodoItem.vue

<template>
	<div class="view">

		<!-- <label>{{todo.title}}</label> -->
		<label @dblclick="editTodo(todo)">
			{{todo.title}}</label>

	</div>
</template>

<script>

export default {

	methods: mapMutations({

		editTodo: 'editTodo'
	})
}
</script>

src/store.js

export default new Vuex.Store({
	state: {

		editedTodo: null
	},

	mutations: {

		editTodo(state, todo) {
			state.editedTodo = todo;
		},
	}
});

図001■項目をダブルクリックすると編集ボックスが表れる

図001

02 リスト表示コンポーネントにヘルパー関数を使う

リスト表示コンポーネントsrc/components/TodoList.vueには、Storeのstateから同名の値を返す算出プロパティがふたつ(todoseditedTodo)になりました。ヘルパー関数mapState()でまとめましょう。gettersは、setterがあるとヘルパー関数に渡せないので、使えるプロパティはひとつ(filteredTodos)です。それでも、あとで増えるかもしれません。mapGetters()で揃えることにします。

src/components/TodoList.vue

import {mapState, mapGetters} from 'vuex';

export default {

	computed: {
		/* todos() {
			return this.$store.state.todos;
		},
		editedTodo() {
			return this.$store.state.editedTodo;
		}, */
		...mapState([
			'todos',
			'editedTodo'
		]),
		/* filteredTodos() {
			return this.$store.getters.filteredTodos;
		}, */
		...mapGetters([
			'filteredTodos'
		]),

	},

};

今回、コンポーネントsrc/components/TodoList.vuesrc/components/TodoItem.vueには、これ以上の手は加えません。つぎのコード001にまとめて掲げます。

コード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" />
		<ul class="todo-list">
			<li
				v-for="todo in filteredTodos"
				:class="{completed: todo.completed, editing: todo === editedTodo}"
				:key="todo.id"
			>
				<todo-item
					:todo="todo" />
				<todo-edit
					:todo="todo" />
			</li>
		</ul>
	</section>
</template>

<script>
import {mapState, mapGetters} from 'vuex';
import TodoItem from './TodoItem.vue';
import TodoEdit from './TodoEdit.vue';
export default {
	name: 'TodoList',
	components: {
		TodoItem,
		TodoEdit
	},
	computed: {
		...mapState([
			'todos',
			'editedTodo'
		]),
		...mapGetters([
			'filteredTodos'
		]),
		allDone: {
			get() {
				return this.$store.getters.allDone;
			},
			set(value) {
				this.$store.commit('setAllDone', value);
			}
		}
	},
	methods: {
		onInput() {
			this.allDone = !this.allDone;
		}
	}
};
</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"
			@input="onInput({todo: todo, completed: !todo.completed})"
		>
		<label @dblclick="editTodo(todo)">
			{{todo.title}}</label>
		<button
			class="destroy"
			@click="removeTodo(todo)">
		</button>
	</div>
</template>

<script>
import { mapMutations } from 'vuex';
export default {
	name: 'TodoItem',
	props: {
		todo: Object
	},
	methods: mapMutations({
		removeTodo: 'removeTodo',
		onInput: 'done',
		editTodo: 'editTodo'
	})
}
</script>

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

ダブルクリックで編集ボックスに切り替えてテキストは書き直せるものの、項目リストのデータはもとのままです。項目の追加と同じように、[enter]キーでデータが改められるようにしましょう。

コンポーネントsrc/components/TodoEdit.vueのテキスト入力フィールド(<input type="text">要素)から[enter]キー入力のイベント(keypress.enter)で呼び出すメソッド(doneEdit())は、Storeにコミット(doneEdit)を送ります。引数は、編集したテキスト(editedTitle)です。コミットを受け取るモジュールsrc/store.jsmutationsに新しく加えたメソッド(doneEdit())は、編集している項目(editedTodo)のテキスト(title)を引数に受け取った文字列に書き替えたうえで、編集項目の値は空に戻します。

src/components/TodoEdit.vue

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

		@keypress.enter="doneEdit">
</template>

<script>
export default {

		doneEdit(event) {
 			this.editedTitle = event.target.value;
 			this.$store.commit('doneEdit', this.editedTitle);
 		},
	}
};
</script>

src/store.js

export default new Vuex.Store({

	mutations: {

		doneEdit(state, todoTitle) {
			if (!state.editedTodo) {
				return;
			}
			const title = todoTitle.trim();
			if (title) {
				state.editedTodo.title = title;
			} else {
				this.commit('removeTodo', state.editedTodo);
			}
			state.editedTodo = null;
		},
 }
});

これで、編集した項目テキストが[enter]キーで書き改まります。けれど、途中で止めることができません。あえてやろうとするなら、別項目を(あった場合は)ダブルクリックして、そのまま[enter]キーを押すくらいしかないのです。

04 [esc]キーで編集を取り消す

そこで、[esc]キーで編集を取り消せるようにしましょう。[esc]キーのイベント(keyup.esc)を加えるのは、前項と同じくコンポーネントsrc/components/TodoEdit.vueのテキスト入力フィールド(<input type="text">要素)です。呼び出したメソッド(cancelEdit())は、編集していたテキストをもとに戻してStoreにコミット(cancelEdit)を送ります。モジュールsrc/store.jsmutationsに加えたメソッド(cancelEdit())が、編集項目(editedTodo)の値を空にすれば、取り消しは完了です。

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.$store.commit('cancelEdit');
		}
	}
};
</script>

src/store.js

export default new Vuex.Store({

	mutations: {

		cancelEdit(state) {
			state.editedTodo = null;
		}
	}
});

編集ボックスのコンポーネントsrc/components/TodoEdit.vueとモジュールsrc/store.jsの記述全体は、つぎのコード002のとおりです。アプリケーションの動きとコードを確かめるためのサンプル001も、CodeSandboxに掲げます。この「Vue.js + Vuex入門」シリーズがお題にしたVue.js公式サイト「TodoMVC の例」の基本的な機能はほぼ整いました。ただ、少し操作してみると、まだ気になるところがあります。次回は、残った問題点をかたづけて、アプリケーションの仕上げです。

コード002■項目の編集を[enter]で確定し[esc]で取り消す

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
		};
	},
	methods: {
		onInput(event) {
			this.editedTitle = event.target.value;
		},
		doneEdit(event) {
			this.editedTitle = event.target.value;
			this.$store.commit('doneEdit', this.editedTitle);
		},
		cancelEdit(event) {
			event.target.value = this.todo.title;
			this.$store.commit('cancelEdit');
		}
	}
};
</script>

src/store.js

import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const STORAGE_KEY = 'todos-vuejs-2.6';
const todoStorage = {
	fetch() {
		const todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
		todos.forEach(function(todo, index) {
			todo.id = index;
		});
		todoStorage.uid = todos.length;
		return todos;
	},
	save(todos) {
		localStorage.setItem(STORAGE_KEY, JSON.stringify(todos));
	}
};
const filters = {
	all(todos) {
		return todos;
	},
	active(todos) {
		return todos.filter((todo) =>
			!todo.completed
		);
	},
	completed(todos) {
		return todos.filter((todo) =>
			todo.completed
		);
	}
};
export default new Vuex.Store({
	state: {
		todos: todoStorage.fetch(),
		visibility: 'all',
		editedTodo: null
	},
	getters: {
		filteredTodos: (state) =>
			filters[state.visibility](state.todos),
		remaining: (state) => {
			const todos = state.todos.filter((todo) => !todo.completed);
			return todos.length;
		},
		filters: (state) => filters,
		allDone: (state, getters) => getters.remaining === 0
	},
	mutations: {
		addTodo(state, todoTitle) {
			const newTodo = todoTitle && todoTitle.trim();
			if (!newTodo) {
				return;
			}
			state.todos.push({
				id: todoStorage.uid++,
				title: newTodo,
				completed: false
			});
		},
		removeTodo(state, todo) {
			state.todos = state.todos.filter((item) => item !== todo);
		},
		done(state, {todo, completed}) {
			state.todos = state.todos.map((item) => {
				if(item === todo) {
					item.completed = completed
				}
				return item;
			});
		},
		save(state) {
			todoStorage.save(state.todos);
		},
		hashChange(state) {
			const visibility = window.location.hash.replace(/#\/?/, '');
			if (filters[visibility]) {
				state.visibility = visibility;
			}
		},
		setAllDone(state, value) {
			state.todos.forEach((todo) =>
				todo.completed = value
			);
		},
		removeCompleted(state) {
			state.todos = filters.active(state.todos);
		},
		editTodo(state, todo) {
			state.editedTodo = todo;
		},
		doneEdit(state, todoTitle) {
			if (!state.editedTodo) {
				return;
			}
			const title = todoTitle.trim();
			if (title) {
				state.editedTodo.title = title;
			} else {
				this.commit('removeTodo', state.editedTodo);
			}
			state.editedTodo = null;
		},
		cancelEdit(state) {
			state.editedTodo = null;
		}
	}
});

サンプル001■vue-vuex-todo-mvc-07

Vue.js + Vuex入門


作成者: 野中文雄
作成日: 2019年11月09日


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