23

First I've created a User class:

export class User {
  name: string;
  email: string;
}

Then I've got my CoreComponent which uses the FormInputComponent as well as creating a public user from the User class:

import {Component} from 'angular2/core';
import {FormInputComponent} from '../form-controls/form-input/form-input.component';
import {User} from '../core/user';

@Component({
  selector: 'core-app',
  templateUrl: './app/assets/scripts/modules/core/core.component.html',
  styleUrls: ['./app/assets/scripts/modules/core/core.component.css'],
  directives: [FormInputComponent]
})

export class CoreComponent {
  public user: User = {
    name: '',
    email: ''
  }
}

Then I've created an input component, which is a re-useable input component that will take a model value as an input and when changes are made export the new value so that CoreComponent can update the model with the new value:

import {Component, Input, Output, EventEmitter, DoCheck} from 'angular2/core';

@Component({
  selector: 'form-input',
  templateUrl: './app/assets/scripts/modules/form-controls/form-input/form-input.component.html',
  styleUrls: ['./app/assets/scripts/modules/form-controls/form-input/form-input.component.css'],
  inputs: [
    'model',
    'type',
    'alt',
    'placeholder',
    'name',
    'label'
  ]
})

export class FormInputComponent implements DoCheck {
  @Input() model: string;
  @Output() modelExport: EventEmitter = new EventEmitter();

  ngDoCheck() {
    this.modelExport.next(this.model);
  }
}

The CoreComponent's template uses two FormInputComponents and passes user.name and user.email as the input for them:

<form-input [model]="user.name" type="text" name="test" placeholder="This is a test" alt="A test input" label="Name"></form-input>
<form-input [model]="user.email" type="email" name="test" placeholder="This is a test" alt="A test input" label="Email"></form-input>
<pre>{{user.name}}</pre>

The FormInputComponent template:

<div>
  <label attr.for="{{name}}">{{label}}</label>
  <input [(ngModel)]="model" type="{{type}}" placeholder="{{placeholder}}" alt="{{alt}}" id="{{name}}">
</div>
<pre>{{model}}</pre>

Now the problem is that I can only see the changes from the pre element that lies inside the FormInputComponent template, but the parent, CoreComponent's pre element remains unchanged.

I looked at this question which is in the ballpark of what I want to achieve but not quite since using a service for just returning a value up the hierarchy seems like overkill and a bit messy if you have multiple FormInputComponents on the same page.

So my question is simple, how can I pass a model to FormInputComponent and letting it return a new value whenever the value changes so that the public user in CoreComponent changes automatically?

Community
  • 1
  • 1
Chrillewoodz
  • 27,055
  • 21
  • 92
  • 175

5 Answers5

33

To be able to use two way binding short when using your component you need to readme your output property to modelChange:

export class FormInputComponent implements DoCheck {
  @Input() model: string;
  @Output() modelChange: EventEmitter = new EventEmitter();

  ngDoCheck() {
    this.modelChange.next(this.model);
  }
}

And use it this way:

<form-input [(model)]="user.name" type="text" name="test" placeholder="This is a test" alt="A test input" label="Name"></form-input>
<form-input [(model)]="user.email" type="email" name="test" placeholder="This is a test" alt="A test input" label="Email"></form-input>
<pre>{{user.name}}</pre>
Thierry Templier
  • 198,364
  • 44
  • 396
  • 360
  • So my solution wasn't working because I didn't have () in [model]? Also do you know a better way of telling when the input changes than using ngDoCheck? I checked ngOnChanges but that only seems to work if binding to objects or something? – Chrillewoodz Mar 27 '16 at 12:13
  • 1
    And also the name of your input ;-) You could leverage the ngModelUpdate event on your input: – Thierry Templier Mar 27 '16 at 12:17
  • Cool it works! Looks like ngDoCheck is horrible for performance, cuz I was suffering from the server disconnecting all the time even from just having 2 inputs.. But once I removed it everything runs smoothly, odd one. Big thanks for your help! – Chrillewoodz Mar 27 '16 at 12:27
  • Yes, the ngDoCheck will dramatically drop performance. See here for my suggestion: http://stackoverflow.com/a/39943368/2966331 – Mario Eis Oct 09 '16 at 12:02
  • 4
    is this naming standard `[..]Change` documented anywhere? – phil294 Feb 23 '17 at 23:23
  • @Blauhirn right here: https://angular.io/docs/ts/latest/guide/template-syntax.html#!#two-way – ssmith Jun 08 '17 at 04:04
27

As an addition: Using ngDoCheck to emit model changes will DRASTICALLY affect performance as the new value gets emited on every check cycle, no matter if it was changed or not. And that is really often! (Try a console.log!)

So what I like to do to get a comparable convenience, but without the side effects, is something like this:

private currentSelectedItem: MachineItem;
@Output() selectedItemChange: EventEmitter<MachineItem> = new EventEmitter<MachineItem>();

@Input() set selectedItem(machineItem: MachineItem) {
    this.currentSelectedItem = machineItem;
    this.selectedItemChange.emit(machineItem); 
}

