rails+VueでCRUDをやってみた(spa)

このサイトを読むとrails+VueでCRUDができるようになります。
多分駆け出しエンジニアさんが一番最初にバックエンドとフロントエンドで
やりたいことだと思うので、まずはやってみましょう。

環境構築はこちらから

https://qiita.com/gabakugik/items/cded7e8aaa014025d8ba


Readはこちら

https://qiita.com/gabakugik/items/9125033b96dab8bdd3cf


Createから行きます。

階層
```
├── App.vue
├── assets
│   └── vue.svg
├── components
│   └── HelloWorld.vue
├── lib
│   └── axios.ts
├── main.ts
├── router.ts
├── style.css
├── views
│   ├── BookEdit.vue
│   ├── Create.vue
│   └── Home.vue
└── vite-env.d.ts
```

```
yarn add vue-router
```

```App.vue
<template>
  <nav>
    <router-link to="/">Home</router-link> | <router-link to="/create">Create</router-link> |
    <router-link to="/BookEdit">edit</router-link>
  </nav>
  <main>
    <router-view />
  </main>
</template>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
```

```package.json
{
  "name": "vite-project",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "axios": "^1.4.0",
    "bootstrap": "^5.3.1",
    "vue": "^3.3.4",
    "vue-class-component": "^7.2.6",
    "vue-property-decorator": "^9.1.2",
    "vue-router": "^4.2.4"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.2.3",
    "typescript": "^5.0.2",
    "vite": "^4.4.5",
    "vue-tsc": "^1.8.5"
  }
```

```main.ts
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'

createApp(App).use(router).mount('#app')
```

```router.ts
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'

export default createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      component: Home,
    },
    {
      path: '/create',
      component: () => import('@/views/Create.vue'),
    },
    {
      path: '/edit/:id',
      component: () => import('@/views/BookEdit.vue'),
    },
  ],
})
```

```Home.vue
<template>
  <div class="container">
    <h1 class="#f3e5f5 purple lighten-5 center">[Rails+Vue.js]~Bookshelf~</h1>
    <div class="row #e3f2fd blue lighten-5">
      <div class="col s4 m6" v-for="book in books" :key="book.id">
        <div class="card btn">
          <span class="card-title" @click="setBookInfo(book.id)">
            {{ book.title }}
          </span>
        </div>
      </div>
    </div>
    <div class="row" v-show="bookInfoBool">
      <div class="col s12 m12">
        <div class="card blue-grey darken-1">
          <div class="card-content white-text">
            <span class="card-title">
              【{{ bookInfo.title }}】
            </span>
            <div class="detail">
              ・著者：{{ bookInfo.author }}
            </div>
            <div class="detail">
              ・出版社：{{ bookInfo.publisher }}
            </div>
            <div class="detail">
              ・ジャンル：{{ bookInfo.genre }}
            </div>
<router-link :to="{ path: `/edit/${bookInfo.id}` }" class="btn">本の編集</router-link>
<button class="btn #e53935 red darken-1" @click="deleteBook(bookInfo.id)">削除</button>
        </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import axios from 'axios';

const bookInfo = ref({});
const bookInfoBool = ref(false);
const books = ref([]);

const deleteBook = (id: number) => {
  axios.delete(`http://localhost:3000/api/books/${id}`).then(() => {
    books.value = [];
    bookInfo.value = '';
    bookInfoBool.value = false;
    fetchBooks();
  });
};
const fetchBooks = () => {
  axios.get('http://localhost:3000/api/books').then(
    (res) => {
      books.value = res.data;
    },
    (error) => {
      console.log(error);
    }
  );
};

const setBookInfo = (id: number) => {
  axios.get(`http://localhost:3000/api/books/${id}.json`).then((res) => {
    bookInfo.value = res.data;
    bookInfoBool.value = true;
  });
};

onMounted(fetchBooks);
</script>

<style scoped></style>
```

```Create.vue
<template>
  <div class="container">
    <h1 class="f3e5f5 purple lighten-5 center">本の登録</h1>
    <form class="col">
      <div class="row">
        <div class="input-field">
          <input placeholder="Title" type="text" class="validate" v-model="book.title" required />
        </div>
      </div>
      <div class="row">
        <div class="input-field">
          <input placeholder="Author" type="text" class="validate" v-model="book.author" required />
        </div>
      </div>
      <div class="row">
        <div class="input-field">
          <input placeholder="Publisher" type="text" class="validate" v-model="book.publisher" required />
        </div>
      </div>
      <div class="row">
        <div class="input-field">
          <input placeholder="Genre" type="text" class="validate" v-model="book.genre" required />
        </div>
      </div>
      <button class="btn btn-info waves-effect waves-light" @click="createBook">本を登録</button>
    </form>
  </div>
</template>

