あまブログ

ドキドキ......ドキドキ2択クイ〜〜〜〜〜〜〜ズ!!

【Vue3/create-vue】SPA版メモアプリを作る

この記事ではVue.jsで基本的なSPA(Single Page Application)を作成する方法を紹介します。

1. 実行環境

  • macOS:13.1
  • Node.js:18.12.1
  • npm:8.19.2
  • Vue:3.2.45
  • create-vue:3.5.0
  • Vite:4.0.4

2. アプリの要件

以下の機能を持つメモアプリを作成します。

  • 一覧
    • メモの1行目を一覧表示する。タイトルをクリックするとそのメモの編集状態に移行する。
  • 詳細
    • 編集状態 = 詳細
  • 追加
    • 新規作成ボタンをクリックすると新規メモが作成され、編集状態に移行する。
  • 編集
    • テキストエリアにメモの内容を表示し、編集できる。編集ボタンをクリックすると保存される。
  • 削除
    • 編集状態で削除ボタンをクリックするとメモは削除される。

3. 作成手順

3-1. create-vueを実行

$ npm create vue@3

Vue.js - The Progressive JavaScript Framework

✔ Project name: … vue-project
✔ Add TypeScript? … No
✔ Add JSX Support? … No
✔ Add Vue Router for Single Page Application development? … No
✔ Add Pinia for state management? … No
✔ Add Vitest for Unit Testing? … No
✔ Add an End-to-End Testing Solution? › No
✔ Add ESLint for code quality? … Yes
✔ Add Prettier for code formatting? … Yes
  • ESLintPrettier以外全部No
$ cd vue-project
$ git init
$ npm install
$ npm run dev
  • git initは任意のタイミングで
  • npm run dev実行後、ブラウザからhttp://localhost:5173/にアクセスして確認。

3-2. 不要なファイルとコードを削除

以下のファイルを削除します。

  • src/assets/*
  • src/components/*

以下のファイルを編集します。

src/main.js

import { createApp } from "vue";
import App from "./App.vue";

createApp(App).mount("#app");

src/App.vue

<script setup></script>

<template>
  <h1>Hello World!</h1>
</template>

<style scoped></style>

3-3. メモのCRUDを実装

src/App.vue

<script setup>
import { ref } from "vue";

const memos = ref([]);
const editingMemo = ref();

function createMemo() {
  const memo = { id: Date.now(), content: "" };
  editMemo(memo);
}

function editMemo(memo) {
  editingMemo.value = { id: memo.id, content: memo.content };
}

function doneEdit() {
  editingMemo.value.content = editingMemo.value.content.trim();
  if (!editingMemo.value.content) {
    removeMemo();
  } else {
    const memo = memos.value.find((memo) => memo.id === editingMemo.value.id);
    if (memo) {
      memo.content = editingMemo.value.content;
    } else {
      memos.value.push(editingMemo.value);
    }
    editingMemo.value = null;
  }
}

function removeMemo() {
  memos.value = memos.value.filter((memo) => memo.id !== editingMemo.value.id);
  editingMemo.value = null;
}

function getFirstLine(text) {
  return text.split(/\n/)[0];
}
</script>

<template>
  <form @submit.prevent="createMemo">
    <button>新規作成</button>
  </form>
  <div class="main">
    <ul>
      <li v-for="memo in memos" :key="memo.id">
        <a
          @click.prevent="editMemo(memo)"
          href="#"
          :class="memo.id === editingMemo?.id ? 'selected' : ''"
        >
          {{ getFirstLine(memo.content) }}
        </a>
      </li>
    </ul>
    <div v-if="editingMemo">
      <textarea v-model="editingMemo.content"></textarea>
      <div>
        <button @click="doneEdit">保存</button>
        <button @click="removeMemo">削除</button>
      </div>
    </div>
  </div>
</template>

<style scoped>
.main {
  display: flex;
}
.main ul {
  padding-left: 0px;
  padding-right: 20px;
  margin: 0;
}
button {
  cursor: pointer;
}
form {
  margin-bottom: 1em;
}
.selected {
  color: inherit;
  text-decoration: none;
  pointer-events: none;
}
</style>

3-4. メモをlocalStrageに保存

src/App.vue

  <script setup>
  import { ref } from "vue";
  
+ const STORAGE_KEY = "vue-memoapp";
+ const memos = ref(JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]"));
  const editingMemo = ref();
  
  //省略
  
  function doneEdit() {
    editingMemo.value.content = editingMemo.value.content.trim();
    if (!editingMemo.value.content) {
      removeMemo();
    } else {
      const memo = memos.value.find((memo) => memo.id === editingMemo.value.id);
      if (memo) {
        memo.content = editingMemo.value.content;
      } else {
        memos.value.push(editingMemo.value);
      }
+     localStorage.setItem(STORAGE_KEY, JSON.stringify(memos.value));
      editingMemo.value = null;
    }
  }
  
  function removeMemo() {
    memos.value = memos.value.filter((memo) => memo.id !== editingMemo.value.id);
+   localStorage.setItem(STORAGE_KEY, JSON.stringify(memos.value));
    editingMemo.value = null;
  }
  
  //省略
  </script>

  //省略

3-5. メモの編集フォームをコンポーネント

src/App.vue

  <script setup>
  import { ref } from "vue";
+ import MemoForm from "./components/MemoForm.vue";
  //省略
  </script>
  
  <template>
    <form @submit.prevent="createMemo">
      <button>新規作成</button>
    </form>
    <div class="main">
      //省略
+     <MemoForm
+       v-if="editingMemo"
+       v-model:content="editingMemo.content"
+       @save="doneEdit"
+       @remove="removeMemo"
+     >
+     </MemoForm>
    </div>
  </template>
  //省略

src/components/MemoForm.vue

<script setup>
defineProps({ content: String });
defineEmits(["update:content", "save", "remove"]);
</script>

<template>
  <div>
    <textarea
      :value="content"
      @input="$emit('update:content', $event.target.value)"
    ></textarea>
    <div>
      <button @click="$emit('save')">保存</button>
      <button @click="$emit('remove')">削除</button>
    </div>
  </div>
</template>

<style scoped>
button {
  margin-right: 10px;
  cursor: pointer;
}
</style>

3-6. メモの編集フォームを修正

src/components/MemoForm.vue

  <script setup>
+ import { ref, onMounted, onUpdated } from "vue";
+ 
  defineProps({ content: String });
  defineEmits(["update:content", "save", "remove"]);
+ 
+ const textarea = ref(null);
+ 
+ onMounted(() => {
+   textarea.value.focus();
+ });
+ 
+ onUpdated(() => {
+   textarea.value.focus();
+ });
  </script>
  
  <template>
    <div>
      <textarea
+       cols="30"
+       rows="10"
        :value="content"
        @input="$emit('update:content', $event.target.value)"
+       ref="textarea"
+       placeholder="文字を入力してください"
      ></textarea>
      <div>
        <button @click="$emit('save')">保存</button>
        <button @click="$emit('remove')">削除</button>
      </div>
    </div>
  </template>
  
  <style scoped>
  button {
    margin-right: 10px;
    cursor: pointer;
  }
  </style>

【参考】