50

I am making a component that contains 3 child components in this way:

<header-component>
<side-component>
<main-component>

The main component contains list of heroes. The header component contains two buttons that are suppose to switch the view on the main component to list or grid view.

The problem I have now is passing data from the header-component to the main component. So when I click grid button the view on the main content should change to grid view , same for the row view.

How can the data be passed between child components in angular 1.5 ?

isherwood
  • 58,414
  • 16
  • 114
  • 157
Beslinda N.
  • 4,808
  • 5
  • 27
  • 33
  • 1
    Absolutely the same. To make it clear, what Subash Selvaraj is suggested is not something I would recommend. Instead I would suggest you to align with Angular 2 component approach and use inputs/outputs approach. If you do so you then will be able to easily migrate to ng2. If you are interested, I can post an answer. Otherwise, you can of course go with custom events, but I think this is not the way. – dfsq Mar 16 '16 at 11:31
  • I am using controllerAs syntax and vm variable, so I don't want to use $scope or $rootscope. I would love to see how you approach this problem. I am assuming a lot of developers will find your answer useful. So please do I am very interested to see how child components can communicate together – Beslinda N. Mar 16 '16 at 11:37

3 Answers3

87

Component approach

I would suggest you to align with Angular 2 component approach and use inputs/outputs approach. If you do so, you will be able to easily migrate to Angular 2, because components will be conceptually identical (with difference only in syntax). So here is the way you do it.

So we basically want header and main components to share piece of state with header to be able to change it. There are several approaches we can use to make it work, but the simplest is to make use of intermediate parent controller property. So let's assume parent controller (or component) defines this view property you want to be used by both header (can read and modify) and main (can read) components.

Header component: input and output.

Here is how simple header component could look like:

.component('headerComponent', {
  template: `
    <h3>Header component</h3>
    <a ng-class="{'btn-primary': $ctrl.view === 'list'}" ng-click="$ctrl.setView('list')">List</a>
    <a ng-class="{'btn-primary': $ctrl.view === 'table'}" ng-click="$ctrl.setView('table')">Table</a>
  `,
  controller: function() {
    this.setView = function(view) {
      this.view = view
      this.onViewChange({$event: {view: view}})
    }
  },
  bindings: {
    view: '<',
    onViewChange: '&'
  }
})

The most important part here is bindings. With view: '<' we specify that header component will be able to read outer something and bind it as view property of the own controller. With onViewChange: '&' components defined outputs: the channel for notifying/updating outer world with whatever it needs. Header component will push some data through this channel, but it doesn't know what parent component will do with it, and it should not care.

So it means that header controller can be used something like

<header-component view="root.view" on-view-change="root.view = $event.view"></header-component> 

Main component: input.

Main component is simpler, it only needs to define input it accepts:

.component('mainComponent', {
  template: `
    <h4>Main component</h4>
    Main view: {{ $ctrl.view }}
  `,
  bindings: {
    view: '<'
  }
})

Parent view

And finally it all wired together:

<header-component view="root.view" on-view-change="root.view = $event.view"></header-component>
<main-component view="root.view"></main-component>

Take a look and play with simple demo.

angular.module('demo', [])

.controller('RootController', function() {
  this.view = 'table'
})

.component('headerComponent', {
  template: `
    <h3>Header component</h3>
    <a class="btn btn-default btn-sm" ng-class="{'btn-primary': $ctrl.view === 'list'}" ng-click="$ctrl.setView('list')">List</a>
    <a class="btn btn-default btn-sm" ng-class="{'btn-primary': $ctrl.view === 'table'}" ng-click="$ctrl.setView('table')">Table</a>
  `,
  controller: function() {
    this.setView = function(view) {
      this.view = view
      this.onViewChange({$event: {view: view}})
    }
  },
  bindings: {
    view: '<',
    onViewChange: '&'
  }
})

