Rails + Nuxt CRUDアプリ作成(新規登録、詳細表示編)

Rails + Nuxt CRUDアプリ作成(新規登録、詳細表示編)

今回の記事ではデータの追加から詳細の表示までを実装します。

前回の記事はこちら: Rails + Nuxt CRUDアプリ作成(一覧表示編)

1. 新規作成ボタンの作成(Nuxt)

まずは一覧画面に新規作成ボタンを作成します。

front/pages/posts/index.vue

<template>
  <div class="container py-5 position-relative"><!-- 追加 -->
    <b-card
      v-for="post in posts"
      :key="post.id"
    >
      <b-card-text>
        {{ post.content }}
      </b-card-text>
    </b-card>
    
    <!-- ここから -->
    <b-button
      v-b-modal.new-modal
      class="position-absolute mt-4 action-btn"
      pill
      variant="primary"
      size="lg"
    >
      +
    </b-button>

    <b-modal
      hide-header
      hide-footer
      id="new-modal"
    >
      <b-form-textarea
        v-model="content"
        autofocus
      />
      <b-button
        class="mt-3"
        variant="primary"
        @click="save()"
      >
        保存
      </b-button>
    </b-modal>
    <!-- ここまで -->
  </div>
</template>

....

<style scoped>
.action-btn {
  right: 16px;
}
</style>

これで画面の右下にボタンが表示されそれをクリックすると下記のようなモーダル が表示されるようになりました。

保存ボタンを押すとsave()メソッドが実行されますが、今はまだ定義していないのでエラーになります。

v-b-modalの記述はbootstrapの記述なので今回は説明を省略します。

気になるかたは「bootstrap-vue」のmodalの箇所をみてください。

https://bootstrap-vue.org/docs/components/modal#modals

2. axios通信: 送信(Nuxt)

次にモーダル の「保存」ボタンをクリックした時にRailsのcreateアクションを実行するように実装していきます。

front/pages/posts/index.vue

<script>
export default {
  ...省略
  computed: {
    params() {
      return {
        post: {
          content: this.content
        }
      }
    }
  },

  methods: {
    save() {
      const url = "/api/v1/posts"
      this.$axios.post(url, this.params)
        .then((res) => {
          // 保存成功時
        })
        .catch((err) => {
          // 保存失敗時
        })
    }
  }
}

一覧で取得したときのようにaxios通信を行います。

変更点は以下の2つ

  • 新規作成はサーバーにデータを送る必要があるのでHTTPリクエストはPOST
  • 引数にパラメータを追加

this.params computed で定義したparamsなので中身は { post: { content: { this.content } } } が入っています。 this.contentdata で定義したcontentなのでモーダル のtextaraで入力したデータが入ります。

これで「保存」を押した時に

http://localhost:3000/api/v1/postsPOSTリクエストを送りパラメータにはthis.paramsが入ります。

次はRailsの/api/v1/postsPOSTリクエスト すなわちPostsコントローラのcreateアクションを実装していきます。

3. createアクション実装(Rails)

では、PostsControllerのcreateアクションを実装していきます。indexアクションを実装したように下記の手順で実装します。

  • ルーティングの生成
  • createアクションの実装

成功したかのメッセージを返してあげればいいので、jbuilderの実装は不要です。

  • ルーティングの生成
api/config/routes.rb

Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :hello, only: [:index]
      resources :posts, only: [:index, :create]
    end
  end
end
  • createアクションの実装
api/app/controllers/api/v1/posts_controller.rb

... 省略

def create
  post = Post.new(content: params[:post][:content])
  if post.save
    render json: '作成に成功しました', status: 200
  else
    render json: '作成に失敗しました', status: 500
  end
end

作成の可否でstatusを分けてjsonでメッセージを返してあげます。

これでcreateアクションを実装できたので次はNuxt側のレスポンスの実装を行います。

4. axios通信: レスポンス(Nuxt)

