HTML5テクニカルノート
Vue.js + Vuex入門 07: 項目のテキストをダブルクリックで再編集する
- ID: FN1911001
- Technique: HTML5 / ECMAScript 2015
- Library: Vue.js 2.6.10
単一ファイルコンポーネントに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.js
のmutations
に加えるメソッド(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.jsexport default new Vuex.Store({ state: { editedTodo: null }, mutations: { editTodo(state, todo) { state.editedTodo = todo; }, } });
図001■項目をダブルクリックすると編集ボックスが表れる
02 リスト表示コンポーネントにヘルパー関数を使う
リスト表示コンポーネントsrc/components/TodoList.vue
には、Storeのstate
から同名の値を返す算出プロパティがふたつ(todos
とeditedTodo
)になりました。ヘルパー関数mapState()
でまとめましょう。getters
は、setterがあるとヘルパー関数に渡せないので、使えるプロパティはひとつ(filteredTodos
)です。それでも、あとで増えるかもしれません。mapGetters()
で揃えることにします。
src/components/TodoList.vueimport {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.vue
とsrc/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>
<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.js
のmutations
に新しく加えたメソッド(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.jsexport 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.js
のmutations
に加えたメソッド(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.jsexport 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>
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入門
- Vue.js + Vuex入門 01: Storeを使う
- Vue.js + Vuex入門 02: クラスバインディングと項目の削除
- Vue.js + Vuex入門 03: データの変化に応じた処理とローカルへの保存
- Vue.js + Vuex入門 04: リスト表示する項目を選び出して切り替える
- Vue.js + Vuex入門 05: チェックをまとめてオン/オフする
- Vue.js + Vuex入門 06: チェックした項目をまとめて削除する
- Vue.js + Vuex入門 07: 項目のテキストをダブルクリックで再編集する
- Vue.js + Vuex入門 08: フォーカスをコントロールする
作成者: 野中文雄
作成日: 2019年11月09日
Copyright © 2001-2019 Fumio Nonaka. All rights reserved.