Accessing a component viewmodel from its containing viewmodel

Viewed 2526

I'm experiencing with knockout.js components and require.js. This is working well so far, but I'm struggling with the following.

Let's say I have one instance of my component in a very simple html page :

<div id="exams">
    <databound-exam-control></databound-exam-control>
</div>

From the containing viewmodel :

require(['knockout', 'viewModel', 'domReady!'], function (ko, viewModel) {
    ko.components.register('databound-exam-control', {
        viewModel: { require: 'databound-exam-control-viewmodel' },
        template: { require: 'text!databound-exam-control-view.html' }
    });

    ko.applyBindings(new viewModel());
});

I would like to get the child viewmodel content to later save all the data of the page when I click a button.

For now I just trying to display the display the parent/child viewmodels in a pre tag :

<div>
    <pre data-bind="text: childViewModel()"></pre>
</div>

With the help of the containing viewmodel :

function childViewModel() {
        var model = ko.dataFor($('databound-exam-control').get(0).firstChild);
        return ko.toJSON(model, null, 2);
};

I get the following error during the call to ko.dataFor, probably because the the page is not completely rendered :

Cannot write a value to a ko.computed unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters.

Is that it ? Or am I completelty missing the point here ?

Any help appreciated.

3 Answers

I had a problem where my parent component needed to have access to a child's view model for sorting. My parent layout code looks something like this

<div class="parent-component">
    <!-- ... -->

    <div data-bind="foreach: { data: itemsSorted() as: 'item' }">
        <child-component params="item: item"></child-component>
    </div>
</div>

The problem is itemsSorted() relies on observables in the child component's view model, e.g.

var ParentViewModel = function(items) {
    var self = this;

    self.items = ko.observable(items);

    self.itemsSorted = ko.pureComputed(function() {
        return self.items().sort(function(a, b) {
            // ERROR here: The timestamp() observable isn't present yet,
            // since ChildViewModel hasn't created the observable
            return a.timestamp() - b.timestamp();
        });
    });
};

var ChildViewModel = function(item) {
    self = this;

    self.timestamp = ko.observable(item.timestamp);
};

Because itemsSorted() doesn't contain view models until the layout is already complete, it's unable to use child observables.


Solution

My solution was to create the child's view model within the parent, and then pass that down through a parameter:

var ParentViewModel = function(items) {
    var self = this;

    self.items = ko.observableArray(items.map(function(item) {
        // Initialize view model in parent to allow for sorting
        return new ChildViewModel(item);
    }));

    self.itemsSorted = ko.pureComputed(function() {
        return self.items().sort(function(a, b) {
            // This works, because ChildViewModel's observables are already instantiated
            return a.timestamp() - b.timestamp();
        });
    });
};

var ChildViewModel = function(item) {
    self = this;

    self.timestamp = ko.observable(item.timestamp);
};

Next, in the HTML, pass the view model to the component

<div class="parent-component">
    <!-- ... -->

    <div data-bind="foreach: { data: itemsSorted(), as: 'viewModel' }">
        <child-component params="viewModel: viewModel"></child-component>
    </div>
</div>

Finally, in the component registration, modify the child-component's view model to use the parent's instantiated version:

ko.components.register('child-component', {
    template: /* ... */,
    viewModel: function(params) {
        // viewModel is already created by parent, pass into component
        return params.viewModel;
    },
});

Now ParentViewModel can create and use ChildViewModel's attributes for sorting without needing to wait for an initial layout. The code in ChildViewModel isn't aware of the difference and doesn't need to be changed. This does couple the child component to the parent which isn't good encapsulation of the component, but it allows for immediate layout and use of child observables by the parent.

This is the way I control my viewmodel component instance from a higher level.

//ViewModel for Component...
function ComponentViewModel(params) {
  const self = this;
  self.Message = ko.observable(params.Message);
};

//Register Component...
ko.components.register("my-component", {
    template: { element: "my-component-template" },
    viewModel: function (params) {
        return params.ViewModel; //Return the instance created on root (or wherever you instace your component)...
    }
}); 

//Principal ViewModel...
function RootViewModel() {
  const self = this;
  self.ComponentViewModel = ko.observable(new ComponentViewModel({
    Message: "Hello from component!" //Params for component go here...
  }));
};

ko.applyBindings(new RootViewModel()  );
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>

<my-component params="ViewModel: ComponentViewModel"> </my-component>

<template id="my-component-template">
  <p data-bind="text: Message"> </p>
</template>

Related