I've got a dynamic select list created with vue.js. I want to update a "details" box on the page with data grabbed via an ajax call. The basic idea is here: https://jsfiddle.net/pznej8dz/1/
I don't see why the sf_detail data isn't being updated when the object is updated from the webservice call. Is there a different way this should be done in vue?
Your object references are getting out of sync! Calling getSourceFieldDetails causes field and sf_detail to reference different objects.
The Problem
At the start of the script, an object is created
{
Name: 'Test',
Label: 'Data',
Type: 'Boolean'
};
And that object is given a reference named sf_detail.
In sf_detail_info, field is set equal to the reference named sf_detail
data: {
field: sf_detail
}
But, in getSourceFieldDetails, sf_detail is set to reference a new object. Thus sf_detail references the new object, but field still references the old one.
The Solution
The simplest solution is to never set sf_detail equal to a new object. Instead, update the properties of the existing object. The modified version of getSourceFieldDetails looks like this:
function getSourceFieldDetails(val) {
// this would actually call an ajax endpoint to get this data
console.log(val[0]);
sf_detail.Name = val[0];
sf_detail.Label = val[0] + 'Label',
sf_detail.Type = val[0] + 'DataType'
console.dir(sf_detail);
}
Here is a fork of your fiddle with the change.
Related
I've been trying to figure out how to do this, but can't seem to get it to work. I have created a function that is being called when I click a component using v-on:click. I am also trying to pass in a value that I can then use to access a particular array that is coming in the form of data from a backend.
Here is the function in the component:
<v-card
#click="getContent('topSellers')"
>
I am then passing the 'topSellers' value as an "id" into the function that is being used to get access the exact array that I am looking for:
getContent(id) {
this.accordion = this.data.id;
console.log("data", id);
}
First of all, this.data.topSellers works. And I am seeing that the id value is correct in the console. Obviously, this is not working because id is currently a string, but I don't know how to fix that issue. Any help would be appreciated!
You need this.data[id] instead of this.data.id.
See property accessors on MDN for reference.
data[id] will access the property of data with the name of the value of id, in your case topSellers. So data.topSellers is equivalent to data["topSellers"]
[] allows you to access objects with variables.
It is recommended to handle exceptions because the variable received is not safe.
getContent(id) {
const accordion = this.data[id] || null;
if(accordion){
console.log("data", accordion);
this.accordion = accordion;
}
}
When I started with Vue.js I read about a case where you return a data property with return and sometimes without. I cannot find that article anymore that's why I'm asking here.
That's how I use it today
data: function () {
return {
myData : "data"
}
},
But that's how I see it in documentation very often - don't know the difference anymore:
data: {
myData: "data"
},
https://vuejs.org/2016/02/06/common-gotchas/#Why-does-data-need-to-be-a-function
Why does data need to be a function?
In the basic examples, we declare the data directly as a plain object. This is because we are creating only a single instance with new Vue(). However, when defining a component, data must be declared as a function that returns the initial data object. Why? Because there will be many instances created using the same definition. If we still use a plain object for data, that same object will be shared by reference across all instance created! By providing a data function, every time a new instance is created, we can simply call it to return a fresh copy of the initial data.
I've written a component called Upload which allows users to upload files and then report back with a JSON object with these files. In this particular instance, the Upload component has a parameter which comes from a parent view model:
<upload params="dropzoneId: 'uploadFilesDropzone', postLocation: '/create/upload', uploadedFiles: uploadedFiles"></upload>
The one of importance is called uploadedFiles. The parameter binding here means I can reference params.uploadedFiles on my component and .push() new objects onto it as they get uploaded. The data being passed, also called uploadedFiles, is an observableArray on my parent view model:
var UploadViewModel = function () {
// Files ready to be submitted to the queue.
self.uploadedFiles = ko.observableArray([]);
};
I can indeed confirm that on my component, params.uploadedFiles is an observableArray, as it has a push method. After altering this value on the component, I can console.log() it to see that it has actually changed:
params.uploadedFiles.push(object);
console.log(params.uploadedFiles().length); // was 0, now returns 1
The problem is that this change does not seem to be reflected on my parent viewmodel. self.uploadedFiles() does not change and still reports a length of 0.
No matter if I add a self.uploadedFiles.subscribe(function(newValue) {}); subscription in my parent viewmodel.
No matter if I also add a params.uploadedFiles.valueHasMutated() method onto my component after the change.
How can I get the changes from my array on my component to be reflected in the array on my parent view model?
Why do you create a new observable array when the source already is one? You can't expect a new object to have the same reference as another one: simply pass it to your component viewModel as this.uploads = params.uploads. In the below trimmed-down version of your example, you'll see upon clicking the Add button that both arrays (well the same array referenced in different contexts) stay in sync.
ko.components.register('upload', {
viewModel: function(params) {
this.uploads = params.uploads;
this.addUpload = function() { this.uploads.push('item'); }.bind(this);
},
template: [
'<div><button type="button" data-bind="click: addUpload">Add upload</button>',
'<span data-bind="text: uploads().length + \' - \' + $root.uploads().length"></span></div>'].join('')
});
var app = {
uploads: ko.observableArray([])
};
ko.applyBindings(app);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div data-bind="component: {name: 'upload', params: {uploads: uploads}}"></div>
It is only in case your source array is not observable that things get a little more complicated and you need to have a manual subscription to update the source, eg. you would insert the following in the viewModel:
this.uploads.subscribe(function(newValue) { params.uploads = newValue; });
Additionally the output in the text binding would not be updated for the source because it is not observable. If for some reason that I cannot conceive of you would want to have 2 different observableArrays (1 source & 1 component), you should still be able to do with the line above, but replace the function code with params.uploads(newValue)
The problem may be related to this bug (to be confirmed): https://github.com/knockout/knockout/issues/1863
Edit 1: So this was not a bug. You have to unwrap the raw param to access the original observable. In your case, it would be:
params.$raw.uploadedFiles() //this would give you access to the original observableArray and from there, you can "push", "remove", etc.
The problem is that when you pass a param to a component, it gets wrapped in a computed observable and when you unwrap it, you don't have the original observableArray.
Reference: http://knockoutjs.com/documentation/component-custom-elements.html#advanced-accessing-raw-parameters
While Binding Property that involves Parent --> Child Relation
Use Binding in this way
If You want to bind data to Child Property
data-bind='BindingName : ParentViewmodel.ChildViewModel.ObservableProperty'
Here it seems you want to subscibe to a function when any data is pushed in Array for that you can write subscribe on Length of Observable array which can help you capture event that you want.
This should solve your problem.
My dilemma is that I would like to pass multiple object properties to an iron:router route in Meteor. The reasoning is that I would like to pass it a property to name my url with and a property to find a collection item with. They are completely independent of each other and I can't use the url property because it is not a value in the collection item. This is what I have:
Template.items.events({
'click': function () {
itemName = this.name.replace(/ /g,'')
Router.go('itemDetails', {itemName: itemName})
}
});
The problem is that although the Router handles this fine and sends me to the correct url, I cannot use itemName to find the collection item object that I am looking for (assume this is impossible).
Router.route('/items/:itemName', {
name: 'itemDetails',
data: function() {return Items.findOne({name: this.params.itemName})}
});
The above Router configuration will not return anything because name != this.params.itemName for any object.
I've tried passing the this object, or creating objects with multiple properties, but iron:router won't have it.
Any help is appreciated, thanks.
Edit #1: To help explain the question further, my problem is the same as routing to a page that uses multiple id's in the URL. For example, how would I go about passing properties to iron:router to fill the :_id and :itemId properties?
Router.route('items/:_id/:_itemId', {
name: 'detailDetails',
data: function() {...}
});
Edit #2: What I would like to do specifically is pass two properties to iron:router and have one of them be appended to the URL, and the other be used in the data property of the route to return a collection item. Example:
....
Router.go('itemDetails', {_id: itemBeingPassedId, itemName: nameToBeAppendedToURL})
....
Router.route('/items/:itemName', {
name: 'itemDetails',
data: function(){return Items.findOne(_id)
});
Whenever I try to do that, it says that _id is undefined. So basically, how can I pass a property to data without having it be a part of the URL and using this.params?
Is the question how to pass multiple parameters to Router.go? Just put all of them in the object for the second parameter:
Router.go('itemDetails', {_id: 'foo', '_itemId': bar});
Edit:
Ok, if you want to pass arbitrary values to the url, you can use query paramters:
Router.go('itemDetails', {itemName: 'foo'}, {query: 'id=bar'});
The id will still be in the url though, it will look like this:
http://example.com/items/foo?id=bar
And you can retrieve it like this:
Router.route('/items/:itemName', {
name: 'itemDetails',
data: function(){
return {
item: Items.findOne(this.params.query.id),
itemName: this.params.itemName
};
}
);
I'm currently having problems having the UI refresh when I'm getting new data from the server for a single item which is in an observableArray of wrapper objects which holds an object of several observables.
Consider the following:
var vm = {
....
localEdited: ko.mapping.fromJS(new ItemWrapper(defaultModelSerialised)),
selected: ko.observable(null),
editItem: function(data) {
// clone a temporary copy of data for the dialog when opening (*.localEdited on dialog)
var clonedData = ko.toJS(data);
ko.mapping.fromJS(clonedData, null, this.localEdited);
// selected should now point to the item in the obserable array which will be refreshed
this.selected(data);
// open dialog...
},
submitDialog: function(data) {
// submit data to server...
// (1) commit the data back to UI (new item is return in resp.entity from server)
vm.selected(new ItemWrapper(resp.entity));
// at this point the UI isn't showing the updated value
// (2) however if I do this it reflects the data change in the UI
this.selected().Name("changed"); // updates the UI.
}
Can someone explain why passing in the ItemWrapper into vm.selected isn't updating the UI whereas in (2) it works. I don't want to have to set-up each property like in (2) for every property.
ItemWrapper looks like so:
function PoolWrapper(pool) {
this.Name = ko.observable(pool.Name);
// more properties...
}
OK- the issue is that your clones end up with mapping meta-data on them and eventually this causes recursion when trying calling ko.mapping.fromJS.
The solution is to create your clones using ko.mapping.toJS instead of ko.toJS, so that you get a clean clone (without mapping meta-data).
Here is an updated fiddle: http://jsfiddle.net/rniemeyer/tDDBp/
Something I also stumbled upon today that I thought I'd share:
If you clone using:
var clone = ko.mapping.fromJS(ko.mapping.toJS(itemToClone));
Then the clone will be stripped of any computed observables. They will exist as the last value of the function, but no longer function as a computed observable.
If your item is a complex model with computed observables that you would like to keep on your clone you can do the following:
var clone = ko.mapping.fromJS(ko.mapping.toJS(itemToClone), null, new itemModel());
Where itemModel is your complex model for your item containing your computed observables.