Vuejs 3 alternative select binding - vue.js

I'm trying to get a select element bound to a value for a custom object. The crux here is that the object property in question has a custom getter. The value is set as a number, but when accessed returns an associated value as a string. Why I do this is a long story.
So I have an object of key-value pairs making some options:
<select v-model="myObject.myProperty">
<option v-for="v, k in myOptions" :key="k" :value="k">{{v}}</option>
</select>
{{myObject.myProperty}} //this line prints out the correct value
But the options are not showing as selected. The value is updated for myObject.myProperty and it returns what I expect. I suspect that behind the scenes, it's correctly assigning k to my custom object, but that because it returns a different string value, Vue can't inherently figure out which option to mark 'selected'.
Manually adding :selected does not help:
<option v-for="v, k in myOptions" :key="k" :value="k" :selected="v === myObject.myProperty">{{v}}</option>
I also tried to manually bind the select instead of using the v-model attribute, also no:
<select :value="myObject.myProperty" #input="myObject.myProperty = $event.target.value"
Is there an alternative way to wire up a select/option situation? If not, building a custom component with faux-select functionality is my next step.
For clarity, myOptions is a key-value like this
{
0 : 'Option 1',
1 : 'Option 2',
}
But myObject has special setters that take and remember the key, then also a special getter than returns the value from myOptions.
So then:
myObject.myProperty = 0;
console.log(myObject.myProperty) //logs 'Option 1'
When when I set the value to the key (k) I get back the corresponding value when the option is selected and the value of 'myObject.myProperty' is what I expect. Example: I pick 'Option 1' from the drop-down, which has a value of 0 derived from the key k.
However, although myObject.myProperty has the value I want, I can't get Vue to display the the actual html option as selected, probably because the value returned by myObject.myProperty is 'Option 1' and not 0

Alright, the actual answer:
<select #input="myObject.myProperty = $event.target.value">
<option v-for="v, k in myOptions" :selected="myObject.myProperty === v" :key="k" :value="k">{{v}}</option>
</select>
v-model won't work here because it simply doesn't care that you've manually applied selected: it will always try to match the option value to the the v-model property value. As this object takes one value with a setter and returns another with a getter, these will never align.
Instead, manually assign the value to the object with #input and match the value from the getter to the value in the options for selected.

