2018年4月16日
オトナのプログラミング勉強会@未来会議室
新垣圭祐
<html>
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.0/css/bulma.css">
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
</head>
<body>
<div>
<div id="images" class="columns">
</div>
</div>
<script>
(function() {
var url = "https://dog.ceo/api/breed/hound/images";
$.getJSON(url)
.done(function(data) {
$.each(data.message.slice(0, 8), function(i, item) {
var html = "<img src='" + item + "' />";
$("<div class='column' />").html(html).appendTo("#images");
});
});
})();
</script>
</body>
</html>
<html>
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.0/css/bulma.css">
<script src="https://unpkg.com/vue"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
</head>
<body>
<div id="app">
<div class="columns">
<div v-for="item in items" class='column'>
<img v-bind:src='item' />
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
items: []
},
mounted() {
let self = this;
let url = "https://dog.ceo/api/breed/hound/images";
axios.get(url)
.then(function(response) {
self.items = response.data.message.slice(0, 8);
});
}
})
</script>
</body>
</html>
<div>
<div id="images" class="columns">
</div>
</div>
<script>
(function() {
var url = "https://dog.ceo/api/breed/hound/images";
$.getJSON(url)
.done(function(data) {
$.each(data.message.slice(0, 8), function(i, item) {
var html = "<img src='" + item + "' />";
$("<div class='column' />").html(html).appendTo("#images");
});
});
})();
</script>
APIで取得して、
imgタグを作って
IDで親(DOM)を取得して
ひっつける
<div id="app">
<div class="columns">
<div v-for="item in items" class='column'>
<img v-bind:src='item' />
</div>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
items: []
},
mounted() {
let self = this;
let url = "https://dog.ceo/api/breed/hound/images";
axios.get(url)
.then(function(response) {
self.items = response.data.message.slice(0, 8);
});
}
})
</script>
← APIからデータを取得
← データを保存
← v-forディレクティブ
アイテムのリストを配列内のデータを使って表示
← v-bindディレクティブ
要素の属性(attribute)を束縛(バインディング)する
<div>
<div id="images" class="columns">
</div>
</div>
<script>
(function() {
var url = "https://dog.ceo/api/breed/hound/images";
$.getJSON(url)
.done(function(data) {
$.each(data.message.slice(0, 8), function(i, item) {
var html = "<img src='" + item + "' />";
if (i < 3) {
html += '<span v-if="i < 3" class="tag is-danger">NEW</span>';
}
$("<div class='column' />").html(html).appendTo("#images");
});
});
})();
</script>
表示(View)の修正であるにも関わらず、
ロジック部分を修正している
<div id="app">
<div class="columns">
<div v-for="(item, i) in items" class='column'>
<img v-bind:src='item' />
<span v-if="i < 3" class="tag is-danger">NEW</span>
</div>
</div>
</div>
v-forのindexを
v-ifで判定してNEWラベルをつける
<div>
<div id="images" class="columns">
</div>
<div id="modal" class="modal">
<div class="modal-background"></div>
<div class="modal-content"></div>
<button class="modal-close is-large" aria-label="close" onclick="closeModal()"></button>
</div>
</div>
<script>
var items = []; //APIのレスポンス保持用の配列
(function() {
var url = "https://dog.ceo/api/breed/hound/images";
$.getJSON(url)
.done(function(data) {
items = data.message.slice(0, 8);
$.each(items, function(i, item) {
var html = "<img src='" + item + "' onclick='openModal(" + i + ")' />"; // イベントをフック
if (i < 3) {
html += '<span v-if="i < 3" class="tag is-danger">NEW</span>';
}
$("<div class='column' />").html(html).appendTo("#images");
});
});
})();
/**
Modalのオープン
**/
function openModal(i) {
$("#modal .modal-content").html("<p class='image'><img src='" + items[i] + "' /></p>");
$("#modal").addClass("is-active");
}
/**
Modalのクローズ
**/
function closeModal() {
$("#modal").removeClass("is-active");
}
</script>
items = data.message.slice(0, 8);
$.each(items, function(i, item) {
var html = "<img src='" + item + "' onclick='openModal(" + i + ")' />";
if (i < 3) {
html += '<span v-if="i < 3" class="tag is-danger">NEW</span>';
}
$("<div class='column' />").html(html).appendTo("#images");
});
DOM生成時にイベントを記述
→どこでイベントが記述されているかわかりづらい
/**
Modalのオープン
**/
function openModal(i) {
$("#modal .modal-content").html("<p class='image'><img src='" + items[i] + "' /></p>");
$("#modal").addClass("is-active");
}
/**
Modalのクローズ
**/
function closeModal() {
$("#modal").removeClass("is-active");
}
状態を保存しているのはDOMのみ
→ロジックを組みにくい(状態を取得したいと思ったらDOMから取らないといけない)
<div id="app">
<div class="columns">
<div v-for="(item, i) in items" class='column'>
<img v-bind:src='item' v-on:click="modal=i" />
<span v-if="i < 3" class="tag is-danger">NEW</span>
</div>
</div>
<div class="modal" v-bind:class="modal!==null ? 'is-active': ''">
<div class="modal-background"></div>
<div class="modal-content">
<p class="image">
<img :src="items[modal]" />
</p>
</div>
<button class="modal-close is-large" aria-label="close" v-on:click="modal=null"></button>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
items: [],
modal: null,
},
mounted() {
let self = this;
let url = "https://dog.ceo/api/breed/hound/images";
axios.get(url)
.then(function(response) {
self.items = response.data.message.slice(0, 8);
});
}
})
</script>
<script>
new Vue({
el: '#app',
data: {
items: [],
modal: null,
},
mounted() {
let self = this;
let url = "https://dog.ceo/api/breed/hound/images";
axios.get(url)
.then(function(response) {
self.items = response.data.message.slice(0, 8);
});
}
})
</script>
← Modalで開いているitemを保持する変数を追加
<div id="app">
<div class="columns">
<div v-for="(item, i) in items" class='column'>
<img v-bind:src='item' v-on:click="modal=i" />
<span v-if="i < 3" class="tag is-danger">NEW</span>
</div>
</div>
<div class="modal" v-bind:class="modal!==null ? 'is-active': ''">
<div class="modal-background"></div>
<div class="modal-content">
<p class="image">
<img :src="items[modal]" />
</p>
</div>
<button class="modal-close is-large" aria-label="close" v-on:click="modal=null"></button>
</div>
</div>
← clickイベントを追加(modal変数にindexを代入)
↑ modalがnullでなければ表示
↑ itemsから画像URLを取得
↑ closeボタンでmodalをnullに戻す
状態はjsの変数で管理→リアクティブにViewに反映される
ディレクティブでイベントハンドリングを記述→見やすい
<div>
<div id="images" class="columns">
</div>
<div id="modal" class="modal">
<div class="modal-background"></div>
<div class="modal-content"></div>
<button class="modal-close is-large" aria-label="close" onclick="closeModal()"></button>
</div>
</div>
<script>
var items = []; //APIのレスポンス保持用の配列
(function() {
var url = "https://dog.ceo/api/breed/hound/images";
$.getJSON(url)
.done(function(data) {
items = data.message.slice(0, 8).map(function(element) {
return {
url: element,
like: 0
};
});
$.each(items, function(i, item) {
var html = "<img src='" + item.url + "' onclick='openModal(" + i + ")' />"; // イベントをフック
// NEWラベルを追加
if (i < 3) {
html += '<span v-if="i < 3" class="tag is-danger">NEW</span>';
}
// いいねボタンを追加
html += " <a class='button is-warning is-small' onclick='like( " + i + ")'><span>いいね! " + item.like + "件</span></a>";
$("<div id='item" + i + "' class='column' />").html(html).appendTo("#images");
});
});
})();
/**
Modalのオープン
**/
function openModal(i) {
console.log(items[i]);
$("#modal .modal-content").html("<p class='image'><img src='" + items[i].url + "' /></p>");
$("#modal").addClass("is-active");
}
/**
Modalのクローズ
**/
function closeModal() {
$("#modal").removeClass("is-active");
}
/**
like!
**/
function like(i) {
items[i].like += 1;
$("#item" + i + " .button").html("<span>いいね! " + items[i].like + "件</span>");
}
</script>
(function() {
var url = "https://dog.ceo/api/breed/hound/images";
$.getJSON(url)
.done(function(data) {
items = data.message.slice(0, 8).map(function(element) {
return {
url: element,
like: 0
};
});
$.each(items, function(i, item) {
var html = "<img src='" + item.url + "' onclick='openModal(" + i + ")' />"; // イベントをフック
// NEWラベルを追加
if (i < 3) {
html += '<span v-if="i < 3" class="tag is-danger">NEW</span>';
}
// いいねボタンを追加
html += " <a class='button is-warning is-small' onclick='like( " + i + ")'><span>いいね! " + item.like + "件</span></a>";
$("<div id='item" + i + "' class='column' />").html(html).appendTo("#images");
});
});
})();
← likeカウント用に変数を定義
↓いいねボタンを表示&イベントハンドラを追加
↑DOMを取得するためにIDを追加
/**
like!
**/
function like(i) {
items[i].like += 1;
$("#item" + i + " .button").html("<span>いいね! " + items[i].like + "件</span>");
}
↓いいねボタン
DOM操作のためにIDを追加
表示部分&イベントフックをjs内に記述
状態がDOMのみに保存される(今回は変数をもたせた)
<div id="app">
<div class="columns">
<div v-for="(item, i) in items" class='column'>
<img v-bind:src='item.url' v-on:click="modal=i" />
<span v-if="i < 3" class="tag is-danger">NEW</span>
<a class="button is-warning is-small" v-on:click="item.like += 1">
<span>いいね!{{item.like}}件</span>
</a>
</div>
</div>
<div class="modal" v-bind:class="modal!==null ? 'is-active': ''">
<div class="modal-background"></div>
<div class="modal-content">
<p class="image">
<img v-if="modal" :src="items[modal].url" />
</p>
</div>
<button class="modal-close is-large" aria-label="close" v-on:click="modal=null"></button>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
items: [],
modal: null,
},
mounted() {
let self = this;
let url = "https://dog.ceo/api/breed/hound/images";
axios.get(url)
.then(function(response) {
self.items = response.data.message.slice(0, 8).map(function(element) {
return {
url: element,
like: 0
};
});
});
},
methods: {
like: function(i) {
if (!this.likes[i] && parseInt(this.likes[i])) {
this.likes[i] = this.likes[i] + 1;
} else {
this.likes[i] = 1;
}
console.log(Number(this.likes[i]));
}
}
})
</script>
<div id="app">
<div class="columns">
<div v-for="(item, i) in items" class='column'>
<img v-bind:src='item.url' v-on:click="modal=i" />
<span v-if="i < 3" class="tag is-danger">NEW</span>
<a class="button is-warning is-small" v-on:click="item.like += 1">
<span>いいね!{{item.like}}件</span>
</a>
</div>
</div>
<div class="modal" v-bind:class="modal!==null ? 'is-active': ''">
<div class="modal-background"></div>
<div class="modal-content">
<p class="image">
<img v-if="modal" :src="items[modal].url" />
</p>
</div>
<button class="modal-close is-large" aria-label="close" v-on:click="modal=null"></button>
</div>
</div>
<script>
new Vue({
el: '#app',
data: {
items: [],
modal: null,
},
mounted() {
let self = this;
let url = "https://dog.ceo/api/breed/hound/images";
axios.get(url)
.then(function(response) {
self.items = response.data.message.slice(0, 8).map(function(element) {
return {
url: element,
like: 0
};
});
});
}
})
</script>
mounted() {
let self = this;
let url = "https://dog.ceo/api/breed/hound/images";
axios.get(url)
.then(function(response) {
self.items = response.data.message.slice(0, 8).map(function(element) {
return {
url: element,
like: 0
};
});
});
}
jQuery同様likeカウント用の変数を定義
<div id="app">
<div class="columns">
<div v-for="(item, i) in items" class='column'>
<img v-bind:src='item.url' v-on:click="modal=i" />
<span v-if="i < 3" class="tag is-danger">NEW</span>
<a class="button is-warning is-small" v-on:click="item.like += 1">
<span>いいね!{{item.like}}件</span>
</a>
</div>
</div>
<div class="modal" v-bind:class="modal!==null ? 'is-active': ''">
<div class="modal-background"></div>
<div class="modal-content">
<p class="image">
<img v-if="modal" :src="items[modal].url" />
</p>
</div>
<button class="modal-close is-large" aria-label="close" v-on:click="modal=null"></button>
</div>
</div>
↑likeのカウントアップ
↑like数の表示
イベントハンドラわかりやすい
変数と表示が自動で同期(リアクティブ)
Vue.jsで解決
<span v-if="seen">Now you see me</span>
<li v-for="todo in todos">
{{ todo.text }}
</li>
<div v-bind:class="{ active: isActive }"></div>
v-if : 条件付きレンダリング
v-for : リストレンダリング
v-bind : 属性バインディング
<button v-on:click="greet">Greet</button>
v-on : DOMイベントの購読
data: {
items: []
}
<div v-for="item in items" class='column'>
<img v-bind:src='item' />
</div>
JSの中の変数
HTMLビュー(DOM)
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
const router = new VueRouter({
routes
})
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
↑stateの値を変更するためには必ずmutationを通る
The internet's biggest collection
of open source dog pictures.
https://dog.ceo/dog-api/
犬の画像がただで取得できます
https://dog.ceo/dog-api/
犬の種別を取得(LIST ALL BREEDS)
種別毎の犬の画像を取得(BY BREED)
https://dog.ceo/api/breeds/list/all
https://dog.ceo/api/breed/hound/images
種別一覧
画像一覧
いいね!ボタン
NEWラベル
$ vue init nuxt-community/starter-template inukoro
$ cd inukoro
$ npm install
$ npm run dev
.
├── README.md
├── assets
├── components
├── layouts
├── middleware
├── node_modules
├── nuxt.config.js
├── package-lock.json
├── package.json
├── pages
├── plugins
├── static
└── store
←Vueのコンポーネントを入れる場所
←Vueのページコンポーネントを入れる場所
←Nuxt.jsの設定ファイル
←Vuexストアのファイルを入れる
pages/
--| user/
-----| index.vue
-----| one.vue
--| index.vue
router: {
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'user',
path: '/user',
component: 'pages/user/index.vue'
},
{
name: 'user-one',
path: '/user/one',
component: 'pages/user/one.vue'
}
]
}
ディレクトリ・ファイルを作成すると
自動でルーティングされる
pages/
--| _slug/
-----| comments.vue
-----| index.vue
--| users/
-----| _id.vue
--| index.vue
router: {
routes: [
{
name: 'index',
path: '/',
component: 'pages/index.vue'
},
{
name: 'users-id',
path: '/users/:id?',
component: 'pages/users/_id.vue'
},
{
name: 'slug',
path: '/:slug',
component: 'pages/_slug/index.vue'
},
{
name: 'slug-comments',
path: '/:slug/comments',
component: 'pages/_slug/comments.vue'
}
]
}
動的なパラメータはアンダースコアで表現
store/index.jsを作成し、
ストアインスタンスをエクスポートする
import Vuex from 'vuex'
const store = () => new Vuex.Store({
state: {
counter: 0
},
mutations: {
increment (state) {
state.counter++
}
}
})
export default store
<template>
<button @click="$store.commit('increment')">{{ $store.state.counter }}</button>
</template>
Vueインスタンスでstoreが使えるようになる
$ npm install @nuxtjs/bulma
module.exports = {
...
modules: [
'@nuxtjs/bulma'
],
...
}
npmでインストール
nuxt.config.jsを修正
<template>
<div>
<nav class="navbar">
<div class="container">
<div class="navbar-brand">
<nuxt-link to="/breeds" class="navbar-item">イヌコロ</nuxt-link>
<span class="navbar-burger burger" data-target="navbarMenu">
<span></span>
<span></span>
<span></span>
</span>
</div>
</div>
</nav>
</div>
</template>
pages/index.vueを修正
.
├── README.md
├── assets
├── components
├── layouts
├── middleware
├── node_modules
├── nuxt.config.js
├── package-lock.json
├── package.json
├── pages
├── plugins
├── static
└── store
<template>
<div>
<nav class="navbar">
<div class="container">
<div class="navbar-brand">
<nuxt-link to="/breeds" class="navbar-item">イヌコロ</nuxt-link>
<span class="navbar-burger burger" data-target="navbarMenu">
<span></span>
<span></span>
<span></span>
</span>
</div>
</div>
</nav>
</div>
</template>
pages/index.vue
<template>
<nav class="navbar">
<div class="container">
<div class="navbar-brand">
<nuxt-link to="/breeds" class="navbar-item">イヌコロ</nuxt-link>
<span class="navbar-burger burger" data-target="navbarMenu">
<span></span>
<span></span>
<span></span>
</span>
</div>
</div>
</nav>
</template>
components/AppHeader.vue
<template>
<div>
<AppHeader></AppHeader>
<nuxt/>
</div>
</template>
<script>
import AppHeader from '@/components/AppHeader.vue'
export default {
components: {
AppHeader
}
}
</script>
layouts/default.vue
<template>
<div></div>
</template>
pages/index.vue
import axios from 'axios'
class DogApi {
constructor() {
this.apiBase = 'https://dog.ceo/api';
}
breeds() {
return axios.get(`${this.apiBase}/breeds/list/all`)
.then(json => {
return json.data.message;
})
.catch(e => ({ error: e }));
}
}
const dogApi = new DogApi();
export default dogApi;
api/dog.js
Dog API用にクラスを作成
$ npm install axios
Ajax用のライブラリをインストール
import Vuex from 'vuex'
const appStore = () => {
return new Vuex.Store({
state: {
breed_list: {},
},
mutations: {
breed_list_update(state, payload) {
state.breed_list = {...payload}
},
}
})
};
export default appStore
store/index.js
犬種リスト保存用のストアを作成
<template>
<div></div>
</template>
<script>
import dogApi from '@/api/dog'
export default {
async fetch({store}) {
let json = await dogApi.breeds();
store.commit('breed_list_update', json)
},
}
</script>
pages/index.js
pageコンポーネントから実行
データを取得→ストアに保存ができていることを確認
fetchメソッド
export default {
async fetch({store}) {
let json = await dogApi.breeds();
store.commit('breed_list_update', json)
},
}
import Vuex from 'vuex'
const appStore = () => {
return new Vuex.Store({
state: {
breed_list: {},
},
mutations: {
breed_list_update(state, payload) {
state.breed_list = {...payload}
},
}
})
};
export default appStore
状態(state)
ミューテーション
<template>
<section class="container">
<div class="columns is-multiline">
<div v-for="(item, i) in breed_list" v-bind:key='i' class='column is-2'>
<a class="button">{{ i }}</a>
</div>
</div>
</section>
</template>
<script>
import dogApi from '@/api/dog'
import { mapState } from 'vuex'
export default {
async fetch({store}) {
let json = await dogApi.breeds();
store.commit('breed_list_update', json)
},
computed: mapState(['breed_list']),
}
</script>
pages/index.vue
VuxのmapStateヘルパーでstateを取得
v-forディレクティブで描画
URL : /
URL : /dogs/犬種名/
├── pages
│ ├── README.md
│ ├── dogs
│ │ └── _breed
│ │ └── index.vue
│ └── index.vue
<template>
<div>
test
</div>
</template>
http://localhost:3000/dogs/test/
pages/dogs/_breed/index.vue
class DogApi {
....
dogs(breed) {
return axios.get(`${this.apiBase}/breed/${breed}/images`)
.then(json => {
return json.data.message.map(function(element) {
return {
url: element,
like: 0
};
});
})
.catch(e => ({ error: e }));
}
...
}
api/dog.jsにメソッドを追加
import Vuex from 'vuex'
const appStore = () => {
return new Vuex.Store({
state: {
breed_list: {},
dog_list: [],
},
mutations: {
breed_list_update(state, payload) {
state.breed_list = {...payload}
},
dog_list_update(state, payload) {
state.dog_list = [...payload]
},
}
})
};
export default appStore
store/index.jsにstateとmutationを追加
<template>
<div>
test
</div>
</template>
<script>
import dogApi from '@/api/dog'
import { mapState } from 'vuex'
export default {
async fetch({store, params}) {
let json = await dogApi.dogs(params.breed);
store.commit('dog_list_update', json)
},
computed: mapState(['dog_list']),
}
</script>
pages/dogs/_breed/index.vue
<div v-for="(item, i) in breed_list" v-bind:key='i' class='column is-2'>
<nuxt-link :to="{ path: 'dogs/'+ i }" class="button">{{ i }}</nuxt-link>
</div>
pages/index.vueのリンクを修正
データの取得を確認
pages/dogs/_breed/index.vue
<template>
<section class="container">
<div class="columns is-multiline">
<div v-for="(item, i) in dog_list" v-bind:key='i' class='column is-1'>
<img v-bind:src='item.url' />
</div>
</div>
</section>
</template>
<script>
import dogApi from '@/api/dog'
import { mapState } from 'vuex'
export default {
async fetch({store, params}) {
let json = await dogApi.dogs(params.breed);
store.commit('dog_list_update', json)
},
computed: mapState(['dog_list']),
}
</script>