rails vue.js deep nested (attributes of attribute) - javascript

I created one nested form by watching gorails tutorial It is fine and i done it. Issue started when i want to creat nested model under on other nested model. I have Survey model and it is main model. Then i added Question model and made form with vue.js. So I added Choice model under question ( you can notice in survey controller params) First problem is; i don't know how i can define/implemen in vue.js control.(hello_vue.js) And second importan point is: how i can create form elements in new.html
This is my survey.rb model:
class Survey < ApplicationRecord
has_many :questions, dependent: :destroy
accepts_nested_attributes_for :questions, allow_destroy: true
belongs_to :user
end
and surveys_controller.rb
class SurveysController < ApplicationController
before_action :set_survey, only: [:show, :edit, :update, :destroy]
def survey_params
params.require(:survey).permit(:user_id, :name, questions_attributes:[:id,:survey_id, :title, :qtype, :_destroy, choices_attributes:[:id,:question, :ctext]])
end
end
This is nested model of Survey : question.rb:
class Question < ApplicationRecord
enum qtype: [:multiple_choice, :check_boxes, :short_answer]
belongs_to :survey
has_many :choices
accepts_nested_attributes_for :choices, allow_destroy: true
end
So finaly vue.js file:
import TurbolinksAdapter from 'vue-turbolinks'
import Vue from 'vue/dist/vue.esm'
import VueResource from 'vue-resource'
Vue.use(VueResource)
Vue.use(TurbolinksAdapter)
Vue.component('app', App)
document.addEventListener('turbolinks:load', () => {
Vue.http.headers.common['X-CSRF-Token'] = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
var element = document.getElementById("survey-form")
if (element != null){
var survey = JSON.parse(element.dataset.survey)
var questions_attributes = JSON.parse(element.dataset.questionsAttributes)
var choices_attributes = JSON.parse(element.dataset.choicesAttributes)
questions_attributes.forEach(function(question) { question._destroy = null })
survey.questions_attributes = questions_attributes
var app = new Vue({
el: element,
//mixins: [TurbolinksAdapter],
data: function(){
return { survey: survey }
},
methods:{
addQuestion: function(){
this.survey.questions_attributes.push({
id: null,
title:"",
qtype:"",
_destroy: null
})
},
removeQuestion: function(index) {
var question = this.survey.questions_attributes[index]
if (question.id == null) {
this.survey.questions_attributes.splice(index, 1)
} else {
this.survey.questions_attributes[index]._destroy = "1"
}
},
undoRemove: function(index) {
this.survey.questions_attributes[index]._destroy = null
},
saveSurvey: function() {
// Create a new survey
if (this.survey.id == null) {
this.$http.post('/surveys', { survey: this.survey }).then(response => {
Turbolinks.visit(`/surveys/${response.body.id}`)
}, response => {
console.log(response)
})
// Edit an existing survey
} else {
this.$http.put(`/surveys/${this.survey.id}`, { survey: this.survey }).then(response => {
Turbolinks.visit(`/surveys/${response.body.id}`)
}, response => {
console.log(response)
})
}
},
existingSurvey: function() {
return this.survey.id != null
}
}
})
}
})
_form.html.erb
<%= content_tag :div,
id: "survey-form",
data: {
survey: survey.to_json(except: [:created_at, :updated_at]),
questions_attributes: survey.questions.to_json,
} do %>
<label>Survey Name</label>
<input qtype="text" v-model="survey.name">
<h4>Questions</h4>
<div v-for="(question, index) in survey.questions_attributes">
<div v-if="question._destroy == '1'">
{{ question.title }} will be removed. <button v-on:click="undoRemove(index)">Undo</button>
</div>
<div v-else>
<label>Question</label>
<input qtype="text" v-model="question.title" />
<label>Qestion qtype</label>
<select v-model="question.qtype">
<option v-for="qtype in <%= Question.qtypes.keys.to_json %>"
:value=qtype>
{{ qtype }}
</option>
</select>
<button v-on:click="removeQuestion(index)">Remove</button>
</div>
<hr />
</div>
<button v-on:click="addQuestion">Add Question</button>
<br>
<button v-on:click="saveSurvey" >Save Survey</button>
<% end %>

