LoginSignup
6
2

More than 3 years have passed since last update.

【Todo】いまさらNuxt.jsに入門したエンジニアの話【作ってみた】

Posted at

はじめに

みなさんこんにちは。
エンジニア1年生のふっくーです。超駆け出しエンジニアです。

都内のベンチャー企業でバックエンドエンジニアとして働きつつ、TechCommitというエンジニアコミュニティーの運営をしています。

この度 TechCommit のAdvent Calender に参加させていただくことになったので、この機に「そろそろ学ばねば」とずっと思い続けていたNuxt.jsを勉強してみました。

なにかの技術の入門といえばTodoアプリと相場が決まっているので、今回はTodoアプリを作成します。

アプリ作成

(今回の記事は以下の記事のコードを全面的にコピペ、もとい参考にさせていただきました)
https://qiita.com/ayapon/items/d93807e7699434279531

まずはアプリケーションの大枠を作成します。非常に簡単。

$ npx create-nuxt-app 【好きな名前】

これを実行するとコンソールが荒ぶりはじめ、様々な設定について「どうしますか?」と聞かれます。とりあえず全てEnter連打でOKです。

作成し終わったらローカルサーバーを立ち上げます。

$ cd 【さっき決めた名前】
$ npm run dev

成功したらlocalhost:3000にアクセス。Nuxt.jsのロゴが大きく表示されている画面が出ればOKです。

Todoリストの機能を作る

Todoリストに最低限必要な、Todo作成、削除、検索の機能を用意します。

