2
votes

I've got an Angular component which does some fairly heavy calculations upon detecting changes.

@Component (
    selector: 'my-table',
    ... 400+ lines of angular template ...
)
class MyTable implements OnDestroy, AfterContentInit, OnChanges {
    ...
    @override
    ngOnChanges(Map<String, SimpleChange> changes) {
        log.info("ngOnChanges" + changes.keys.toString());
        _buildTableContent();
    }
    ...
}

This works beautifully when all the inputs are String, int, bool; in other words, ngOnChanges only triggers once these properties actually change.

I now need to add a custom renderer for one of the fields in order to render data that is not just a simple String and I do it using

@Input("customRenderer") Function customRenderer;

The customRenderer function will now decide if it should return the value as is or if the value is an object / list, extract certain values out of it and return a readable String instead of just Instance of ___;

As soon as I add this @Input("customRenderer"), ngOnChanges fires the whole time even though that function reference hasn't changed.

enter image description here

Is there a way I can tell Angular to not trigger change detection on certain fields after the initial value is set?

A quick hack would be to just have an if-statement in the ngOnChanges function that checks if customRenderer is the only change, but change detection will continue to trigger which feels inefficient.

Does Angular have a hook I can override that will basically say, if field is customRenderer, do not trigger change detection, otherwise do normal change detection?

Update based on @pankaj-parkar's answer:

@Component (
    selector: 'my-table',
    ... 400+ lines of angular template ...
)
class MyTable implements OnDestroy, AfterContentInit, OnChanges, DoCheck {

    ...

    final ChangeDetectorRef cdr;

    int renderOldValue = 0;
    @Input() int render = 0;

    MyTable(this.cdr);

    @override
    ngOnChanges(Map<String, SimpleChange> changes) {
        log.info("ngOnChanges" + changes.keys.toString());
        _buildTableContent();
    }

    @override
    ngDoCheck() {
        if (renderOldValue != render) {
            cdr.reattach();
            cdr.detectChanges();
            cdr.detach();
            renderOldValue = render;
        }
    }

    @override
    ngAfterContentInit() {

        // detach table from angular change detection;
        cdr.detach();

       ...

    }

    ...

}

Now the idea is to call render++ to manually trigger change detection

    <my-table 
        (change)="change(\$event)"
        (click2)="editResource(\$event)"
        [custom]="['tags', 'attributes']"
        [customRenderer]="customRenderer"
        [data]="data ?? []"
        [debug]="true"
        [editable]="enableQuickEdit ?? false"
        [loading]="loading ?? true"
        [render]="render ?? 0"
        [rowDropdownItems]="rowDropdownItems"
        [tableDropdownItems]="tableDropdownItems ?? []">
        <column *ngFor="let column of visibleColumns ?? []"
            [editable]="column.editable"
            [field]="column.field"
            [title]="column.title">
        </column>
    </my-table>

Doesn't make a difference though ...

2
I'd guess that change detection falsely recognizes the same function reference as two different functions (perhaps a bug in Angular or Dart with function equality). You could try to wrap the function with a wrapper class and check if you get the desired behavior this way. Your question doesn't contain enough information to reproduce (where does customRenderer come from). You could also just skip the expensive calculation, if the value didn't actualky change.Günter Zöchbauer
I ended up wrapping it in a map and that seems to work correctly. See my answer.Jan Vladimir Mostert
Great. Have you considered creating a bug report? I think change detection should be able to recognize if a function reference has changed. What I can imagine is happening, is that each time the function is compared a new tearof is created and that 2 tearroffs even from the same function are not considered equal. Perhaps just assigning the funtion to a class-level field and returning this instance could fix your issue.Günter Zöchbauer
Yes, will do so as soon as I get time to do so, probably tomorrow :-)Jan Vladimir Mostert

2 Answers

3
votes

For such cases, you should consider detach your component from change detector tree (you could do it inside ngOnInit hook or use ChangeDetectionStrategy.Detach in component metadata) and add add ngDoCheck life cycle hook which fires for every CD.

MyTable(this.cd);

ngOnInit() {
   cd.detach();
}

In that hook you will be specifically checking your conditionSo whenever condition gets satisfied attach your change detector and detach it once again. So the next change detection cycle will take care of updating your component binding and calling ngOnChanges method.

ngDoCheck() {
    if(model.value == 'specific value') {
       cd.reattach();
       cd.detectChanges(); //run cd once again for enabled component
       cd.detach(); //detached component
       //or
       //cd.markForCheck();
    }
}
1
votes

Found a workaround for now that works. By wrapping these functions inside a map, change detection seems to behave correctly:

final Map renderers = {
    "tags": (List<TagVO> tags) {
        final List<String> tagStrings = [];
        tags.forEach((tag) => tagStrings.add(tag.name));
        return tagStrings.join(", ");
    },
    "attributes": (List<Attribute> attributes) {
        return "ATTRIBUTES!!!";
    }
};

And passing it in just works for some reason:

<my-table 
    ...
    [render]="render ?? 0"
    [renderers]="renderers"
    ...
    <column *ngFor="let column of visibleColumns ?? []"
        [editable]="column.editable"
        [field]="column.field"
        [title]="column.title">
    </column>
</my-table>

And the @Input:

@Input("renderers") Map<String, Function> renderers;

Will see if I can reproduce the issue at some point in a standalone project and log a bug for it.