Knockout component updating observable not being subscribed to by parent view model?

1.3k views Asked by At

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?

3

There are 3 answers

2
webketje On

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)

0
Manoj Singh On

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.

0
W3Max On

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