HTML5テクニカルノート
Vue + <script setup> + TypeScript: TodoMVC 06 項目のテキストをダブルクリックで再編集する
- ID: FN2212004
- Technique: ECMAScript 2015
- Package: Vue 3.2
Vue + <script setup> + TypeScriptの構文で、公式サイト「TodoMVC」の例をつくるシリーズ第6回です。Todoリストの基本的な機能は揃ってきました。さらに加えたいのは、すでに入力したTodo項目をダブルクリックで再編集できるようにすることです。項目をいったん削除して入力し直すという手間が省けるでしょう。
01 Todo項目のダブルクリックで編集のテキスト入力フィールドを出す
Todo項目を書き替えるときもボタンは使いません。項目テキスト(<label>
要素)のダブルクリックで進めます。リスナーを定めるイベントはv-on:dblclick
(省略記法@dblclick
)です。リスナーメソッド(editTodo()
)は、つぎのようにイベント(event: 'editTodo'
)ともにTodo項目のオブジェクト(todo
)を親コンポーネントに送ります。
src/components/TodoItem.vue
<script setup lang="ts"> interface Emits { (event: 'editTodo', todo: Todo): void; } const editTodo = () => { emit('editTodo', props.todo); }; </script> <template> <div class="view"> <!-- <label>{{ todo.title }}</label> --> <label @dblclick="editTodo">{{ todo.title }}</label> </div> </template>
親モジュールsrc/components/TodoList.vue
がダブルクリックのイベント(editTodo
)受け取ると、呼び出されるのがリスナーメソッド(editTodo()
)です。引数として受け取ったTodo項目(todo
)は、ref
オブジェクト(editedTodo
)に定められます。これで、親コンポーネントはどのTodo項目が編集中か知ることができるのです(編集中の項目がない場合の値はnull
)。子コンポーネントTodoItem
の親要素(<li>
)には、つぎのようにクラスediting
をバインディング(:class
)に加えました。なお、新たに差し込んだ項目編集用の子コンポーネント(TodoEdit
)は、のちほどつくります。プロパティとしてtodo
をバインディングしたのは、ダブルクリックされた項目データを編集するためです。
src/components/TodoList.vue
<script setup lang="ts"> import { ref } from 'vue'; import TodoEdit from './TodoEdit.vue'; const editedTodo = ref<Todo | null>(null); const editTodo = (todo: Todo) => { editedTodo.value = todo; }; </script> <template> <section class="main"> <ul class="todo-list"> <!-- :class="['todo', { completed: todo.completed }]" --> <li v-for="todo in filteredTodos" :class="[ 'todo', { completed: todo.completed, editing: todo == editedTodo }, ]" :key="todo.id" > <!-- <TodoItem :todo="todo" @removeTodo="removeTodo" @done="done" /> --> <TodoItem :todo="todo" @removeTodo="removeTodo" @done="done" @editTodo="editTodo" /> <TodoEdit :todo="todo" /> </li> </ul> </section> </template>
Todo項目のダブルクリックにより、子コンポーネントTodoItem
とTodoEdit
の表示を切り替えなければいけません。そのために加えたのが、親コンポーネントTodoList
にバインディングしたediting
クラスです。各親子コンポーネントには、CSSのクラスがつぎのように定められています。
TodoList
コンポーネント
<ul>
要素にtodo-list
クラス<li>
要素にediting
クラスをバインディング
TodoItem
コンポーネント
- ルート(
<div>
)要素にview
クラスTodoEdit
コンポーネント
- ルート(
<input>
)要素にedit
クラス
これらのクラスの扱いを抜き出したのが、つぎのindex.css
の記述です。view
クラスの要素はすでに見てきたとおり、はじめから表示されていました。それに対して、edit
クラスはもとが非表示です。ここに、Todo項目のダブルクリックでediting
クラスのバインディングが変わると、要素の表示と非表示は切り替わるのです。
https://unpkg.com/todomvc-app-css@2.4.2/index.css
.todo-list li.editing .edit { display: block; } .todo-list li.editing .view { display: none; } .todo-list li .edit { display: none; }
新たなモジュールsrc/components/TodoEdit.vue
の記述は、動作を確認するため、いったんつぎの記述で済ませておきましょう。Todo項目をダブルクリックすると、編集用のテキスト入力フィールドに表示が切り替わるはずです(図001)。
src/components/TodoEdit.vue
<script setup lang="ts"> import type { Todo } from '../App.vue'; interface Props { todo: Todo; } defineProps<Props>(); </script> <template> <input :id="`edit-${todo.id}`" class="edit" type="text" :value="todo.title" /> </template>
図001■Todo項目をダブルクリックすると編集用のテキスト入力フィールドが表れる

