24

in the new project I'm working on I've started using the components instead of directives.

however, I've encountered an issue where I cannot find a concrete standard way to do it.

It's easy to notify an event from child to parent, you can find it on my plunkr below, but what's the correct way to notify a event from parent to child?

Angular2 seems to solve this issue by using something like this: https://angular.io/docs/ts/latest/cookbook/component-communication.html#!#parent-to-child-local-var But I don't tink there's a possibilty to define a "pointer" to the child component like the example did with #timer

In order to mantain a possible easy conversion to Angular2 I want to avoid:

  • event emitting (emit and broadcast from the scopes)
  • using the require from the child (and then add a callback to the parent..UGLY)
  • using a one-way binding, injecting the scope in the child and then "watch" this property.. MORE UGLY

Example code:

var app = angular.module('plunker', []);

app.controller('RootController', function() {
});

app.component('parentComponent', {
  template: `
    <h3>Parent component</h3>
    <a class="btn btn-default btn-sm" ng-click="$ctrl.click()">Notify Child</a>
    <span data-ng-bind="$ctrl.childMessage"></span>
    <child-component on-change="$ctrl.notifiedFromChild(count)"></child-component>
  `,
  controller: function() {
    var ctrl = this;
    ctrl.notifiedFromChild = function(count){
      ctrl.childMessage = "From child " + count;
    }
    ctrl.click = function(){
    }
  },
  bindings: {
  }
});

app.component('childComponent', {
  template: `
    <h4>Child component</h4>
    <a class="btn btn-default btn-sm" ng-click="$ctrl.click()">Notify Parent</a>
  `,
  controller: function() {
    var ctrl = this;
    ctrl.counter = 0;
    ctrl.click = function(){
        ctrl.onChange({ count: ++ctrl.counter });
    }
  },
  bindings: {
    onChange: '&'
  }
});

You can find an example here:

http://plnkr.co/edit/SCK8XlYoYCRceCP7q2Rn?p=preview

This is a possible solution I created

http://plnkr.co/edit/OfANmt4zLyPG2SZyVNLr?p=preview

where the child requires the parent, and then child sets a parent reference to the child... now parent can use the child... ugly but it's like angular2 example above

georgeawg
  • 48,608
  • 13
  • 72
  • 95
Luca Trazzi
  • 1,240
  • 1
  • 13
  • 30
  • 1
    What exactly do you want to notify child about? You shouldn't normally need to do it. Most of the cases are covered with bindings, for example: http://plnkr.co/edit/NcbNOAnOqMhxqvvLR74O?p=info Isn't it enough for you? – dfsq May 25 '16 at 14:55
  • 2
    Example, child is a grid, parent has a search button, when I press this button I want to refresh the grid, so I make an ajax call, this cannot be achiieved with a binding, right? – Luca Trazzi May 25 '16 at 15:55
  • It can. You can react to changes in those bindings (without $scope.$watch). Can post an example when I'm back in 2 hours if you didn't get an answer by then. – dfsq May 25 '16 at 16:00
  • Great, will wait for your answer anyway! thanks mate – Luca Trazzi May 25 '16 at 16:06

4 Answers4

31

Communicating Events from Parent to Child in AngularJS Components

Publish Directive $API Using Expression Binding

To allow parent components to communicate events to a child component, have the child publish an API:

<grid-component grid-on-init="$ctrl.gridApi=$API; $ctrl.someFn($API)">
</grid-component>    

JS

app.component('gridComponent', {
  //Create API binding
  bindings: {gridOnInit: "&"},
  template: `
    <h4>Grid component</h4>
    <p> Save count = {{$ctrl.count}}</p>
  `,
  controller: function() {
    var ctrl = this;
    this.$onInit = function() {
        ctrl.count = 0;
        ctrl.api = {};
        //Publish save function
        ctrl.api.save = save;
        //Invoke Expression with $API as local
        ctrl.gridOnInit({$API: ctrl.api});
    };
    function save(){
      console.log("saved!");
      ctrl.count++;
    }
  }
});

The above example invokes the Angular Expression defined by the grid-on-init attribute with its API exposed as $API. The advantage to this approach is that the parent can react to child initialization by passing a function to the child component with the Angular Expression.

