🔖

Vue3+Quasarで始めるTodoアプリ開発

2022/11/15に公開

はじめに

Vue3ではViteがデフォルトになり、構築手順が色々と変更されているので、今回はQuasarを使ったMaterial DesignベースのToDoアプリ開発手順を紹介します。Quasarについては、この記事も参考にしてください。
基本的には、Why Quasar?にあるYoutubeも参考しましたが、色々と改良しています。

WSL2での開発環境構築

setup.sh
# ubuntu update
sudo apt update
sudo apt upgrade
# npm
sudo apt-get install curl
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
source .bashrc
nvm install --lts

プロジェクト作成

$ npm init vue@latest
Need to install the following packages:
  create-vue@3.4.0
Ok to proceed? (y) y

Vue.js - The Progressive JavaScript Framework

✔ Project name: … quasar-todo
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit Testing? … No / Yes
✔ Add an End-to-End Testing Solution? › No
✔ Add ESLint for code quality? … No / Yes
✔ Add Prettier for code formatting? … No / Yes
...省略

$ cd quasar-todo
$ npm run lint

環境設定

各種設定ファイルを修正します。前に入れていた命名規則チェックはエラーが出ており、調査中です。

  • 拡張機能の推奨を追加する。その後、各自でインストールしてください。
.vscode/extensions.json
{
  "recommendations": [
    // ESLint
    "dbaeumer.vscode-eslint",
    // Prettier
    "esbenp.prettier-vscode",
    // Vue Language Features (Volar)
    "vue.volar",
    // TypeScript Vue Plugin (Volar)
    "Vue.vscode-typescript-vue-plugin",
    // EditorConfig
    "editorconfig.editorconfig",
    // TODO Highlight
    "wayou.vscode-todo-highlight"
  ]
}
  • ブラケット(括弧)ペアを可視化する。
  • フォーマッタをPrettierにする。jsonやtsは、vscode標準が適用されるため、上書き
  • 保存、ペースト、改行時に整形する。
.vscode/settings.json
{
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.formatOnSave": true,
  "editor.formatOnPaste": true,
  "editor.formatOnType": true
}
  • printWidth ... 単純に行数が減るため。
  • htmlWhitespaceSensitivity ... タグ改行時の空白を許可する。
  • useTabs ... インデントを空白にする。
.prettierrc.json
{
  "printWidth": 150,
  "useTabs": false,
  "endOfLine": "auto",
  "htmlWhitespaceSensitivity": "ignore"
}
  • editorconfig ... 全ファイルの文字コード、インデントを統一する。
.editorconfig
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

設定変更後は、VS Codeの再起動(F1キー + Reload Window)を行ってください。

Quasarセットアップ

QuasarのGetting StartedにあるVite Pluginの手順に従い、Quasarを使えるようにする。Using Quasarでセットアップするモジュールを選択できる。今回、DialogやNotifyのプラグインを利用するので、追加している。

$ npm install quasar @quasar/extras
$ npm install -D @quasar/vite-plugin sass
package.json
  ...
  "dependencies": {
    "@quasar/extras": "^1.15.5",
    "pinia": "^2.0.23",
    "quasar": "^2.10.1",
    "vue": "^3.2.41",
    "vue-router": "^4.1.5"
  },
  "devDependencies": {
    "@quasar/vite-plugin": "^1.2.3",
    "@rushstack/eslint-patch": "^1.1.4",
    "@types/node": "^16.11.68",
    "@vitejs/plugin-vue": "^3.1.2",
    "@vue/eslint-config-prettier": "^7.0.0",
    "@vue/eslint-config-typescript": "^11.0.0",
    "@vue/tsconfig": "^0.1.3",
    "eslint": "^8.22.0",
    "eslint-plugin-vue": "^9.3.0",
    "npm-run-all": "^4.1.5",
    "prettier": "^2.7.1",
    "typescript": "~4.7.4",
    "vite": "^3.1.8",
    "vue-tsc": "^1.0.8",
    "sass": "1.32.12"
  }
