サイトトップ

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

HTML5テクニカルノート

Vue.js + ES6入門 10: コンポーネントの応用とデータのチェック


Vue.js + ES6入門 09: アプリケーションをコンポーネントに分ける」でつくったTodoリストのアプリケーション(サンプル001)から、さらにコンポーネントを分けて入れ子にします。そして、データのチェックも加えて、つくりをしっかりしましょう。

01 項目入力部分をコンポーネントに切り分ける

先に、入力した項目の追加部分をコンポーネントに切り分けます(図001)。

図001■項目入力の部分をコンポーネントに切り分ける

図001

ここでひとつ考えることは、v-modelディレクティブで双方向バインディングしているデータ(todoText)の扱いです。フィールドに入力しているテキストそのものは、アプリケーションが気にしなくて構いません。追加ボタンが押されたとき(clickイベント)、そのテキストを受け取れば済むからです。ハンドラメソッド(addTodo())にはそのために、引数を新たに加えます。

<body>要素

<div id="app" class="container">

    <!-- <p>
        <input type="text" v-model="todoText" placeholder="add new todo here">
        <button class="btn btn-primary btn-sm" @click="addTodo">追加</button>
    </p> -->
    <add-todo @add-todo="addTodo"></add-todo>

</div>

<script>要素

const app = new Vue({
    data: {
        // todoText: '',

    },

    methods: {
        // addTodo() {
        addTodo(newTodo) {
            // const newTodo = this.todoText;
            // this.todoText = '';
            this.todos = [
                ...this.todos,
                {text: newTodo, done: false}
            ];
        },

    }
});

そうすると、データ(dataオプション)は、子コンポーネントの側に持たせます。そのとき「dataは関数でなければなりません」。オブジェクトにしてしまうと、コンポーネントを同時に使いまわしたとき、参照が同じになってしまうからです。関数(メソッド)にすることで、コンポーネントごとに異なる戻り値のオブジェクトが与えられます。

項目入力のコンポーネントのJavaScriptコードはつぎのとおりです。なお、項目を加えるメソッド(addTodo())は、この機会に空の項目や余分な空白が入らないようにしました(図002)。

コード001■ヘッダコンポーネントと親のテンプレート

<script>要素

Vue.component('add-todo', {
    data() {
        return {
            todoText: ''
        };
    },
    methods: {
        addTodo() {
            const todoText = this.todoText.trim();
            this.todoText = '';
            if (todoText) {
                this.$emit('add-todo', todoText);
            }
        }
    },
    template: `
        <p>
            <input type="text" v-model="todoText" placeholder="add new todo here">
            <button class="btn btn-primary btn-sm" @click="addTodo">追加</button>
        </p>
    `
});

図002■項目追加のメソッドに手を加える前

図002

02 リスト表示のコンポーネントを切り分ける

つぎに切り分けるのが、リスト表示のコンポーネントです(図003)。項目のコンポーネントが含まれるので、入れ子ということになります。

図003■リスト表示部分をコンポーネントに切り分ける

図003

親アプリケーションのテンプレートを、つぎのようにリスト表示のコンポーネント(todo-list)に差し替えます。子が参照する算出プロパティ(filteredTodos)はv-bind(:)でバインドしてください。

<body>要素

<div id="app" class="container">

    <!-- <ul class="list-unstyled">
        <li v-for="todo in filteredTodos">
            <todo-item :todo="todo" @remove-todo="removeTodo">
            </todo-item>
        </li>
    </ul> -->
    <todo-list :filtered-todos="filteredTodos" @remove-todo="removeTodo">
    </todo-list>

</div>

リスト表示コンポーネントのJavaScriptコードはつぎのとおりです(コード002)。項目データのオブジェクト(todo)は入れ子コンポーネントにv-bind(:)で渡し、v-on(@)で項目削除のイベント(remove-todo)を受け取ったら、ハンドラメソッド(removeTodo)から親アプリケーションにバケツリレーのように送ります。vm.$emit()メソッドに渡す第2引数(todo)は削除する項目のオブジェクトです。

コード002■リスト表示のコンポーネント

<script>要素

