Send data from one Angular component to another

Posted on by

Kind of complicated task is to send data from one component or controller in Angular to another. It’s not so hard to share the data between different controllers using services. But, what if you would like to be able to update data from outside of the controller as well?

Let’s say we have a simple application. We are displaying image thumbnails, and each thumbnail is an Angular component. The application also has a separate component named preview. It supposed to present the information of the chosen thumbnail.

Our goal is to pass image object from the thumbnail component to the preview and update it on click.

Solutions

There are plenty of solutions. The most widely used method is to use Angular watchers and broadcast the changes. We also could use callbacks. You can check dtheodor’s solution at StackOverflow. In our case it will look something like that:


// image.service.js
  export default function imageService() {
    let observerCallbacks = [];
    // Register an observer
    this.registerObserverCallback = (callback) => {
        observerCallbacks.push(callback);
        console.log(observerCallbacks);
    };
    // Call this when `image` is changed
    let notifyObservers = () => {
        angular.forEach(observerCallbacks, (callback) => {
            callback();
        });
    };

    this.image = {};
    this.set = (image) => {
        this.image = image;
        notifyObservers();
    };
}

Then inside our thumbnail component, we are setting the new image as:


// thumbnail.component.js
export default {
  ...
  controller: thumbnailCtrl($element, imageService) {
    this.$postLink = () => {
      $element.on("click", () => {
        // Set image and notify observers
        imageService.set(this.image);
      });
    };
  }
}

After that, let’s register a callback in a preview. It should be executed each time we set a new image.


// preview.component.js
export default {
    ...
    controller: function previewCtrl(imageService, $scope) {
        this.$onInit = () => {
            this.updateImage = () => {
                this.image = imageService.image;
                /** 
                 * That's a drawback of this solution,
                 * read below.
                 */
                $scope.$apply();
            };
            getImage.registerObserverCallback(this.updateImage);
        };
    },
}

The only thing is that the preview component won’t automatically update its expressions. I’ve spent some time to figure it out when tried this solution for the first time. To achieve that you have to use Angular’s $apply() method.

As an alternative, we could even create a new preview component each time a user clicks on the thumbnail and pass image object as an attribute via bindings.

Better alternative

I like the other solution a lot. Using the promises instead of passing callbacks. Stackoverflow’s user Krym suggested using Angular’s $q service for that purpose. This service helps you run functions asynchronously and use their return values when they did the processing. You can find additional info about $q in Angular’s official documentation.

So, in this post, I will demonstrate you how to use promises to achieve our goal.

First of all, we need to create our image service and inject $q and create a new instance of a deferred object.


// image.service.js
export default function imageService($q) {
    let defer = $q.defer();
    this.image = {};

    this.observeImage = () => {
        return defer.promise;
    };

    this.setImage = (image) => {
        this.image = image;
        // Provides updates on the status of the promise's execution
        defer.notify(this.image);
    };
}

No changes needed for the thumbnail component. Just run imageService.setImage() method to update the image and the status of the promise’s execution.

The only thing left is to use the promise API to call the callback asynchronously as soon as the result is available. We will use .this method only to notify via the return of the value of the notifyCallback method. The code will look like that:


// preview.component.js
export default {
    ...
    controller: function previewCtrl(getImage) {
        this.$onInit = () => {
            getImage.observeImage().then(null, null, (image) => {
                this.image = image;
            });
        };
    },
}

That’s it, the simple and elegant solution to watch service variables via promises.