src/main.ts
import { createApp } from "vue";
import { createPinia } from "pinia";
import { Quasar, Dialog, Notify } from "quasar";
import quasarLang from "quasar/lang/ja";

import App from "./App.vue";
import router from "./router";

// Import quasar CSS
import "@quasar/extras/material-icons/material-icons.css";
import "quasar/src/css/index.sass";

const app = createApp(App);

app.use(createPinia());
app.use(router);
// Setup quasar 
app.use(Quasar, {
  plugins: [Dialog, Notify],
  lang: quasarLang,
});

app.mount("#app");
src/vite.config.ts
import { fileURLToPath, URL } from "node:url";

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { quasar, transformAssetUrls } from "@quasar/vite-plugin";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue({
      template: { transformAssetUrls },
    }),
    quasar({
      sassVariables: "src/quasar-variables.scss",
    }),
  ],
  resolve: {
    alias: {
      "@": fileURLToPath(new URL("./src", import.meta.url)),
    },
  },
});
src/quasar-variables.scss
$primary: #1976d2;
$secondary: #26a69a;
$accent: #9c27b0;

$dark: #1d1d1d;

$positive: #21ba45;
$negative: #c10015;
$info: #31ccec;
$warning: #f2c037;

ToDo画面作成

今回、ナビゲーション付きのToDo画面を、以下の手順で作成していきます。

  1. HomeやToDo画面を空で作成し、404ページも適当に用意します。
src/views/ToDoView.vue
<template>
  <q-page class="q-pa-lg">
    <h5 class="q-mt-none">ToDo</h5>
  </q-page>
</template>
views/AboutView.vue
<template>
  <q-page class="q-pa-lg">
    <h5 class="q-mt-none">About</h5>
  </q-page>
</template>
src/views/Error404View.vue
<template>
  <q-page class="q-pa-lg">
    <div class="fixed-center text-center">
      <h5 class="text-faded">
        Sorry, nothing here...
        <strong>(404)</strong>
      </h5>
      <q-btn color="secondary" style="width: 100px" @click="$router.push('/')">Go back</q-btn>
    </div>
  </q-page>
</template>
  1. routerを修正します。
src/router/index.ts
import { createRouter, createWebHistory } from "vue-router";

import AboutView from "@/views/AboutView.vue";
import Error404View from "@/views/Error404View.vue";

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: "/",
      name: "home",
      // 非同期にするとファイルが分割され、初回の読み込みが早くなる。
      component: () => import("@/views/ToDoView.vue"),
    },
    {
      path: "/about",
      name: "about",
      component: AboutView,
    },
    {
      path: "/:catchAll(.*)*",
      component: Error404View,
    },
  ],
});

export default router;
  1. ナビゲーションバーを設定します。実装する流れは、以下の通り。
  • Layout and Grid - Layout - Layout Builderからレイアウトを作成する。
  • Vue Component - List & List Items - Menuでメニューサンプルをコピーし、v-forでループする。
  • q-itemにv-modelを追加すると、メニューが選択され、active-classで色を変更できる。
src/App.vue
<script setup lang="ts">
import { ref } from "vue";

export interface Menu {
  icon: string;
  label: string;
  link: string;
  separator: boolean;
}

const menuList: Menu[] = [
  {
    icon: "list",
    label: "ToDo",
    link: "/",
    separator: false,
  },
  {
    icon: "help",
    label: "About",
    link: "/about",
    separator: false,
  },
];

const selectdMenu = ref("");
const leftDrawerOpen = ref(false);
</script>
<template>
  <q-layout view="hHh lpR fFf">
    <q-header elevated class="bg-primary text-white">
      <q-toolbar>
        <q-btn dense flat round icon="menu" @click="leftDrawerOpen = !leftDrawerOpen" />
        <q-toolbar-title>ToDo App</q-toolbar-title>
      </q-toolbar>
    </q-header>

    <q-drawer show-if-above v-model="leftDrawerOpen" side="left" bordered>
      <q-scroll-area class="fit">
        <q-list>
          <template v-for="(menuItem, index) in menuList" :key="index">
            <q-item v-model="selectdMenu" clickable :to="menuItem.link" active-class="bg-indigo-6 text-white" v-ripple>
              <q-item-section avatar>
                <q-icon :name="menuItem.icon" />
              </q-item-section>
              <q-item-section>
                {{ menuItem.label }}
              </q-item-section>
            </q-item>
            <q-separator :key="'sep' + index" v-if="menuItem.separator" />
          </template>
        </q-list>
      </q-scroll-area>
    </q-drawer>

    <q-page-container>
      <router-view />
    </q-page-container>
  </q-layout>