Todo項目単体表示のモジュールsrc/components/TodoItem.vue
には、今回これ以上の手は加えません。つぎのコード001に記述全体をまとめましょう。
コード001■Todo項目単体表示のモジュール
src/components/TodoItem.vue
<script setup lang="ts">
import type { Todo } from '../App.vue';
interface Props {
todo: Todo;
}
interface Emits {
(event: 'removeTodo', todo: Todo): void;
(event: 'done', todo: Todo, completed: boolean): void;
(event: 'editTodo', todo: Todo): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const removeTodo = () => {
emit('removeTodo', props.todo);
};
const onChange = () => {
emit('done', props.todo, !props.todo.completed);
};
const editTodo = () => {
emit('editTodo', props.todo);
};
</script>
<template>
<div class="view">
<input
type="checkbox"
class="toggle"
:checked="todo.completed"
@change="onChange"
/>
<label @dblclick="editTodo">{{ todo.title }}</label>
<button class="destroy" @click="removeTodo" />
</div>
</template>
ノート01■DOMイベントハンドラ
Vue公式サイトの「メソッドハンドラー」には、とくにdblclick
イベントは記載されていません。けれど、JavaScriptネイティブのDOMイベントはサポートされます(「Why not add v-on:doubleClick」参照)。dblclick
イベントもそのひとつです。
メソッドハンドラーは、トリガーとなるネイティブのDOMイベントオブジェクトを自動的に受け取ります(「メソッドハンドラー」) 。
02 [enter]キーでTodo項目の編集を確定する
編集した項目テキストはモジュールsrc/components/TodoEdit.vue
のテンプレートに、つぎのように@input
イベントのリスナーメソッド(onInput()
)を加え、ref
オブジェクト(editedTitle
)に反映しました。項目の追加と同じく、編集の確定は[enter]キーです。@keypress.enter
イベントで、リスナーメソッド(doneEdit()
)から親のコンポーネントにイベント(event: 'doneEdit'
)と新たな項目テキスト(editedTitle
)を送ります。
src/components/TodoEdit.vue
<script setup lang="ts"> import { ref } from 'vue'; interface Emits { (event: 'doneEdit', editedTitle: string): void; } const emit = defineEmits<Emits>(); const editedTitle = ref<string | null>(null); const doneEdit = ({ target }: Event) => { if (!(target instanceof HTMLInputElement)) return; editedTitle.value = target.value; emit('doneEdit', editedTitle.value); }; const onInput = ({ target }: Event) => { if (target instanceof HTMLInputElement) { editedTitle.value = target.value; } }; </script> <template> <!-- <input :id="`edit-${todo.id}`" class="edit" type="text" :value="todo.title" /> --> <input :id="`edit-${todo.id}`" class="edit" type="text" :value="todo.title" @input="onInput" @keypress.enter="doneEdit" /> </template>
親モジュールsrc/components/TodoList.vue
が子コンポーネント(TodoEdit
)から受け取ったイベント(doneEdit
)のハンドラメソッドがdoneEdit()
です。引数(todoTitle
)が有効な文字列であれば、Todo項目のテキスト(title
)が書き替わります。実質テキストがない(空文字列を含む)場合は、Todo項目そのものを削除することにしました。これで、項目をダブルクリックして書き替えたテキストが、[enter]キーで確定できるようになったのです(図002)。
src/components/TodoList.vue
<script setup lang="ts"> const doneEdit = (todoTitle: string) => { if (!editedTodo.value) return; const title = todoTitle.trim(); if (title) { editedTodo.value.title = title; } else { removeTodo(editedTodo.value); } editedTodo.value = null; }; </script> <template> <section class="main"> <ul class="todo-list"> <li > <!-- <TodoEdit :todo="todo" /> --> <TodoEdit :todo="todo" @doneEdit="doneEdit" /> </li> </ul> </section> </template>
図002■編集したテキストが[enter]キーで差し替わる

03 [esc]キーでTodo項目の編集を中止する
編集したTodo項目のテキストを[enter]で確定せず、[esc]キーを押したときはとり止めることにしましょう。モジュールsrc/components/TodoEdit.vue
のテンプレートに加えるのは、つぎのような@keyup.esc
イベントのリスナーメソッド(cancelEdit()
)です。<input type="text">
要素のテキスト(value
プロパティ)はTodo項目オブジェクト(todo
)の文字列(title
)に戻し、親コンポーネントには取り消しのイベント(event: 'cancelEdit'
)を送ります。
src/components/TodoEdit.vue
<script setup lang="ts"> interface Emits { (event: 'cancelEdit'): void; } // defineProps<Props>(); const props = defineProps<Props>(); const cancelEdit = ({ target }: Event) => { if (!(target instanceof HTMLInputElement)) return; target.value = props.todo.title; emit('cancelEdit'); }; </script> <template> <input @keyup.esc="cancelEdit" /> </template>
イベント(doneEdit
)を受け取った親モジュールsrc/components/TodoList.vue
が呼び出すのは、リスナーメソッドcancelEdit()
です。編集中の項目データを示すref
オブジェクト(editedTodo
)の値はnull
、つまりなしに戻ります。項目のデータはもとのまま変わりません。そして、表示するTodo項目単体の子コンポーネントは、クラスバインディングにより編集中(TodoEdit
)から入力済み(TodoItem
)に切り替わるのです。
src/components/TodoList.vue
<script setup lang="ts"> const cancelEdit = () => { editedTodo.value = null; }; </script> <template> <section class="main"> <ul class="todo-list"> <li > <!-- <TodoEdit :todo="todo" @doneEdit="doneEdit" /> --> <TodoEdit :todo="todo" @cancelEdit="cancelEdit" @doneEdit="doneEdit" /> </li> </ul> </section> </template>
書き上がったTodo項目編集(src/components/TodoEdit.vue
)とリスト表示(src/components/TodoList.vue
)のモジュールの記述全体を、つぎのコード002にまとめました。
コード002■Todo項目編集とリスト表示のモジュール
src/components/TodoEdit.vue
<script setup lang="ts">
import { ref } from 'vue';
import type { Todo } from '../App.vue';
interface Props {
todo: Todo;
}
interface Emits {
(event: 'cancelEdit'): void;
(event: 'doneEdit', editedTitle: string): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const editedTitle = ref<string | null>(null);
const cancelEdit = ({ target }: Event) => {
if (!(target instanceof HTMLInputElement)) return;
target.value = props.todo.title;
emit('cancelEdit');
};
const doneEdit = ({ target }: Event) => {
if (!(target instanceof HTMLInputElement)) return;
editedTitle.value = target.value;
emit('doneEdit', editedTitle.value);
};
const onInput = ({ target }: Event) => {
if (target instanceof HTMLInputElement) {
editedTitle.value = target.value;
}
};
</script>
<template>
<input
:id="`edit-${todo.id}`"
class="edit"
type="text"
:value="todo.title"
@input="onInput"
@keypress.enter="doneEdit"
@keyup.esc="cancelEdit"
/>
</template>
src/components/TodoList.vue
<script setup lang="ts">
import { ref } from 'vue';
import type { Todo } from '../App.vue';
import TodoEdit from './TodoEdit.vue';
import TodoItem from './TodoItem.vue';
interface Props {
allDone: boolean;
filteredTodos: Todo[];
}
interface Emits {
(event: 'removeTodo', todo: Todo): void;
(event: 'done', todo: Todo, completed: boolean): void;
(event: 'toggleAll', checked: boolean): void;
}
defineProps<Props>();
const emit = defineEmits<Emits>();
const editedTodo = ref<Todo | null>(null);
const removeTodo = (todo: Todo) => {
emit('removeTodo', todo);
};
const done = (todo: Todo, completed: boolean) => {
emit('done', todo, completed);
};
const editTodo = (todo: Todo) => {
editedTodo.value = todo;
};
const cancelEdit = () => {
editedTodo.value = null;
};
const doneEdit = (todoTitle: string) => {
if (!editedTodo.value) return;
const title = todoTitle.trim();
if (title) {
editedTodo.value.title = title;
} else {
removeTodo(editedTodo.value);
}
editedTodo.value = null;
};
const onChange = ({ target }: Event) => {
if (!(target instanceof HTMLInputElement)) return;
emit('toggleAll', target.checked);
};
</script>
<template>
<section class="main">
<input
id="toggle-all"
type="checkbox"
class="toggle-all"
:checked="allDone"
@change="onChange"
/>
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li
v-for="todo in filteredTodos"
:class="[
'todo',
{ completed: todo.completed, editing: todo == editedTodo },
]"
:key="todo.id"
>
<TodoItem
:todo="todo"
@removeTodo="removeTodo"
@done="done"
@editTodo="editTodo"
/>
<TodoEdit :todo="todo" @cancelEdit="cancelEdit" @doneEdit="doneEdit" />
</li>
</ul>
</section>
</template>
ソース01■TodoMVC 06 項目のテキストをダブルクリックで再編集する
ノート02■@keyup.esc
テキストの入力を[return]/[Enter]キーで確定しようとしたとき、@keyup
イベントで扱うと、日本語入力の場合不都合がありました(「keypress
とkeyup
」)。[esc]キーでも問題は同じです。漢字変換候補を表示したあと、変換を戻すつもりが、入力の取り消しとみなされてしまいます。けれど、この場合イベントを@keypress
にしても変わりません。当面、解決策は見当たらない状況です。
Vue + <script setup> + TypeScript: TodoMVCシリーズ
- TodoMVC 01 リスト項目の表示と追加
- TodoMVC 02 リスト項目の削除とスタイル変更
- TodoMVC 03 算出プロパティとデータのローカルへの保存
- TodoMVC 04 リスト表示する項目を選び出して切り替える
- TodoMVC 05 チェックをまとめてオン/オフしたり削除する
- TodoMVC 06 項目のテキストをダブルクリックで再編集する
- TodoMVC 07 フォーカスをコントロールする
- TodoMVC 08 要素にアニメーションを加える
- TodoMVC 09 コンポーネントからロジックをコンポーザブル関数に切り分ける
作成者: 野中文雄
作成日: 2022年12月22日
Copyright © 2001-2020 Fumio Nonaka. All rights reserved.