Preview multiple images right after selection with stimulus javascript (ruby on rails) - ruby-on-rails

I am dealing with a rails project where I am trying to upload multiple images and have them previewed right after image selection. I am using stimulus, ruby on rails, js6
The HTML is a form used to create a new product - I am also using cloudinary - to upload multiple pictures and simpleformfor
app/views/products/_form.html.erb
<%= simple_form_for product do |m| %>
<div class="d-flex justify-content-between" data-controller="upload">
<%= m.input :photos, as: :file, input_html: { multiple: true, class: 'hidden', id: 'photo-input',
data: {action: 'change->upload#displayPreview'} },
label_html: { class: 'upload-photo'}, label: ':camera: Upload a photo' %>
<% if #product.photos.attached? %>
<% #product.photos.each do |photo| %>
<%= cl_image_tag photo.key, height: 100, width: 200, crop: :fill, data: { target: 'upload.image'} %>
<% end %>
<% end %>
</div>
<%= m.button :submit, class: 'btn-primary' %>
<% end %>
the HTML form code for 1 image is as follow
<div data-controller="upload">
<label class="file optional upload-photo" for="photo-input">Upload photo</label>
<input class="form-control-file file optional hidden" id="photo-input" data-action="change->upload#displayPreview" type="file" name="product[photos][]">
<%= cl_image_tag "", height: 100, width: 200, crop: :fill, data: { 'upload-target': 'image', 'upload-index-value': 0 } %>
</div>
</div>
The code for 1 image is as follow and working fine if the form was just for 1 image, but how would i transform the following code to accommodate for multiple image uploading?
I tried several things using for loops and this.imageTargets.forEach((element) => {.. and even indexes but at no avail..
javascript/controllers/upload_controller.js
import { Controller } from "stimulus"
export default class extends Controller {
static targets = ['image']
displayPreview(event) {
const input = event.target
if (input.files && input.files[0]) {
const reader = new FileReader();
reader.onload = (event) => {
this.imageTarget.src = event.currentTarget.result;
}
reader.readAsDataURL(input.files[0])
this.imageTarget.classList.remove('hidden');
}
}
}
I appreciate your comments and alternative solutions..

For file fields with multiple images, you need to use the for loop in your JS which will detect the input files length. For your reference you can update your js as like.
image_controller.js
import { Controller } from "#hotwired/stimulus"
export default class extends Controller {
static targets = ["input"]
preview() {
var input = this.inputTarget
var files = input.files
var imgLoc = document.getElementById("Img")
for (var i = 0; i < files.length; i++) {
let reader = new FileReader()
reader.onload = function() {
let image = document.createElement("img")
imgLoc.appendChild(image)
image.style.height = '100px'
image.src = reader.result
}
reader.readAsDataURL(files[i])
}
}
}
As you are displaying the output by replacing the preview.png, for this you don't need the image preview. This will create images.
_form.html.erb
<div class="mb-3" data-controller="image" id = "Img">
<%=f.label :images, class: "form-label" %>
<%= f.file_field :images, multiple: true, class: "form-control", accept: "image/png, image/jpeg, image/jpg", "data-image-target": "input", "data-action": "image#preview" %>
</div>

Related

Error with Rails Sortable - Cannot read properties of undefined (reading 'dataset')

I followed this tutorial to implement drag and drop. https://www.driftingruby.com/episodes/drag-and-drop-with-hotwire
I am getting an error on drop that says Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'dataset'). Any thoughts on why this is happening?
Project model:
acts_as_list scope: :project
_project.html.erb
<div id="projects" data-controller="position">
<%= content_tag :div, data: { sgid: project.to_sgid_param, id: "#{dom_id project}" }, class: 'table-row' do %> <div><%= project.name %> </div>
<div><%= project.description %> </div>
<div><%= project.status %> </div>
<div><%= project.user.email %> </div>
<div class="flex justify-end">
<%= link_to "View", project_path(project) , data: {turbo: false} %>
<%= button_to "Delete", project, method: :delete, form: {data: {turbo_confirm: "Are you sure?"}} %> </div>
</div>
<% end %>
</div>
position_controller.js
import { Controller } from "#hotwired/stimulus";
import Sortable from "sortablejs"
import { put, get, post, patch, destroy } from "#rails/request.js"
export default class extends Controller {
connect() {
this.sortable = Sortable.create(this.element, {
animation: 150,
onEnd: this.updatePosition.bind(this)
})
}
async updatePosition(event) {
const response = await put('/projects', {
body: JSON.stringify({
sgid: event.project.dataset.sgid,
position: event.newIndex + 1
})
})
if (response.ok) {
console.log(event.project.dataset.sgid)
}
}
}
well event doesn't have a method project. You may have meant event.target. Anyway it looks as if the target of the event is probably the #projects element (but you need to check this in dev tools and see where the event is being captured).
Let's suppose that the event is being captured by the #projects element, well that element has an empty dataList. So wherever the capture is occurring, you need to put data: {sgid: project.to_sgid_param} on that element.
Finally, you should be able to populate the ajax request with the value sgid: event.target.dataset.sgid.

Rails/Stimulus: What is the best way to get form field values when handling form submission with Stimulus?

I have a form but Stimulus will handle its submission.
<%= form_with model: #booking, data: { controller: 'booking', action: 'booking#submitForm:prevent' } do |form| %>
<%= form.text_field :name, data: { 'booking-target' => 'name' } %>
<%= form.text_field :pet_name, data: { 'booking-target' => 'pet' } %>
<%= form.submit "Submit form" %>
<% end %>
import { Controller } from "#hotwired/stimulus"
export default class extends Controller {
static targets = ['name', 'pet']
submitForm() {
console.log('Submitting form')
console.log('Name: ' + this.nameTarget.value)
console.log('Pet: ' + this.petTarget.value)
}
}
The above code works but I have specified the target name for each field in the form. This isn't a problem with a small form but would be cumbersome if the form had many more fields. Is there a more elegant way to expose the field data?
You don't have to specify targets and just use the form directly:
submitForm(e) {
// get it directly from the form
console.log(e.target.name, e.target.name.value)
// or like this
console.log(e.target["pet_name"], e.target["pet_name"].value)
// get everything
const data = Object.fromEntries(new FormData(e.target).entries())
console.log(data)
}
BTW, you can also do this:
# <%= form.text_field :name, data: { 'booking-target' => 'name' } %>
<%= form.text_field :name, data: { booking_target: :name } %>

Rails-7 : Prevent form clear on get request

My problem seems simple.
<%= form_with(url: projects_path, method: :get, data: { turbo_frame: "projects", turbo_action: "advance", controller: "search-form" }) do |form| %>
<%= form.text_field :title, data: { action: "input->search-form#search" } %>
<%= form.collection_select :category, Project.categories.keys, :to_s, :titlecase, { include_blank: true, default: 0 }, data: { action: "change->search-form#search" } %>
<%= form.submit "Search" %>
<% end %>
I've got this form that clears after submitting the get request. I need it to be Get because I'm using Turbo. Is there a way to prevent that via stimulus or simple Rails/Ruby ?
Here is my stimulus file if needed :
import { Controller } from "#hotwired/stimulus"
// Connects to data-controller="search-form"
export default class extends Controller {
search() {
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.element.requestSubmit();
}, 150)
}
}
Preventing the form to empty on an asynchronous request is good but surely when I submit every key stroke. xD
Thanks for the help !

