[JAVA] Let's roughly implement the image preview function with Rails + refile + jQuery.

Good morning, hello, good evening. I belong to Infratop Co., Ltd. I am working as a mentor at "DMM WEBCAMP" for programming learning.

The year before last, I wrote an article entitled "Let's compare what Rails is doing" course for super beginners. https://qiita.com/kouichi_s/items/6480586ab7f6356a91c3

I didn't participate last year, but I decided to participate this year. I'm pretty bad at (still) JavaScript. However, if you do not touch JavaScript at all, you can not use the API at all, so I thought I'd get used to it for a while I would like to write about the function that I made as a trial a long time ago.

This is a hands-on article that relies heavily on reference articles, but thank you for your cooperation.

Execution environment: CentOs (Cloud9) Rails:5.2.4.3 Ruby:2.5.7

-Prerequisites for reading this article-

--Can handle Rails CRUD functions --Already implemented the image upload function with Gem's refile --Rails jQuery can be introduced ――I'm interested in JavaScript, but I'm in trouble because I don't have any hints to handle with Rails.

What happened

A few years ago, I was worried that even if I called a JavaScript book, I couldn't understand it. "Relatively low implementation difficulty" "Connecting to understanding JavaScript" "It will be a fairly practical content" I was thinking about the function. That's a convenient function, so ...

was.

"Image preview function of image posting function"

It is a function to preview what kind of image was selected when an image is selected in refile. Sometimes people who make this feature ask me, In some cases, I read the code and answer it, I thought I had never made it myself, so I looked it up.

But how do Rails and JavaScript actually get involved?

This was the first problem I had when combining Rails and JavaScript. At first glance, it doesn't seem to mesh at all. Even if you look at the code for beginners Write it in Javascript, display it with alert or prompt, and do something. I can say that, but I couldn't understand anything else right away.

But as you get better at it, you'll realize that it wasn't the case. It was okay if I knew exactly what Rails was doing to make the application.

In Rails, you can use embedded Ruby (the part described by <% =%> or <% =%>). With this description method, if you write a certain amount, it will be converted to HTML. By using embedded Ruby, even if you don't understand HTML You can easily implement an input form as a function just by remembering a certain amount of description. Without this, you would have to learn the details of how to write in HTML and think about how to send it to the database. Rails' strength as a framework is that it can automate this point.

In other words, Rails' embedded Ruby is finally HTML.

That's why you can create web applications and access them with a browser.

That means

"You can use the method of linking HTML and JavaScript as it is"

It means that.

When that happens ... You can use the method used to implement the "Like Asynchronous Communication" function. If you want to entangle with Rails, you can use the HTML "id". By using id

――You can describe what causes a change, such as "where on the page to change". ――You can check where the change has occurred, such as "where in the page has changed".

Functions such as will be able to be created with JavaScript.

As a supplement to HTML knowledge, id is not reused like class, It is used when you want to name an element that is only one in the page. With it, you don't have to get involved with other elements.

In the case of JavaScript, use "document.getElementById" in the DOM.

In the case of jQuery, you can read it with "Selector".

Touch this area a little, If you don't read the code again, it will be difficult to understand, so Let's actually make it.

For the image preview function, I did a Google search with the keyword "javascript image preview" and found a good page.

Reference: https://www.softel.co.jp/blogs/tech/archives/5676

This page used a combination of jQuery selector and Javascript code. So how do you actually get involved with Rails' "image upload function"? Let's make it referring to the above article.

First, create a Post model and set the name column for the name and the image column for posting the image.

Terminal


$ rails g scaffold Post name:string image:string
$ rails db:migrate

You can use scaffold as it is just a trial. In addition to making a model, both view and controller are automatically created.

Next is the introduction of refile and jQuery. This is based on the assumption that you have experience in introducing it. __ Please use the methods and articles that you usually refer to. __

And if the installation is successful, set the attachment to the Post model as usual.

app/models/post.rb



class Post < ApplicationRecord
    attachment :image
end

For the submission form, delete the one made with scaffold and make it as follows.

erb:app/views/posts/new.html.erb


<%= form_with(model: @post,url: posts_path,method: :post, local: true) do |f| %>
  <% if @post.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h2>

      <ul>
      <% @post.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %>
    <%= f.text_field :name %>
  </div>

  <div class="field">
Product image:<%= f.attachment_field :image %>
  </div>

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

If you are creating a post model form Since id is attached by post_column name, specify this with the selector. For the time being, start the server to see if you made a mistake, and check the id using the verification tool of Google chorme.

スクリーンショット 2020-12-13 000621.png

In this way, the verification tool can be used to identify the id. You can optionally give your own name to the id, but if you don't need it, you can leave it as it is.