From the Docs:

The 'isolate' scope object hash defines a set of local scope properties derived from attributes on the directive's element. These local properties are useful for aliasing values for templates. The keys in the object hash map to the name of the property on the isolate scope; the values define how the property is bound to the parent scope, via matching attributes on the directive's element:

  • & or &attr - provides a way to execute an expression in the context of the parent scope. If no attr name is specified then the attribute name is assumed to be the same as the local name. Given <my-component my-attr="count = count + value"> and the isolate scope definition scope: { localFn:'&myAttr' }, the isolate scope property localFn will point to a function wrapper for the count = count + value expression. Often it's desirable to pass data from the isolated scope via an expression to the parent scope. This can be done by passing a map of local variable names and values into the expression wrapper fn. For example, if the expression is increment($amount) then we can specify the amount value by calling the localFn as localFn({$amount: 22}).

-- AngularJS Comprehensive Directive API -- scope

As a convention, I recommend prefixing local variables with $ to distinguish them from parent variables.


Alternately use Bi-Directional Binding

NOTE: To ease the transition to Angular 2+, avoid the use of bi-directional = binding. Instead use one-way < binding and expression & binding. For more information, see AngularJS Developer Guide - Understanding Components.

To allow parent components to communicate events to a child component, have the child publish an API:

<grid-component api="$ctrl.gridApi"></grid-component>

In the above example, the grid-component uses bindings to publish its API onto the parent scope using the api attribute.

app.component('gridComponent', {
  //Create API binding
  bindings: {api: "="},
  template: `
    <h4>Grid component</h4>
    <p> Save count = {{$ctrl.count}}</p>
  `,
  controller: function() {
    var ctrl = this;
    this.$onInit = function() {
        ctrl.count = 0;
        ctrl.api = {};
        //Publish save function
        ctrl.api.save = save;
    };
    function save(){
      console.log("saved!");
      ctrl.count++;
    }
  }
});

Then the parent component can invoke the child save function using the published API:

ctrl.click = function(){
  console.log("Search clicked");
  ctrl.gridApi.save();
}

The DEMO on PLNKR.

Community
  • 1
  • 1