Paperclip Cropping rails

I referred this episode of railscasts
http://railscasts.com/episodes/182-cropping-images
But whenever I press on the crop button
I get an error
convert.exe: invalid argument for option `-crop': 'YYYxYYY+YYY+YYY'-auto-orient
# error/convert.c/ConvertImageCommand/1124.
Y being the values depending on the selected area.
Please suggest me to rectify this bug.
This is my controller code:
def crop_photo
#artist_profile=ArtistProfile.find(params[:id])
#artist_profile.update_attributes(params[:artist_profile])
if #artist_profile.save
format.js
end
My cropping view file :
<%= stylesheet_link_tag "jquery.Jcrop" %>
<%= javascript_include_tag "jquery.Jcrop" %>
<div style="max-width: 720px;">
<%= nested_form_for #artist_profile, :url => cropped_photo_path ,:html => { :method => :post } ,:remote => true do |f| %>
<%= image_tag #artist_profile.photo.url(:large), :id => "cropbox" %>
<%= hidden_field_tag "artist_profile_id", #artist_profile.id %>
<h4>Preview:</h4>
<div style="width:100px; height:100px; overflow:hidden">
<%= image_tag #artist_profile.photo.url(:thumb), :id => "preview_image" %>
</div>
<% for attribute in [:crop_x, :crop_y, :crop_w, :crop_h] %>
<%= f.hidden_field attribute, :id => attribute %>
<% end %>
<p><%= f.submit "Crop" %></p>
<% end %>
</div>
<script type="text/javascript" charset="utf-8">
$(function() {
$('#cropbox').Jcrop({
onChange: update_crop,
onSelect: update_crop,
setSelect: [0, 0, 500, 500],
aspectRatio: 1
});
});
function update_crop(coords) {
var rx = 100.00/coords.w;
var ry = 100.00/coords.h;
$('#preview_image').css({
width: Math.round(rx * <%= #artist_profile.photo_geometry(:large).width %>) + 'px',
height: Math.round(ry * <%= #artist_profile.photo_geometry(:large).height %>) + 'px',
marginLeft: '-' + Math.round(rx * coords.x) + 'px',
marginTop: '-' + Math.round(ry * coords.y) + 'px'
});
var ratio = <%= #artist_profile.photo_geometry(:original).width %> / <%= #artist_profile.photo_geometry(:large).width %>;
$("#crop_x").val(Math.round(coords.x * ratio));
$("#crop_y").val(Math.round(coords.y * ratio));
$("#crop_w").val(Math.round(coords.w * ratio));
$("#crop_h").val(Math.round(coords.h * ratio));
}
</script>
Looks like your system is either not sending or processing the required crop co-ordinates:
Javascript is not updating the hidden fields
JCrop is not capturing / processing the right data
Rails is not reading the params properly
JS
You may want to write your JS code like this:
var jCrop = function() {
$('#cropbox').Jcrop({
onChange: update_crop,
onSelect: update_crop,
setSelect: [0, 0, 500, 500],
aspectRatio: 1
});
};
$(document).ready(jCrop);
$(document).on("page:load", jCrop);
Your JCrop could be prevented from loading due to not loading correctly
JCrop
The other issue you may have is JCrop not capturing the right data
You need to make sure your hidden fields are actually getting populated with the data. Your update_crop code looks okay - so I would look at view_source > hidden fields
Rails
The likely problem here is probably Rails' processing of the params
You're passing your form_for var as #artist_profile
I would do this in your controller (considering you're using Rails 4):
def crop_photo
#artist_profile = ArtistProfile.find(params[:id])
#artist_profile.update_attributes(crop_params)
if #artist_profile.save
format.js
end
end
private
def crop_params
params.require(:artist_profile).permit(:crop_x, :crop_y, :crop_h, :crop_w)
end

nested_form rails update

i am trying to use nested_form.
every thing works fine except the child remove operation. it works fine with add/update. i didn't even change any of he controller class.
seems like event though i remove the item from the view... it sends all the child information with some hidden value to the project controller and it automatically again add/update all the child element. what i am missing?
My Models: Project & Workpackage
class Project < ActiveRecord::Base
has_many :workpackages, :dependent => :destroy
accepts_nested_attributes_for :workpackages
end
class Workpackage < ActiveRecord::Base
belongs_to :project
end
View
_form
<%= nested_form_for #project do |f| %>
<% if #project.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(#project.errors.count, "error") %> prohibited this project from being saved:</h2>
<ul>
<% #project.errors.full_messages.each do |msg| %>
<li>
<%= msg %>
</li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= f.label :name %>
<br />
<%= f.text_field :name %>
</div>
<div class="field">
<%= f.label :description %>
<br />
<%= f.text_field :description %>
</div>
<div>
<%= f.fields_for :workpackages do |wp| %>
<div class="field">
<%= wp.label :title %>
<br />
<%= wp.text_field :title %>
</div>
<div class="field">
<%= wp.label :wp_type %>
<br />
<%= wp.text_field :wp_type %>
</div>
<%= wp.link_to_remove 'Remove' %>
<% end %>
<%= f.link_to_add 'Add new WorkPackage', :workpackages %>
</div>
<div class="actions">
<%= f.submit %>
</div>
<% end %>
and all my controller class are default generated with scaffold.
EDIT
The generated nested_form java script is given bellow.
jQuery(function($) {
window.NestedFormEvents = function() {
this.addFields = $.proxy(this.addFields, this);
this.removeFields = $.proxy(this.removeFields, this);
};
NestedFormEvents.prototype = {
addFields: function(e) {
// Setup
var link = e.currentTarget;
var assoc = $(link).attr('data-association'); // Name of child
var content = $('#' + assoc + '_fields_blueprint').html(); // Fields template
// Make the context correct by replacing new_<parents> with the generated ID
// of each of the parent objects
var context = ($(link).closest('.fields').find('input:first').attr('name') || '').replace(new RegExp('\[[a-z]+\]$'), '');
// context will be something like this for a brand new form:
// project[tasks_attributes][new_1255929127459][assignments_attributes][new_1255929128105]
// or for an edit form:
// project[tasks_attributes][0][assignments_attributes][1]
if (context) {
var parentNames = context.match(/[a-z_]+_attributes/g) || [];
var parentIds = context.match(/(new_)?[0-9]+/g) || [];
for(var i = 0; i < parentNames.length; i++) {
if(parentIds[i]) {
content = content.replace(
new RegExp('(_' + parentNames[i] + ')_.+?_', 'g'),
'$1_' + parentIds[i] + '_');
content = content.replace(
new RegExp('(\\[' + parentNames[i] + '\\])\\[.+?\\]', 'g'),
'$1[' + parentIds[i] + ']');
}
}
}
// Make a unique ID for the new child
var regexp = new RegExp('new_' + assoc, 'g');
var new_id = new Date().getTime();
content = content.replace(regexp, "new_" + new_id);
var field = this.insertFields(content, assoc, link);
$(link).closest("form")
.trigger({ type: 'nested:fieldAdded', field: field })
.trigger({ type: 'nested:fieldAdded:' + assoc, field: field });
return false;
},
insertFields: function(content, assoc, link) {
return $(content).insertBefore(link);
},
removeFields: function(e) {
var link = e.currentTarget;
var hiddenField = $(link).prev('input[type=hidden]');
hiddenField.val(1);
// if (hiddenField) {
// $(link).v
// hiddenField.value = '1';
// }
var field = $(link).closest('.fields');
field.hide();
$(link).closest("form").trigger({ type: 'nested:fieldRemoved', field: field });
return false;
}
};
window.nestedFormEvents = new NestedFormEvents();
$('form a.add_nested_fields').live('click', nestedFormEvents.addFields);
$('form a.remove_nested_fields').live('click', nestedFormEvents.removeFields);
});
just manage to find out the solution. See accepts_nested_attributes_for
I had to add :allow_destroy => true
class Project < ActiveRecord::Base
has_many :workpackages, :dependent => :destroy
accepts_nested_attributes_for :workpackages, :allow_destroy => true
end

Resources