Since the id of the input of image upload is post_image, write this in the selector. And let's write the code for JavaScript & jQuery. Normally you write to app/assets/javascripts/application.js, This time, for the sake of clarity, it's a bit ill-mannered, but I'll write it at the bottom of posts/new.html.erb. A little above that, I prepared an img tag for image preview and set id to preview.

javascript:app/views/posts/new.html.erb


//Prepare the img tag for preview here. It does not appear when not previewed.
<img id="preview" style="width:40%;">

<script>
  $('#post_image').on('change', function (e) {
    var reader = new FileReader();
    reader.onload = function (e) {
        $("#preview").attr('src', e.target.result);
    }
    reader.readAsDataURL(e.target.files[0]);
});
</script>

You just have to synchronize it with the preview img tag, so Let's set the id to "preview" on the Javascript side as well. If you are using refile, the file selection will be input as in the reference article, so it seems to be okay.

thus….

スクリーンショット 2020-12-13 011624.png

did it!

スクリーンショット 2020-12-13 011601.png

The final code is below.

erb:app/views/posts/new.html.erb


<%= form_with(model: @post, local: true) do |f| %>
  <% if @post.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h2>

      <ul>
      <% @post.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

  <div class="field">
    <%= f.label :name %>
    <%= f.text_field :name %>
  </div>

  <div class="field">
Product image:<%= f.attachment_field :image %>
  </div>

  <div class="actions">
    <%= f.submit %>
  </div>
<% end %>

//Prepare the img tag for preview here. It does not appear when not previewed.
<img id="preview" style="width:40%;">

<script>
  $('#post_image').on('change', function (e) {
    var reader = new FileReader();
    reader.onload = function (e) {
        $("#preview").attr('src', e.target.result);
    }
    reader.readAsDataURL(e.target.files[0]);
});
</script>

Roughly speaking, if you explain the contents of the code,

$('#post_image').on('change', function (e) {

Look for the id of post_image in the jQuery selector, It means that when the contents are changed (when the event called change occurs), function (e) is executed. If you are not familiar with Javascript, you may be a little wondering when you see the description of unction (e). __function Function name () {... processing content} __ I thought that if I had just studied a little, I should prepare a function in the above form, It's hard to understand what you're doing because it doesn't have a function name.

This is called __anonymous function __. Anonymous functions, also called immediate functions, are literally functions that are executed on the fly. For the time being, think of it as a "function that summarizes the processes that are used immediately, although they are not called many times."

__ In other words, as mentioned above, I am writing the content that I want you to execute immediately "when an event called change occurs". __

By the way, e is a variable that passes data to the event handler (the process that runs when an event occurs). The data of post_image of id that is the target of processing this function is passed to variable e.

To summarize the contents so far,

__ "If there is a change in the place with the id post_image, Executes the immediate function function (e). e is the state that contains the data of post_image ”__

Will be said. I will explain the code that follows.

    var reader = new FileReader();
    reader.onload = function (e) {
        $("#preview").attr('src', e.target.result);
    }
    return reader.readAsDataURL(e.target.files[0]);
});

Here we define a variable reader function and define a FileReader to read the file. And when reader.onload can read the file, try to execute the contents of another anonymous function again. Since we are using e here as well, we are pulling data from the location where the id is post_image. $("#preview").attr('src', e.target.result); Then, set the image data acquisition result to be reflected in the place where the id prepared for preview is preview. Look for the place where id is preview, and the img tag is designed to read the image by specifying src, so In order to reflect the data of e, that is, the place where id is post_image, the part of src

e.target.result

With the description, it is set so that data can be taken. By doing this, "Find the place where id which is img tag is preview and rewrite src for image display" The work is completed.

Finally,

reader.readAsDataURL(e.target.files[0]);

It is set to actually read the read contents. However, looking at the source code so far raises questions.

    reader.onload = function (e) {
        $("#preview").attr('src', e.target.result);
    }

In that part, "Huh? Didn't you rewrite the place where id is already preview in this description?" You might think, but this is a description that onload works when a file is loaded, so It does not work when reading is not occurring.

return reader.readAsDataURL(e.target.files[0]);

"Reading a file" and "Generating a URL to refer to a file" are performed with the description. files [0] receives files in an array (plural), so It is set to get the very first file. That is, unless this description is given

    reader.onload = function (e) {
        $("#preview").attr('src', e.target.result);
    }

This description is in a state of being stuck.

Roughly speaking, it has this structure.

Like this, when I wrote the code, I was able to display it quickly. Alright ...

__ Image preview function, complete. __

......

__ No no no no no no. __

That's not good. Didn't you just make the reference article available in Rails? That's it, it's not a practice ... Even if you teach someone, you can just tell the reference article as it is ...

When I was trying it, I tried to do it myself At the very least, I thought about supporting multiple postings of images.

Let's challenge the preview of multiple images.

There are several ways to handle multiple posts with refile, but this time I referred to this article.

https://qiita.com/nekosuke_/items/501e7ba1b286c51c1be9

Column names, model names, etc. are based on the reference article. Create an association with the Product model (parent) and ProductImage model (child), The image column of the PostImage model has an image.

For the time being,

--Up to 5 photos can be posted at the same time ――Posting more than 5 sheets is prohibited, causing an error

I roughly decided the condition and started writing.

And what I wrote was this code.

erb:app/views/products/new.html.erb


<%= form_with model: @product,url: products_path,method: :post,local: true do |f| %>
  <div class="field">
Product name:<%= f.text_field :name %>
  </div>
  <div class="field">
Product image:<%= f.attachment_field :product_images_images, multiple: true %>
  </div>
  <%= f.submit %>
<% end %>
  <img id="preview_0" style="width:15%;">
  <img id="preview_1" style="width:15%;">
  <img id="preview_2" style="width:15%;">
  <img id="preview_3" style="width:15%;">
  <img id="preview_4" style="width:15%;">
<script>
$('#product_product_images_images').on('change', function (e) {
    
    if(e.target.files.length > 5){
       //If you have selected 5 or more images, reset the selected file.
      alert('You can only post up to 5 at a time.');
      $('#product_product_images_images').val = "";
      
      //If you still have a preview of the image,
      //If you do not reset it, it will induce a misunderstanding that "you can still select", so initialize it.
      for( var i = 0; i < 5; i++) {
        $('#preview_[i]').attr('src', "");
      }
      
    }else{
      var reader = new Array(5);
      
      //When you select an image twice, if the number is less than the first time, it will remain on the screen, so initialize it.
      for( var i = 0; i < 5; i++) {
        $('#preview_[i]').attr('src', "");
      }

      for(var i = 0; i < e.target.files.length; i++) {
          $('#post_image_[i]').on('change', function (e) {
              var reader = new FileReader();
              reader.onload = function (e) {
                  $('#preview_[i]').attr('src', e.target.result);
              }
              reader.readAsDataURL(e.target.files[i]);
          });
        }
      }
});
</script>

Img tag for preview,

<img id="preview_0" style="width:15%;">

Prepare five things that say, with a for statement and e.target.files.length, I made it so that it loops as many times as the number of times the file is read.

But it doesn't work. Hmm ... I feel like the code isn't wrong. Moreover, there is no error. There seems to be no choice but to crush them one by one. After some research, I found some problems with this code.

Problem 1: JavaScript requires a "template literal" when putting a loop variable in a string

Actually, the procedure to read the i variable for the loop was wrong. $('#post_image[i]') This way of writing doesn't read variables. If you think about it, even if you put the variable i as an array number in the for statement in the character string, it will be just a character string. I realized that it was necessary to write in a way that corresponds to "expression expansion" and "variable expansion" in Ruby. If you do a Google search with the keyword "javascript variable expansion" as it is, The word "template literal (template string)" came out.

according to this,

    1. If you write $ {variable name} in the string, the contents of the variable will be added to the string.
    1. Change the single quotation mark (') around the string to a grave accent (`) If you do not follow the procedure, the variable will not be interpreted. Reference: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Template_literals#%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E5 % AE% 9F% E8% A3% 85% E7% 8A% B6% E6% B3% 81
 $('#preview_[i]')
this,
 ↓
$(`#preview_${i}`)

If you fix it like this, the contents of the variable will be expanded and In the first week, it was treated as "# preview_0" and responded safely.

Problem 2: Onload may not fire at the right time.

When I did a Google search with the keyword "onload for statement", I came across this article. Reference: https://qiita.com/gonshi_com/items/615226b5fa355869a01c Apparently onload is a little slow, When using it, it seems that you have to define it as a function separately and pass the data.

Problem 3: I am using var in the first place and it is not compliant with ES2015 (ES6).

Javascript has a long history, and as much as it is called "you can do anything" There are many grammars for killing beginners and beginners. It is said that let will be used instead of var, You may just leave it as "because it moves". At the very least, you need to change the variable to let properly, If it doesn't work, it's bad if you don't write the code that changed accordingly (even if you can't write it well). The above template literal is a description method of ES2015 (ES6), so Let's make it compliant as well.

The final code looks like this.

erb:app/views/products/new.html.erb


<%= form_with model: @product,url: products_path,method: :post,local: true do |f| %>
  <div class="field">
Product name:<%= f.text_field :name %>
  </div>
  <div class="field">
Product image:<%= f.attachment_field :product_images_images, multiple: true %>
  </div>
  <%= f.submit %>
<% end %>

