Help us understand the problem. What is going on with this article?

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

はじめに

みなさんこんにちは。
エンジニア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

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした