24

I've got a custom select component which sets the model when you click on a li item, but since I manually call this.modelChange.next(this.model) every time I change the model it's quite messy and repeatable which I want to avoid.

So my question is if there's something similar to $scope.$watch where I can watch if a value changes and then call this.modelChange.next(this.model) each time it happens.

I've been reading about Observables but I can't figure out how this is supposed to be used for just a simple value since all examples I see are with async requests to external api:s.

Surely there must be a more simple way to achieve this?

(Not that I can't use ngModelChanges since I'm not actually using an input for this).

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

@Component({
  selector: 'form-select',
  templateUrl: './app/assets/scripts/modules/form-controls/form-select/form-select.component.html',
  styleUrls: ['./app/assets/scripts/modules/form-controls/form-select/form-select.component.css'],
  inputs: [
    'options',
    'callback',
    'model',
    'label'
  ]
})

export class FormSelectComponent implements OnInit, OnChanges {
  @Input() model: any;
  @Output() modelChange: EventEmitter = new EventEmitter();

  public isOpen: boolean = false;

  ngOnInit() {

    if (!this.model) {
      this.model = {};
    }

    for (var option of this.options) {

      if (option.model == (this.model.model || this.model)) {
        this.model = option;

      }
    }
  }

  ngOnChanges(changes: {[model: any]: SimpleChange}) {
    console.log(changes);
    this.modelChange.next(changes['model'].currentValue);
  }

  toggle() {
    this.isOpen = !this.isOpen;
  }

  close() {
    this.isOpen = false;
  }

  select(option, callback) {
    this.model = option;
    this.close();

    callback ? callback() : false;
  }

  isSelected(option) {
    return option.model === this.model.model;
  }
}

Edit: I tried using ngOnChanges (see updated code above), but it only runs once on initialization then it doesn't detect changes. Is there something wrong?

Chrillewoodz
  • 22,596
  • 18
  • 73
  • 147

4 Answers4

25

So my question is if there's something similar to $scope.$watch where I can watch if the value of input property model changes

If model is a JavaScript primitive type (Number, String, Boolean), then you can implement ngOnChanges() to be notified of changes. See cookbook example and lifecycle doc, OnChanges section.
Another option is to use a setter and a getter. See cookbook example.

If model is a JavaScript reference type (Array, Object, Date, etc.), then how you detect changes depends on how the model changes:

  • If the model reference changes (i.e., you assign a new array, or a new object, etc.), you can implement ngOnChanges() to be notified of changes, just like for primitive types.
  • However, if the model reference doesn't change, but some property of the model changes (e.g., the value of an array item changes, or an array item is added or removed, or if an object property value changes), you can implement ngDoCheck() to implement your own change detection logic.
    See lifecycle doc, DoCheck section.
agilob
  • 5,517
  • 2
  • 31
  • 43
Mark Rajcok
  • 348,511
  • 112
  • 482
  • 482
  • Set/get doesn't work it just throws a whole bunch of errors, ngOnChanges doesn't work because I change the value inside the component and then two way bind it to the parent component. ngDoCheck() seems like overkill since I just need to know if it has changed, I don't need any specific logic inside it. What I would really like is to use an Observer, but I'm losing my hair over how to implement it. Can you show me how to achieve this with an observer? Using my code, not some random example that's completely different to my use case. I'd appreciate it like mad. – Chrillewoodz Mar 28 '16 at 15:19
  • Regarding ngDoCheck(), Angular can't magically know the structure of our ReferenceTypes, so it is not overkill if we have to write our own change detection logic for them. (This is discussed in the Lifecycle doc I referenced.) Since `model` is an input property, I would use ngDoCheck() instead of an observable. Do you have a simple plunker that demonstrates just the non-working piece? – Mark Rajcok Mar 28 '16 at 15:28
  • "Since model is an input property, I would use ngDoCheck() instead of an observable." Why is this? What exactly should you use observables for other than http requests? I got it working with doCheck by checking against the previous model, or it ended up as an infinite loop. – Chrillewoodz Mar 28 '16 at 15:39
  • @Chrillewoodz, if you want to manage your data through a service (e.g., the cookbook example uses a service to enable bi-directional communication between multiple components), use Subjects and Observables. If you just have a single parent-to-child relationship, I tend to like input and output properties. Of course these are just some general guidelines, and it depends on the application. – Mark Rajcok Mar 28 '16 at 15:56
  • Ok, at the moment I'm just building reusable components so trying to make them as generic and customizable as possible. And as efficient as possible as well, so perhaps DoCheck is the cleanest solution after all. – Chrillewoodz Mar 28 '16 at 16:04
3

If you select custom component internally uses form input(s), I would leverage the ngModelChange event on it / them:

<select [ngModel]="..." (ngModelChange)="triggerUpdate($event)">…</select>

or the valueChanges observable of corresponding controls if any. In the template:

<select [ngFormControl]="myForm.controls.selectCtrl">…</select>

In the component:

myForm.controls.selectCtrl.valueChanges.subscribe(newValue => {
  (...)
});
alehro
  • 2,131
  • 2
  • 24
  • 38
