20201023のvue.jsに関する記事は9件です。

GraphQLとVueを使ってミニアプリを作る

はじめに

本記事ではVue Apolloを利用して、GraphQLとVueアプリを接続した後に、CRUD機能並びにサブスクリプションの実装を目指す内容となります。

完成イメージ
スクリーンショット 2020-10-23 23.00.35.png

大まかな概要としては、Apolloサーバーから取得したデータをVueを利用して表示・クライアント側からGraphQLを操作することがゴールとなります。

記事内でGraphQL等の基本的な説明は省略しております。
もし宜しければ、GraphQLの基礎の基礎、並びにApolloサーバーとサブスクリプションについて解説している前回の記事も合わせてご一読ください(Apolloサーバー部分は前回と同一のコードを利用します

Apolloサーバーを作る(前回の記事で既に作ってある方は省略)

先ずはApolloを使ってGraphQLのAPIサーバーを作ります!

プロジェクトを作った後に、npm経由でapollo-serverをインストールします。

$ mkdir Apollo-Server
$ cd Apollo-Server
$ mkdir resolver
$ touch index.js db.js 
$ resolver/{Query.js,Mutation.js,Subscription.js}
$ npm init
$ npm install apollo-server --save

データベース代わりのJavaScriptファイルを用意します。

db.js
const posts = [{
    id: '1',
    title: 'こころ',
    author: '夏目漱石'
}, {
    id: '2',
    title: '舞姫',
    author: '森鴎外'
}, {
    id: '3',
    title: '羅生門',
    author: '芥川龍之介'
}]

const db = {
    posts,
}

module.exports  = db;

続いてQuery,Mutation,そして今回のテーマであるSubscriptionのリゾルバ関数を書いたファイルを順番に用意していきます。

Query.js
const Query = {
    posts(parent, args, { db }, info) {
       //クエリを書いた時に引数が「ない」時
       //模擬データベースの内容を全て表示
        if (!args.query) {
            return db.posts
       //クエリを書いた時に引数が「ある」時
       //引数とtitle or authorが一致したものだけを表示
        }else{
            return db.posts.filter((post) => {
            const isTitleMatch = post.title.toLowerCase().includes(args.query.toLowerCase())
            const isAuthorMatch = post.author.toLowerCase().includes(args.query.toLowerCase())
            return isTitleMatch || isAuthorMatch
        })
    }
    }
}

module.exports  = Query

クエリのリゾルバ関数です。

Mutation.js
const Mutation = {
    createPost(parent, args, { db, pubsub }, info) {
        const postNumTotal = String(db.posts.length + 1)
        const post = {
            id: postNumTotal,
            ...args.data
        }

        //データベース更新
        db.posts.push(post)
        //サブスクリプション着火
        pubsub.publish('post', { 
                post: {
                    mutation: 'CREATED',
                    data: post
                }
             })
        return post
    },
    updatePost(parent, args, { db, pubsub }, info) {
        const { id, data } = args
        const post = db.posts.find((post) => post.id === id)
        if (!post) {
            throw new Error('Post not found')
        }

        if (typeof data.title === 'string' && typeof data.author === 'string') {
            //データベース更新
            post.title = data.title
            post.author = data.author
            console.log(post)
            //サブスクリプション着火
            pubsub.publish('post', {
            post: {
                mutation: 'UPDATED',
                data: post
            }
        })
        }

        return post
    },
    deletePost(parent, args, { db, pubsub }, info) {
        const post = db.posts.find((post) => post.id === args.id)
        const postIndex = db.posts.findIndex((post) => post.id === args.id)

        if (postIndex === -1) {
            throw new Error('Post not found')
        }
        //データベース更新
        db.posts.splice(postIndex, 1)
        //サブスクリプション着火
        pubsub.publish('post', {
                post: {
                    mutation: 'DELETED',
                    data: post
                }
            })
        return post
    },
}

module.exports  = Mutation

ミューテーションのリゾルバ関数では、データベースの更新とサブスクリプションの着火をしています。

Subscription.js
const Subscription = {
    post: {
        subscribe(parent, args, { pubsub }, info) {
            return pubsub.asyncIterator('post')
        }
    }
}

module.exports = Subscription

サブスクリプションのリゾルバ関数です。
pubsub.asyncIteratorでサブスクリプションのイベントを非同期でリッスンします。

説明の関係で最後になりましたが、
スキーマの定義とサーバー起動のファイルになります。

index.js
const  {ApolloServer,PubSub,gql} = require('apollo-server');
const db = require('./db')
const Query = require('./resolver/Query')
const Mutation = require('./resolver/Mutation')
const Subscription = require('./resolver/Subscription')

//スキーマ定義
const typeDefs = gql`
type Query {
  posts(query: String): [Post!]!
}

type Mutation {
  createPost(data: CreatePostInput!): Post!
  deletePost(id: ID!): Post!
  updatePost(id: ID!, data: UpdatePostInput!): Post!
}

# サブスクリプション
type Subscription {
  post: PostSubscriptionPayload!
}

input CreatePostInput {
  title: String!
  author: String!
}

input UpdatePostInput {
  title: String
  author: String!
}

type Post {
  id: ID!
  title: String!
  author: String!
}

######################
# サブスクリプションで利用
######################

# enum型でMutation.js内のサブスクリプション着火と連動
enum MutationType {
  CREATED
  UPDATED
  DELETED
}

# サブスクリプションのフィールド
type PostSubscriptionPayload {
  mutation: MutationType!
  data: Post!
}

`
//PubSubのインスタンスを作成,サブスクリプションが利用可能に!
const pubsub = new PubSub()

const server = new ApolloServer({
    typeDefs: typeDefs,
    resolvers: {
        Query,
        Mutation,
        Subscription,
    },
    context: {
        db,
        pubsub
    }
})

server.listen().then(({ url, subscriptionsUrl }) => {
    console.log(`? Server ready at ${url}`);
    console.log(`? Subscriptions ready at ${subscriptionsUrl}`);
  });

サーバーを立てる際にスキーマやリゾルバ、PubSubなどを引数に指定しています。
指定した引数はクエリやミューテーションそしてサブスクリプション、それぞれの処理で利用をしています。

準備が出来たらターミナルから起動させます。

$ node index.js
? Server ready at http://localhost:4000/
? Subscriptions ready at ws://localhost:4000/graphql

こちらをGraphQLのエンドポイントとして利用します。
本記事ではサーバーが立ち上がっていないと、クライアント側からデータの表示や更新が出来ないので、必ずサーバーを立てることを忘れないようにしてください?

Apolloクライアントの設定

Vueプロジェクトを作ろう

Vue CLIを使ってプロジェクトを作ります。

$ vue create apollo-client #2系を選択してVueプロジェクトの作成
$ cd apollo-client 
$ npm run serve # 起動

プロジェクトの作成に成功していた場合、下記のような画面になります。
http://localhost:8080/

スクリーンショット 2020-10-23 21.02.27.png

Vueプロジェクト内の設定

続いてVueでGraphQLを扱うために、Vue Apolloをインストールしていきます。
本記事においては設定は全てデフォルトで進みます(色々聞かれますが「No」と答えてます)

$ vue add apollo

見た目をリッチにするために、Vuetifyもインストールします。

$ vue add vuetify

以上にて、プロジェクトの設定は完了です。
Vue-ApolloとVuetifyの設定はmain.jsに記述があります。

main.js
import Vue from 'vue'
import App from './App.vue'
import { createProvider } from './vue-apollo'
import vuetify from './plugins/vuetify';

Vue.config.productionTip = false

new Vue({
  apolloProvider: createProvider(),
  vuetify,
  render: h => h(App)
}).$mount('#app')

importしているvue-apollo.jsではエンドポイントやログイン/ログアウトなどの設定が出来ます。

クエリの実装

クエリとミューテーションの実装はこちらの記事を参考にさせて頂きました。ありがとうございます!

模擬データベース内のデータを「読み取り」するGraphQLのクエリを作成します。

$ mkdir src/graphql
$ touch src/graphql/post-query.js

前回までの記事でlocalhost内で立ち上げたIDEに書いていたクエリがこちらの部分に該当します。importしているgqlについてはこちらを参照ください。

post-query.js
import gql from 'graphql-tag'

export const ALL_POSTS = gql`
query{
  posts{
    id
    title
    author
  }
}
`

続いてApp.vue内を書き換えます(説明の省略のため、こちらのファイルに書いていますが、component化した方が良いです!)

「apollo」のオプション内で別ファイルで定義したクエリを呼び出し、
dataプロパティで定義したpostsオブジェクトに格納します。

App.vue
<template>
  <v-app>
    <v-main>
        <v-container>
 <v-row
   style="width: 550px;"
>
    <!--ツールバー-->
    <v-toolbar  color="grey lighten-1">
      <v-toolbar-title>本棚</v-toolbar-title>
      <v-spacer></v-spacer>
      <v-btn color="primary" dark class="mb-1">新規追加</v-btn>
    </v-toolbar>
    <!--本棚の中身-->
<div
v-for="post in posts" :key="post.id"
>
  <v-card
    class="mx-auto"
    width="550px"
    outlined
  >
    <v-list-item three-line>
      <v-list-item-content>
        <v-list-item-title class="headline mb-1">
           {{ post.title}}/{{ post.author}}
        </v-list-item-title>
        <v-list-item-subtitle>From Apollo-Server</v-list-item-subtitle>
      </v-list-item-content>
    </v-list-item>
  </v-card>
  </div>
  </v-row>
  </v-container>
    </v-main>
  </v-app>
</template>

<script>
//Query
import {ALL_POSTS} from "./graphql/post-query"

export default {
   name: "App",
    data: () => ({
      //本棚の中身を定義
      posts: [],
    }),
  apollo: {
    //本棚の中身
    posts: {
      //クエリを書いている部分
      query: ALL_POSTS,
        }
  },
 methods: {
    }
  }
</script>

ここまで書き終えたらブラウザで確認しましょう。

$ npm run serve

http://localhost:8080/

Vueを利用して模擬データベースの値を表示することが出来ました!

スクリーンショット 2020-10-23 22.31.34.png

ミューテーションの実装

続いてミューテーションの実装を行います。
こちらの章を終えると①書き込み、②更新、③削除 が出来るようになります。

先ずはファイルを作りましょう。

$ touch src/graphql/post-mutation.js

ミューテーションのクエリを書いていきます。

post-mutation.js
import gql from 'graphql-tag'

// POSTの新規追加
export const CREATE_POST = gql`mutation ($title: String!, $author: String!) {
    createPost(data: { title: $title, author: $author}) {
        id
         title
        author
     }
  }`

// POSTの更新
export const UPDATE_POST = gql`
    mutation updatePost($id: ID!, $title: String!, $author: String!) {
        updatePost(id:$id,data: {title: $title, author: $author}) {
            id
            title
            author
        }
    }
`

// // POSTの削除
export const DELETE_POST = gql`
    mutation deletePost($id: ID!) {
        deletePost(id:$id){
            title
            author
        }
    }
`

App.vueのmethod内に①書き込み、②更新、③削除の関数を作成。
updateQueryメソッドで上記3つを実行することが出来ます。

App.vue
<template>
  <v-app>
    <v-main>
        <v-container>
   <!--入力フォーム-->
    <v-dialog v-model="dialog" max-width="500px">
      <v-card>
        <v-container>
          <h2 v-if="isCreate">本棚に追加する</h2>
          <h2 v-if="!isCreate">本棚を更新する</h2>
          <v-form ref="form" v-model="valid" lazy-validation>
            <!--名前-->
            <v-text-field
                v-model="post.title"
                :rules="titleRules"
                :counter="20"
                label="タイトル"
                required
            ></v-text-field>
            <v-text-field
                v-model="post.author"
                :rules="authorRules"
                :counter="20"
                label="作者"
                required
            ></v-text-field>
            <!--追加ボタン-->
            <v-btn
                v-if="isCreate"
                :disabled="!valid"
                @click="createPost"
            >
              追加
            </v-btn>
            <!--更新ボタン-->
            <v-btn
                v-if="!isCreate"
                :disabled="!valid"
                @click="updatePost"
            >
              更新
            </v-btn>
            <v-btn @click="clear">クリア</v-btn>
          </v-form>
        </v-container>
      </v-card>
    </v-dialog>


          <v-row
   style="width: 550px;"
>
    <!--ツールバー-->
    <v-toolbar  color="grey lighten-1">
      <v-toolbar-title>本棚</v-toolbar-title>
      <v-spacer></v-spacer>
      <v-btn color="primary" dark class="mb-1" @click="showDialogNew">新規追加</v-btn>
    </v-toolbar>

    <!--本棚の中身-->
<div
v-for="post in posts" :key="post.id"
>
  <v-card
    class="mx-auto"
    width="550px"
    outlined
  >
    <v-list-item three-line>
      <v-list-item-content>
        <v-list-item-title class="headline mb-1">
           {{ post.title}}/{{ post.author}}
        </v-list-item-title>
        <v-list-item-subtitle>From Apollo-Server</v-list-item-subtitle>
      </v-list-item-content>
    </v-list-item>
    <!-- 編集・削除ボタン -->
    <v-card-actions>
       <v-btn
              color="success"
              small
              @click="showDialogUpdate(post.id,post.title,post.author)"
          >
            <v-icon small>
              編集する
            </v-icon>
          </v-btn>
          <v-btn
              color="error"
              small
              @click="deletePost(post.id,post.title)"
          >
            <v-icon small>
              削除する
            </v-icon>
          </v-btn>
    </v-card-actions>
  </v-card>
  </div>
  </v-row>
  </v-container>
    </v-main>
  </v-app>
</template>

<script>
//Query
import {ALL_POSTS} from "./graphql/post-query"
//Mutation
import {CREATE_POST,UPDATE_POST,DELETE_POST} from "./graphql/post-mutation";

export default {
   name: "App",
    data: () => ({
      //本棚の中身を定義
      posts: [],
       // フォーム入力値
      post: {
        id: '',
        title: '',
        author: '',
      },
      // バリデーション
      valid: true,
      titleRules: [
        v => !!v || 'タイトルは必須項目です',
        v => (v && v.length <= 20) || 'タイトルは20文字以内で入力してください'
      ],
      authorRules: [
        v => !!v || '作者名は必須項目です',
      ],
      // ローディングの表示フラグ
      progress: false,
      // ダイアログの表示フラグ
      dialog: false,
      // 新規・更新のフラグ
      isCreate: true,
    }),
  apollo: {
    //本棚の中身
    posts: {
      //クエリを書いている部分
      query: ALL_POSTS,
        }
  },
 methods: {
    // --------------------------------
      // 新規作成
      // --------------------------------
      createPost: function () {
        if (this.$refs.form.validate()) {
          this.progress = true
          this.$apollo.mutate({
           mutation: CREATE_POST,
            variables: {
              title: this.post.title,
              author: this.post.author,
            },
          })
          .then(() => {
            //UIの更新
            this.$apollo.queries.posts.fetchMore({
              updateQuery: (previousResult, {fetchMoreResult}) => {
                // console.log(previousResult)  //変更前
                // console.log(fetchMoreResult) //変更後
                return {
                  posts: fetchMoreResult.posts
                }
              }
            })
            this.dialog = false
            this.progress = false
          }).catch((error) => {
            console.error(error)
          })
        }
      }
      ,
      // --------------------------------
      // 更新
      // --------------------------------
      updatePost: function () {
        this.progress = true
        this.$apollo.mutate({
          mutation: UPDATE_POST,
          variables: {
            id: this.post.id,
              title: this.post.title,
              author: this.post.author,
          }
        }).then(() => {
          this.$apollo.queries.posts.fetchMore({
            updateQuery: (previousResult, {fetchMoreResult}) => {
              // console.log(previousResult)  //変更前
              // console.log(fetchMoreResult) //変更後
              return {
                posts: fetchMoreResult.posts
              }
            }
          })
          this.dialog = false
          this.progress = false
        }).catch((error) => {
          console.error(error)
        })
      },
      // --------------------------------
      // 削除
      // --------------------------------
      deletePost: function (id,title) {
        console.log(id)
        console.log(title)
        if (!confirm(title + 'を削除してもよろしいですか?')) {
          return
        }
        this.progress = true
        this.$apollo.mutate({
          mutation: DELETE_POST,
          variables: {
            id: id
          }
        }).then(() => {
          this.$apollo.queries.posts.fetchMore({
            updateQuery: (previousResult, {fetchMoreResult}) => {
              // console.log(previousResult)  //変更前
              // console.log(fetchMoreResult) //変更後
              return {
               posts: fetchMoreResult.posts
              }
            }
          })
          this.progress = false
        }).catch((error) => {
          console.error(error)
        })
      },
       // --------------------------------
      // フォームのクリア
      // --------------------------------
      clear: function () {
        this.$refs.form.reset()
      },
      // --------------------------------
      // 新規追加ダイアログの表示
      // --------------------------------
      showDialogNew: function () {
        // this.clear()
        this.isCreate = true
        this.dialog = true
      },
      // --------------------------------
      // 更新ダイアログの表示
      // --------------------------------
      showDialogUpdate: function (id, title, author) {
        this.post.id = id
        this.post.title = title
        this.post.author = author
        this.isCreate = false
        this.dialog = true
      },
    }
  }
</script>

CRUD処理が出来るようになりました!!

ezgif-4-bad1856686d7.gif

サブスクリプションの実装

最後にサブスクリプションの実装を行います。
こちらの章を終えるとリアルタイムでの書き込みの共有が出来るようになります。
実際のアプリケーションでは通知機能を作る時などに使う部分となります。

今回もファイルを作ります。

$ touch src/graphql/post-subscription.js

サブスクリプションのクエリを書きます。

post-subscription.js
import gql from 'graphql-tag'

// サブスクリプション
export const SUBSCRIPTION_POST = gql`
subscription {
  post{
    mutation
  data{
      id
    title
    author
  }
  }
}
`

App.vueを変更します。
「apollo」のオプション内にサブスクリプションの処理を追加しており、既存の投稿と同一の投稿がなかった場合、新規作成した投稿を本棚に追加しています。

App.vue
<template>
  <v-app>
    <v-main>
        <v-container>
   <!--入力フォーム-->
    <v-dialog v-model="dialog" max-width="500px">
      <v-card>
        <v-container>
          <h2 v-if="isCreate">本棚に追加する</h2>
          <h2 v-if="!isCreate">本棚を更新する</h2>
          <v-form ref="form" v-model="valid" lazy-validation>
            <!--名前-->
            <v-text-field
                v-model="post.title"
                :rules="titleRules"
                :counter="20"
                label="タイトル"
                required
            ></v-text-field>
            <v-text-field
                v-model="post.author"
                :rules="authorRules"
                :counter="20"
                label="作者"
                required
            ></v-text-field>
            <!--追加ボタン-->
            <v-btn
                v-if="isCreate"
                :disabled="!valid"
                @click="createPost"
            >
              追加
            </v-btn>
            <!--更新ボタン-->
            <v-btn
                v-if="!isCreate"
                :disabled="!valid"
                @click="updatePost"
            >
              更新
            </v-btn>
            <v-btn @click="clear">クリア</v-btn>
          </v-form>
        </v-container>
      </v-card>
    </v-dialog>


          <v-row
   style="width: 550px;"
>
    <!--ツールバー-->
    <v-toolbar  color="grey lighten-1">
      <v-toolbar-title>本棚</v-toolbar-title>
      <v-spacer></v-spacer>
      <v-btn color="primary" dark class="mb-1" @click="showDialogNew">新規追加</v-btn>
    </v-toolbar>

    <!--本棚の中身-->
<div
v-for="post in posts" :key="post.id"
>
  <v-card
    class="mx-auto"
    width="550px"
    outlined
  >
    <v-list-item three-line>
      <v-list-item-content>
        <v-list-item-title class="headline mb-1">
           {{ post.title}}/{{ post.author}}
        </v-list-item-title>
        <v-list-item-subtitle>From Apollo-Server</v-list-item-subtitle>
      </v-list-item-content>
    </v-list-item>
    <!-- 編集・削除ボタン -->
    <v-card-actions>
       <v-btn
              color="success"
              small
              @click="showDialogUpdate(post.id,post.title,post.author)"
          >
            <v-icon small>
              編集する
            </v-icon>
          </v-btn>
          <v-btn
              color="error"
              small
              @click="deletePost(post.id,post.title)"
          >
            <v-icon small>
              削除する
            </v-icon>
          </v-btn>
    </v-card-actions>
  </v-card>
  </div>
  </v-row>
  </v-container>
    </v-main>
  </v-app>
</template>

<script>
//Query
import {ALL_POSTS} from "./graphql/post-query"
//Mutation
import {CREATE_POST,UPDATE_POST,DELETE_POST} from "./graphql/post-mutation";
//Subscription
import {SUBSCRIPTION_POST} from "./graphql/post-subscription";

export default {
   name: "App",
    data: () => ({
      //本棚の中身を定義
      posts: [],
       // フォーム入力値
      post: {
        id: '',
        title: '',
        author: '',
      },
      // バリデーション
      valid: true,
      titleRules: [
        v => !!v || 'タイトルは必須項目です',
        v => (v && v.length <= 20) || 'タイトルは20文字以内で入力してください'
      ],
      authorRules: [
        v => !!v || '作者名は必須項目です',
      ],
      // ローディングの表示フラグ
      progress: false,
      // ダイアログの表示フラグ
      dialog: false,
      // 新規・更新のフラグ
      isCreate: true,
    }),
  apollo: {
    //本棚の中身
    posts: {
      //クエリを書いている部分
      query: ALL_POSTS,
      //サブスクリプション
       subscribeToMore: {
        document: SUBSCRIPTION_POST,
        updateQuery:(previousResult, { subscriptionData }) =>{
            // console.log(previousResult) //前の投稿
            // console.log(subscriptionData.data) //新規作成した投稿
            // 既存の投稿と同一の投稿がなかった場合、新規作成した投稿を本棚に追加
        if (previousResult.posts.find(post => post.id === subscriptionData.data.post.data.id)) {
          return previousResult
        }else{
           return {
          posts: [
            ...previousResult.posts,
            // Add the new data
            subscriptionData.data.post.data,
          ],
        }
        }
            }
            }
        }
  },
 methods: {
    // --------------------------------
      // 新規作成
      // --------------------------------
      createPost: function () {
        if (this.$refs.form.validate()) {
          this.progress = true
          this.$apollo.mutate({
           mutation: CREATE_POST,
            variables: {
              title: this.post.title,
              author: this.post.author,
            },
          })
          .then(() => {
            //UIの更新
            this.$apollo.queries.posts.fetchMore({
              updateQuery: (previousResult, {fetchMoreResult}) => {
                // console.log(previousResult)  //変更前
                // console.log(fetchMoreResult) //変更後
                return {
                  posts: fetchMoreResult.posts
                }
              }
            })
            this.dialog = false
            this.progress = false
          }).catch((error) => {
            console.error(error)
          })
        }
      }
      ,
      // --------------------------------
      // 更新
      // --------------------------------
      updatePost: function () {
        this.progress = true
        this.$apollo.mutate({
          mutation: UPDATE_POST,
          variables: {
            id: this.post.id,
              title: this.post.title,
              author: this.post.author,
          }
        }).then(() => {
          this.$apollo.queries.posts.fetchMore({
            updateQuery: (previousResult, {fetchMoreResult}) => {
              // console.log(previousResult)  //変更前
              // console.log(fetchMoreResult) //変更後
              return {
                posts: fetchMoreResult.posts
              }
            }
          })
          this.dialog = false
          this.progress = false
        }).catch((error) => {
          console.error(error)
        })
      },
      // --------------------------------
      // 削除
      // --------------------------------
      deletePost: function (id,title) {
        console.log(id)
        console.log(title)
        if (!confirm(title + 'を削除してもよろしいですか?')) {
          return
        }
        this.progress = true
        this.$apollo.mutate({
          mutation: DELETE_POST,
          variables: {
            id: id
          }
        }).then(() => {
          this.$apollo.queries.posts.fetchMore({
            updateQuery: (previousResult, {fetchMoreResult}) => {
              // console.log(previousResult)  //変更前
              // console.log(fetchMoreResult) //変更後
              return {
               posts: fetchMoreResult.posts
              }
            }
          })
          this.progress = false
        }).catch((error) => {
          console.error(error)
        })
      },
       // --------------------------------
      // フォームのクリア
      // --------------------------------
      clear: function () {
        this.$refs.form.reset()
      },
      // --------------------------------
      // 新規追加ダイアログの表示
      // --------------------------------
      showDialogNew: function () {
        // this.clear()
        this.isCreate = true
        this.dialog = true
      },
      // --------------------------------
      // 更新ダイアログの表示
      // --------------------------------
      showDialogUpdate: function (id, title, author) {
        this.post.id = id
        this.post.title = title
        this.post.author = author
        this.isCreate = false
        this.dialog = true
      },
    }
  }
</script>


確認してみましょう。ブラウザのウィンドウを2つ用意します。

subscriptionVue.gif

こちらも上手くいきました!!!

おわりに

以上、今回はGraphQLをVueで扱ってみました。
今回の内容を元にクエリやスキーマの数を増やしていくことで、発展的なアプリケーションを作ることが出来るかと思います。

それでは、また?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsでテーブルを動的に行追加する(メモ)

完成品

参考にしたもの

【Vue.js】データ配列を使ってテーブルを動的に生成してみる。
Vue.jsを100時間勉強して分かったこと
HeartRails Geo API

コード(記事の下の方に完成品のCodePen置いてます)

・HTML

 <div class="row my-3">
    <div class="col-sm-6 mx-auto">
      <input class="form-control" v-model:value="text" placeholder="郵便番号を入力…"><br>
      郵便番号:{{ text }}<button v-on:click="addTask" class="btn btn-primary">検索</button>
    </div>
  </div>

  <table class="table table-striped">
    <thead>
      <tr>
        <th>都道府県</th>
        <th>市区町村</th>
        <th>町域</th>
        <th>緯度</th>
        <th>経度</th>
      </tr>
    </thead>
    <tbody>
        <tr v-for="address in addressList"> 
          <td>{{ address[0] }}</td>
          <td>{{ address[1] }}</td>
          <td>{{ address[2] }}</td>
          <td>{{ address[3] }}</td>
          <td>{{ address[4] }}</td>
        </tr>
    </tbody>
</table>

・Javascript

const app = new Vue({
    el: '#app', // Vueが管理する一番外側のDOM要素
    data: {
      // Vue内部で使いたい変数は全てこの中に定義する
      text: '',
      prefecture: '',
      addressList:[],
    },
    methods: {
      addTask: async function() {
        //郵便番号から住所情報取得
        try {
          const address = await axios.get('https://geoapi.heartrails.com/api/json?method=searchByPostal&postal=' + this.text);
          this.addressList.unshift([address.data['response']['location'][0].prefecture,address.data['response']['location'][0].city,address.data['response']['location'][0].town,address.data['response']['location'][0].x,address.data['response']['location'][0].y]);
        } catch (e){
          console.log('住所情報の取得に失敗:', this.todoList);
          console.error(e);
        }
      }
      ,
      clearAll: function() {
        console.log('検索履歴が削除されました。');
        this.addressList = [];
      }
    },
});

作成中のメモ

・axios.getの中のURL、node.jsはhttpでいけたけど、Vue.jsはhttpsじゃないとダメ(なぜ)

・CodePenの履歴(メモ書くのめんどくさいからCodePen分けて履歴にすればええやん。のスタイル。)
テーブル使ってないやつ
https://codepen.io/sawakoshi_yy/pen/MWeboLm

テーブル使ってるけど1行の中に追加されてるやつ
https://codepen.io/sawakoshi_yy/pen/eYzgpNN

完成品
https://codepen.io/sawakoshi_yy/pen/qBNROwY

・await使うなら、functionの前にasyncつける

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.jsでテーブルを動的に行追加する

完成品

完成品のWebアプリ

https://naughty-franklin-898e0a.netlify.app/
郵便番号7桁入力したら住所検索します。
検索ボタンクリックするたびに行追加して検索します。

参考にしたもの

【Vue.js】データ配列を使ってテーブルを動的に生成してみる。
Vue.jsを100時間勉強して分かったこと
HeartRails Geo API

コード(記事の下の方に完成品のCodePen置いてます)

・HTML

 <div class="row my-3">
    <div class="col-sm-6 mx-auto">
      <input class="form-control" v-model:value="text" placeholder="郵便番号を入力…"><br>
      郵便番号:{{ text }}<button v-on:click="addTask" class="btn btn-primary">検索</button>
    </div>
  </div>

  <table class="table table-striped">
    <thead>
      <tr>
        <th>都道府県</th>
        <th>市区町村</th>
        <th>町域</th>
        <th>緯度</th>
        <th>経度</th>
      </tr>
    </thead>
    <tbody>
        <tr v-for="address in addressList"> 
          <td>{{ address[0] }}</td>
          <td>{{ address[1] }}</td>
          <td>{{ address[2] }}</td>
          <td>{{ address[3] }}</td>
          <td>{{ address[4] }}</td>
        </tr>
    </tbody>
</table>

・Javascript

const app = new Vue({
    el: '#app', // Vueが管理する一番外側のDOM要素
    data: {
      // Vue内部で使いたい変数は全てこの中に定義する
      text: '',
      prefecture: '',
      addressList:[],
    },
    methods: {
      addTask: async function() {
        //郵便番号から住所情報取得
        try {
          const address = await axios.get('https://geoapi.heartrails.com/api/json?method=searchByPostal&postal=' + this.text);
          this.addressList.unshift([address.data['response']['location'][0].prefecture,address.data['response']['location'][0].city,address.data['response']['location'][0].town,address.data['response']['location'][0].x,address.data['response']['location'][0].y]);
        } catch (e){
          console.log('住所情報の取得に失敗:', this.todoList);
          console.error(e);
        }
      }
      ,
      clearAll: function() {
        console.log('検索履歴が削除されました。');
        this.addressList = [];
      }
    },
});

作成中のメモ

・axios.getの中のURL、node.jsはhttpでいけたけど、Vue.jsはhttpsじゃないとダメ(なぜ)

・CodePenの履歴(メモ書くのめんどくさいからCodePen分けて履歴にすればええやん。のスタイル。)
テーブル使ってないやつ
https://codepen.io/sawakoshi_yy/pen/MWeboLm

テーブル使ってるけど1行の中に追加されてるやつ
https://codepen.io/sawakoshi_yy/pen/eYzgpNN

完成品
https://codepen.io/sawakoshi_yy/pen/qBNROwY

・await使うなら、functionの前にasyncつける

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vuetifyのtext-fieldにgoogleなどでキャッシュされている情報が入らないように

Nuxtを使って開発を進めているが、ちょっと躓いたことがあったので備忘録として。

ログイン画面やらプロフィール画面を作っていて、
入力できるコンポーネントとして、Vuetifyの"v-text-filed"をよく使用するのだが、
なぜか、googleで保存しているパスワードやらメールアドレスやらが入ってしまう。

困ったと思ってググったら

Nuxt.js
<v-text-filed
 autocomplete = "false">
</v-text-filed>

とやるのは上手くいかない。

Nuxt.js
<v-text-filed
 autocomplete = "new-password">
</v-text-filed>

これだとばっちり!

参考:https://github.com/vuetifyjs/vuetify/issues/2792

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel8.9.0 + Vue.jsでaxiosを使ったときにthenとcatchの両方を通るときの対処法

@babel/runtimeをインストールする

npm install --save @babel/runtime
npm install --save-dev @babel/plugin-transform-runtime
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails6 Vue.js】axiosを使ってCSVインポート処理を実装する

Vue.jsとRailsAPIを使って複数のCSVファイルをそれぞれのテーブルにインポートする必要があったので記録しておきます。

ご留意ください

Rails,Vue.js学習中の初学者が備忘録を兼ねて書いています。
内容に誤りを含む可能性・さらに良い手法がある可能性が多分にありますので、参考にする際はその点ご留意ください。
何かお気付きの点がございましたら、お気軽にコメントいただけると幸いです。

実現したいこと

inputボックスを用意し、選択したCSVファイル(2種類)をそれぞれ用意したテーブルにaxiosを使ってPOSTしたい。

csv1 ⇨ code_listsテーブル
csv2 ⇨ financialsテーブル
テーブルの定義は下記の通りです。

code_listsテーブル
  create_table "code_lists", force: :cascade do |t|
    t.string "edinet"
    t.string "securities"
    t.string "company"
    t.string "sector"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["company"], name: "index_code_lists_on_company"
    t.index ["edinet"], name: "index_code_lists_on_edinet", unique: true
    t.index ["securities"], name: "index_code_lists_on_securities"
  end
financialsテーブル
  create_table "financials", force: :cascade do |t|
    t.string "edinet"
    t.date "rec_date"
    t.string "account_name"
    t.float "value"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["account_name"], name: "index_financials_on_account_name"
    t.index ["edinet", "rec_date", "account_name"], name: "index_financials_on_edinet_and_rec_date_and_account_name", unique: true
    t.index ["edinet"], name: "index_financials_on_edinet"
  end

下準備

事前にライブラリを追加します。

config/application.rb
require 'csv' #追記

Gemのrooを追加してbundle installします

Gemfile
gem 'roo'

axios-post 時にcsrfトークン対策が必要な為、別途プラグインを設定します。
pluginsフォルダを新たに作成してください。

app/javascript/packs/plugins/vue-axios.js
const VueAxiosPlugin = {}
export default VueAxiosPlugin.install = function(Vue, { axios }) {
  const csrf_token = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
  axios.defaults.headers.common = {
    "X-Requested-With": "XMLHttpRequest",
    "X-CSRF-Token": csrf_token
  }

  Vue.axios = axios
  Object.defineProperties(Vue.prototype, {
    axios: {
      get () {
        return axios
      }
    }
  })
}

エントリーファイルにプラグインをimportします

hello_vue.js
import Vue from "vue/dist/vue.esm";
import axios from "axios"; // 追加
import VueAxiosPlugin from "./plugins/vue-axios";  // 追加
import App from "./components/App.vue";

Vue.use(VueAxiosPlugin, { axios: axios }) // 追加

new Vue({
  el: "#app",
  render: h => h(App),
})

ルーティング

ルーティングは次のように設定しました。
必要な箇所のみ記載しています。
それぞれRailsAPIのルーティング設定にしています。
code_listsテーブル用のcsvファイルはapi/code_lists/importへ
financialsテーブル用のcsvファイルはapi/financials/importへPOSTします。

config/routes.rb
Rails.application.routes.draw do
  (上記省略)
  namespace :api, format: 'json' do
    resources :code_lists do
      post :import, on: :collection
    end
  end

  namespace :api, format: 'json' do
    resources :financials do
      post :import, on: :collection
    end
  end
  (以下省略)
end

View

ビューは次のようにしました。

Import.vue
<template>
  <div>
    <div class="import-form">
      <input @change="selectedFile" type="file" name="file">
    </div>
    <div class="import-form">
      <button @click="upload('/api/code_lists/import')" type="submit">コードリストのアップロード</button>
    </div>
    <div class="import-form">
      <button @click="upload('/api/financials/import')" type="submit">財務データのアップロード</button>
    </div>
  </div>
</template>

<script>
import axios from 'axios'
export default {
  data: function(){
    return {
      uploadFile: null
    };
  },
  methods: {
    selectedFile: function(e) {
      // 選択された File の情報を保存しておく
      e.preventDefault();
      let files = e.target.files;
      this.uploadFile = files[0];
    },
    upload: function(url) {
      // FormData を利用して File を POST する
      let formData = new FormData();
      formData.append('file', this.uploadFile);
      // let config = {
      //     headers: {
      //         'content-type': 'multipart/form-data'
      //     }
      // };
      axios
          .post(url, formData)
          .then(function(response) {
              // response 処理
          })
          .catch(function(error) {
              // error 処理
          })
    }
  }
}
</script>

inputボックスのファイル選択後にselectedFileメソッドでファイルをuploadFileに保存します。そして、buttonのクリックイベントでuploadメソッドが実行されます。uploadメソッドの引数のurl(先ほどroutesで設定した2つのurl)へファイルをPOSTします。POSTする際に事前に保存したuploadFileをFormDataオブジェクトに渡している事がポイントです。
今回はconfigは使用しなかった為コメントアウトしました。

サーバサイド

今回は2つのcsvファイルをそれぞれのテーブルにImportする実装となり、それぞれのモデル毎に実装が必要となりますが、ほぼ同じ内容となる為Financial Modelの実装のみ記載しておきます。
まずコントローラについて。

app/controllers/api/financials_controller.rb
class Api::FinancialsController < ApplicationController
  def import
    Financial.import(params[:file])
  end
end

コントローラはFinancial Classのクラスメソッドを呼び出すだけとなります。

続いて、financialモデルの記述は以下の通りです。

app/models/financial.rb
class Financial < ApplicationRecord
  validates :edinet, presence: true
  validates :rec_date, presence: true
  validates :account_name, presence: true
  validates :value, presence: true
  def self.import(file)
    CSV.foreach(file.path, headers: true) do |row|
      # IDが見つかれば、レコードを呼び出し、見つかれなければ、新しく作成
      financial = find_by(edinet: row["edinet"], rec_date: row["rec_date"], account_name: row["account_name"]) || new
      # CSVからデータを取得し、設定する
      financial.attributes = row.to_hash.slice(*updatable_attributes)
      # 保存する
      financial.save
    end
  end
  def self.updatable_attributes
    ["edinet", "rec_date", "account_name", "value"]
  end
end

インポートしたいCSVのカラム名をupdatable_attributesに入力。
またfind_byで既に取り込み済みのレコードかどうかを確認しており、取り込み済みの場合は上書きされます。

最後に

現何かお気付きの点がございましたら、お気軽にコメントいただけると幸いです。
上場企業の財務データを活用したアプリケーションを開発中です。フロントエンドをVue.js、バックエンドはRailsAPIを使って開発を進めています。引き続き、得られた知見は記事にしてまとめていきたいと思います。
ここまでお付き合いいただきありがとうございました!

参考にさせていただいた記事:
csrf対策:【Vue】axiosで、デフォルトでCSRFトークンを設定できるようにする
フロントエンド実装:Vue.js でファイルをポストしたいとき
バックエンド実装:【Ruby on Rails】CSVインポート

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails6+Vue.js】axiosを使ってCSVインポート処理を実装する

Vue.jsとRailsAPIを使って複数のCSVファイルをそれぞれのテーブルにインポートする必要があったので記録しておきます。

ご留意ください

Rails,Vue.js学習中の初学者が備忘録を兼ねて書いています。
内容に誤りを含む可能性・さらに良い手法がある可能性が多分にありますので、参考にする際はその点ご留意ください。
何かお気付きの点がございましたら、お気軽にコメントいただけると幸いです。

実現したいこと

inputボックスを用意し、選択したCSVファイル(2種類)をそれぞれ用意したテーブルにaxiosを使ってPOSTしたい。

csv1 ⇨ code_listsテーブル
csv2 ⇨ financialsテーブル
テーブルの定義は下記の通りです。

code_listsテーブル
  create_table "code_lists", force: :cascade do |t|
    t.string "edinet"
    t.string "securities"
    t.string "company"
    t.string "sector"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["company"], name: "index_code_lists_on_company"
    t.index ["edinet"], name: "index_code_lists_on_edinet", unique: true
    t.index ["securities"], name: "index_code_lists_on_securities"
  end
financialsテーブル
  create_table "financials", force: :cascade do |t|
    t.string "edinet"
    t.date "rec_date"
    t.string "account_name"
    t.float "value"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.index ["account_name"], name: "index_financials_on_account_name"
    t.index ["edinet", "rec_date", "account_name"], name: "index_financials_on_edinet_and_rec_date_and_account_name", unique: true
    t.index ["edinet"], name: "index_financials_on_edinet"
  end

下準備

事前にライブラリを追加します。

config/application.rb
require 'csv' #追記

Gemのrooを追加してbundle installします

Gemfile
gem 'roo'

axios-post 時にcsrfトークン対策が必要な為、別途プラグインを設定します。
pluginsフォルダを新たに作成してください。

app/javascript/packs/plugins/vue-axios.js
const VueAxiosPlugin = {}
export default VueAxiosPlugin.install = function(Vue, { axios }) {
  const csrf_token = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
  axios.defaults.headers.common = {
    "X-Requested-With": "XMLHttpRequest",
    "X-CSRF-Token": csrf_token
  }

  Vue.axios = axios
  Object.defineProperties(Vue.prototype, {
    axios: {
      get () {
        return axios
      }
    }
  })
}

エントリーファイルにプラグインをimportします

hello_vue.js
import Vue from "vue/dist/vue.esm";
import axios from "axios"; // 追加
import VueAxiosPlugin from "./plugins/vue-axios";  // 追加
import App from "./components/App.vue";

Vue.use(VueAxiosPlugin, { axios: axios }) // 追加

new Vue({
  el: "#app",
  render: h => h(App),
})

ルーティング

ルーティングは次のように設定しました。
必要な箇所のみ記載しています。
それぞれRailsAPIのルーティング設定にしています。
code_listsテーブル用のcsvファイルはapi/code_lists/importへ
financialsテーブル用のcsvファイルはapi/financials/importへPOSTします。

config/routes.rb
Rails.application.routes.draw do
  (上記省略)
  namespace :api, format: 'json' do
    resources :code_lists do
      post :import, on: :collection
    end
  end

  namespace :api, format: 'json' do
    resources :financials do
      post :import, on: :collection
    end
  end
  (以下省略)
end

View

ビューは次のようにしました。

Import.vue
<template>
  <div>
    <div class="import-form">
      <input @change="selectedFile" type="file" name="file">
    </div>
    <div class="import-form">
      <button @click="upload('/api/code_lists/import')" type="submit">コードリストのアップロード</button>
    </div>
    <div class="import-form">
      <button @click="upload('/api/financials/import')" type="submit">財務データのアップロード</button>
    </div>
  </div>
</template>

<script>
import axios from 'axios'
export default {
  data: function(){
    return {
      uploadFile: null
    };
  },
  methods: {
    selectedFile: function(e) {
      // 選択された File の情報を保存しておく
      e.preventDefault();
      let files = e.target.files;
      this.uploadFile = files[0];
    },
    upload: function(url) {
      // FormData を利用して File を POST する
      let formData = new FormData();
      formData.append('file', this.uploadFile);
      // let config = {
      //     headers: {
      //         'content-type': 'multipart/form-data'
      //     }
      // };
      axios
          .post(url, formData)
          .then(function(response) {
              // response 処理
          })
          .catch(function(error) {
              // error 処理
          })
    }
  }
}
</script>

inputボックスのファイル選択後にselectedFileメソッドでファイルをuploadFileに保存します。そして、buttonのクリックイベントでuploadメソッドが実行されます。uploadメソッドの引数のurl(先ほどroutesで設定した2つのurl)へファイルをPOSTします。POSTする際に事前に保存したuploadFileをFormDataオブジェクトに渡している事がポイントです。
今回はconfigは使用しなかった為コメントアウトしました。

コントローラーとモデル

今回は2つのcsvファイルをそれぞれのテーブルにImportする実装となり、それぞれのモデル毎に実装が必要となりますが、ほぼ同じ内容となる為Financial Modelの実装のみ記載しておきます。
まずコントローラについて。

app/controllers/api/financials_controller.rb
class Api::FinancialsController < ApplicationController
  def import
    Financial.import(params[:file])
  end
end

コントローラはFinancial Classのクラスメソッドを呼び出すだけとなります。

続いて、financialモデルの記述は以下の通りです。

app/models/financial.rb
class Financial < ApplicationRecord
  validates :edinet, presence: true
  validates :rec_date, presence: true
  validates :account_name, presence: true
  validates :value, presence: true
  def self.import(file)
    CSV.foreach(file.path, headers: true) do |row|
      # IDが見つかれば、レコードを呼び出し、見つかれなければ、新しく作成
      financial = find_by(edinet: row["edinet"], rec_date: row["rec_date"], account_name: row["account_name"]) || new
      # CSVからデータを取得し、設定する
      financial.attributes = row.to_hash.slice(*updatable_attributes)
      # 保存する
      financial.save
    end
  end
  def self.updatable_attributes
    ["edinet", "rec_date", "account_name", "value"]
  end
end

インポートしたいCSVのカラム名をupdatable_attributesに入力。
またfind_byで既に取り込み済みのレコードかどうかを確認しており、取り込み済みの場合は上書きされます。

最後に

現何かお気付きの点がございましたら、お気軽にコメントいただけると幸いです。
上場企業の財務データを活用したアプリケーションを開発中です。フロントエンドをVue.js、バックエンドはRailsAPIを使って開発を進めています。引き続き、得られた知見は記事にしてまとめていきたいと思います。
ここまでお付き合いいただきありがとうございました!

参考にさせていただいた記事:
csrf対策:【Vue】axiosで、デフォルトでCSRFトークンを設定できるようにする
フロントエンド実装:Vue.js でファイルをポストしたいとき
バックエンド実装:【Ruby on Rails】CSVインポート

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VuetifyのClearableはtextfieldとselectで戻り値が違う

TL;TR

textfieldの場合にはnull
selectの場合にはundefined

何が問題なの?

人によっては問題ではないのかもしれませんが、axiosでポストする際に、オブジェクトのバリューがundefinedだとサーバーサイドにそもそも送信されません。

axios.post('http://localhost:3000/api', {sample: 'a', test: undefined})

//Request Body {sample: 'a'}

そのため単純にv-selectにてクリアブルを使用していると、そのプロパティに関しては、まず存在チェックをかける必要があり、その上で値の確認をする必要があります。
テキストフィールドの場合と挙動が違うのとあらかじめ設定しておいたプロパティに存在チェックをかけなければいけないのも面倒だったので、selectに関してもクリアしたときにnullを返すように修正してみました。

<template>
  <div>
    <v-text-field
      label="sample"
      v-model="form.sampleText"
     ></v-text-field>
     <v-select
       label="select"
       v-model="form.sampleSelect"
       :items="selection"
       append-icon="close"
       @click:append="form.sampleSelect = null"
      ></v-select>
  </div>
</template>
<script>
export default {
  data: () => ({
    form: {
      sampleText: '',
      sampleSelect: null,
    }    
  }) 
}
</script>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Vue.js のサンプルアプリケーションを立ち上げるときの手順メモ

欲しいもの

  • Vue
  • TypeScript
  • vue-property-decorator
  • eslint
  • prettier
  • tailwindcss

tailwindcss 以外は Vue CLI でプロジェクトを作成するとデフォルトで入る。

手順

Vue CLI でプロジェクト作成

$ vue create vue-sample

vue-ts-sample ([Vue 2] babel, typescript, eslint) を選択

? Please pick a preset: (Use arrow keys)
❯ vue-ts-sample ([Vue 2] babel, typescript, eslint)
  vue-simple-sample ([Vue 2] babel, eslint)
  Default ([Vue 2] babel, eslint)
  Default (Vue 3 Preview) ([Vue 3] babel, eslint)
  Manually select features

tailwindcss を追加する(スタイルをあてるのが楽だから)

$ yarn add tailwindcss

tailwindcss を有効にするために src/postcss.config.js と src/assets/index.css を追加

src/postcss.config.js
const autoprefixer = require("autoprefixer");
const tailwindcss = require("tailwindcss");

module.exports = {
  plugins: [tailwindcss, autoprefixer]
};
src/assets/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;

src/assets/index.css を main.ts で import

main.ts
import Vue from "vue";
import App from "./App.vue";
import "./assets/index.css";

Vue.config.productionTip = false;

new Vue({
  render: h => h(App)
}).$mount("#app");

package.json の eslint の extends を下記のような感じにする。Vue のスタイルガイドになるべく沿うようにする。

  "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "plugin:vue/strongly-recommended",
      "plugin:vue/recommended",
      "eslint:recommended",
      "@vue/prettier",
      "@vue/typescript/recommended",
      "@vue/prettier/@typescript-eslint"
    ],
    "parserOptions": {
      "ecmaVersion": 2020
    },
    "rules": {}
  }

あとはデフォルトの無駄な HelloWorld などを削除して終わり。

ハマったところ

VSCode 上で ESLint が効かない

原因

ESLint Server が立ち上がっていなかった

解決方法

VSCode の画面右下の「ESLint」をクリックして有効化
image.png

prettier の自動フォーマットが効かない

原因

package.json の設定漏れ

解決方法
 "eslintConfig": {
    "root": true,
    "env": {
      "node": true
    },
    "extends": [
      "plugin:vue/essential",
      "plugin:vue/strongly-recommended",
      "plugin:vue/recommended",
      "eslint:recommended",
      "@vue/typescript/recommended",
+     "@vue/prettier",
      "@vue/prettier/@typescript-eslint"
    ],
    "parserOptions": {
      "ecmaVersion": 2020
    },
    "rules": {}
  },

@vue/prettier/@typescript-eslint だけで prettier が有効になると勘違いしていました。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む