.component('mainComponent', {
  template: `
    <h4>Main component</h4>
    Main view: {{ $ctrl.view }}
  `,
  bindings: {
    view: '<'
  }
})
<script src="https://code.angularjs.org/1.5.0/angular.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.css" />

<div class="container" ng-app="demo" ng-controller="RootController as root">
  
    <pre>Root view: {{ root.view }}</pre>
    
    <header-component view="root.view" on-view-change="root.view = $event.view"></header-component>
    <main-component view="root.view"></main-component>
    
</div>

Demo: http://plnkr.co/edit/ODuY5Mp9HhbqA31G4w3t?p=info


Here is a blog post I wrote covering component-based design in details: http://dfsq.info/site/read/angular-components-communication

Ondrej Slinták
  • 31,386
  • 20
  • 94
  • 126
dfsq
  • 191,768
  • 25
  • 236
  • 258
  • 1
    I just wanted to ask one more question. In the list I have sorting, pagination, grouping, and filtering by type. Is it a good practice for all of this things to use the input/output approach. My code gets quite messy with all the binding and alias – Beslinda N. Mar 20 '16 at 20:46
  • 2
    @BesaNeziri You don't have to introduce input/output property for everything. If they logically belong to the same thing you can create single property, like `Input() config` which will be an object like `config.sorting`, `config.pagination`, `config.filter`, etc. So it's less messy. Also for complex situations it makes sense to use common service, like `GridService` in both components, that would handle these things. – dfsq Mar 21 '16 at 08:47
  • This is nice, but is there any reason your `onViewChange` callback passes `{$event: {view: view}}` as opposed to just `view`? – Chris Sep 12 '16 at 10:06
  • 1
    @Chris There is no reason, it's just a preference. I like to group data pushed in callback into `$event`, so that I can easily distinguish them in the HTML and know that it comes from component's `&` binding. But it's not necessary of course. – dfsq Sep 12 '16 at 10:26
  • it seems you can simply use "=" for the view and save all the trouble of outputing ... – MartianMartian Sep 16 '16 at 08:59
  • 1
    @Matian2040 Yes, you can. But two-way binding should be ideally avoided. – dfsq Sep 16 '16 at 09:00
  • also, it seems that ui.route has a better solution than all these output thing...especially for switching views – MartianMartian Sep 16 '16 at 09:01
  • I believe this is the best answer, I have ever seen on StackOverflow. Thanks. – nicolaib Mar 09 '17 at 13:55
  • What does $event has got to do with all this? Is $event an internal angular service or custom variable? I know angular has $event service but it is not supposed to be used like this. – duggi Jun 27 '17 at 16:55
  • Ok, got it, $event is custom variable, see this [link](http://plnkr.co/edit/gZFquqWmxRzeMPgF6xcb?p=preview) I've replaced $event.view with a dumb variable. – duggi Jun 27 '17 at 17:07
  • @duggi Yes, you got it right. Native angular $even is constructed exactly like this. – dfsq Jun 27 '17 at 20:37
  • @dfsq Does the solution work for directives in AngularJS? – Tomasz Waszczyk Feb 09 '18 at 15:32
16

Although the parent component approach (passing down data via attributes) is a perfect valid and yet good implementation, we can achieve the same thing in a simpler way using a store factory.

Basically, data is hold by the Store, which is referenced in both components scope, enabling reactive updates of the UI when the state changes.

Example:

angular
    .module('YourApp')
    // declare the "Store" or whatever name that make sense
    // for you to call it (Model, State, etc.)
    .factory('Store', () => {
        // hold a local copy of the state, setting its defaults
        const state = {
            data: {
              heroes: [],
              viewType: 'grid'
            }
        };
        // expose basic getter and setter methods
        return {
            get() {
                return state.data;
            },
            set(data) {
                Object.assign(state.data, data);
            },
        };
    });

Then, in your components you should have something like:

angular
    .module('YourApp')
    .component('headerComponent', {
        // inject the Store dependency
        controller(Store) {
            // get the store reference and bind it to the scope:
            // now, every change made to the store data will
            // automatically update your component UI
            this.state = Store.get();

            // ... your code
        },
        template: `
            <div ng-show="$ctrl.state.viewType === 'grid'">...</div>
            <div ng-show="$ctrl.state.viewType === 'row'">...</div>
            ...
        `
    })
    .component('mainComponent', {
        // same here, we need to inject the Store
        controller(Store) {
            // callback for the switch view button
            this.switchViewType = (type) => {
                // change the Store data:
                // no need to notify or anything
                Store.set({ viewType: type });
            };

            // ... your code
        },
        template: `
            <button ng-click="$ctrl.switchViewType('grid')">Switch to grid</button>
            <button ng-click="$ctrl.switchViewType('row')">Switch to row</button>
            ...
        `

If you want to see a working example, check out this CodePen.

Doing so you can also enable the communication between 2 or N components. You just only have to:

  1. inject the store dependency
  2. make sure you link the store data to your component scope

like in the example above (<header-component>).

In the real world, a typical application needs to manage a lot of data so make more sense to logically split the data domains in some way. Following the same approach you can add more Store factories. For example, to manage the current logged user information plus an external resource (i.e. catalog) you can build a UserStore plus a CatalogStore -- alternatively UserModel and CatalogModel; those entities would also be good places to centralize things like communication with the back-end, add custom business logic, etc. Data management will be then sole responsibility of the Store factories.

Keep in mind that we're mutating the store data. Whilst this approach is dead simple and clear, it might not scale well because will produce side effects. If you want something more advanced (immutability, pure functions, single state tree, etc.) check out Redux, or if you finally want to switch to Angular 2 take a look at ngrx/store.

Hope this helps! :)

You don't have to do it the Angular 2 way because just in case you would migrate sometimes... Do it if it make sense for you to do it.

Andrea Puddu
  • 718
  • 8
  • 13
  • Very cool! Angularjs with Redux style solution. You can also take a look at ng-redux for similar out of the box solution (https://github.com/angular-redux/ng-redux) – Kfir Erez Nov 19 '16 at 21:09
  • @KfirErez Glad you enjoyed it, thanks! I think this approach can remember in some way Redux but the latter is more explicit because it requires to `dispatch` actions instead of modifying directly the store (also it keeps the whole changes tree). – Andrea Puddu Nov 22 '16 at 11:38
7

Use custom events to achieve this. you can pass message across your application using event dispatchers $emit(name, args); or $broadcast(name, args); And you can listen for this events using method $on(name, listener);

Hope it helps

Ref: https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$emit

Example: you can notify change like below from your header-component

$rootScope.$emit("menu-changed", "list");

And you can listen for the change in your main-component directive like

$rootScope.$on("menu-changed", function(evt, arg){
  console.log(arg);
});
Subash Selvaraj
  • 3,385
  • 1
  • 14
  • 17
  • 1
    I actually don't want to use $scope or $rootscope on this one. Can I make it work using controllerAs syntax and vm variable ? – Beslinda N. Mar 16 '16 at 11:32
  • @BesaNeziri If that is the case, you can use factory to share the data between controllers. Check this link https://thinkster.io/a-better-way-to-learn-angularjs/services – Subash Selvaraj Mar 16 '16 at 11:51
  • 3
    This is bad practice. You should be trying to follow the methods described in the component documentation https://docs.angularjs.org/guide/component – Stevo Oct 04 '16 at 10:00
  • This actually works very well, and is the smallest amount of code. Using `emit` is recommended by other developers: https://stackoverflow.com/q/37291799/584846 and https://velesin.io/2016/05/18/communication-between-angular-1-5-components/ – Brent Washburne Jul 27 '17 at 17:34
  • This would work much better and create better isolated components IF event subscription wasn't directly in the component itself but 1 layer up. That way the component itself is isolated and more reusable. It's the event subscription layer that then becomes app specific which is better than the components themselves. – user441521 Sep 26 '17 at 18:57