<!--Prepare 5 img tags for preview. Nothing is displayed at first.-->
<img id="preview_0" style="width:15%;">
<img id="preview_1" style="width:15%;">
<img id="preview_2" style="width:15%;">
<img id="preview_3" style="width:15%;">
<img id="preview_4" style="width:15%;">

<script>
$('#product_product_images_images').on('change', function (e) {
    
    if(e.target.files.length > 5){
      
      alert('You can only post up to 5 at a time.');
      //If you have selected 5 or more images, reset the selected file.
      $('#product_product_images_images').val = "";
      
      //If you still have a preview of the previous image,
      //Initialized to induce misunderstanding if the image is still selected.
      for( let i = 0; i < 5; i++) {
        $(`#preview_${i}`).attr('src', "");
      }
      
    }else{
      let reader = new Array(5);
      
      //When you select an image twice, if the number is less than the first time, it will remain on the screen, so initialize it.
      for( let i = 0; i < 5; i++) {
        $(`#preview_${i}`).attr('src', "");
      }

      for(let i = 0; i < e.target.files.length; i++) {
        reader[i] = new FileReader();
        reader[i].onload = finisher(i,e); 
        reader[i].readAsDataURL(e.target.files[i]);
        
        //Since onload cannot be used in the for statement unless it is prepared by another function, prepare the function.
        function finisher(i,e){
          return function(e){
          $(`#preview_${i}`).attr('src', e.target.result);
          }
        }
      }
   }
});
</script>

When I try to select it ... スクリーンショット 2020-12-13 195554.png

It's done! スクリーンショット 2020-12-13 202553.png

This time there are a lot of imitations, but I think the code is easy to understand. You may need to combine the code by referring to multiple articles like this.

But to do that, you need to get the basics right, sometimes referring to another programming language, You have to be good at choosing search words.

If you get used to it a little, you will feel that you can easily create any function by referring to the articles on the net. There may be a slight catch like this.

Also, there are quite a few cases where no error statement appears.

In this case, there is a case where the error does not appear if "the selector is not working due to a grammatical error". In that case, you also need to be able to reconfirm the basic grammar and search the situation well. Once you get used to this point, you will be able to do various things, but it is painful not to proceed. However, if you investigate most things, you can find a clue to the solution.

"I did it according to the article ..." "I spent so much time ..." "I wonder if I'm talented ..."

You may think that, but when you grow up and look back, I often find myself stumbling in a place that isn't a big deal. You may feel stressed, but let's work steadily. If you get used to "getting changes using id" like this time, Since you will be able to understand the handling of GoogleMapAPI etc. to some extent, If you are not good at it, it will be easier to understand if you practice while paying attention to this point.

That made the article a little long, but Thank you for visiting!

~ Editor's Note ~

Finally. While writing this article, I thought

__ "I don't want to leave bad code, so I'm reluctant to push to Github, It's not good to make it in Cloud9 ”__

about it. The content of this time is the code I wrote a long time ago, before the completed version is completed, It was quite difficult to remember where I was stuck at that time. If you don't leave the evolutionary process, it will be difficult to look back ... Not just the portfolio and the finished product It's a good idea to leave some code for the growth process. You can also back it up. Really. I thought again.

Well then, I would like to finish the article soon. Have a good programming life!

Recommended Posts

Let's roughly implement the image preview function with Rails + refile + jQuery.
I tried to implement the image preview function with Rails / jQuery
[Ruby on Rails] Post image preview function in refile
[Rails] Implement image posting function
[Rails] Implementation of image preview function
Yes, let's preview the image. ~ part5 ~
Image processing: Let's play with the image
[Rails] Implement the product purchase function with a credit card registered with PAY.JP
Image preview function
Let's make a search function with Rails (ransack)
Implement the Like feature in Ajax with Rails.
[Implementation procedure] Implement image upload function with Active Storage
[Rails] Implement search function
Let's understand the function!
Image preview function implementation
Roughly the flow of web application development with Rails.
Let's implement a function to limit the number of access to the API with SpringBoot + Redis
[Ruby on Rails] Implement login function by add_token_to_users with API
Implement login function simply with name and password in Rails (3)
Implement application function in Rails
[Rails] Implement User search function
Introduced graph function with rails
Implement follow function in Rails
Implementation of image preview function
Rails ~ Understanding the message function ~
Login function implementation with rails
Implement search function with form_with
Replace preview by uploading by clicking the image in file_field of Rails
[JQuery] How to preview the selected image immediately + Add image posting gem
[Rails] About the Punk List function
Implemented mail sending function with rails
Let's verify the image search filter
Create pagination function with Rails Kaminari
Implement simple login function in Rails
Implement CSV download function in Rails
Timeless search with Rails + JavaScript (jQuery)
Let's unit test with [rails] Rspec!
How to implement search function with rails (multiple columns are also supported)