Thierry Templier
  • 182,931
  • 35
  • 372
  • 339
  • Mmh, this is how I would go about it for actual form elements. But it doesn't work for my custom select that uses ul/li elements, so I think an observable solution is the way to go. If only I could understand how they work, lol. – Chrillewoodz Mar 28 '16 at 13:13
  • Just a thought: why don't you call the emit method with thé select one of your component? ;-) – Thierry Templier Mar 28 '16 at 13:17
  • Because it's not the only place where the model changes, it can change 3 different ways. That's how I initially did it, but then realised the model also had to update when first recieving an existing value, and also if there is no value and the model has to be set to an empty object. – Chrillewoodz Mar 28 '16 at 13:21
  • Plat I see. If this model is purely internally you could use a TypeScript setter: "set model(newModelValue) { ... }". This setter will be called each time you assigning a value to model: this.model= 'something'; – Thierry Templier Mar 28 '16 at 13:26
  • When I add a setter then it causes this.model to always be undefined for some reason.. Seems like it doesn't like having an @Input() model as well as a set model – Chrillewoodz Mar 28 '16 at 13:37
  • Yes because we need now to have to rename the internal property used. Something like _model and set it in the setter. A getter can be also implement to return this internal value. It's because setter and will "simulate" the property but it no lingerie really existe un your class... – Thierry Templier Mar 28 '16 at 13:41
  • Is "internal property" the same as the @Input() one? Because I need to keep the "external" model in sync with the internal one at all times. – Chrillewoodz Mar 28 '16 at 13:49
1

ngAfterViewChecked is a better lifecycle method as it gets called after the DOM is rendered and any manipulation can be done here.

https://stackoverflow.com/a/35493028/6522875

Community
  • 1
  • 1
0

You can either make model a getter/setter or implement OnChanges by adding an ngOnChanges(changes) {} method which is called every time after an @Input() value has changed.

The ngOnChanges() example from the docs (linked above):

@Component({
  selector: 'my-cmp',
  template: `<p>myProp = {{myProp}}</p>`
})
class MyComponent implements OnChanges {
  @Input() myProp: any;
  ngOnChanges(changes: {[propName: string]: SimpleChange}) {
    console.log('ngOnChanges - myProp = ' + changes['myProp'].currentValue);
  }
}
@Component({
  selector: 'app',
  template: `
    <button (click)="value = value + 1">Change MyComponent</button>
    <my-cmp [my-prop]="value"></my-cmp>`,
  directives: [MyComponent]
})
export class App {
  value = 0;
}
bootstrap(App).catch(err => console.error(err));

Update

If the internal state of the model changes but not the model itself (different model instance) then change detection doesn't recognize it. You need to implement your own mechanism to notify interested code, like using an Observable in model that emits an event on change that your component can subscribe to.

Also ngOnChanges() is only called when model is changed by data-binding (when someFieldInParent has changed in <my-comp [model]="someFieldInParent"> and Angular passes the new value to model in MyComponent.

return option.model === this.model.model; doesn't cause ngOnChanges() to be called. For this to work the getter/setter approach is a better fit.

Günter Zöchbauer
  • 490,478
  • 163
  • 1,733
  • 1,404
  • Ok, I will look at ngOnChanges, but please include some example code also if you want your answer to get accepted ;) – Chrillewoodz Mar 28 '16 at 12:47
  • Check updated question, can't seem to get it to detect anything after the first go. – Chrillewoodz Mar 28 '16 at 12:57
  • Hey Gunter, what is this line - changes: {[propName: string]: SimpleChange. How does myProp get bound to propName – shiv Mar 28 '16 at 12:57
  • `ngOnChanges(changes)` is called for all `@Input()`s. `propName` is the name of the property the change record is for. In your case this would be `changes['model']`. See also my updated answer. I haven't seen in your question what kind of value you actually pass to `model`. – Günter Zöchbauer Mar 28 '16 at 13:00
  • Model can be anything but an array or object really, so I put changes: {[model: any].... but the linter says it has to be a string or number. Which seems odd. Do you have any examples on how to implement an observable for this? Because like I said I haven't found any short and to the point articles about it, only 2-5+ pages of absolute nonsense on the topic. – Chrillewoodz Mar 28 '16 at 13:03
  • Here is an example http://stackoverflow.com/a/36020225/217408 (it doesn't have to be a service or `@Injectable()`. You can use `Observable` or `Subject` in any class. – Günter Zöchbauer Mar 28 '16 at 13:06
  • See also http://stackoverflow.com/questions/34376854/delegation-eventemitter-or-observable-in-angular2/35568924 – Günter Zöchbauer Mar 28 '16 at 13:07
  • If the internal state of the model changes but not the model itself (different model instance) then change detection doesn't recognize it. What scenario might this happen? – shiv Mar 28 '16 at 13:11
  • If your `model` is an array and you add/remove/modify an item, or if your `model` is an object, and you change the value of the field of that object. – Günter Zöchbauer Mar 28 '16 at 13:12