I followed this same tutorial and started running into issues using JSON.parse with more complex nested attributes. Try using Jbuilder to build your JSON objects and look into the gon gem to pass your Rails variables into Javascript. It'll be much easier to query your database and pass the results into your Javascript file using the nested naming that Rails needs. For example...
survey = #survey
json.id survey.id
json.survey do
json.(survey, :user_id, :name)
json.questions_attributes survey.questions do |question|
json.(question, :id, :title, :qtype, :_destroy)
json.choices_attributes question.choices do |choice|
json.(choice, :id, :ctext)
end
end
end
It allows you to do things like...
var survey = gon.survey
Instead of...
var survey = JSON.parse(element.dataset.survey)
And you can pass gon.jbuilder from your controller action and have your defined JSON object ready and available in Vue.

Related

StimulusJS Target is undefined but .querySelector works

I'm confused. I'm getting some inconsistent behaviour with a target in a stimulus controller.
Using StimulusJS via importmap pin "#hotwired/stimulus", to: "stimulus.min.js", preload: true
I have a basic form with stimulus controller.
<%= form_with model: #message, data: { controller: "message-form" } do |form| %>
<%= form.file_field :attachments, class: 'file-input', id: 'file-input', multiple: true, hidden: "hidden",
data: { message_form_target: "attachmentInput" } %>
<i class="fa-solid fa-paperclip fa-lg" data-action="click->message-form#openAttachments"></i>
<% end %>
import {Controller} from "#hotwired/stimulus"
export default class extends Controller {
static targets = ["messageInput", "sendBtn", "attachmentInput"]
connect() {
this.inputIsEmpty()
console.log(this.attachmentInputTarget) // Outputs HTML element
}
openAttachments(){
this.attachmentInputTarget.click()
this.attachmentInputTarget.removeAttribute("hidden")
setInterval(this.hideAttachments, 5000)
}
hideAttachments(){
let attachmentInput = document.querySelector('#file-input')
console.log(this.attachmentInputTarget) // Undefined
console.log(attachmentInput) // Outputs HTML element
console.log(document.getElementById('file-input') == this.attachmentInputTarget) // false
if (!document.getElementById('file-input').files[0]) {
attachmentInput.setAttribute("hidden", "hidden")
} else {
attachmentInput.removeAttribute("hidden")
}
}
}
So, attatchmentInputTarget in connect() acts as I would expect it to and outputs the HTML element, but when hideAttachments is called attatchmentInputTarget is undefined.
Okay, as I've written this all out I figured it out.
Because of the delayed call with setInterval, hideAttachments gets called outside of the stimulus controller as vanilla JS, so has no reference to the Target.

Setting state when page loads, Depending on (database?) contents