<script lang="ts">
import { ref } from 'vue';
import axios from 'axios';
import { Router, useRouter } from 'vue-router';
export default {
  setup() {
    const book = ref({
      title: '',
      author: '',
      publisher: '',
      genre: ''
    });

    const router: Router = useRouter();

    const createBook = () => {
      if (!book.value.title) return;
      axios
        .post('http://localhost:3000/api/books', { book: book.value })
        .then((res) => {
          router.push({ path: '/' });
        })
        .catch((error) => {
          console.error(error);
        });
    };

    return { book, createBook };
  }
};
</script>

<style scoped></style>
```

```books_controller.rb
class Api::BooksController < ApplicationController

  def index
    books = Book.all

    render json: books

    # render json: {book: books}
  end

  def show
    book = Book.find(params[:id])
    render json: book
  end

  def create
    @book = Book.new(book_params)
    if @book.save
      head :no_content
    else
      render json: @book.errors, status: :unprocessable_entity
    end
  end

  def update
    @book = Book.find(params[:id])
    if @book.update(book_params)
      render json: @book
    else
      render json: @book.errors, status: :unprocessable_entity
    end
  end

  def destroy
    @book = Book.find(params[:id])
    if @book.destroy
      head :no_content
    else
      render json: @book.errors, status: :unprocessable_entity
    end
  end


  def book_params
    params.fetch(:book, {}).permit(
    :title, :author, :publisher, :genre
    )
  end
end
```
完成
http://localhost:5173/create
![スクリーンショット 2023-10-09 211930.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270337/911d215d-f7b7-6f97-f6e4-0416f752fb81.png)


edit

```BookEdit.vue

<template>
  <div class="container">
    <h1 class="f3e5f5 purple lighten-5 center">本の編集</h1>
    <form class="col s12">
      <div class="row">
        <div class="input-field">
          <input placeholder="Title" type="text" class="validate" v-model="book.title" required />
        </div>
      </div>
      <div class="row">
        <div class="input-field">
          <input placeholder="Author" type="text" class="validate" v-model="book.author" required />
        </div>
      </div>
      <div class="row">
        <div class="input-field">
          <input placeholder="Publisher" type="text" class="validate" v-model="book.publisher" required />
        </div>
      </div>
      <div class="row">
        <div class="input-field">
          <input placeholder="Genre" type="text" class="validate" v-model="book.genre" required />
        </div>
      </div>
      <div class="btn" @click="updateBook(book.id)">本の情報を変更</div>
    </form>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import axios from 'axios';

interface Book {
  id: string;
  title: string;
  author: string;
  publisher: string;
  genre: string;
}
export default defineComponent({
  name: 'BookEdit',
  data() {
    return {
      id: this.$route.params.id,
      book: {
        id: '',
        title: '',
        author: '',
        publisher: '',
        genre: '',
      } as Book,
    };
  },
  mounted() {
    this.setBookEdit(this.id);
  },
  methods: {
    async setBookEdit(id: string) {
      try {
        const response = await axios.get<Book>(`http://localhost:3000/api/books/${id}.json`);
        const data = response.data;
        this.book.id = data.id;
        this.book.title = data.title;
        this.book.author = data.author;
        this.book.publisher = data.publisher;
        this.book.genre = data.genre;
      } catch (error) {
        console.error(error);
      }
    },
    async updateBook(id: string) {
      if (!this.book.title) return;
      try {
        await axios.put(`http://localhost:3000/api/books/${id}`, { book: this.book });
        this.$router.push({ path: '/' });
      } catch (error) {
        console.error(error);
      }
    },
  },
});
</script>

<style scoped></style>
```

下記を追加
```Home.vue
<router-link :to="{ path: `/edit/${bookInfo.id}` }" class="btn">本の編集</router-link>
```

![スクリーンショット 2023-10-09 212116.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270337/c40ef9ec-85db-8682-651d-cac713ba9224.png)

http://localhost:5173/edit/4
![スクリーンショット 2023-10-09 212326.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270337/09c543df-a57a-43e0-822f-e6e9bd697090.png)



delete
```Home.vue
<button class="btn #e53935 red darken-1" @click="deleteBook(bookInfo.id)">削除</button>
```

```Home.vue
const deleteBook = (id: number) => {
  axios.delete(`http://localhost:3000/api/books/${id}`).then(() => {
    books.value = [];
    bookInfo.value = '';
    bookInfoBool.value = false;
    fetchBooks();
  });
};
```
追加

![スクリーンショット 2023-10-09 212757.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/270337/d8205013-b7b5-bdbe-1cd2-aee754fccc8f.png)





これでCRUDはできるはずです。
見にくいかもしれませんがHome.vueがわかればわかるはず。
いろんな人に助けてもらってここまでできました。
ありがとうございます。
ここどうなっているのとかこここう書いてほしいとか要望がありましたら言ってください。