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
--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
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
$ 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.
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
$ 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
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
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
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 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 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>
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
The source code is available on GitHub I hope it helps https://github.com/youichiro/rails-vue-file-uploader-sample
-Overview of Active Storage -Post images with Vue and Rails -Use Active Storage with API
Recommended Posts