Jan Vladimir Mostert
Jan Vladimir Mostert

Reputation: 12972

Force Angular to not do change detection on certain @Input

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 ...

Upvotes: 2

Views: 2201

Answers (2)

Jan Vladimir Mostert
Jan Vladimir Mostert

Reputation: 12972

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.

Upvotes: 1

Pankaj Parkar
Pankaj Parkar

Reputation: 136124

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();
    }
}

Upvotes: 3

Related Questions