So in my code, I want it where a single User can only have one booking/reservation per Hotel, but every time after the page is refreshed, my state keeps reverting back to it's default value, even if the Hotel has already been reserved...
BTW, all of the code is functioning, it is just the state that is giving me a problem...
I've been troubleshooting and debugging the code for a couple of hours but nothing concrete so far.
I've included code from most of the files, but I can't bring in the code from all of the ones needed, or it wouldn't quite be an "minimal" reproducible example, so I'll try to summarize the structure here:
There is a many-to-many relationship between Users and Hotels, which has Bookings belonging to both. This enables me to use hotel.users, etc.
I've just tried adding in a new state (const [bookingObject, setBookingObject] = useState();), but I haven't been able to get it to give me a truthy value 100% of the time... Can anyone see what it is that I'm doing wrong? Anyone have any suggestions or ideas?
bookings_controller.rb:
class BookingsController < ApplicationController
skip_before_action :authorize, only: [:create, :destroy]
def show
render json: #booking
end
def create
booking = Booking.new(booking_params)
if booking.save
render json: booking, status: :created
else
render json: {errors: "Something went wrong!"}
end
end
def destroy
#booking = Booking.find(params[:id])
#booking.destroy
end
private
def set_booking
#booking = Booking.find(params[:id])
end
def booking_params
params.require(:booking).permit(:id, :user_id, :hotel_id)
end
end
hotels_controller.rb:
class HotelsController < ApplicationController
skip_before_action :authorize, only: [:create, :destroy]
def index
render json: Hotel.all
end
def show
render json: #hotel
end
def hotel_params
params.require(:hotel).permit(:name, :city, :country, :company)
end
end
Hotel.jsx:
import React, {useContext, useState} from 'react';
const {bookings, setBookings} = useContext(BookingsContext);
const [booked, setBooked] = useState(false);
const [bookingObject, setBookingObject] = useState();
function toggleBooking(e){
if(booked){
const booking = bookings.find(booking => {return booking.hotel.id == e.target.id});
deleteBookings(booking.id);
setBooked(!booked);
} else if(booked === false){
postBookings();
setBooked(!booked);
}
}
function postBookings(){
const newBooking={
user_id: (currentUser.id),
hotel_id: (hotel.id)
}
fetch(`http://localhost:3001/users/${currentUser.id}/bookings`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
booking: newBooking
}),
}).then((r) => r.json())
.then((data) => {
setBookingObject(data);
setBookings([...bookings, data]);
})
}
function deleteBookings(bookingId){ fetch(`http://localhost:3001/users/${currentUser.id}/bookings/${bookingId}`, {
method: "DELETE"
})
.then((r) => {
if(r.ok)onDeleteBookings(bookingId);
})
}
function onDeleteBookings(deletedBooking){
const updatedBookings = bookings.filter((booking) => booking.id !== deletedBooking.id);
setBookings(updatedBookings);
setBookingObject();
}
return(
<div id='hotels'>
<p>{hotel.name}</p><p>{hotel.city}, {hotel.country}</p>
<button id={hotel.id} onClick={toggleBooking} >{booked ? ("Booked") : ("Book Now")}</button>
</div>
);
}
export default Hotel;

RoR, Stimulus.js: A simple input calculation?

I am new to stimulus. I am trying to add up number/currency inputs and display in another field (the fields are decimal attributes) as the user types in their values but I can't get it to work.
Here I want to add 'land cost' + 'prof services' and display it in 'total cost' field.
My controller:
cost_calculate.controller.js
import { Controller } from 'stimulus'
export default class extends Controller {
static targets = [ 'landCost', 'profServices', 'totalCost' ]
updateTotalCost () {
const totalCost = this.landCost() + this.profServices()
this.totalCostTarget.value = Number(totalCost).toFixed(2)
}
landCost () {
return parseInt(this.landCostTarget)
}
profServices () {
return parseInt(this.profServicesTarget)
}
totalCost () {
return parseInt(this.totalCostTarget)
}
}
My form:
<%= simple_form_for #project, html: { "data-controller" => "cost-calculate" }, url: wizard_path(step, project_id: #project.id) do |f| %>
<%= f.text_field :land_cost, data: { target: 'cost-calculate.landCost', action: "change->cost-calculate#updateTotalCost" }, class: "project-dropdown-width" %>
<%= f.text_field :prof_services, data: { target: 'cost-calculate.profServices', action: "change->cost-calculate#updateTotalCost" }, class: "project-dropdown-width" %>
<%= f.text_field :total_cost, data: { target: 'cost-calculate.totalCost' }, label: false, class: "project-dropdown-width" %>
<% end %>
It keeps printing NaN in the 'totalCost' field. I'm not entirely confident my code is right in controller or view either for what I want to do.
For example, I want to achieve this but just adding two fields together
https://ngaunhien.net/blog/simple-input-calculation-with-stimulusjs
Would appreciate any guidance. ty.
You should use the value and parse and set the default value of 0 if no value exists.
You should modify the controller as
import { Controller } from 'stimulus'
export default class extends Controller {
static targets = [ 'landCost', 'profServices', 'totalCost' ]
updateTotalCost () {
const totalCost = this.landCost() + this.profServices()
this.totalCostTarget.value = Number(totalCost).toFixed(2)
}
landCost () {
return parseInt(this.landCostTarget.value || 0)
}
profServices () {
return parseInt(this.profServicesTarget.value || 0)
}
totalCost () {
return parseInt(this.totalCostTarget.value)
}
}

