StimulusJS Target is undefined but .querySelector works - javascript

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.

Related

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)
}
}

Render js file in js.erb

Is it possible to render a javascript file inside a js.erb file? I have a modal that's being render in my new.js.haml file. However I have some javascript code that needs to run only when that modal gets called.
new.js.haml
$('.modal-content').html("#{ j(render partial: 'new') }");
I want to be able to call this file or render this file when new.js.haml is called upload.js
$ ->
files = [];
$("#upload_button").click ->
$("#file_input").trigger "click";
$("#file_input").change (event) ->
file = event.target.files[0];
files.push(file);
uploadfile files.length - 1, file;
uploadfile = (index, file) ->
formData = new FormData();
formData.append "file", file;
fileitem = "
<li class='file-item success'>
<div class='active'>
<span class='remove'>×</span>
<span>#{file.name}</span>
</div>
<div class='lds-ellipsis'><div></div><div></div><div></div><div></div></div>
</li>
";
$('#filelist').append fileitem;
setRemove();
setRemove = ->
$(".remove").unbind();
$(".remove").click ->
index = $(#).parent().parent().index();
files.splice index, 1;
$('.file-item').eq(index).remove();
Here is my form file that gets rendered but the upload.js file doesn't get called because it's already loaded on the main page. When the the new form button gets clicked it renders this form in a modal.
- #ticket.errors.full_messages.each do |message|
.error= message
= render 'layouts/standard_flash'
%ZenTicketForm{ title: 'Submit Help Request', :multipart => true, data: { remote: true } }
.col-xs-12.col-sm-12.col-md-12.col-lg-12{style: 'padding:0;'}
.col-xs-12.col-sm-12.col-md-12.col-lg-6
%dl.dl-horizontal
%dt Type
%dd.show-more-container
%ZenSelect#ticket_type{options: ['Questions', 'Incidents', 'Problems', 'Tasks'],
class: 'select2',
placeholder: t('activerecord.placeholders.zen_support/ticket.ticket_type') }
.col-xs-12.col-sm-12.col-md-12.col-lg-12
%dl.dl-horizontal
%dt Subject
%dd.show-more-container
%input.form-control{ type: 'text',
name: 'zen_support_ticket[subject]',
placeholder: t('activerecord.placeholders.zen_support/ticket.subject') }
.col-xs-12.col-sm-12.col-md-12.col-lg-6
%dl.dl-horizontal
%dt Priority
%dd.show-more-container
%ZenSelect#priority{ options: ['Low', 'Normal', 'High', 'Urgent'],
data: { placeholder: "activerecord.placeholders.zen_support/ticket.priority" },
class: 'select2' }
.col-xs-12.col-sm-12.col-md-12.col-lg-12
%dl.dl-horizontal
%dt Comment
%dd.show-more-container
%ZenTextArea#comment{ placeholder: t('activerecord.placeholders.zen_support/ticket.comment'),
error: #zendesk_ticket.errors&.dig(:comment) }
%dl.dl-horizontal
.new-show-less
%button.fileUpload.btn.btn-default#upload_button{type: "button"}
Attach File
%input.upload#file_input{:name => "attachments[]", multiple: true, :type => "file"}
%ul#filelist
You can put the JS code directly inside the js.erb file and it'll only be executed when that partial is rendered.
See more:
https://guides.rubyonrails.org/working_with_javascript_in_rails.html

rails vue.js deep nested (attributes of attribute)

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.

Dynamic button values from javascript?

