[Rails API + Vue] Upload and display images using Active Storage

I will summarize how to upload and display images using Active Storage when the back end is Rails and the front end is Vue, while creating a project from scratch The source code is available on GitHub

Roughly the flow of the process of uploading and displaying images

--Create a screen to select and send images with Vue --When you press the submit button, call the Rails API that uploads the image. --Rails saves the received image in the storage directory and returns the URL of the saved image --Receive and display the URL of the image with Vue

Create a Rails project

I will create it with a directory structure like ↓

rails-vue-file-uploader-sample
└── backend   #Rails project
└── frontend  #Vue project

First, create a Rails project in API mode

$ mkdir rails-vue-file-uploader-sample
$ cd rails-vue-file-uploader-sample
$ rails _6.0_ new backend --api
$ cd backend
$ rails db:create

Enable Active Storage

$ rails active_storage:install
$ rails db:migrate

Running these will create two tables named ** active_storage_blobs ** and ** active_storage_attachments ** These are handled by two models, ** ActiveStorage :: Blob ** and ** ActiveStorage :: Attachment **.

--ActiveStorage :: Blob: Model for managing upload file meta information --ActiveStorage :: Attachment: Model corresponding to the intermediate table between the main model and ActiveStorage :: Blob

For example, if you want the Post model to have an image, the relationship will be as follows. スクリーンショット 2020-11-15 16.54.33.png

Create a model

Create a Post model with title and image as attributes Specify attachment for the image type

$ rails g model post title:string image:attachment
$ rails db:migrate

Running these will create a posts table As you can see by looking at the migration file, the image column is not created in the posts table The contents of the image attribute are saved in ActiveStorage :: Blob and ActiveStorage :: Attachment, and they will be referenced.

Looking at the generated app / models / post.rb, has_one_attached: image is specified By this specification, you can refer to the image.

app/models/post.rb


class Post < ApplicationRecord
  has_one_attached :image
end

Create a controller

$ rails g controller posts

app/controllers/posts.rb


class PostsController < ApplicationController
  def index
    render json: Post.all
  end

  def create
    post = Post.new(post_params)
    if post.save
      render json: post
    else
      render json: post.errors, status: 422
    end
  end

  def destroy
    post = Post.find(params[:id])
    post.destroy!
    render json: post
  end

  private

  def post_params
    params.permit(:title, :image)
  end
end

I will write it normally for the time being Also set routes

config/routes.rb


Rails.application.routes.draw do
  scope :api do
    resources :posts, only: [:index, :create, :destroy]
  end
end

Return the URL of the saved file

Add a method to get the URL of the associated image to the Post model You need to include Rails.application.routes.url_helpers to use the url_for method

app/models/post.rb


class Post < ApplicationRecord
  include Rails.application.routes.url_helpers

  has_one_attached :image

  def image_url
    #Get the URL of the associated image
    image.attached? ? url_for(image) : nil
  end
end

Add the value of image_url to the JSON returned by the action

app/controllers/posts.rb


class PostsController < ApplicationController
  def index
    render json: Post.all, methods: [:image_url]  #Change here
  end

  def create
    post = Post.new(post_params)
    if post.save
      render json: post, methods: [:image_url]  #Change here
    else
      render json: post.errors, status: 422
    end
  end

  def destroy
    post = Post.find(params[:id])
    post.destroy!
    render json: post
  end

  private

  def post_params
    params.permit(:title, :image)
  end
end

You need to add the following settings to config / environments / development.rb to get the URL of the image

config/environments/development.rb


Rails.application.configure do
  ...

  #Add this
  Rails.application.routes.default_url_options[:host] = 'localhost'
  Rails.application.routes.default_url_options[:port] = 3000
end

Set CORS for API communication with Vue Uncomment gem'rack-cors' in the Gemfile, do bundle install, and write config / initializers / cors.rb as follows:

config/initializers/cors.rb


Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'http://localhost:8080'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

Create a Vue project

From here I will write Vue First, go back to the root directory and create a Vue project

$ cd rails-vue-file-uploader-sample
$ vue create frontend
$ cd frontend

I chose the vue create settings as follows

? Please pick a preset: Manually select features
? Check the features needed for your project: Vuex, Linter
? Pick a linter / formatter config: Prettier
? Pick additional lint features: Lint on save
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No

Create a Vuex store

Write Vuex as follows I will use axios, so install it

$ npm install --save axios

src/store/modules/posts.js


import axios from "axios";

const apiUrlBase = "http://localhost:3000/api/posts";
const headers = { "Content-Type": "multipart/form-data" };

const state = {
  posts: []
};

const getters = {
  posts: state => state.posts.sort((a, b) => b.id - a.id)
};

const mutations = {
  setPosts: (state, posts) => (state.posts = posts),
  appendPost: (state, post) => (state.posts = [...state.posts, post]),
  removePost: (state, id) =>
    (state.posts = state.posts.filter(post => post.id !== id))
};