Get attribute from model of selected option

I have an employee dropdown that lists all the employees. I want to be able to select an employee and get the address of the employee from the model so that I may display it. the following is the code of my collection_select.
<div class="form-group col-md-2 field">
<%= form.label :employee_id %>
<%= form.collection_select :employee_id, Employee.all, :id, :full_name,{:prompt=>"Select Employee"},{:id=>"emp_select",class:"form-control",:onchange=>"getEmployee();"} %>
</div>
Next is the code I am using to grab the value of the employee that was selected and it does work.
function getEmployee() {
var selectedVal=$('#emp_select option:selected').val();}
From here what do I do to get the address of the employee that was selected?
You will have to retrieve the employee's address via ajax call. Here are the steps:
Define an action in your rails app to return employee's address by json.
Make an ajax request to that action and get the info needed.
Render result into view.
For more information, take a look at this link:
https://guides.rubyonrails.org/working_with_javascript_in_rails.html
routes.rb
controller :ajax do
get 'ajax/get_employee_address/:employee_id', action: :get_employee_address, as: :get_employee_address
end
ajax_controller.rb
class AjaxController < ActionController::Base
def get_employee_address
employee = Employee.find(params[:employee_id])
render json: employee.address.to_json
rescue ActiveRecord::RecordNotFound
render json: 'Employee not found', status: 422
end
end
Your js code
function getEmployee() {
var selectedVal=$('#emp_select option:selected').val();
$.ajax({
url: '/ajax/get_employee_address/' + selectedVal,
success: function (address) {
// Render your address to view
},
error: function () {
// Handle error here or just return nothing
return null;
}
})
}
Note: This ajax endpoint will expose your employee address to outside so be sure to make authentication to prevent leaking info.
Add address to option data-attribute:
<%= form.select :employee_id,
options_for_select(Employee.all.map {
|e| [e. full_name, e.id, { 'data-address' => e.address }]
}),
{ prompt: "Select Employee" },
{ id: "emp_select", class: "form-control", onchange: "getEmployee();" } %>
On change get it with js:
function getEmployee() {
var selectedVal=$('#emp_select option:selected').data("address");}
And insert it to needed place

Rails, javascript and updating forms

I have a form that allows the user to choose a country. Depending on which country is selected, I need to change the State/Province drop-down to include either a list of states or a list of provinces. I was going about this using the observe_field tag, but that was depreciated in rails 3...
So.., how should one go about this now? I am using select_tag to populate the drop-downs, and the arrays used in the options_for_select are all stored server-side and made accessible in the controller action, so I can't access them from javascript..
Using the Carmen gem: https://github.com/jim/carmen.
I did the following some times ago (AJAX).
HTML:
<p>
<label>Country <span>*</span></label>
<%= profile_form.select(:country,Carmen.countries, {:include_blank => 'Select a Country'}, :id => "profile_country") %>
</p>
<p>
<label>State <span>*</span></label>
<%= profile_form.select(:state, "" , {:include_blank => 'Select a Country first'}, :id => "profile_state") %>
</p>
Controller:
def states
begin
render :json => Carmen::states(CGI::unescape(params[:country]))
rescue
render :json => {"content" => "None"}.to_json
end
end
Javascript with jQuery:
$('#profile_country').change(function() {
if ($(this).val() == '')
{
$('#profile_state').empty();
$('#profile_state').append( $('<option>No state provided for your country</option>'));
}
else {
$.ajax({
type: "GET",
url: "/remote/get_states/" + encodeURIComponent($(this).attr('value')),
success: function(data){
if (data.content == 'None')
{
$('#profile_state').empty();
$('#profile_state').append( $('<option>No state provided for your country</option>'));
}
//handle the case where no state related to country selected
else
{
$('#profile_state').empty();
$('#profile_state').append( $('<option>Select your State</option>'));
jQuery.each(data,function(i, v) {
$('#profile_state').append( $('<option value="'+ data[i][1] +'">'+data[i][0] +'</option>'));
});
}
}
});
}
});

Categories

Resources