save() {
  const url = "/api/v1/posts"
  this.$axios.post(url, this.params)
    .then((res) => {
      // 保存成功時
      console.log(res)
    })
    .catch((err) => {
      // 保存失敗時
    })

console.log()で中身を確認します。下記の画像のようにレスポンスが返ってきたら成功です。

ただ今のままだと保存に成功したのかがわかりにくいので保存成功時に下記の処理を加えます。

  • 成功メッセージのトーストを表示する
  • fetchContents() メソッドを実行する

後、作成に失敗した時の処理もcatchの中に記述します。

さらにcontentは空では「保存」ボタンを入力できないようにdisabledプロパティを加えます。

完成コードはこちら

front/pages/posts/index.vue

<template>
  <div class="container py-5 position-relative">
    <b-card
      v-for="post in posts"
      :key="post.id"
    >
      <b-card-text>
        {{ post.content }}
      </b-card-text>
    </b-card>

    <b-button
      v-b-modal.new-modal
      class="position-absolute mt-4 action-btn"
      pill
      variant="primary"
      size="lg"
    >
      +
    </b-button>

    <b-modal
      hide-header
      hide-footer
      id="new-modal"
    >
      <b-form-textarea
        v-model="content"
        autofocus
      />
      <b-button
        class="mt-3"
        variant="primary"
        :disabled="disabled"
        @click="save()"
      >
        保存
      </b-button>
    </b-modal>
  </div>
</template>

<script>
export default {
  data: () => {
    return {
      posts: [],
    }
  },

  mounted() {
    this.fetchContents()
  },

  computed: {
    disabled() {
      return this.content.length === 0
    },

    params() {
      return {
        post: {
          content: this.content
        }
      }
    }
  },

  methods: {
    fetchContents() {
      const url = "/api/v1/posts"
      this.$axios.get(url)
        .then((res) => {
          this.posts = res.data.posts
        })
    },

    save() {
      // 保存処理
      const url = "/api/v1/posts"
      this.$axios.post(url, this.params)
        .then((res) => {
          console.log(res)
          this.$bvModal.hide('new-modal')
          this.content = ''
          this.fetchContents()
          this.$bvToast.toast(res.data, {
            title: '成功',
            variant: 'success'
          })
        })
        .catch((err) => {
          const message = err.response.data
          this.$bvToast.toast(message, {
            title: 'エラー',
            variant: 'danger'
          })
        })
    },
  }
}
</script>

<style scoped>
.action-btn {
  right: 16px;
}
</style>

これで新規登録ボタンからフォームに入力して保存までを実装できました。

次は詳細画面を作成していきます。

5. 詳細画面作成(Nuxt)

ここからは一覧画面で表示される要素をクリックしたら詳細ページに遷移するように実装していきます。

template

  • classの追加
  • クリックイベントを追加

script

  • クリックイベントの実装
front/pages/posts/index.vue

<template>
  <div class="container py-5 position-relative">
    <b-card
      v-for="post in posts"
      :key="post.id"
      class="cursor-pointer"
      @click="toShow(post.id)"
    >
      <b-card-text>
        {{ post.content }}
      </b-card-text>
    </b-card>

...省略

  methods: {
      toShow(id) {
      this.$router.push(`/posts/${id}`)
    }
  }

これで要素にカーソルを合わせるとポインターになり、クリックするとURLが http://localhost:8080/posts/id ←idは作成したpostのid

に変更しました。まだ詳細ページは実装していないのでエラーになります。

詳細画面のファイル名は _id.vue になります。詳しくはこちらの公式ページに記載されています。

動的なルーティング: https://ja.nuxtjs.org/docs/2.x/features/file-system-routing/

front/pages/posts 配下に _id.vue ファイルを作成します。

front/pages/posts/_id.vue

<template>
  <div class="container py-5">
    <b-card>
      <b-card-text>
        詳細画面
      </b-card-text>
      <b-button
        size="sm"
        @click="toTop()"
      >
        Topへ
      </b-button>
    </b-card>
  </div>
</template>

<script>
export default {
    toTop() {
      this.$router.push('/posts')
    },
  }
}
</script>

これで一覧画面から要素をクリックした時に詳細画面に遷移できました。

次は詳細画面でaxios通信で単一レコードの表示を実装します。

6. axios通信(Nuxt)

先ほど作成した詳細ページにaxios通信の処理を加えていきます。

内容は一覧画面同様で今回は全てのレコードではなくurlのidの箇所レコードのみを取得します。

front/pages/posts/_id.vue

<template>
  <div class="container py-5">
    <b-card>
      <b-card-text>
        {{ post.content }}
      </b-card-text>

...省略

export default {
  data: () => {
    return {
      post: {},
    }
  },

  mounted() {
    this.fetchContent()
  },

  methods: {
    fetchContent() {
      const url = `/api/v1/posts/${this.$route.params.id}`
      this.$axios.get(url)
        .then((res) => {
          this.post = res.data.post
        })
        .catch(() => {
          this.toTop()
        })
    },

urlのid取得は今回は $route.params.id で取得できます。

動的ルートマッチング

これで詳細画面に遷移時axios通信をするようになったのでRailsのshowアクションを実装していきます。

7. showアクション実装(Rails)

一覧画面と同じように

  • ルーティングの生成
  • showアクションの実装
  • jbuilderで返すデータを整形する

の順序で実装していきます。

api/config/routes.rb

Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :hello, only: [:index]
      resources :posts, only: [:index, :create, :show]
    end
  end
end
api/app/contollers/api/v1/posts/controller.rb

def show
  @post = Post.find_by(id: params[:id])

  unless @post
    render json: @post, status: 500
  end
end
api/app/views/api/v1/posts/show.jbuilder

json.post do
  json.extract! @post, :id, :content, :updated_at
end

これでRails側のshowアクションを実装できました。

もう詳細画面をリロードし、下記画像のようにPostデータを取得できれば完成です。

次の記事はコンテンツの編集、削除を実装していきます。

Rails + Nuxt CRUDアプリ作成(編集、削除編)

Rails + Nuxtカテゴリの最新記事