const actions = {
  async fetchPosts({ commit }) {
    try {
      const response = await axios.get(`${apiUrlBase}`);
      commit("setPosts", response.data);
    } catch (e) {
      console.error(e);
    }
  },
  async createPost({ commit }, post) {
    try {
      const response = await axios.post(`${apiUrlBase}`, post, headers);
      commit("appendPost", response.data);
    } catch (e) {
      console.error(e);
    }
  },
  async deletePost({ commit }, id) {
    try {
      axios.delete(`${apiUrlBase}/${id}`);
      commit("removePost", id);
    } catch (e) {
      console.error(e);
    }
  }
};

export default {
  namespaced: true,
  state,
  getters,
  mutations,
  actions
};

src/store/index.js


import Vue from "vue";
import Vuex from "vuex";
import posts from "./modules/posts";

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    posts
  }
});

Create a component to upload an image

Create a src / components / PostForm.vue to display the form to select and submit an image

src/components/PostForm.vue


<template>
  <div>
    <h2>PostForm</h2>
    <section>
      <label for="title">title: </label>
      <input type="text" name="title" v-model="title" placeholder="title" />
    </section>
    <section>
      <label for="image">image: </label>
      <input type="file" id="image" name="image" accept="image/png,image/jpeg" @change="setImage" />
    </section>
    <section>
      <button type="submit" @click="upload" :disabled="title === ''">upload</button>
    </section>
  </div>
</template>

<script>
import { mapActions } from "vuex";

export default {
  name: "PostForm",
  data: () => ({
    title: "",
    imageFile: null
  }),
  methods: {
    ...mapActions("posts", ["createPost"]),
    setImage(e) {
      e.preventDefault();
      this.imageFile = e.target.files[0];
    },
    async upload() {
      let formData = new FormData();
      formData.append("title", this.title);
      if (this.imageFile !== null) {
        formData.append("image", this.imageFile);
      }
      this.createPost(formData);
      this.resetForm();
    },
    resetForm() {
      this.title = "";
      this.imageFile = null;
    }
  }
};
</script>

The selected images can be retrieved with e.target.files When sending a POST request, specify the required value for FormData as an append as a parameter.

Create a component that displays an image

Create a src / components / PostList.vue to retrieve and display the saved images

src/components/PostList.vue


<template>
  <div>
    <h2>PostList</h2>
    <div v-for="post in posts" :key="post.id">
      <h3>{{ post.title }}</h3>
      <img :src="post.image_url" />
      <br />
      <button type="submit" @click="del(post.id)">delete</button>
    </div>
  </div>
</template>

<script>
import { mapActions, mapGetters } from "vuex";

export default {
  name: "PostList",
  created() {
    this.fetchPosts();
  },
  computed: {
    ...mapGetters("posts", ["posts"])
  },
  methods: {
    ...mapActions("posts", ["fetchPosts", "deletePost"]),
    del(id) {
      this.deletePost(id);
    }
  }
};
</script>

Specify the URL obtained in src with <img: src =" post.image_url "/> and display it.

Finally edit App.vue to show the components

src/App.vue


<template>
  <div id="app">
    <PostForm />
    <PostList />
  </div>
</template>

<script>
import PostForm from "./components/PostForm.vue";
import PostList from "./components/PostList.vue";

export default {
  name: "App",
  components: {
    PostForm,
    PostList
  }
};
</script>

Complete

Select an image and press the upload button to save and display the image Images are stored in binary format in the backend / storage directory

スクリーンショット 2020-11-15 17.43.33.png

The source code is available on GitHub I hope it helps https://github.com/youichiro/rails-vue-file-uploader-sample

reference

-Overview of Active Storage -Post images with Vue and Rails -Use Active Storage with API

Recommended Posts

[Rails API + Vue] Upload and display images using Active Storage
[Rails 6] Add images to seed files (using Active Storage)
[Rails] How to upload images to AWS S3 using Carrierwave and fog-aws
Upload multiple images to Cloudinary on Active Storage and publish to Heroku
[Rails] How to upload images to AWS S3 using refile and refile-s3
[rails6.0.0] How to save images using Active Storage in wizard format
[Rails] How to upload images using Carrierwave
Rails Active Storage shrinks images before uploading
[Rails] How to upload multiple images using Carrierwave
[Rails] Show avatars in posts using Active Storage
How to link images using FactoryBot Active Storage
Create a drag-and-drop markdown editor in Rails 6 (using Active Storage, SimpleMDE and Inline Attachment)
[Rails] Save images using carrierwave
Save and display multiple images
[Rails] Create API to download files with Active Storage [S3]
Let's create a file upload system using Azure Computer Vision API and Azure Storage Java SDK
Display API definition in Swagger UI using Docker + Rails6 + apipie
[Java] Upload images and base64
[Rails 6] API development using GraphQL (Query)
[Rails] How to use Active Storage
Try using the Rails API (zip code)
Rails API server environment construction using docker-compose
Post / delete multiple images with Active Storage
Multiple image upload function using Rails Carrierwave
[Java] Get and display the date 10 days later using the Time API added from Java 8.
[Ruby on Rails] Add and delete tags and display (success / error) messages using ajax.
How to implement image posting function using Active Storage in Ruby on Rails
[Rails] We have summarized the storage locations and usage of developer and user images.