georgeawg
  • 48,608
  • 13
  • 72
  • 95
  • I find the second way much more elegant but is this way not less efficient than the first one as it relies on the two way binding instead a single way binding ? – davidxxx Mar 19 '17 at 16:12
  • Second approach is the correct approach. Instead of calling the binding "api" it's conventional to use "name" (see ngForm). – G-Wiz Nov 06 '17 at 17:01
  • The second approach with two-way, `=`, binding creates more overhead by adding two watchers. The first approach using expression, `&`, binding avoids adding any watchers. – georgeawg Nov 06 '17 at 17:40
  • **NOTE:** To ease the transition to Angular 2+, avoid the use of bi-directional, `=`, binding. Instead use one-way < binding and expression & binding. For more information, see [AngularJS Developer Guide - Understanding Components.](https://docs.angularjs.org/guide/component) – georgeawg Nov 06 '17 at 17:41
  • 1
    Would it be possible to create a PLNKR for the first example also? It would be very helpful. – mareoraft Nov 09 '18 at 13:56
5

Here is an easy way: http://morrisdev.com/2017/03/triggering-events-in-a-child-component-in-angular/

basically, you add a bound variable called "command" (or whatever you want) and use the $onChanges to pay attention to changes of that variable and trigger whatever event it says to trigger manually.

I personally like to put all my variables into an object called "settings" and send that to all my components. However, a change to a value within an object does NOT trigger the $onChanges event, so you NEED to tell it to trigger the event with a flat variable.

I'd say it is not the "proper" way to do it, but it sure is a lot easier to program, a lot easier to understand, and a lot easier to convert to A2 later on down the road.

Daniel Morris
  • 329
  • 3
  • 4
  • This is a good approach. As noted the `$onChanges` life-cycle hook only triggers on changes to the *identity* of an object. The `$doCheck` life-cycle can be used to check for changes to the *contents* of a object. For more information, see [AngularJS Comprehensive Directive API - Life-Cycle Hooks](https://docs.angularjs.org/api/ng/service/$compile#life-cycle-hooks). – georgeawg Sep 25 '17 at 20:36
  • I have to admit, I got it to work with $onChanges, breathed as sigh of relief, and moved onto the next problem. I never tried $doCheck. That sounds like the bow-on-top of this approach. – Daniel Morris Sep 26 '17 at 20:56
2

I faced with same question. What do you think about this approach: to use inheritance via require instead of Bi-Directional Binding?

http://plnkr.co/edit/fD1qho3eoLoEnlvMzzbw?p=preview

var app = angular.module('plunker', []);

    app.controller('RootController', function() {
    });

    app.component('filterComponent', {
      template: `
        <h3>Filter component</h3>
        <a class="btn btn-default btn-sm" ng-click="$ctrl.click()">Search</a>
        <span data-ng-bind="$ctrl.childMessage"></span>

        <grid-component api="$ctrl.gridApi"></grid-component>
      `,
      controller: function() {
        var ctrl = this;

        ctrl.click = function(){
          console.log("Search clicked");
          ctrl.gridApi.save();
        };
      }
    });

    app.component('gridComponent', {
      require: {parent:'^^filterComponent'},
      bindings: {api: "<"},
      template: `
        <h4>Grid component</h4>
        <p> Save count = {{$ctrl.count}}
      `,
      controller: function() {
        var ctrl = this;



        this.$onInit = function() {
            ctrl.count = 0;
            ctrl.api = {};
            ctrl.api.save = save;

            ctrl.parent.gridApi = ctrl.api;
        };
        function save(){
          console.log("saved!");
          ctrl.count++;
        }
      }
    });

Or we can define setter method for parent to make it more explicit.

http://plnkr.co/edit/jmETwGt32BIn3Tl0yDzY?p=preview

var app = angular.module('plunker', []);

app.controller('RootController', function() {
});

app.component('filterComponent', {
  template: `
    <h3>Filter component</h3>
    <a class="btn btn-default btn-sm" ng-click="$ctrl.click()">Search</a>
    <span data-ng-bind="$ctrl.childMessage"></span>

    <grid-component pass-api="$ctrl.setGridApi(api)"></grid-component>
  `,
  controller: function() {
    var ctrl = this;

    var gridApi = {};

    ctrl.setGridApi = function(api){
      gridApi = api;
    };

    ctrl.click = function(){
      console.log("Search clicked");
      gridApi.save();
    };
  }
});

app.component('gridComponent', {
  bindings: {
    passApi:'&'
  },
  template: `
    <h4>Grid component</h4>
    <p> Save count = {{$ctrl.count}}
  `,
  controller: function() {
    var ctrl = this;

    this.$onInit = function() {
        ctrl.count = 0;
        ctrl.api = {};
        ctrl.api.save = save;

        ctrl.passApi({api: ctrl.api});
    };
    function save(){
      console.log("saved!");
      ctrl.count++;
    }
  }
});
ViES
  • 351
  • 3
  • 11
0

SIMPLE: You just need one property 1 way bound because 2 way binding only calls onChanges at creation.

  1. Set a new boolean property on the parent controller.

    vm.changeNow = false; //update this to vm.changeNow = !vm.changeNow when you want to tell the component to //call a method.

  2. Open Child Component, in the bindings section,

    bindings: { a2waybind: '=', changenow: '<' }

  3. You now need an $onChanges event on the child.

    $onChanges() { // do that sweet stuff you wanted to listen from the parent for. }

  4. Now when calling the template:

    childComponent a2waybind="$ctrl.mycoolvalue" changenow="$ctrl.changeNow" /childComponent"

A second way to do this is in your child component:

        var vm = this;
        var myprop;
        Object.defineProperty(vm, 'mytwowayprop', {
            get() {
                return myprop;
            },
            set(value) {
                myprop = value; 
                vm.onchangeseventbecausemypropchanged();               
            }
        });
       vm.onchangeseventbecausemypropchanged = function () {//woot}

This will allow you to have a defined onchanges event when your two way binding property changes both internally and externally.

Patrick Knott
  • 1,666
  • 15
  • 15