I'm using KnockoutJS to manage my web front-end. I am writing a CRUD/Admin site, but I am having problems with the selectedOptions binding.
The case:
The view model has form.products.all and form.products.selected observables. Both of these are generated by ko.mapping.
The HTML form has a multi-select of the form:
<select required="" multiple="" data-bind="options: products.all, optionsText: function (item) { return item.value.name.unName(); }, selectedOptions: products.selected" class="form-control">
The HTML form correctly shows all the options.
The HTML form does not show the selected options on load. In particular, if I inspect the viewModel object, I can see that the right objects get loaded into the products.selected array on load. But the multi-select does not select them automatically.
If I select objects in the form and then inspect the products.selected observable, I do see the objects in the array.
If I post the form, the right objects end up in the database, and then end up in the viewModel object on the next page load (so the only part missing in the cycle is actually marking the form based on what is in products.selected.
What am I doing wrong? I've seen conflicting advice, and some of it is outdated, so I'm not sure how to proceed.
The selectedOptions binding works as designed.
Your error is very likely that your selected observable does not contain the identical objects (i.e., references to the objects in all), but merely objects that have equal property values.
Knockout maintains the binding through object identity, it does not make any other comparisons.
Consider this simple example:
var vm = {
products: {
selected: ko.observableArray(),
all: ko.observableArray([
{
value: {
name: {
unName: ko.observable("Foo")
}
}
}, {
value: {
name: {
unName: ko.observable("Bar")
}
}
}
])
}
};
vm.products.selected.push(vm.products.all()[1]);
ko.applyBindings(vm);
pre {
font-size: small;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.0.0/knockout-min.js"></script>
<select required="" multiple="" class="form-control" data-bind="
options: products.all,
optionsText: function (item) {
return item.value.name.unName();
},
selectedOptions: products.selected
"></select>
<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>
Unrelated side-note: Try to avoid defining functions in the binding attribute. It's ugly, inefficient, potentially repetitive and not very idiomatic. Make a corresponding property on the view model, for example like this:
function Product(data) {
ko.utils.extend(this, data);
this.displayName = this.value.name.unName;
}
and
<div data-bind="with: products">
<select required="" multiple="" class="form-control" data-bind="
options: all,
optionsText: 'displayName',
selectedOptions: selected
"></select>
</div>
Related
I am retrieving a list of data from an api and need to fill the specific <select></select> tags, which is associated to a few radio button, with some of the data as <options></options>. The radio buttons waiting for an event (#change/#click) and executing and axios get request. Everthing works fine. I click on a radio button and retrieving the data as response (vue tools also showing the right data) but the <option></option> tags are not updating. Now when I click on another radio button, I am getting again the right data from the api BUT now the <option></option> tags are refreshing with the data from the previous response.
Template
<!-- CREATING 7 RADIO BUTTONS FOR THE CURRENT WEEK FROM MON-SUN -->
<div class="wrapper" v-for="item in inputDetails">
<input :id="'datetime[0]['+item.labelText+']'" type="radio" name="datetime[0][date]" v-model="formData.datetime[0].date" :value="item.inputValue" #change="getTimes" />
</div>
<!-- CREATING THE TIME PICKER -->
<select id="datetime[0][time]" name="datetime[0][time]" v-model="formData.datetime[0].time">
<option selected="selected"></option>
<option v-for="item in selectOptionTimes[0]" :value="item.value">{{ item.label }}</option>
</select>
<!--
2 MORE RADIO BUTTON SECTION AND TIME PICKER SECTIONS WITH DIFFERENT INDEXES
<input id="datetime[1][time]"...
-->
Script
data() {
return {
formData: {
datetime: [
{date: '', time: ''},
{date: '', time: ''},
{date: '', time: ''},
]
}
selectOptionTimes: [],
}
},
methods: {
getTimes: function (current) {
let instanceIndex = current.currentTarget.id.match(/(?<=\[)([0-9])(?=])/g)[0]; // getting the index of the current datetime section
axios.get('/api-url', {
params: {
location_id: this.formData.location_id,
date: current.currentTarget.value
}
}).then(response => {
this.selectOptionTimes[instanceIndex] = response.data;
});
}
}
Does someone know what the problem is here?
You cannot assign a value to an arbitrary index within an empty Array in this way. You must either completely replace the Array with values that hydrate that index, or you must use $set.
So, to recap:
BAD
this.selectOptionTimes[instanceIndex] = response.data
GOOD
this.$set(this.selectOptionTimes, instanceIndex, response.data)
Note though, that this has an unintended consequence. If you have an empty array, and call this.$set on an index greater than 0, the array will be filled with empty values up to your index.
What might make more sense is using an {} instead along with this.$set and looping over the Object.keys instead of the array directly.
Fiddle showing $set on index with an empty array
Fiddle showing Object usage instead
I'm using v-validate with Vue. I'm trying to figure out how to force v-validate to update rules. For example, I have something like this:
<template>
<div v-for="field in fields">
<input :name="field.name" v-validate="field.rules">
</div>
</template>
<script>
export default {
data() {
fields: [
{
name: "city",
rules: {
included: []
}
}
]
}
}
</script>
As you can see, my "included" array is empty on page load. I get the array from an AJAX request, and then I update my data:
this.fields[0].rules.included = cities
But v-validate doesn't seem to acknowledge the newly-added array. It only works if I hardcode the cities into my data. How can I force v-validate to respond to the updated rules?
Vue.js is unable to track updates on nested reference types.
Try:
let fields = [...this.fields]
fields[0].rules = cities
this.fields = fields
Use Vue.set to track changes : https://v2.vuejs.org/v2/guide/reactivity.html
Vue.set(this.fields[0], 'rules', cities);
I am trying to make dynamic select input that populates it's option elements depending on the the response of ajax with the following code. Everything is working fine if the data is hardcoded however when I try to make option dynamic based on the response I don't get reactivity.
In short let's say I have an object {Foo: 'Bar', Lorem: 'Ipsum'} I get
<select>
<option value="Foo">Bar</option>
<option value="Lorem">Ipsum</option>
</select>
My problem is when I want to make the data dynamic using the function below I still get the same option even though I now have {Enet: 'Dolor', Magna: 'Aliqua', mollit: 'Anim'} Is there a better approach to this? Or Am I missing Something.
Function responsible for populating the object:
$.get('/registrar/levels', function (data) {
for (const datum of data) {
addModal.levelFields[datum.name] = datum.id;
}
});
The component:
Vue.component("modal-add-form", {
props: {
formName: String,
formType: [String, Number],
options: Object,
},
template: `<div class="form-group" v-if="formType !== 'select'">
<label :for="formName" v-text="formName"></label>
<input :type="formType" class="form-control" :name="formName" :id="formName" value="" :placeholder="formName">
</div>
<div v-else>
<select :name="formName" class="form-control">
<option v-for="(value, name) in options" :value="value" v-text="name"></option>
</select>
</div>`
});
You shouldn't directly reassign data value because Vue cannot detect property additions and rerender view. You should do it via Vue.set or this.$set like this:
$.get('/registrar/levels', (data) => {
for (const datum of data) {
this.$set(this.addModal.levelFields, datum.name, datum.id)
// addModal.levelFields[datum.name] = datum.id;
}
});
I am using knockout to try to bind data into a dropdown list but for some reason i am only seeing [object][object] instead of the actual value i want to display and not sure what i could be doing wrong. This is what i have so far:
self.views = ko.observableArray();
self.selectedView = ko.observable();
if (views){
for(viewOption = 0; viewOption < views.length; viewOption++){
self.views.push(
new viewModel(views[viewOption])
);
}
}
//Sample data
var sampleData = {
viewers: [
.....
],
views: [
{
vValue: 'View 1'
},
{
vValue: 'View 2'
}
]
};
//HTML
<select data-bind="options: views, value: selectedView"></select>
When i run this i get a dropdown displaying the right count of options but instead of showing View 1 and View 2 it shows [object][object] twice.
When you are using objects in array, you should use optionsText for option label and optionsValue for option value.
var vm = {
myItems: [
{ vValue: 'View 1', id: 1},
{ vValue: 'View 3', id: 3},
{ vValue: 'View 4', id: 4}
],
selected: ko.observable()
};
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<select data-bind="
options: myItems,
optionsText: 'vValue',
optionsValue: 'id',
value:selected" >
</select>
<br/>
Selected: <label data-bind="text: selected"></label>
Since you are supplying an array of complex types to the options binding, Knockout does not know what value you want to use as the "text" of your option item (even though you only have a single name/value pair). In most real-world scenarios, you would have more than just a text value in array of complex types.
You can either use the optionsText binding to instruct Knockout to use your value in the name/value pair of vValue, like this:
<select data-bind="options: views, value: selectedView, optionsText: 'vValue'">
</select>
Another way to handle this is to create the views array in your view model to just be an array of strings, then Knockout knows that the single string value in the array is the value to use as the option's text value.
UPDATE
You can just create a JavaScript array of strings, like this:
self.views = ["View1", "View2"];
Then you can keep your options binding syntax the same, as you do not have to bind to an observable array in Knockout, you can bind to just a plain ol' JavaScript array.
Note - Usually people have an observableArray, because their data is dynamic (either through loading from the server or user interaction), but there is no rule that bound objects must be "observable"; although you will not get two-way binding for something like a text input if you bind to a non-observable.
I have a select dropdown menu which shows a list of objects, with the name property being the text displayed, the id property being the value each option is bound to, and a value: user.id binding which is from a separate property on another object.
<td data-bind=""><select data-bind="options: peopleResponsible, optionsText: 'name', optionsValue: 'id', value: user.id"></select></td>
When I select a new person object from the dropdown list, only the id on the user is being updated. All of the other properties (name, username, age, etc) are not being updated.
What I need for it to do is when a new peopleResponsible option is selected, I want it to copy across all of the properties from that object to the user object.
I have a suspicion that at the moment it currently does not work because the user object itself is not observable, only its properties are. Here is how my data is being mapped:
ko.mapping.fromJS(taskOutlines, {}, mappedTaskOutlines);
Where a TaskOutline contains many Tasks, and each Task contains a single User.
Any ideas?
You can do this:
var vm = {
peopleResponsible: ko.observableArray([{
id: ko.observable(1),
name: ko.observable("p1")
}, {
id: ko.observable(2),
name: ko.observable("p2")
}, {
id: ko.observable(3),
name: ko.observable("p3")
}]),
selectedUser: ko.observable()
}
vm.selectedUser(vm.peopleResponsible()[1]); // pre select a user
ko.applyBindings(vm);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<select data-bind="options: peopleResponsible, optionsText: 'name', value: selectedUser"></select>
<div data-bind="with: selectedUser">
<p>Id:
<label data-bind="text: id"></label>
</p>
<p>Name:
<label data-bind="text: name"></label>
</p>
</div>
When a selection is made that selection will be a reference to the arbitrary object in the observable array, properties and all. This selected object will then be placed in "selectedUser" observable.
So in short, removing the "optionsValue" binding will bind the entire object instead of the id property.
The value binding only sets one observable. Not knowing how things are used, it's hard to say what you should do to get the result you want. One possibility is to make a function that does the copying and subscribe to the user.id.