Angular2: Change detection timing for an auto-scro

2019-06-04 07:15发布

问题:

I've been working on a simple auto-scroll directive for chat-display:

@Directive({
    selector: "[autoScroll]"
})
export class AutoScroll {
    @Input() inScrollHeight;
    @Input() inClientHeight;

    @HostBinding("scrollTop") outScrollTop;

    ngOnChanges(changes: {[propName: string]: SimpleChange}) {
        if (changes["inScrollHeight"] || changes["inClientHeight"]) {
            this.scroll();
        }
    };

    scroll() {
        this.outScrollTop = this.inScrollHeight - this.inClientHeight;
    };
}

This directive will work when I've set enableProdMode() and when the ChangeDetectionStrategy is set to default, but when in "dev mode" I get an exception. I can set the ChangeDetectionStrategy to onPush, in that case the exception doesn't occur but the scroll will lag behind.

Is there a way to better structure this code so that Dom will be updated then the Scroll function can be called? I've tried setTimeout() but that makes the delay worse, tried using ChangeDetectorRef and subscribing to the observable to trigger markForCheck(). Using ngAfterViewChecked() causes browser crashes.

@Component({
    selector: "chat-display",
    template: `
            <div class="chat-box" #this [inScrollHeight]="this.scrollHeight" [inClientHeight]="this.clientHeight" autoScroll>
                <p *ngFor="#msg of messages | async | messageFilter:username:inSelectedTarget:inTargetFilter:inDirectionFilter" [ngClass]="msg.type">{{msg.message}}</p>
            </div>
       `,
    styles: [`.whisper {
            color: rosybrown;
        }`],
    directives: [NgClass, AutoScroll],
    pipes: [AsyncPipe, MessageFilterPipe],
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ChatDisplay implements OnInit {

    username: string;
    @Input() inSelectedTarget: string;
    @Input() inTargetFilter: boolean;
    @Input() inDirectionFilter: boolean;

    messages: Observable<ChatType[]>;

    constructor(private socketService_: SocketService, private authService_: AuthService) {
        this.username = this.authService_.username;
    };

    ngOnInit() {
    }

}

This is the the exception that is triggered when in dev mode:

EXCEPTION: Expression 'this.scrollHeight in ChatDisplay@1:40' has changed after it was checked. Previous value: '417'. Current value: '420' in [this.scrollHeight in ChatDisplay@1:40] angular2.dev.js (23083,9)

回答1:

Is there a way to better structure this code so that DOM will be updated then the Scroll function can be called?

The DOM should be updated before ngAfterViewChecked() is called. See if something like this works:

ngOnChanges(changes: {[propName: string]: SimpleChange}) {
    // detect the change here
    if (changes["inScrollHeight"] || changes["inClientHeight"]) {
        this.scrollAfterDomUpdates = true;
    }
};
ngAfterViewChecked() {
    // but scroll here, after the DOM was updated
    if(this.scrollAfterDomUpdates) {
       this.scrollAfterDomUpdates = false;
       this.scroll();
    }
}

If that doesn't work, try wrapping the call to scroll in a setTimeout:

    if(this.scrollAfterDomUpdates) {
       this.scrollAfterDomUpdates = false;
       this.setTimeout( _ => this.scroll());
    }


回答2:

I found one way to solve this, it involves dividing the chat display into two separate components and use content projection. So there is a flow of changes from parent to child, and not having two functionalities in the same component with one triggering changes in the other. I can used the default changeDetectionStrategy without getting exceptions in dev mode.

@Component({
    selector: "chat-display",
    template: `
    <auto-scroll-display>
        <chat-message *ngFor="#chat of chats | async | messageFilter:username:inSelectedTarget:inTargetFilter:inDirectionFilter" [message]="chat.message" [type]="chat.type"></chat-message>
    </auto-scroll-display>
    `,
    directives: [NgClass, AutoScrollComponent, ChatMessageComponent],
    pipes: [AsyncPipe, MessageFilterPipe]
})
export class ChatDisplay implements OnInit { /* unchanged code */ }

The auto-scroll directive is identical to original post, was trying to figure out if there was a way to combine the directive functionality into the component. It's just acting as a container now.

@Component({
    selector: "auto-scroll-display",
    template: `
    <div #this class="chat-box" [inScrollHeight]="this.scrollHeight" [inClientHeight]="this.clientHeight" autoScroll>
        <ng-content></ng-content>
    </div>
    `,
    directives: [AutoscrollDirective]
})
export class AutoScrollComponent{ }

Here's a github link with working code, link.