I have a page that does a search, using javascript, and I want to take the list of users that it comes up with, and send that as a submit to the next page. What I have, is:
.search_client_users
= form_tag admin_clients_path, method: "get" , class: "search_form" do
= label_tag 'search_term', 'Old domain name:'
= text_field_tag 'search_term', nil, autocomplete: "off", size: "50"
.main_form.client_emails
= simple_form_for(:domainNameSwap, url: { action: "update" }, html: { method: :put }) do |f|
.input-row
= f.hidden_field :users, value: #clients
.submit-row
.row
.col-xs-5
.submit
= f.submit "Update domains", id: "submit", :class => "btn btn-primary submit"
.client_list
- content_for :javascript do
= javascript_include_tag 'admin/search_client_users'
[some of the formatting may not be quite right due to cut and paste, sorry]
The admin/search_client_users creates an #clients, I'm pretty sure, at least, with:
class App.ClientUserList
constructor: ->
#incrementalSearchAttempts = 0
search: (searchTerm, completeCallback) =>
handleResponseWithOrderAwareness = (attemptNumber, response) =>
if attemptNumber >= #incrementalSearchAttempts
completeCallback(response)
#incrementalSearchAttempts++
onComplete = _.partial(handleResponseWithOrderAwareness, #incrementalSearchAttempts)
$.get('/admin/manage_clients/client_list', { search_term: searchTerm }).complete(onComplete)
class App.Views.SearchClientUsers extends Backbone.View
events:
"keyup input[name='search_term']": "search",
"click .profile_attribute": "showClientUserProfile"
initialize: =>
#clientUserList = new App.ClientUserList()
search: =>
searchTerm = $('.search_form input[name=search_term]').val()
#clientUserList.search(searchTerm, #render)
showClientUserProfile: (event) =>
window.location = $(event.currentTarget).closest('tr').data('client-path')
render: (response) =>
#$el.find('.client_list').html(response.responseText)
$ ->
new App.Views.SearchClientUsers(el: $('.search_client_users')).search()
so, I'm trying to take the list of clients, and send it to the update method in the controller. However, due to when javascript and ruby take place, it doesn't seem to be working... is there a way to do this? or do I have to figure out how to do this in Ajax?
ETA: An alternative idea is, I suppose to just turn the initial text_field into a form, so that the text field is used both for the javascript, and THEN submitted to the form, and then the update can re-do the search... My dataset is small enough that doing the search twice is not a huge problem I suppose...
But I'm not quite sure exactly how to merge the two forms...

Select2 rails does not applied to select_tag in form_for

I am working on rails 5 app. i am trying to load the data asynchronously form pgsql using jquery-select2 data is loading perfectly on search. but the problem is, i want to show the select2 tag when radio button is selected. i have two [radio buttons][1]. code for radio buttons is following
`
<%= radio_button(:new, :agent, :yes, { required: true, checked: true,
class: 'invite-rb' }) %>
<%= label_tag :new_agent_yes, 'New Agent' %>
<%= radio_button(:new, :agent, :no, { required: true, class: 'invite-rb' }) %>
<%= label_tag :new_agent_no, 'Existing Agent' %>`
I want to show the select2 box when user clicks on existing agent. but jquery-select2 is not applied to the select box.
here is my code for agent.js
$(document).on('turbolinks:load', function() {
$('.invite-rb').change(function() {
if(this.value == 'yes'){ //when i change this value to 'no' every thing works perfectly but not working with this condition
$('#agents').removeAttr('required')
$('#agent_first_name, #agent_last_name, #agent_email, #agency_code, #agent_code').attr('required', true)
$('#existing-agent-fields').addClass('hidden')
$('#new-agent-fields').removeClass('hidden')
$('#applications-div').addClass('hidden')
}
else{
$('#agent_first_name, #agent_last_name, #agent_email, #agent_agency_code, #agent_agent_code').removeAttr('required')
$('#agents').attr('required', true)
$('#existing-agent-fields').removeClass('hidden')
$('#new-agent-fields').addClass('hidden')
$('.chosen-select').select2()
$('#applications-div').addClass('hidden')
$('.chosen-select').select2({
minimumInputLength: 2,
placeholder: "Search by Agency code, agent code, name or email",
ajax: {
url: "/dashboard/agent_invitations/search_agents",
dataType: 'json',
type: 'GET',
data: function (term) {
return { q: term }
},
processResults: function (data) {
return { results: data.results }
}
}
});
}
});
i don't know whats wrong with this conditions. i don't want to change the values assigned to radio buttons because complete logic of this page depends upon them.
agent.js [1]: https://i.stack.imgur.com/AYrel.png

Categories

Resources