Vue.component('todo-list', {
    props: ['filteredTodos'],
    methods: {
        removeTodo(todo) {
            this.$emit('remove-todo', todo);
        }
    },
    template: `
        <ul class="list-unstyled">
            <li v-for="todo in filteredTodos">
                <todo-item :todo="todo" @remove-todo="removeTodo">
                </todo-item>
            </li>
        </ul>
    `
});

03 開発バージョンのVue.jsライブラリを使う

ここまで書き上げたアプリケーションを試すと、つぎのような警告が早速出されるでしょう。Vueインスタンス(app)の外の以下のオブジェクト(filters)を、コンポーネントが直に参照しているからです。


[Vue warn]: Property or method "filters" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.

<script>要素

const filters = {
    all(todos) {
        return todos;
    },
    active(todos) {
        return todos.filter((todo) =>
            !todo.done
        );
    },
    completed(todos) {
        return todos.filter((todo) =>
            todo.done
        );
    }
};

const app = new Vue({

});

データを管理するために、参照するオブジェクトやメソッドはVueインスタンス(app)のdataオプションに定めておかなければならないのです(「リアクティブプロパティの宣言」参照)。

<script>要素

const app = new Vue({
    data: {

        filters: filters
    },

}

もちろん、子コンポーネント(list-footer)のプロパティ(filters)にバインドして渡すことは忘れないでください。

<body>要素


<div id="app" class="container">

    <list-footer

        :filters="filters"

    >
    </list-footer>
</div>
<script>要素

Vue.component('list-footer', {
    props: ['visibility', 'filters'],

});

04 プロパティの型を定める

コンポーネントのpropsオプションは、配列にプロパティ名を加える以外に、オブジェクトでもっと細かく定められます。このオブジェクトによる定義が公式スタイルガイドの推奨です(「プロパティの定義」参照)。以下のように、オブジェクトにプロパティ名とその型を与えます。すると、開発版のライブラリであれば、型が違うとコンソールに警告を示してくれるのです。


[Vue warn]: Invalid prop: type check failed for prop "todo". Expected String with value "[object Object]", got Object

found in

---> <TodoItem>
     <TodoList>
       <Root>
<script>要素

Vue.component('todo-item', {
    // props: ['todo'],
    props: {
        todo: String
    },

});

もっとも、デフォルトではプロパティ(notExist)がなくても放っておかれます。

<script>要素

Vue.component('todo-item', {
    props: {
        todo: Object,
        notExist: String  // 存在しない
    },

});

プロパティの有無まで確かめたいときは、さらに値をオブジェクトにして、requiredオプションに定められます(デフォルト値false)。データ型はtypeオプションです。


[Vue warn]: Missing required prop: "notExist"

<script>要素

Vue.component('todo-item', {
    props: {

        notExist: {
            type: String,
            required: true
        }
    },
    `
});

他のコンポーネントも含めて、propsオプションはオブジェクトでつぎのように定めればよいでしょう。

<script>要素

Vue.component('list-header', {
    // props: ['todos', 'remaining'],
    props: {
        todos: {
            type: Array,
            required: true
        },
        remaining: {
            type: Number,
            required: true
        }
    },

});
Vue.component('todo-item', {
    props: {
        todo: {
            type: Object,
            required: true
        }
    },

});
Vue.component('todo-list', {
    // props: ['filteredTodos'],
    props: {
        filteredTodos: {
            type: Array,
            required: true
        }
    },

});
Vue.component('list-footer', {
    // props: ['visibility', 'filters'],
    props: {
        visibility: {
            type: String,
            required: true
        },
        filters: {
            type: Object,
            required: true
        }
    },

});

これで、ふたつのコンポーネントが切り分けられ、プロパティのデータチェックも加わりました。HTMLの記述とJavaScriptコードは、それぞれつぎにまとめたコード003のとおりです。併せて、サンプル001をCodePenに掲げます。

コード003■コンポーネントの切り分けとデータチェックを加えたHTMLとJavaScriptの記述全体

<body>要素

<div id="app" class="container">
	<h2>Todo</h2>
	<list-header
		:todos="todos"
		:remaining="remaining"
		@archive="archive"
	>
	</list-header>
	<todo-list :filtered-todos="filteredTodos" @remove-todo="removeTodo">
	</todo-list>
	<add-todo @add-todo="addTodo"></add-todo>
	<list-footer
		:visibility="visibility"
		:filters="filters"
		@change-filter="changeFilter"
	>
	</list-footer>
</div>

<script>要素

const filters = {const filters = {
	all(todos) {
		return todos;
	},
	active(todos) {
		return todos.filter((todo) =>
			!todo.done
		);
	},
	completed(todos) {
		return todos.filter((todo) =>
			todo.done
		);
	}
};
Vue.component('list-header', {
	props: {
		todos: {
			type: Array,
			 required : true
		},
		remaining: {
			type: Number,
			 required : true
		}
	},
	methods: {
		archive() {
			this.$emit('archive');
		}
	},
	template: `
		<p>
			全{{todos.length}}件中残り{{remaining}}件
			<button class="btn btn-danger btn-sm"  @click="archive">断捨離</button>
		</p>
	`
});
Vue.component('todo-item', {
	props: {
		todo: {
			type: Object,
			 required : true
		}
	},
	methods: {
		removeTodo(todo) {
  			this.$emit('remove-todo', todo);
		}
	},
	template: `
		<div>
			<label>
				<input type="checkbox" v-model="todo.done">
				<span :class="{'done': todo.done}">{{todo.text}}</span>
			</label>
			<button class="btn btn-warning btn-sm" @click="removeTodo(todo)">削除</button>
		</div>
	`
});
Vue.component('todo-list', {
	props: {
		filteredTodos: {
			type: Array,
			 required : true
		}
	},
	methods: {
		removeTodo(todo) {
			this.$emit('remove-todo', todo);
		}
	},
	template: `
		<ul class="list-unstyled">
			<li v-for="todo in filteredTodos">
				<todo-item :todo="todo" @remove-todo="removeTodo">
				</todo-item>
			</li>
		</ul>
	`
});
Vue.component('add-todo', {
	data() {
		return {
			todoText: ''
		};
	},
	methods: {
		addTodo() {
			const todoText = this.todoText.trim();
			this.todoText = '';
			if (todoText) {
				this.$emit('add-todo', todoText);
			}
		}
	},
	template: `
		<p>
			<input type="text" v-model="todoText" placeholder="add new todo here">
			<button class="btn btn-primary btn-sm" @click="addTodo">追加</button>
		</p>
	`
});
Vue.component('list-footer', {
	props: {
		visibility: {
			type: String,
			 required : true
		},
		filters: {
			type: Object,
			 required : true
		}
	},
	methods: {
		changeFilter(key) {
			this.$emit('change-filter', key);
		}
	},
	template: `
	<div>
		<button type="button"
			v-for="(value, key) in filters"
			:class="['btn btn-outline-info btn-sm mr-1', {active: visibility === key}]"
			@click="changeFilter(key)">
			{{ key[0].toUpperCase() + key.substr(1) }}
		</button>
	</div>
	`
});
const app = new Vue({
	data: {
		todos: [
			{text: 'Vue.jsを学ぶ', done: true},
			{text: 'Vue.jsでアプリケーションをつくる', done: false},
		],
		visibility: 'all',
		filters: filters
	},
	computed: {
		remaining() {
			const count =
				this.todos.reduce((count, todo) =>
					count = (todo.done) ? count : ++count
				, 0);
			return count;
		},
		filteredTodos() {
			return filters[this.visibility](this.todos);
		}
	},
	methods: {
		addTodo(newTodo) {
			this.todos = [
				...this.todos,
				{text: newTodo, done: false}
			];
		},
		removeTodo(todo) {
			this.todos = this.todos.filter((_todo) => _todo !== todo);
		},
		archive() {
			this.todos = this.todos.filter((todo) => !todo.done);
		},
		changeFilter(visibility) {
			this.visibility = visibility;
		}
	}
});
document.addEventListener('DOMContentLoaded', () =>
	app.$mount('#app')
);

サンプル001■Vue.js + ES6: Separating components and validating props

See the Pen Vue.js + ES6: Separating components and validating props by Fumio Nonaka (@FumioNonaka) on CodePen.

Vue.js + ES6


作成者: 野中文雄
作成日: 2019年7月19日


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