0
votes

I am new to KnockoutJS. I have an app that works in a following way:

  1. when page is loaded a complex data structure is passed to frontend
  2. this datastructure is splitted into smaller chunks and these chunks of data are passed to components
  3. user interacts with components to edit chunks of data
  4. upon clicking a button updated complex data structure should be passed to backend

I have troubles with a fourth step. I've been reading throught documentation and yet I couldn't figure out how I am supposed to get updated data.

Here is a JSFiddle: https://jsfiddle.net/vrggyf45

Here is the same code in snippet. See the bottom of the question for what I've tried.

ko.components.register('firstComponent', {
  viewModel: function(params) {
    var self = this;
    self.firstComponentValue = ko.observable(params.firstComponentValue);
  },
  template: '<div><pre data-bind="text: JSON.stringify(ko.toJS($data), null, 2)"></pre><input data-bind="value: firstComponentValue, valueUpdate: \'afterkeydown\'"></div>'
});

ko.components.register('secondComponent', {
  viewModel: function(params) {
    var self = this;
    self.secondComponentValue = ko.observable(params.secondComponentValue);
  },
  template: '<div><pre data-bind="text: JSON.stringify(ko.toJS($data), null, 2)"></pre><input data-bind="value: secondComponentValue, valueUpdate: \'afterkeydown\'"></div>'
});


var json = '{"items":[{"componentName":"firstComponent","firstComponentValue":"somevalue"},{"componentName":"secondComponent","secondComponentValue":"someothervalue"}]}';
var data = JSON.parse(json);
var mainVM = {};
mainVM.items = ko.observableArray(
  ko.utils.arrayMap(data.items,
    function(item) {
      return ko.observable(item);
    }));

ko.applyBindings(mainVM);

$('input[type=button]').click(function() {
  var updatedData = ko.dataFor(document.getElementById('main'));
  //get updated json somehow?
  console.log(data);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div id="main">
  <pre data-bind="text: JSON.stringify(ko.toJS($data), null, 2)"></pre>
  <div data-bind="foreach: {data: items, as: 'item'}">
    <hr>
    <div data-bind="component: {name:componentName, params: item}">
    </div>
  </div>
  <hr>
  <input type="button" value="post data">
</div>

If I had a single view model I could have just used ko.dataFor($("#rootElement")) and send it to backend. However I intend to use components and they have their own viewmodels, which are not connected to the root viewmodel. I could have find them all with jQuery and use ko.dataFor but it looks like a big hack to me. I also could have define all the viewmodels, including the components in the main viewmodel, but it makes components kind of useless.

Also I tried to change components viewmodels constructors so they would mutate input data and override incomming values with observables, but it seems like a hack to me as well.

Is there a function like ko.components or something that could give me all living viewmodels?

1

1 Answers

1
votes

You can pass observables to the components so that their edits are immediately reflected in the parent object. ko.mapping is handy for converting plain JS objects to observables. Then when you want the data back, ko.toJS() it.

ko.components.register('firstComponent', {
  viewModel: function(params) {
    var self = this;
    self.firstComponentValue = params.firstComponentValue;
  },
  template: '<div><pre data-bind="text: JSON.stringify(ko.toJS($data), null, 2)"></pre><input data-bind="value: firstComponentValue, valueUpdate: \'afterkeydown\'"></div>'
});

ko.components.register('secondComponent', {
  viewModel: function(params) {
    var self = this;
    self.secondComponentValue = params.secondComponentValue;
  },
  template: '<div><pre data-bind="text: JSON.stringify(ko.toJS($data), null, 2)"></pre><input data-bind="value: secondComponentValue, valueUpdate: \'afterkeydown\'"></div>'
});


var json = '{"items":[{"componentName":"firstComponent","firstComponentValue":"somevalue"},{"componentName":"secondComponent","secondComponentValue":"someothervalue"}]}';
var data = JSON.parse(json);
var mainVM = {};
mainVM.items = ko.mapping.fromJS(data.items);

ko.applyBindings(mainVM);

$('input[type=button]').click(function() {
  var updatedData = ko.toJS(ko.dataFor(document.getElementById('main')));
  //Or, more simply: updatedData = ko.toJS(mainVM);
  console.log(updatedData);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div id="main">
  <pre data-bind="text: JSON.stringify(ko.toJS($data), null, 2)"></pre>
  <div data-bind="foreach: {data: items, as: 'item'}">
    <hr>
    <div data-bind="component: {name:componentName, params: item}">
    </div>
  </div>
  <hr>
  <input type="button" value="post data">
</div>