Not sure I understand your question properly or not. Hence, adding my input on your requirement below.
As myObject.myProperty returning the value you passed in the select and as per your code you are passing the index as value.
Hence, while comparing in :selected both LHS and RHS should contain index of the item you passed.
Working Demo :
const app = new Vue({
el: '#app',
data() {
return {
myOptions: [{
id: 1,
name: 'alpha'
}, {
id: 2,
name: 'beta'
}, {
id: 3,
name: 'gama'
}],
myObject: {
myProperty: ''
}
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<select v-model="myObject.myProperty">
<option v-for="(item, index) in myOptions" :selected="myObject.myProperty === item.id" :key="item.id" :value="item.id">{{item.name}}</option>
</select>
{{myObject.myProperty}}
</div>

UPDATE Based on the newly provided information.
In Template:
<select v-model="selectedOption">
<option v-for="(v, k) in options" :key="k" :value="k">
{{ v }}
</option>
</select>
In JS
data() {
return {
selectedOption: null,
options: {
0: 'Option 1',
1: 'Option 2',
},
};
}
By setting selectedOption with the key of options will correctly display the selected option.
Here is a link to a working example using your data structure. It contains two selects both using the same object for its data source. One with a default selection. the other without.
https://stackblitz.com/edit/vue-efekym?file=src/App.vue
THIS IS NOW OUTDATED BASED ON THE LATEST INFO
Based on the logic in your further examples I think the problem is within conflating your k & v variables in your loop. Although it is hard to tell because there isn't any sample data or a complete isolated component outlying the behavior.
However in your later examples you have:
<option v-for="v, k in myOptions" :key="k" :value="k" :selected="v === myObject.myProperty">{{v}}</option>
I am inferring that you believe that myObject.myProperty is holding the value from variable v, when in fact you are setting the value as variable k as witnessed in :value="k"
By correcting this I believe your issue will be resolved, you also noted a int to string conversion. depending on how/where this is happening this could also contribute to your headaches because this will never equate to true. '1231' === 1231

Related

Ionic Vue Checkboxes

I have the following Problem:
I'm using Ionic Vue and a VueX Store to save my data from an API.
Now I have set an array, which contains the IDs of entries, which shall be checked or unchecked.
Since I should not modify the API-Model class, I have saved the IDs of checked entries in a seperate Array in my VueX Store, which I update as needed.
Now I'm trying to make the checkboxes checked / unchecked depending on that array.
I tried it by adding v-model = "checkedVehicles.included(vehicle.vehicle_id)", but all I get is an Error:
'v-model' directives require the attribute value which is valid as LHS vue/valid-v-model
Heres the Part whit the checkboxes, hope that is all you need :)
<IonItem v-for="vehicle in vehicleList" v-bind:key="vehicle.vehicle_id">
<IonLabel>
<h2>{{ vehicle.manufacturer }} {{ vehicle.model }}</h2>
<p>{{ vehicle.display_name }}</p></IonLabel>
<IonCheckbox slot="end"
v-model="checkedVehicles.includes(vehicle.vehicle_id)"
#click="checkIfAllDeselected"
#update="this.updateCheckboxOnClick(vehicle.vehicle_id)"/>
</IonItem>
The checkedVehicles Arrays is intialized as String[].
Also tried to use a function, which returns true or false, depending on the checkedVehicles Array has the ID included or not, but that also gives the same error
The other functions, which add or remove entires to the correspondig arrays are working fine, already checked that. only the Checkboxes are not working as intended.
Has anyone a clue, what I'm doing wrong?
This is obvious because we can't evaluate a condition in v-model. We generally bind a variable to the v-model.
For eg:
Consider you have a attr called vehicle in data.
data() {
return {
Vehicle list: [{
checked: true,
manufacturer: '',
display_name: '',
vehicle_id: ''
},
{
checked: true,
manufacturer: '',
display_name: '',
vehicle_id: ''
},
]
}
then you can bind it as
<IonItem v-for="vehicle in vehicleList" v-bind:key="vehicle.vehicle_id">
<IonLabel>
<h2>{{ vehicle.manufacturer }} {{ vehicle.model }}</h2>
<p>{{ vehicle.display_name }}</p></IonLabel>
<IonCheckbox slot="end"
v-model="vehicle.checked"
#click="checkIfAllDeselected"
#update="this.updateCheckboxOnClick(vehicle.vehicle_id)"/>
</IonItem>
To conclude, variables that can hold value can only be used in v-model

Vue - set v-model dynamically (with a variable containing a string)

I haven't been able to set v-model dynamically.
It works if I type explicitly:
<div class="form-group mr-3 mb-2">
<input type="text"
v-model="form[filters][firstlastname]"
>
</div>
But I want to loop through an object wherein I have string , like: 'form[filters][firstlastname]'
The parent has the form with properties:
data() {
return {
form: new Form({
filters: {
gender: [],
firstlastname: 'My firstlastname'
So, from the parent I pass down the form and filters into the child component, here is filters:
let formFilters = { filters: [
{
type: 'text',
property: 'form[filters][firstlastname]', // <-- string
placeholder: 'Name',
},
{
type: 'number',
property: 'paginate',
placeholder: 'Max rows'
},
]
}
Child component: (here I loop through the object and generate the input fields)
<div v-for="(filter,index) in formFilters.filters"
:key="`${index}_${filter.property}`"
>
<input
v-if="filter.type === 'text' || filter.type === 'number'"
:placeholder="filter.placeholder"
:type="filter.type"
v-model="filter.property" //<--- set the property
>
This doesn't work. The v-model just interprets it as a string and not a reference to a form property.
I tested other ways, like: v-model="``${[filter.property]}``" (single, not double ```` but it wont show in stackoverflow otherwise) and other crazy things but it isn't valid.
So how do I set v-model with a variable containing a string (so that it can be set dynamically)?
This is a very tricky problem....
You can access any property present in the data inside html template using 2 ways,
Referring to the property directly
Using $data
data() {
return {
firstlastname: 'Mr First last name'
}
}
so, in html template you can use either
<p>{{firstlastname}}</p>
or
<p>{{$data.firstlastname}}</p>
For your scenario $data can be used for primitive data types like string or number,
<input
v-if="filter.type === 'text' || filter.type === 'number'"
:placeholder="filter.placeholder"
:type="filter.type"
v-model="$data[filter.property]">
But this will not work for your second scenario where you are trying to access nested property of an object form.filters.firstlastname
You can access this property using the following notation $data[form][filters][firstlastname]
In your case, the for loop will result as $data[form.filters.firstlastname] or $data[[form][filters][firstlastname]] which will throw an exception
As suggested in the comments, try different approach or flatten the object. You can refer to this link to see how to flatten the object https://stackoverflow.com/a/25370536/2079271

Vuejs2 - How to update the whole object from the server not losing reactivity?

I have a list of objects that can be updated from the database.
So, when I load the list, objects have only id and name.
When I click on an object I load other fields that can be of any length - that's why I don't load them with the objects in the list.
I found that when I update an object it can be difficult to keep reactivity https://v2.vuejs.org/v2/guide/reactivity.html so I need to find some workaround.
this code works almost okay:
axios.get('/api/news', item.id).then(function(response){
if (response){
Object.assign(item, response.data.item);
}
});
But the problem is the fields that have not been presented from the beginning is not 100% reactive anymore. What is going on is a new field has been updated reactively only when I change another, previous one. So, if I show 2 text field with old and new properties, if I change the second property, the field will not be updated until I change the first one.
I got item object from the component:
data () {
return {
items: [],
}
},
and
<div v-for="item in items" #click="selectItem(item)" >
<span>{{item.name}}</span>
</div>
Then item's been passed to the function selectItem.
What is the proper pattern to load new fields and keep them reactive? (NB: it's not the case when I can assign field by field - I want to reuse the same code no matter which object it is, so I need so the solution for updating an object at a time without listing all new fields.)
Note. This code works inside the component.
Completely revised post: Ok, the example you give uses an array, which has its own caveats, in particular that you can't directly set values like vm.items[indexOfItem] = newValue and have it react.
So you have to use Vue.set with the array as the first argument and the index as the second. Here's an example that adds a name property to object items and then uses Vue.set to set the item to a new object created by Object.assign.
new Vue({
el: '#app',
data: {
items: [{
id: 1,
other: 'data'
}, {
id: 2,
other: 'thingy'
}]
},
methods: {
selectItem(parent, key) {
const newObj = Object.assign({}, parent[key], {
name: 'some new name'
});
Vue.set(parent, key, newObj);
setTimeout(() => {parent[key].name = 'Look, reactive!';}, 1500);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="item, index in items" #click="selectItem(items, index)">
<span>{{item.name || 'empty'}}</span>
</div>
<pre>
{{JSON.stringify(items, null, 2)}}
</pre>
</div>
Have a look at Change-Detection-Caveats Vue cannot detect property addition or deletion if you use "normal assign" methods.
You must use Vue.set(object, key, value)
Try something like the following:
axios.get('/api/news', item.id).then(function(response){
if (response){
let item = {}
Vue.set(item, 'data', response.data.item)
}
});
Than item.data would than be reactiv.
Can simply use Vue.set to update this.item reactively
axios.get('/api/news', item.id).then(function(response){
if (response){
this.$set(this, "item", response.data.item);
}
});

get selected object from options loop

I'm trying to find a way to bind array of objects within Vue select-element. The case is somewhat as follows:
data: {
ideas: [
{ id: 1, code: "01A", text: "option 1", props: [] },
{ id: 2, code: "02A", text: "option 2 , props: [{ details: "something" }]}
]},
currentForm: {
something: "foo",
else: "bar",
ideaCode: "01A",
text: "option 1"
}
];
... and in HTML ...
<select v-model="currentForm.ideaCode" #change="setCodeAndLabelForForm(???)">
<option v-for="i in ideas" value="i">{{ i.text }}<option>
</select>
Basically I need to be able to track which object user selects, trigger my own change-event, all the while having binding with a single key from another object... selected value / reference-key should be separated from user-selected option/object. Note: currentForm is not same object-type as option! It only contains some of those properties which option happens to have, and which I'm trying to transfer to options by triggering change-event for user-selection.
The problem is I haven't figured out how to pass currently selected value for the function OR how to write something like:
<select v-model="selectedIdea" #change="setCodeAndLabelForForm" :track-by="currentForm.ideaCode">
<option v-for="i in ideas" value="i">{{ i.text }}<option>
</select>
One possible (and working) approach is:
<select v-model="currentForm.ideaCode" #change="setCodeAndLabelForForm">
<option v-for="i in ideas" value="i.ideaCode">{{ i.text }}<option>
</select>
setCodeAndLabelForForm: function() {
var me = this;
this.ideas.forEach(function(i) {
if(i.ideaCode == me.currentForm.ideaCode) {
me.currentForm.ideaCode = i.selectedIdea.ideaCode;
me.currentForm.text = i.text;
... do stuff & run callbacks ...
}
});
}
... but it just seems terrible. Any better suggestions?
You can implement like this:
Create empty object data to track the selected value:
currentForm: {}
Watch currentForm on the model and pass the selected object:
<select v-model="currentForm" #change="setCodeAndLabelForForm(currentForm)">
Pass in the selected value in option: (you were doing right in this step, but I just changed i to idea as it's little confusing looping index)
<option v-for="idea in ideas" :value="idea">{{ idea.text }}<option>
Apply your method:
setCodeAndLabelForForm(selected) {
// Now, you have the user selected object
}
A little bit better workaround: use index in v-for
<select v-model="selIdeaIndex" #change="setCodeAndLabelForForm">
<option v-for="(i,idx) in ideas" value="idx">{{ i.text }}<option>
</select>
For the js:
data: {
selIdeaIndex:null,
ideas: [
{ id: 1, code: "01A", text: "option 1", props: [] },
{ id: 2, code: "02A", text: "option 2 , props: [{ details: "something" }]}
]
},
methods:{
setCodeAndLabelForForm: function() {
var selIdea = this.ideas[this.selIdeaIndex];
//Do whatever you wanna do with this selIdea.
}
}
I don't know if this is the best solution, but I solve this problem using computed properties like this:
In the JavaScript file (ES6):
data () {
return {
options: [
{ id: 1, text: "option 1" },
{ id: 2, text: "option 2" }
],
selectedOptionId: 1
}
},
computed: {
selectedOption () {
return _.find(this.options, (option) => {
return option.id === this.selectedOptionId
});
}
}
In the HTML file:
<select v-model="selectedOptionId">
<option v-for="option in options" :value="option.id" :key="option.id">{{ option.text }}<option>
</select>
The '_' symbol is a common JavaScript library called Lodash and I highly recommend the usage. It can make you save some precious time.
If you know your options will only come from that v-for="i in ideas" then the <option> indexes will be the same as the item indexes.
Thus <select>.selectedIndex will be the index of the selected this.item.
new Vue({
el: '#app',
data: {
ideas: [
{ id: 1, code: "01A", text: "option 1", props: [] },
{ id: 2, code: "02A", text: "option 2" , props: [{ details: "something" }]}
],
currentForm: {ideaCode: "01A", text: "option 1"}
},
methods: {
setCodeAndLabelForForm: function(selectedIndex) {
var selectedIdea = this.ideas[selectedIndex];
this.currentForm = {ideaCode: selectedIdea.code, text: selectedIdea.text};
}
}
})
<script src="https://unpkg.com/vue#2.5.13/dist/vue.js"></script>
<div id="app">
<select v-model="currentForm.ideaCode" #change="setCodeAndLabelForForm($event.target.selectedIndex)">
<option v-for="i in ideas" :value="i.code">{{ i.text }}</option>
</select>
<br> currentForm: {{ currentForm }}
</div>
Differences from yours: #change="setCodeAndLabelForForm($event.target.selectedIndex)" and the setCodeAndLabelForForm implementation.
A humble way is using $ref.
There is a solution using $ref and #change.
Vue.js get selected options' raw object
It's been a while since I asked this question and there have been good suggestions for handling this situation. I cannot remember the exact business-case presented here, but just by a quick glance it looks like I couldn't figure out how to set/initialize right selection afterwards, because handling just the #change event is childs play -- it's the pairing of one single value against list of object-based-options which is harder. What I was most likely looking for was something AngularJS used to have (track-by -property, which matches any given value against selected-option).
Personally now-a-days I would separate UI-logics instead of trying to force 'em to blend together. Most viable approach for myself would be handling list of options and the selected option as one logical area (ie. data: { list: [...options], selectedIdea: Object }) and separate the "currentForm"-object from selection. Let's break this out:
selectedIdea is something which needs to trigger change into currentForm-object. It's not any kind of hybrid-model, it's just a plain object, one of the available selections, pure and simple.
... and once again: Whenever selectedValue === one of the options, the select-dropdown is automatically set to right selection.
"currentForm"-object has a property which can be used to set the selectedIdea. In this case it's the "ideaCode". This ideaCode doesn't automatically do any pairing or such Component logic needs to represent the rules, which trigger selecting the correct option, which matches "ideaCode".
Just an extra-though: selectedIdea and currentForm-object are two different logical elements. They could be even separated to different components if one would want do, and in some cases it's really good thing to separate 'em.
So by these statements I guess I would change my select's v-model to be exactly what it's supposed to be: One of the selected objects (change v-model="currentForm.ideaCode" into v-model="selectedIdea" or such). Then I would simply add watcher for that selectedIdea and make any alterations to currentForm-object from there.
How about initializing that option by currentForm.ideaCode ? Do one of the following on create-method:
Iterate list of available options. When you find option where currentForm.ideaCode == option.code => this.selectedIdea = option
... or use ecmascript find-method to do the same
... or use underscore/lodash find-method to do the same
Another way would be by using computed value, as suggested Augusto Escobar. Also $ref would work, as suggested by feng zhang, but this approach would still require solution for initializing correct option afterwards (when loading editor with initial values). Thanks to Bhojendra Rauniyar as well -- you were right all along, but I just couldn't comprehend the answer as I couldn't have figured out how to backtrack initial selection.
Thanks for all the suggestions over the year!

How do you properly clear Aurelia binding of a complex object?

Background: I'm trying to create a form using Aurelia. I have a person object that I would like to be able to fill in data for. If the user knows some identifying information about the person's family, they can enter it in an input and a select box will be displayed to allow the user to select the individual from that family for this particular form. The form will then fill in any information it knows about that individual into input fields allowing the user to overwrite any of the information if necessary. The form also allows them to clear the selected person if they want to choose another one.
Most of the functionality seems to work as expected, but when I try to allow the user to clear out the selected person, I'm seeing some behavior that I wouldn't have expected.
I have created a GistRun. The bottom pane is working as I would expect, after the user gets data, selects a person and then clears their selection, they are provided with the select element again. If you uncomment the input element, you will see that the user now has to click the clear action twice before they see the select element again. Why?
How can I update the application so that the user will only need to clear out the person once and the select box will appear again to allow the user to make another selection?
If you have an Aurelia application, you should be able to reproduce this by replacing the app.html with the following:
<template>
<select value.bind="val2" if.bind="opts2 && !val2">
<option repeat.for="opt of opts2" model.bind="opt">${opt.firstName}</option>
</select>
<div if.bind="!opts2 || val2">
<span>${val2.firstName}</span>
<button click.delegate="clearVal2()" if.bind="val2">Clear</button>
</div>
<button click.delegate="getOpts2()">Get</button>
<div>${val2.blah}</div>
<!--<input type="text" value.bind="val2.blah"/>-->
</template>
An the app.js with this:
export class App {
opts2;
val2;
getOpts2(){
this.opts2 = [
undefined,
{
blah: 1,
firstName: 'foo',
address: {
line1: '123 Main St.'
}
},
{
blah: 2,
firstName: 'bar',
address: {
line1: '456 Other Wy.'
}
}
];
}
clearVal2(){
this.val2 = null;
}
}
Any help would be greatly appreciated. Thanks!
UPDATE
If I put the input in a custom element and bind to that, things seem to work as expected. The values that I'm putting into my form though aren't in one location that I could utilize a custom element for. I have updated the Gist with an example.
How can I achieve the same functionality without the need for a custom element?
In all honesty I'm not sure why, but if you add if.bind="val2"on the input element, it clears the value and the select button returns.
<input type="text" if.bind="val2" value.bind="val2.blah"/>
Hope this (slightly) helps
Give that you are allowing the user to either select a value from the list or create a completely new entry, I would tend towards separating the value selected in the list and the data backing up the text boxes. Whenever the value of the select changes, I would set the value of the object backing the text boxes to the value of the select. The way I chose to do this in my sample code is to use the observable decorator on the value the select is bound to.
Here's an example: https://gist.run?id=e4b594eaa452b47d9b3984e7f9b04109
app.html
<template>
<div>
<select value.bind="val" if.bind="opts && !val">
<option repeat.for="opt of opts" model.bind="opt">${opt.firstName}</option>
</select>
<button click.delegate="getOpts()">Get</button>
</div>
<div if.bind="!opts || person">
<span>First Name: ${person.firstName}</span>
<button click.delegate="resetForm()" if.bind="val">Clear Selection</button>
</div>
Address: <input type="text" value.bind="person.address.line1" />
<hr />
val
<pre><code>
${toJSON(val)}
</code></pre>
person
<pre><code>
${toJSON(person)}
</code></pre>
</template>
app.js
import {observable} from 'aurelia-framework';
export class App {
#observable val = null;
person = {};
getOpts(){
this.opts = [
null,
{
blah: 1,
firstName: 'foo',
address: {
line1: '123 Main St.'
}
},
{
blah: 2,
firstName: 'bar',
address: {
line1: '456 Other Wy.'
}
}
];
}
valChanged() {
this.person = this.val;
console.log("set person");
}
resetForm(){
this.val = null;
console.log("reset val");
}
toJSON(value) {
if(!(value === false) && !value) {
return '';
}
return JSON.stringify(value);
}
}
You can see something interesting is happening when I reset the form. Aurelia is creating the properties necessary for bindings to person (namely person.address.line1 when we set person = null. But it doesn't create a firstName property, b/c that property isn't being bound until person stops being falsey.
Another option here is to simply use the with attribute to scope the input.
https://gist.run/?id=7b9d230f7d3c6dc8c13cefdd7be50c7f
<template>
<template with.bind="val.address">
<input value.bind="line1" />
</template>
</template>
Although I agree that mixing the logic of selections and inputs like that is probably not the best idea :)