so I have an error array in data and whenever user focuses out of an input it checks if its empty. If it is empty it add's an object to the error array like so:
[
"0": {
"product_name": {
"message": "to polje je obvezno"
},
"barcode": {
"message": "to polje je obvezno"
}
},
"1": {
"barcode": {
"message": "to polje je obvezno"
}
},
"2": {
"product_name": {
"message": "to polje je obvezno"
}
}
]
so the 0,1,2 stand for the index of the item because I have a v-for loop and then product_name or barcode stand for the input in that item/index.(component is at the end of the post if you need it). So now I am trying to display an error when product_name or barcode exists.
I am trying like this:
<span class="tooltip"
v-if="errors && errors[index] && errors[index]['product_name']" style="left: 5px">
test123 (this is product_name error, above index is the current index in v-for so 0 or 1 or 2...)
</span>
<span class="tooltip"
v-if="errors && errors[index] && errors[index]['product_name']"style="left: 5px">
test123 (this is barcode error, above index is the current index in v-for so 0 or 1 or 2...)
</span>
but it doesnt display the span
component:
<tr v-for="(item, index) in documentItems" :key="item.id">
<td>{{index + 1}}.</td>
<td>
<div>
<textarea v-model="item.barcode"
#focusout="checkInput('barcode',index)"
cols="15" rows="2">
</textarea>
<span v-if="errors && errors[index] && errors[index]['barcode']">
test123
</span>
</div>
</td>
<td>
<div>
<textarea v-model="item.product_name"
#focusout="checkInput('product_name',index)"
cols="15" rows="2">
</textarea>
<span v-if="errors && errors[index] && errors[index]['product_name']">
test123
</span>
</div>
</td>
</tr>
EDIT: is it possible that my checkInput is the problem? this is how I created errors:
checkInput(name, itemIndex){
if(this.documentItems[itemIndex][name] == null){
this.errors[itemIndex][name] = { message: 'to polje je obvezno'}
};
//testing
console.log(this.errors[itemIndex][name]); //works
if(this.errors[1]['product_name']){
console.log("yes"); //works
}
},
EDIT2:
the spans show if I define error object like so:
errors: {
0: {
barcode: '',
product_name: ''
},
1: {
barcode: '',
product_name: ''
}
},
but if I do it with a for loop span don't show (I made a for loop in method where I retrive all the documentItems and gets fired on mounted()):
for(var i = 0;i < response.data.documentItems[0].length;i++){
this.errors[i] = {
barcode: '',
product_name: '',
}
}
Your problem roots in a vue reactivity caveat mentioned in their documentation.
https://v2.vuejs.org/v2/guide/reactivity.html#For-Objects
Vue will create proxy-like objects (a pattern similar to Observer using Object.defineProperty) for every field that is defined in your data function before anything runs, when you manually add fields using this.foo = bar (or something similar), if 'foo' key is not already available in your data field, vue will not make it reactive, hence it will not update your DOM when it changes.
You can achieve what you want in a couple of workarounds.
First way which also mentioned in their documentations is to create whole errors object with Object.assign or spread syntax and re-assign your field in data.
// instead of `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
This solution is similar to treating a field like its immutable
So you can fix your checkInput method with the following change:
checkInput(name, itemIndex){
if(this.documentItems[itemIndex][name] == null){
const newErrorForName = { [name]: { message: 'to polje je obvenzo' }};
this.errors = Object.assign({}, {...this.errors, [itemIndex]: newErrorForName })
};
//testing
console.log(this.errors[itemIndex][name]); //works
if(this.errors[1]['product_name']){
console.log("yes"); //works
}
},
This is because vue cant understand manual object property add/delete.
Your second way is to use an array for errors instead of an object.
This is probably a better idea since your errors object is really an array.
it has fixed integer zero based indexes!
Related
My Greeting.
To put in context, my purpose of asking this question is to be able to render a child component inside a form based on the selected option of the <app-selector> Vue component as simple and silly as that.
For the sake of simplicity. I've made a snippet down here to expose what I'm trying to figure out.
Basically, the aim is to get the component name to be rendered by using the computed property cardTypeComponent. However, I want to fathom the way cardTypeComponent is working, since I cannot see why, in one hand, the first return (return this.form) is giving the object (this.form) with the property I want (card_type) but on the other hand the second return (return this.form.card_type ? this.form.card_type + 'Compose' : '') is giving me an empty string, assuming this.form.card_type is undefined when it is clear looking at the first return that, in fact, is not taking it as undefined.
There is way more context, since once the option is selected there is a validation process from the server before setting the value inside this.form object. Moreover, the form interaction is through steps, so once the user select the option he has to click a button to reach the form fields that corresponds to that type card selected, therefore the component is not going to be rendered the very first moment the user selects an option as in the snippet approach. However, it would entangle what I'm asking. Thanks beforehand.
It is better to use the Fiddle link below.
Snippet
var appSelector = Vue.component('app-selector', {
name: 'AppSelector',
template: `<div>
<label for="card_type">Card Type:</label>
<select :name="name" value="" #change="sendSelectedValue">
<option v-for="option in options" :value="option.value">
{{ option.name }}
</option>
</select>
</div>`,
props: {
name: {
required: false,
type: String,
},
options: {
required: false,
type: Array,
}
},
methods: {
sendSelectedValue: function(ev) {
this.$emit('selected', ev.target.value, this.name)
}
}
});
var guessByImageCompose = Vue.component({
name: 'GuessByImageComponse',
template: `<p>Guess By Image Compose Form</p>`
});
var guessByQuoteCompose = Vue.component({
name: 'GuessByQuoteComponse',
template: `<p>Guess By Quote Compose Form</p>`
});
new Vue({
el: '#app',
components: {
appSelector: appSelector,
guessByImageCompose: guessByImageCompose,
guessByQuoteCompose: guessByQuoteCompose,
},
data() {
return {
form: {},
card_types: [
{
name: 'Guess By Quote',
value: 'GuessByQuote'
},
{
name: 'Guess By Image',
value: 'GuessByImage'
}
],
}
},
computed: {
cardTypeComponent: function() {
return this.form; // return { card_type: "GuessByImage" || "GuessByQuote" }
return this.form.card_type ? this.form.card_type + 'Compose' : ''; // return empty string ("") Why?
}
},
methods: {
setCardType: function(selectedValue, field) {
this.form[field] = selectedValue;
console.log(this.form.card_type); // GuessByImage || GuessByQuote
console.log(this.cardTypeComponent); // empty string ("") Why?
}
},
mounted() {
console.log(this.cardTypeComponent); // empty string ("")
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<form action="#" method="post">
<app-selector
:name="'card_type'"
:options="card_types"
#selected="setCardType"
>
</app-selector>
{{ cardTypeComponent }} <!-- Always empty string !-->
<component v-if="cardTypeComponent !== ''" :is="cardTypeComponent">
</component>
</form>
</div>
https://jsfiddle.net/k7gnouty/2/
You're setting a property on this.form which is not initialized first in data. This means you have run into Vue's change detection caveat. Use Vue.set when setting it:
methods: {
setCardType: function(selectedValue, field) {
Vue.set(this.form, field, selectedValue);
}
}
Alternatively, you could declare the properties first if that works better for you.
I apologize for the title being a little hard to understand. I had a hard time explaining it in one line. But here's what I'm trying to do.
I'm developing a screen within my app that supports a barcode gun reader. Barcode guns can only interact with textfields. And then through a text field(hidden) I can pass a custom barcode that instructs the UI to do something. Here is the UI explanation for clarity:
I have a radio button group with 2 options (yes and no)
I have a hidden textfield to accept the barcode gun read
I have a barcode for "yes" and another for "no"
If I scan the "yes" barcode, the radio button option with value = "Yes", should be checked
If I scan the "no" barcode, the radio button option with value = "No", should be checked
I initially thought that by changing the v-model to the correct value, it will do it, but it didn't check it. Likewise, by changing the v-model.value to true or false it will check to its appropriate value. But no cigar.
My idea on how this would work is by (pseudocode)
if querySelector with name ragrouphidden.value = "Yes" then find the option whose value is Yes and option.checked = true
else if querySelector with name ragrouphidden.value = "No" then find the option whose value is No and option.checked = true
The "find" part is what eludes me, or maybe there is an easier way.
Here's some relevant code
Template
<div>
<q-input
class="hidden-field"
v-model="ragrouphidden"
name="ragrouphidden"
#change="raSelectOption()">
</q-input>
<div>
<label class="col-6 text-weight-medium">Mark for Remote Adjudication</label>
<div>
<q-option-group
v-model="ragroup"
:options="raoptions"
#check="raRules($event.target.value)"/>
</div>
</div>
</div>
Script
data() {
return {
ragrouphidden: "",
ragroup: null,
raoptions: [
{
label: "Yes",
value: true
},
{
label: "No",
value: false
}
],
}
},
methods: {
raSelectOption() {
setTimeout(() => {
let hasFocus = document.querySelector("input[name=ragrouphidden]");
hasFocus.focus();
}, 500);
if (
document.querySelector("input[name=ragrouphidden]").value === "*yes*"
) {
this.ragroup.value = true; //this is what I need
} else if (
document.querySelector("input[name=ragrouphidden]").value === "*no*"
) {
this.ragroup.value = false; //This as well
}
},
}
Hopefully it makes sense to you guys. Thanks in advance.
You don't need to use ragroup.value to set the model value here. You can simply do this.ragroup = true; and vue will automatically set the q-option-group selected value for you behind the scene.
A simple demo with dynamic checkbox:
var demo = new Vue({
el: '#demo',
data: {
checked: [],
categories: [{ Id: 1 }, { Id: 2 }]
},
mounted(){ this.checked = [2] }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.min.js"></script>
<div id="demo">
<ul>
<li v-for="c in categories">
<input type="checkbox" :value="c.Id" :id="c.Id" v-model="checked" />
{{c.Id}}
</li>
</ul>
</div>
I have created a fiddle to explain what I want : https://jsfiddle.net/silentway/aro5kq7u/3/
The standalone code is as follows :
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<div id="mainApp" class="container-fluid">
<p>This is my main list.</p>
<div class="main-list" v-for="(q, index) in questions">
<input type="checkbox"
v-bind:id="'question-' + index"
v-bind:value="{id: index, property: false}"
v-model="answers">
<label v-bind:for="q">{{q}}</label>
</div>
<p>And this is the list of the selected elements in the previous list.</p>
<ul class="selected-list" v-for="(a, index) in answers" :key="a.id">
<li>{{questions[a.id]}} <input type="checkbox"
v-bind:id="'answer-' + index"
v-bind:value="true"
v-model="a.property">
</li>
</ul>
<p>Here's the answer's array for debugging: {{answers}}</p>
</div>
<script>
var mainApp = new Vue({
el: '#mainApp',
data: {
questions: [
"Who are you ?",
"Who, who?",
"You know my name?",
"Look up my number"
],
answers: []
}
});
</script>
I want to display a first list of questions, each with a checkbox. The selected questions are stored in an array called "answers".
From these selected answers I then make another list. Each item has a new corresponding checkbox, for a certain property (which can be true or false). I would like this associated property to be stored in the same array ("answers") as the results from the input in the first list.
What happens with my code is that checking a box in the second list does change the shared array of data ("answers"), but in doing so it also unchecks the corresponding answer in the first list.
Any help would be much appreciated.
I'm having a very hard time following your wording but I gave it a shot anyway. I think you'd be better off keeping selected questions and selected answers in their own array and use a computed property to join them basically. Here's a quick fiddle of it: https://jsfiddle.net/crswll/d8e1g750/21/
new Vue({
data: {
questions: [{
id: 1,
question: 'What the heck 1?'
},
{
id: 2,
question: 'What the heck 2?'
},
{
id: 3,
question: 'What the heck 3?'
},
{
id: 4,
question: 'What the heck 4?'
},
{
id: 5,
question: 'What the heck 5?'
},
],
selectedQuestions: [],
selectedAnswers: [],
},
computed: {
answers() {
return this.selectedQuestions.map(id =>
this.questions.find(question => question.id === id)
)
},
selectedAnswersSimpleList() {
return this.selectedAnswers
.map(id => this.questions.find(question => question.id === id))
.map(question => question.question)
}
},
}).$mount('#app')
I am using an ng-repeat to repeat through questions provided by an ajax response and I need the ng-model of each question input to point to a separate answers array.
The question array looks like this
bookingQuestions: [
0: {
label: 'Any allergies?',
type: 'text',
id: 1234
},
1: {
label: 'Names of attendees',
type: 'text',
id: 1235
}
]
I create the answers array by looping through all the questions and pushing the id and an empty answerValue property into my empty bookingAnswers array. The answers array looks like this:
bookingAnswers: [
0: {
id: 1234,
answerValue: ''
},
1: {
id: 1235,
answerValue: ''
}
]
In the markup, I'm attempting to init the answerObj by getting the correct answer object to match the corresponding question.
<div class="question" ng-repeat="question in bookingQuestions">
<label class="question-label" for="{{question.id}}">{{question.label}}
</label>
<input type="text" name="{{question.id}}" ng-model="answerObj"
ng-init="answerObj = getAnswerObj(question)">
</div>
The JS function:
$scope.getAnswerObj = function(question) {
angular.forEach(bookingAnswers, function(answer) {
if(question.id === answer.id) {
return answer.answerValue;
}
});
}
Even though the JS function successfully returns the correct object property, the ng-model isn't being updated to use it. How do I get this working?
You bind the ng-model of all your inputs to some answerObj meaning they all point to the same variable. Using $index you can access the index of the current iteration. So you could do something like this:
<input type=“text“ name="{{question.id}}"
ng-model="bookingAnswers[$index].answerValue"> </div>
<div class="question" ng-repeat="question in bookingQuestions">
<label class="question-label" for="{{question.id}}">{{question.label}}
</label>
̶<̶i̶n̶p̶u̶t̶ ̶t̶y̶p̶e̶=̶"̶t̶e̶x̶t̶"̶ ̶n̶a̶m̶e̶=̶"̶{̶{̶q̶u̶e̶s̶t̶i̶o̶n̶.̶i̶d̶}̶}̶"̶ ̶n̶g̶-̶m̶o̶d̶e̶l̶=̶"̶a̶n̶s̶w̶e̶r̶O̶b̶j̶"̶
<input type="text" name="{{question.id}}" ng-model="answerObj.answerValue"
ng-init="answerObj = getAnswerObj(question)" />
</div>
$scope.getAnswerObj = function(question) {
angular.forEach(bookingAnswers, function(answer) {
if(question.id === answer.id) {
̶r̶e̶t̶u̶r̶n̶ ̶a̶n̶s̶w̶e̶r̶.̶a̶n̶s̶w̶e̶r̶V̶a̶l̶u̶e̶;̶
return answer;
}
});
}
<!-- Facets in the v-for below is an array of objects, each element (facet) in the
facets array has a property which is an array of facetItems. -->
<div class="row" v-for="(facet, facetsIndex) in facets" :key="facetsIndex">
<!-- Since we are inside the v-for, it creates a search input for each facet.
Each facet search input will only search for facetItems belonging to that facet.
We know which facet to search in because we pass the facetIndex to the searchFilter function. -->
<input type="text" #keyup="searchFilter(facetsIndex)">
<div v-if="facet.facetItems.length > 0">
<div class="facet-header">{{config[core.toLowerCase()].displayNames[facet.facetName]}}</div>
<div class="row facet-scroll" >
<!-- The v-for below is used to iterate over the facetItems for each facet. displayFacetItems() is called
for each array of facetItems corresponding to each facet on initial render. displayFacetItems() is also called
on each keyup event emitting from the corresponding facet search input. displayFacetItems() should return an
array of facetItems objects, and when a search input is entered, it should return a filtererd array of facetItems
based on the search results. -->
<div class="facet-item" v-for="(item, facetItemIndex) in displayFacetItems(facetsIndex)" :key="facetItemIndex">
<facet v-bind:item="item" v-bind:facet="facet"></facet>
</div>
</div>
<hr class="divider"/>
</div>
</div>
methods: {
searchFilter (facetsIndex) {
let searchTerm = event.currentTarget.value
this.displayFacetItems(facetsIndex, searchTerm)
},
displayFacetItems (facetsIndex, searchTerm) {
if (!searchTerm) return this.facets[facetsIndex].facetItems
return this.facets[facetsIndex].facetItems.filter((facetItem) => {
return _.includes(facetItem.name.toLowerCase(), searchTerm.toLowerCase())
})
}
},
Please see the comments in the code above for an explanation of what's happening in my code.
I don't understand why my code above isn't working. I'm trying to implement search functionality for each facet. When searching, the filtering should only happen for facetItems belonging to that specific facet.
I've been able to verify that displayFacetItems does return an array of filtered facetItems but for some reason the filtered array isn't updated in the DOM.
This might have something to do with Vue's data binding or the process in which Vue updates the DOM. Any pointers on what I'm doing wrong is greatly appreciated.
My code took inspiration from this article:
https://nickescobedo.com/1018/introduction-to-vue-js-filtering-with-lodash
You can see my jsfiddle for search items on Vuejs. I hope this will help you.
<div id="app">
<label>
Search name: <input type="text" v-model='searchString'>
</label> <br>
<transition-group tag='ul' name='my-list'>
<li v-for='item in filteredList' :key='item.id' class="my-list-item">
{{item.name}}
</li>
</transition-group>
</div>
<script>
const app = new Vue({
el: '#app',
beforeMount() {
const req = fetch('https://jsonplaceholder.typicode.com/users');
req.then(response => {
if (response.ok) {
return response.json();
}
throw new Error('Bad request: ' + response.status);
})
.then(users => {
this.users = users;
this.nextId = this.users.length + 1;
});
},
data: {
searchString: '',
users: [
{id: 1, name: 'Alex'},
{id: 2, name: 'Bob'},
{id: 3, name: 'Sandy'},
],
},
computed: {
filteredList: function() {
const searchString = this.searchString.toLowerCase();
return this.users.filter(item => item.name.toLowerCase().includes(searchString));
}
}
});
</script>
The param facetsIndex is not reactive, please take a look at your jsconsole, you should have a warning about this and not only this, your code logic is totally wrong.
You could use a v-model on input,
create a filteredFacets property,
use this property in your loop.
Just an example, you should read more about how VueJS works :
https://v2.vuejs.org/v2/guide/