Search code examples
knockout.jsknockout-2.0knockout-3.0

Get access to data of component from another component


I want to get viewModel of 'first-component' from 'second-component' and synchronize self.prop for both components. I use Knockout library. Here is my code:

ko.components.register('first-component', {
    template: '<strong><span data-bind="text: prop"></span></strong>',
    viewModel: function(params){
        var self = this;

        self.prop = ko.observable(params.initialText);
    }
});

ko.components.register('second-component', {
template: '<div><input type="text" data-bind="value: prop">' 
          + '<button data-bind="click: foo">Click Me</button></div>',
viewModel: function(params){
        var self = this;

        self.prop = ko.observable(params.initialText);
        self.foo = function(){
        // HELP ME! I want to get viewModel of 'first-component' and 
        // synchronize self.prop for both components
            if(ko.components.isRegistered("first-component")){
            //...???

            }
        }
    }
});

Using components in markup:

<first-component params="initialText: 'Some text...'"></first-component>

<second-component params="initialText: 'Another text...'"></second-component>

Solution

  • To facilitate interaction between a components and an external view model, I find the best thing is to pass in an observable as a parameter to manage the synchronization of data. I've set up a JSFiddle example using your code above.

    The first change is to create a parent view model to contain the shared state between the components

    var parentViewModel {
      sharedText: ko.observable('shared text')
    }
    

    Then use the shared text as parameters to the components. For the first component, the initialText is set to the sharedText, while the second component, the sharedText is passed in as a second parameter. This is because the shared text is only updated when the "Click Me" button is clicked. If the sharedText observable was set to the initialText for both, then the update would happen automatically. Sometimes this is what is required, sometimes not.

    <first-component params="initialText: sharedText"></first-component>
    <second-component params="initialText: 'Initial value', sharedText: sharedText"></second-component>
    

    And then when assigning the property in the first component, use the reference from the params.initialText instead of creating new observable. When knockout binds the HTML, it will bind to the parentViewModel.sharedText and automatically update the HTML anytime parentViewModel.sharedText is updated.

    ko.components.register('first-component', {
        template: '<strong><span data-bind="text: prop"></span></strong>', 
    viewModel: function(params){
            var self = this;
            self.prop = params.initialText;
        }
    });
    

    In the second component, a new observable is created using the initial text, and the foo click handler updates the params.sharedText reference using the value from self.props.

    ko.components.register('second-component', {
        template: '<div><input type="text" data-bind="value: prop">' 
          + '<button data-bind="click: foo">Click Me</button></div>',
        viewModel: function(params){
            var self = this;
            self.prop = ko.observable(params.initialText);
            self.foo = function(){
                params.sharedText(self.prop());
            }
        }
    });
    

    This is very similar to pulling up the state in React components, where the components are passed in a set of properties, including a set of callbacks from the parent component, and the callbacks are responsible for updating the shared view model.

    I've also added a third component, because it is very important to dispose of components correctly. See Disposal and memory management in the component documentation for more details. The third component is a counter tracking how many times the sharedText has been updated. It uses a subscription on the knockout observable, and if the component is removed from the DOM, then it needs to dispose of the subscription. If you define a dispose function on the component, knockout will automatically call this method when it removes the component from the DOM.

    ko.components.register('third-component', {
        template: '<div>Counter:&nbsp;<span data-bind="text: count"></span></div>',
      viewModel: function (params) {
         var self = this;
         self.count = ko.observable(0);
         self.sub = params.sharedText.subscribe(function () {
            self.count(self.count() + 1);
         });
         self.dispose = function () {
                self.sub.dispose();
         }
      }
    });