nuxt.config.js
link: [
    { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' },
    { rel: 'stylesheet', href: 'https://use.fontawesome.com/releases/v5.6.1/css/all.css'}
]
store/index.js
import Vuex from 'vuex';

const createStore = () => {
    return new Vuex.Store({
        state: () => ({
            todos: [
                {content: 'すごいことをする', created: '2019-03-31 15:30'}, 
                {content: '面白いことをする', created: '2019-03-31 16:00'}
            ]
        }),
        mutations: {
            insert: function(state, obj) {
                var d = new Date();
                var fmt = d.getFullYear() 
                            + '-' + ('00' + (d.getMonth() + 1)).slice(-2) 
                            + '-' + ('00' + d.getDate()).slice(-2) 
                            + ' ' + ('00' + d.getHours()).slice(-2) 
                            + ':' + ('00' + d.getMinutes()).slice(-2);
                state.todos.unshift({
                    content: obj.content,
                    created: fmt
                })
            },
            remove: function(state, obj) {
                for(let i = 0; i < state.todos.length; i++) {
                    const ob = state.todos[i];
                    if(ob.content == obj.content && ob.created == obj.created) {
                        alert('次の項目を削除します: ' + '"' + ob.content + '"');
                        state.todos.splice(i, 1);
                        return;
                    }
                }
            }
        }
    })
}

export default createStore;
pages/index.vue
<template>
  <section class="container">
    <h1>Todo App</h1>
    <div>
      <input type="text" name="content" v-model="content" @focus="set_flg"/>
      <button @click="insert"><i class="fas fa-plus"></i></button>
      <button @click="find"><i class="fas fa-search"></i></button>
    </div>
    <ul>
      <li v-for="(todo, index) in display_todos" :key="index">
        <span class="todo__content">{{ todo.content }}</span><br>
        <span class="todo__created">{{ todo.created }}</span><span class="todo__remove" @click="remove(todo)">delete</span>
      </li>
    </ul>
  </section>
</template>

<script>
import {mapState} from 'vuex';

export default {
  data: function() {
    return {
      content: '',
      find_flg: false
    }
  },
  computed: {
    ...mapState(['todos']),
    display_todos: function() {
      if(this.find_flg) {
        var arr = [];
        var data = this.todos;
        data.forEach(element => {
          if(element.content.toLowerCase() == this.content.toLowerCase()) {
            arr.push(element);
          }
        });
        return arr;
      } else {
        return this.todos;
      }
    }
  },
  methods: {
    insert: function() {
      this.$store.commit('insert', {content: this.content});
      this.content = '';
    },
    find: function() {
      this.find_flg = true;
    },
    set_flg: function() {
      if(this.find_flg) {
        this.find_flg = false;
        this.content = '';
      }
    },
    remove: function(todo) {
      this.$store.commit('remove', todo)
    }
  }
}
</script>

結果

before.png
はい、できました。
フォームにTodoの内容を入力して「+」ボタンを押すと、リストに登録されます。
deleteを押すと削除します(現状だとやや分かりにくいですが)
虫眼鏡ボタンを押すとフォーム内の文字列で検索します。もう一度フォームをクリックすると検索状態を解除します。

検索機能を改善する

このままだと本当にコードをコピペしただけになってしまうので、自分の勉強のために検索機能を改善しようと思います。

現状の仕様ですと、
・検索状態を解除するには、検索ワードが入力されているフォームをクリックする。その際フォームは自動的に空になる。
・フォームが空の状態で検索ボタンを押すと、空白で検索したことになり、結果がゼロになる。

こうなっています。

これを以下のように変更します。イメージはGoogle。
・検索状態を解除するには、フォームが空の状態で検索ボタンを押す、または新規投稿をする
・フォームをクリックすると自動的に空になる機能はなしにする。

変更してみた

pages/index.vue
<div>
  <input type="text" name="content" v-model="content" /> // set_flg削除
  ・ ・ ・
</div>

・ ・ ・

methods: {
    insert: function() {
        ・ ・ ・
        this.find_flg = false; // 1行追加
    },
    find: function() {
        this.find_flg = this.content != ''; // trueを変更
    },
    // set_flgメソッド削除
    ・ ・ ・
}

結果

先ほど決めた仕様のように大体動きました。
しかし、「検索ワードを変更しようとすると、1文字消しただけで検索結果が全消しされてしまう」というバグが。

つまりこれは、1文字消しただけでもその時点でのフォームの中身で検索しているということです。Railsのように同期処理が当たり前という感覚で実装していたので、「Vueってすげー」と思いました。

これを解消するためには、フォームの値とは別に「今どういう値で検索しているか」を記憶しておく必要があります。

ということで、dataにsearch_wordという値を持たせてみます。

最終的なコード

pages/index.vue
<template>
  <section class="container">
    <h1>Todo App</h1>
    <div>
      <input type="text" name="content" v-model="content" />
      <button @click="insert"><i class="fas fa-plus"></i></button>
      <button @click="find"><i class="fas fa-search"></i></button>
    </div>
    <ul>
      <li v-for="(todo, index) in display_todos" :key="index">
        <span class="todo__content">{{ todo.content }}</span><br>
        <span class="todo__created">{{ todo.created }}</span><span class="todo__remove" @click="remove(todo)">delete</span>
      </li>
    </ul>
  </section>
</template>

<script>
import {mapState} from 'vuex';

export default {
  data: function() {
    return {
      content: '',
      search_word: '',
      find_flg: false
    }
  },
  computed: {
    ...mapState(['todos']),
    display_todos: function() {
      if(this.find_flg) {
        var arr = [];
        var data = this.todos;
        data.forEach(element => {
          if(element.content.toLowerCase() == this.search_word.toLowerCase()) {
            arr.push(element);
          }
        });
        return arr;
      } else {
        return this.todos;
      }
    }
  },
  methods: {
    insert: function() {
      this.$store.commit('insert', {content: this.content});
      this.content = '';
      this.find_flg = false;
    },
    find: function() {
      this.search_word = this.content
      this.find_flg = this.content != '';
    },
    remove: function(todo) {
      this.$store.commit('remove', todo)
    }
  }
}
</script>

これで、フォームの内容を修正している間は検索結果が変わらないようになりました↓
スクリーンショット 2019-12-06 16.02.04.png

おまけ

ちょっとこのままでは見た目が微妙なので、cssをいい感じに当ててテンションを上げて終わりたいと思います。

nuxt.config.js
css: [
    'pages/style.css'
]
pages/style.css
.container {
    width: 700px;
    margin: 100px auto;
    text-align: center;
}

h1 {
    font-size: 32pt;
    color: #004d3d;
}

input {
    width: 593px;
    margin: 20px 0px;
    padding: 8px 24px;
    font-size: 16pt;
    background-color: #f7f7f7;
    border-radius: 24px;
    display: inline;
}

input:focus {
    outline: none;
}

button {
    width: 48px;
    height: 48px;
    color: #fff;
    background-color: #004d3d;
    font-size: 16pt;
    border-radius: 24px;
    display: inline;
    cursor: pointer;
}

button:hover {
    color: #ddd;
    background-color: #003c2c;
}

ul {
    margin-top: 20px;
    padding: 0;
    text-align: center;
}

li {
    list-style: none;
    text-align: left;
    padding: 7px 15px;
    font-size: 16pt;
    width: 700px;
    border: 1px solid #ddd;
}

span {
    margin: 0 5px;
}

.todo__created, .todo__remove {
    margin-right: 30px;
    font-size: 12px;
    color: #666;
}

.todo__remove {
    font-weight: bold;
    font-size: 12px;
    color: #a66;
    cursor: pointer;
}

.todo__remove:hover {
    color: #733;
}

結果

スクリーンショット 2019-12-06 16.11.26.png

ここまで読んでいただいてありがとうございました。
失礼します。

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2