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?

4 Answers 4

26

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.
Sign up to request clarification or add additional context in comments.

5 Comments

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.
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?
"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, 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.
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.
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 => {
  (...)
});

7 Comments

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.
Just a thought: why don't you call the emit method with thé select one of your component? ;-)
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.
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';
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
|
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

Comments

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.

9 Comments

Ok, I will look at ngOnChanges, but please include some example code also if you want your answer to get accepted ;)
Check updated question, can't seem to get it to detect anything after the first go.
Hey Gunter, what is this line - changes: {[propName: string]: SimpleChange. How does myProp get bound to propName
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.
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.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.