2

I am having an issue with change detection in a dynamically added component in Angular.

I have added a link to a complete Plunker below.

The component has two properties (grid and message), with setters and getters for both properties.

The grid property is based on a "type" interface IGrid, and message is a string. Both grid and message are added to the component instance when the component is dynamically created and added to the parent component.

Setters for both grid and message call a function called consolelog.

Based on changes to their respective inputs, change detection for both grid and message seem to be working.

The issue i am having is that the consolelog function is being correctly called in the message setter, however the consolelog function is not being called correctly in the grid setter.

Template:

<div>
  <div><b>grid.pinnedColumnHeaders</b></div> 
  input: 
  <input type="checkbox" 
    name="grid.pinnedColumnHeaders" 
    [(ngModel)]="grid.pinnedColumnHeaders">
</div>
<div>
  value: {{ grid.pinnedColumnHeaders || '[blank]' }}
</div>
<hr>
<div>
  <div><b>message:</b></div>
  input: <input type="text" name="message" 
  [(ngModel)]="message">
</div>
<div>
  value: {{ message || '[blank]' }}
</div>

Component:

@Component({
  selector: 'app-change-detection-onpush',
  template: `...`,
  changeDetection: ChangeDetectionStrategy.OnPush
})

export class ChangeDetectionOnPushComponent implements IPage {
    private _message: string;
    private _grid: IGrid;

    ngAfterViewInit() {

    }

    constructor(private cdRef: ChangeDetectorRef) { }

    set grid(val: string) {
      this.cdRef.markForCheck();
      this.logconsole();
      this._grid = val;
    }

    get grid() {
      return this._grid;
    }

    set message(val: string) {
      this.cdRef.markForCheck();
      this.logconsole();
      this._message = val;
    }

    get message() {
      return this._message;
    }

   logconsole(){
     console.log('test');
   }
 }    

Complete Plunker: https://next.plnkr.co/edit/t1xK698tsJDnzS5E?open=lib%2Fapp.ts&deferRun=1

2
  • 1
    My guess is that in [(ngModel)] you are pointing to a property of a grid object, so when it changes - setter is not being called, because the object(link) isn't changing, so setter only calls when when the whole new object being passed through @Input. If you need to track changes of checkbox add (change)="handleCheckboxChange($event)" to your html and declare same method in your component. Commented Sep 13, 2018 at 6:28
  • Thanks Maksym! Good idea. I had a similar thought, and that is actually my alternate plan if I cannot get this approach working. My ultimate goal with the approach is to run a function from consolelog that sends the entire grid object to a service where updates to the grid can be accessed globally. I'm also going to have a whole lot of additional properties in addition to pinnedColumnHeaders, so I am trying to find an approach where I can capture all of the changes in one spot. Commented Sep 13, 2018 at 13:06

1 Answer 1

1

According to what you said in comments, I have two more guesses about the approach you probably looking for:

  1. Is the @ngrx/store approach. (it's well documented)
  2. Is to make the grid as a private Subject(or BehaviorSubject from 'rxjs') in your service, and make setter and getter in service.

    import { Injectable } from '@angular/core';
    import { Subject } from 'rxjs';
    
    @Injectable()
      export class MyService{
    
        private grid: Subject<IGrid> = new Subject<IGrid>();
    
        getGrid(): Observable<IGrid> {
          return this.grid.asObservable();
        };
    
        setGrid(values: IGrid): void {
          this.grid.next(values);
        }
    
    }
    

    then in any of your components (note, don't forget to make unsubscribe on ngOnDestroy()):

    export class MyComponent {
      grid: IGrid;
      constructor(private service: Myservice){}
    
      getGrid(): void {
        this.service.getGrid().subscribe((grid: IGrid) => this.grid = grid);
      }
    
      setGrid(gridData: IGrid): void {
        this.service.setGrid(gridData);
      }
    }
    

Still, in this approach, you have to handle input change as suggested above as ngModel won't call the setter if only a part of an object has changed. But in this case you can be sure that if you make changes in one place all the subscribers will receive the changes.

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks again Maksym! I looked at the store approach previously (ngrx/redux), but that approach was ruled out due to architectural concerns (specifically performance and the need to write reducers). My alternate plan is very close to your Observable implementation above, however, I would be using a service that emits a change event that other components could hook into.
@jptrue, what Maksym said in the comments to the question is correct. Change Detection will not fire, because Angular points to the reference of the grid object. Changing the value of a property in the grid object (a state change) does not change the reference to the grid object and therefore Angular change detection will not fire. Both Maksyms, suggestions in this answer are a fine solution to your problem.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.