get selectedItem(): MachineItem {
    return this.currentSelectedItem; 
}

And use it like

<admin-item-list [(selectedItem)]="selectedItem"></admin-item-list>

You can also emit the new value where it is actually changed. But I find it quite convenient to do that gloabaly in a setter method and don't have to bother when I bind it directly in my view.

Mario Eis
  • 2,724
  • 31
  • 32
  • Does this change get captured by Angular's change detection? – Anshul Oct 15 '16 at 08:17
  • 4
    And how does the container element know to listen on selectedItemChange? Is that a convention in Angular? That you can add "Change" to the end of the property name and it will automatically map the property to that output? – Anshul Oct 15 '16 at 09:34
  • 4
    Yes, thats am official, documented and stable angular2 convention. It's how angular2 two-way binding works. You can read more here: https://angular.io/docs/ts/latest/guide/template-syntax.html#!#ngModel under "Inside [(ngModel)]" – Mario Eis Oct 16 '16 at 11:23
  • To your first question (hope I understood that one right): it does not change the value or emit a value on everey change detection cycle. Thats the clue. It only sets and emit, when the value really gets set. This is achieved by using TypeScript accessors (www.typescriptlang.org/docs/handbook/classes.html under "Accessors") annotated by @Input(). Like auto properties in C# with an implemented getter. – Mario Eis Oct 16 '16 at 11:34
  • Ah I see as it's stated in the docs: "[(ngModel)] is a specific example of a more general pattern in which Angular "de-sugars" the [(x)] syntax into an x input property for property binding and an xChange output property for event binding." Thanks for that reference. I've always thought that may be the case but good to see proper documentation. – Anshul Oct 16 '16 at 23:33
  • This looks great but it didn't work for me in Angular 2.3. I had to remove the () in [(selectedItem)]="selectedItem" and even then it only fired the setter on the initial load. – Chris Apr 03 '17 at 20:55
  • Why do you emit machineItem when you just received it as input? Isn't that circular? – cs_pupil Jan 29 '21 at 19:37
  • 1
    @cs_pupil that is a two-way binding ;) more or less it is circular. but only updates when the value changes (aka "is not equal") – Mario Eis Jan 31 '21 at 16:42
8

This was too long to add as a comment to Thierry's answer...

Use emit() instead of next() (which is deprecated), and only call emit(newValue) when the value of model changes. Most likely you want something like this in your FormInputComponent template:

<input [ngModel]="model" (ngModelChange)="onChanges($event)">

Then in your FormInputComponent logic:

onChanges(newValue) {
  this.model = newValue;
  this.changeModel.emit(newValue);
}
Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • Where did you read that next() is deprecated? I haven't seen that anywhere, and the docs are incomplete so can't really tell what is there still and what isn't. – Chrillewoodz Mar 29 '16 at 07:48
  • @Chrillewoodz, someone else on SO mentioned it, but it is also in the source code: https://github.com/angular/angular/blob/2.0.0-beta.12/modules/angular2/src/facade/async.ts#L116 – Mark Rajcok Mar 29 '16 at 14:15
  • @Chrillewoodz, I believe the reason they deprecated next() is because they don't want EventEmitter to look like an Observable. See also http://stackoverflow.com/questions/36076700/what-is-the-proper-use-of-an-eventemitter – Mark Rajcok Mar 29 '16 at 17:01
3

I found the other answers to be a bit confusing so I boiled everything down to what I think is a very simple answer with a Stack Blitz example.

This example requires only 2 simple components, app.component & test.component. The real magic only requires appending "Change" to the name of the Output decorator. Just use the exact name as the @Input decorator and add "Change" to the end of it. In this example the @Input name is "testVal" and the @Output name is "testValChange", it's Angular syntactical sugar, or magic :-) to the laymen.

The following is the complete app.component.ts file,

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

@Component({
  selector: "my-app",
  template: `

  {{testVal}} <-- this value is printed by app.component

  <test-component [(testVal)]="testVal"></test-component>

  `
})
export class AppComponent {
  @Input() testVal = "My Greeting";

}

And the following is the complete test.component file,

import { Component, Input, Output, EventEmitter } from "@angular/core";

@Component({
  selector: "test-component",
  template: `
  <br /><br />
  <input type="button" (click)="hi()" value="update greeting" /> <-- this button in test.component updates the greeting above using two way binding.
  `
})
export class TestComponent {
  @Input() testVal;
  @Output() testValChange = new EventEmitter<string>();
  increment = 0;

  hi(){
    this.testVal = 'Hey There!';
    this.testValChange.emit(this.testVal + String(this.increment));
    this.increment++;
  }
}

Cheers!

user3777549
  • 419
  • 5
  • 8
0

Having a similar issue now, but one additional layer: a component, which contains a child component it passes a data-object to, which contains another child-component with an input-field it passes the same data-object to. I want to edit the input-field and pass the data back up 2 layers to the parent component. Not able to achieve that for some reason...

I added a stackblitz: https://stackblitz.com/edit/angular-8oh2fm

Vortilion
  • 406
  • 2
  • 6
  • 24