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)
}
}
Related
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.
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.
I have a form that is built with Vuejs in my Rails 5.1 app. All my fields work well and persist data to the database, except for file uploads. I get the error
[Vue warn]: Error compiling template: printed at the top of the console, then essentially my entire template code, then
- <input v-model="variation.photo_one" type="file">:
File inputs are read only. Use a v-on:change listener instead.
I am new to Vuejs and cannot figure out how to get this to work even after reading many other online posts regarding this.
_form.html.erb
<%= content_tag :div,
id: "product-form",
data: {
id: product.id,
product: product.to_json(except: [:id, :created_at, :updated_at]),
variations_attributes: product.variations.to_json(except: [:product_id, :created_at, :updated_at]),
} do %>
...
<div class="col-md-4 upload-block">
<label>Photo One</label>
<input type="file" v-model="variation.photo_one" style="margin-bottom: .5em">
</div>
...
<% end %>
app_vue.js
import Vue from 'vue/dist/vue.esm'
import TurbolinksAdapter from 'vue-turbolinks'
import VueResource from 'vue-resource'
Vue.use(VueResource)
Vue.use(TurbolinksAdapter)
document.addEventListener('turbolinks:load', () => {
Vue.http.headers.common['X-CSRF-Token'] = document.querySelector('meta[name="csrf-token"]').getAttribute('content')
var element = document.getElementById("product-form")
if (element != null) {
var id = element.dataset.id
var product = JSON.parse(element.dataset.product)
var variations_attributes = JSON.parse(element.dataset.variationsAttributes)
variations_attributes.forEach(function(variation) { variation._destroy = null })
product.variations_attributes = variations_attributes
var app = new Vue({
el: element,
data: function() {
return { id: id, product: product }
},
methods: {
addVariation: function() {
this.product.variations_attributes.push({
id: null,
name: "",
photo_one: "",
//position: "",
_destroy: null
})
},
removeVariation: function(index) {
var variation = this.product.variations_attributes[index]
if (variation.id == null) {
this.product.variations_attributes.splice(index, 1)
} else {
this.product.variations_attributes[index]._destroy = "1"
}
},
undoRemove: function(index) {
this.product.variations_attributes[index]._destroy = null
},
saveProduct: function() {
// Create a new product
if (this.id == null) {
this.$http.post('/products', { product: this.product }).then(response => {
Turbolinks.visit(`/products/${response.body.id}`)
}, response => {
console.log(response)
})
// Edit an existing product
} else {
this.$http.put(`/products/${this.id}`, { product: this.product }).then(response => {
Turbolinks.visit(`/products/${response.body.id}`)
}, response => {
console.log(response)
})
}
},
existingProduct: function() {
return this.product.id != null
}
}
})
}
})
Files are a bit awkward in Vue. As the message says, you cannot use v-model for an input with type="file". Instead you must use the change event and call a method in your component to manually handle the file.
<input type="file" #change="handleFileChange" />
methods: {
handleFileChange(event) {
//you can access the file in using event.target.files[0]
this.fileField = event.target.files[0];
}
}
When you submit the AJAX request, you will likely need to submit a FormData object instead of submitting a javascript object. The MDN docs have an explanation on how to use that. I find the FormData is the more awkward part of dealing with file uploads. https://developer.mozilla.org/en-US/docs/Web/API/FormData
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
I'm using EasyAutocomplete to select some data.
The problem I'm having is that I only can use one autocomplete input. I tried to find a solution but I didn't find anything in the documentation.
My code:
<%= form_tag('/search', local: true, method: :get, class: 'form-group row' ) do %>
<div class="col-md-5">
<%= text_field_tag(:q, nil, data: { behavior: 'autocomplete-lang' }, placeholder: 'Lenguaje', class: 'form-control') %>
</div>
<% end %>
<%= form_tag('/search', local: true, method: :get, class: 'form-group row' ) do %>
<div class="col-md-5">
<%= text_field_tag(:q, nil, data: { behavior: 'autocomplete-ide' }, placeholder: 'Lenguaje', class: 'form-control') %>
</div>
<% end %>
Controller:
def search
#lang = Content.ransack(name_cont: params[:q],types_id_eq: 1).result(distinct: true)
#ide = Content.ransack(name_cont: params[:q],types_id_eq: 4).result(distinct: true)
respond_to do |format|
format.html {}
format.json {
#lang = #lang.limit(5)
#ide = #ide.limit(5)
}
end
end
Js:
document.addEventListener("turbolinks:load", function() {
var $input = $("[data-behavior='autocomplete-lang']");
var options = {
getValue: "name",
url : function(phrase) {
return "/search.json?q=" + phrase;
},
categories: [
{
listLocation: "lang"
}
],
list: {
onChooseEvent: function () {
var id = $input.getSelectedItemData().id;
console.log(id);
}
}
};
$input.easyAutocomplete(options);
});
document.addEventListener("turbolinks:load", function() {
var $input_ide = $("[data-behavior='autocomplete-ide']");
//IDE
var options_ide = {
getValue: "name",
url : function(phrase) {
return "/search.json?q=" + phrase;
},
categories: [
{
listLocation: "ide"
}
],
list: {
onChooseEvent: function () {
var id = $input_ide.getSelectedItemData().id;
console.log(id);
}
}
};
$input_ide.easyAutocomplete(options_ide);
});
So, I only can use once the easyAutocomplete, if I use it twice the behavior is not correct and only one input work.
I really need a hand to solve this, is must be pretty simple, but I can't get it.
I faced the same problem and solved it by adding unique "id"s to each text field.
In JavaScript, instead of writing:
getValue: "name"
You can write something similar to:
getValue: function (element) {
return element.lang+ element.ide
}
You can follow the discussion on this github link.