</template>
  1. ToDo画面を作成する。実装する流れは、以下の通り。
  • 一覧表示 ... Vue Component - Form Component - Checkbox - With QItemのサンプルソースをコピーし、v-forでループする。
  • タスク完了 ... task.doneを見て、背景色を変え、取り消し線にする。
  • タスク削除 ... タスク完了時に、deleteアイコンを表示し、タスク削除を行う。
  • 削除確認ダイアログ ... Quasar Plugins - Dialog - Basic - CONFIRMのサンプルソースをコピーし、編集する。
  • 削除完了メッセージ ... Quasar Plugins - Notify - Basicのサンプルソースをコピーし、編集する。
  • タスク追加 ... Vue Component - Form Component - Input Textfield - Standardのサンプルをコピーし、編集する。その後、入力後または追加ボタン押下時に、タスク追加を行う。
  • バリデーションチェック ... 確認ダイアログを参考に、0文字だった場合、エラーメッセージを出力する。
src/views/ToDoView.vue
<script setup lang="ts">
import { reactive, ref } from "vue";
import { useQuasar } from "quasar";

export interface Task {
  title: string;
  done: boolean;
}

const tasks: Task[] = reactive([
  {
    title: "Get bananas",
    done: false,
  },
  {
    title: "Eat bananas",
    done: false,
  },
]);

const $q = useQuasar();

const newTask = ref("");

const deleteTask = (index: number) => {
  $q.dialog({
    title: "Confirm",
    message: "Really delete?",
    cancel: true,
    persistent: true,
  }).onOk(() => {
    tasks.splice(index, 1);
    $q.notify({ message: "Task deleted.", color: "info" });
  });
};

const addTask = () => {
  if (newTask.value.length == 0) {
    $q.notify({ message: "Add Task is required.", color: "negative" });
  } else {
    tasks.push({
      title: newTask.value,
      done: false,
    });
    newTask.value = "";
  }
};
</script>

<template>
  <q-page class="bg-grey-3 column">
    <div class="row q-pa-sm bg-primary">
      <q-input outlined class="col" @keyup.enter="addTask" square bg-color="white" v-model="newTask" placeholder="Add Task" dense>
        <q-btn round flat icon="add" @click="addTask" />
      </q-input>
    </div>
    <q-list class="bg-white" separator bordered>
      <q-item v-for="(task, index) in tasks" :key="index" tag="label" :class="{ 'bg-blue-1': task.done }" clickable v-ripple>
        <q-item-section avatar>
          <q-checkbox v-model="task.done" color="primary" />
        </q-item-section>
        <q-item-section>
          <q-item-label v-if="task.done">
            <del>{{ task.title }}</del>
          </q-item-label>
          <q-item-label v-else>
            {{ task.title }}
          </q-item-label>
        </q-item-section>
        <q-item-section v-if="task.done" side>
          <q-btn flat round dense color="primary" icon="delete" @click.stop="deleteTask(index)"></q-btn>
        </q-item-section>
      </q-item>
    </q-list>
    <div v-if="!tasks.length" class="no-task absolute-center">
      <q-icon name="check" size="100px" color="primary" />
      <div class="text-h5 text-primary text-center">No Tasks</div>
    </div>
  </q-page>
</template>
  1. 最後に、assets、componentsやviewsにある不要なファイルは削除して、完成です。

終わりに

以前紹介したToDoアプリに比べて、コーディング量も減り、より高機能がToDoアプリが構築できるようになりました。
今後、Quasarをベースとしたマルチデバイス対応の記事も追加しました。

Discussion