95

I'm using @input to receive a property from parent component in order to activate a CSS class in one of child component's element.

I'm able to receive the property from parent and also activate the class. But this works only once. The property i'm receiving from parent is a boolean data typed and when I set the status of it to false from child component, it does not change in parent.

Plunkr: https://plnkr.co/edit/58xuZ1uzvToPhPtOING2?p=preview

app.ts

import {Component, NgModule} from '@angular/core'
import {BrowserModule} from '@angular/platform-browser'
import { HeaderComponent } from './header';
import { SearchComponent } from './header/search';

@Component({
  selector: 'my-app',
  template: `
    <app-header></app-header>
  `,
})
export class App {
  name:string;
  constructor() {
  }
}

@NgModule({
  imports: [ BrowserModule ],
  declarations: [ App, HeaderComponent, SearchComponent ],
  bootstrap: [ App ]
})
export class AppModule {}

header.ts

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-header',
  template: `<header>
              <app-search [getSearchStatus]="isSearchActive"></app-search>
              <button (click)="handleSearch()">Open Search</button>
            </header>`
})
export class HeaderComponent implements OnInit {
  isSearchActive = false;

  handleSearch() {
    this.isSearchActive = true
    console.log(this.isSearchActive)
  }

  constructor() { }
  ngOnInit() { }
}

header/search.ts

import { Component, OnInit, Input } from '@angular/core';

@Component({
  selector: 'app-search',
  template: `<div id="search" [class.toggled]="getSearchStatus">
              search 
              <button  (click)="getSearchStatus = false" class="close">Close Search</button>
            </div>`
})
export class SearchComponent implements OnInit {
  @Input() getSearchStatus: boolean;

  constructor() { }

  ngOnInit() {

  }
}

Please check the above given plunker. The open search function works only once. After closing the search, it does not trigger again.

Is @input is the proper use case for this scenario? Please help me fix this. (Please update the plunker).

kind user
  • 40,029
  • 7
  • 67
  • 77
Body
  • 3,608
  • 8
  • 42
  • 50

4 Answers4

168

You need to use 2 way data-binding.

@Input() is one way data-binding. to enable 2 way data-binding you need to add an @Output() corresponding to the property, with a "Change" suffix

@Input() getSearchStatus: boolean;
@Output() getSearchStatusChange = new EventEmitter<boolean>();

when you want to publish the change made to your property to the parent, you need to notify the parent with:

this.getSearchStatusChange.emit(newValue)

and in the parent you need to use the banana-in-a-box notation for that property:

[(getSearchStatus)]="myBoundProperty"

you can also bind to the property and trigger a callback when it changes in child:

[getSearchStatus]="myBoundProperty" (getSearchStatusChange)="myCrazyCallback($event)"

see the plnkr

n00dl3
  • 21,213
  • 7
  • 66
  • 76
  • Thank you for your reply. Can you please update my plunker? – Body Jan 04 '17 at 13:32
  • Thank you again. Is this the only way to handle this scenario? Is there any other ways without using input/output combo? – Body Jan 04 '17 at 13:51
  • That's the recommended way, the one which is used for [`[(ngModel)]`](https://angular.io/docs/ts/latest/guide/template-syntax.html#!#ngModel) – n00dl3 Jan 04 '17 at 13:55
  • there are other ways (like injecting parent into child) but they are useful only when you have a child component that couldn't leave without its parent (I think about a custom select/option combo, for example). – n00dl3 Jan 04 '17 at 13:59
  • What about using subscribing to subject instead and calling subject.next(value) ? – Robert Vangor Sep 30 '17 at 11:46
  • 1
    That will add extra work for the same behavior as using `@Output()`. That sounds like reinveting the wheel. @RobertVangor – n00dl3 Sep 30 '17 at 11:53
  • 2
    When using ReactiveForms, even with my child property marked `public`, I could not use the "banana box" approach & got errors that it wasn't a known property of the component ... Binding only to the `(propertyChanged)="setParentProp($event)"` worked perfectly though. – mc01 Dec 22 '17 at 20:36
  • @n00dl3, the above way of updating parent properties causes the below console error: "ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked". Any inputs on how to fix that? – manojadams Jun 15 '18 at 12:49
  • 1
    I can't answer to your question as it would deserve its own post. Your problem is that you are emitting change right after parent's change detection cycle. In other words you have managed to make change detection to trigger some changes which is not allowed by Angular. @manoj – n00dl3 Jun 15 '18 at 16:11
  • Couple of question => 1. is "Change" is mandatory ? 2. I mean Can't I use other suffix? 3. cant I write name like "get_Search_Status_Change" this? – Chandrakant Dec 28 '18 at 07:13
  • 2
    @Chandrakant nope, to use banana box syntax you have to use change suffix. For custom outputs, you can name it whatever you want. Banana box is just a very stupid syntax sugar which many people misuse – Allwe Sep 16 '19 at 11:47
10

Another approach: use rxjs/BehaviorSubject to pass status between different components.
Here's the plunkr.
I name subject with a suffix 'Rxx', so the BehaviorSubject for searchStatus will be searchStatusRxx.

  1. initialize it in parent component like searchStatusRxx = new BehaviorSubject(false);,
  2. pass it to child component using @Input
  3. in child template, you do async pipe.
  4. in both parent and child, you do searchStatusRxx.next(value) to change the latest value.
Timathon
  • 1,049
  • 9
  • 11
4

Edited your code a little bit, it works and looks simplier imo. Tell me if you like it.

https://plnkr.co/edit/oJOjEZfAfx8iKZmzB3NY?p=preview

kind user
  • 40,029
  • 7
  • 67
  • 77
  • Thank you. i want to update the parent property from search component. That's my use case. – Body Jan 04 '17 at 13:49
  • @Body Fine, but you should consider using the way I've modified your buttons and clicking mechanism, together with n00dl3 solution. – kind user Jan 04 '17 at 13:55
2

Yet another way. Plunkr. What we want is a single source of truth. We can put that in child this time.

  • Init in child: searchStatus = false
  • In parent template, get the instance of child as #as or whatever name.
  • Change searchStatus in parent using #as.searchStatus and in child this.searchStatus.
Timathon
  • 1,049
  • 9
  • 11
  • This way is much better and simpler. Is this a valid way to proceed? Why Angular not promoting this method? I'm searching for a solution since a week and everywhere i found input/output cases. – Body Jan 04 '17 at 15:27
  • Let's call this as the "Template reference variables" way. It seems to be simple. But it may be a little bit harder to test and the parent/child components are coupling together which may not be a good thing. Personally, I prefer the BehaviorSubject way, which is easier to test and decouple and can be set as a property of complex object passing between parent and child. A sidenote: angular's eventEmitter is a Subject behind the scenes. – Timathon Jan 04 '17 at 16:08
  • The example above on Plunkr doesn't include a sample child component... – eja May 31 '22 at 13:42