Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refresh not done properly when triggered by a binding in a directive #47594

Closed
divdavem opened this issue Sep 30, 2022 · 2 comments
Closed

Refresh not done properly when triggered by a binding in a directive #47594

divdavem opened this issue Sep 30, 2022 · 2 comments

Comments

@divdavem
Copy link
Contributor

divdavem commented Sep 30, 2022

Which @angular/* package(s) are the source of the bug?

core

Is this a regression?

No

Description

Here is a component:

@Component({
  selector: 'app-root',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <button type="button" (click)="d.toggle()">Toggle</button>
    <button type="button" (click)="state = !state">Toggle by binding</button>
    <ul class="mt-2">
      <li>state = {{ state }}</li>
      <li>d.state = {{ d.stateChange | async }}</li>
      <li>processing = {{ d.processing | async }}</li>
    </ul>

    <div #d="myDirective" [(myDirective)]="state"></div>
  `,
})
export class AppComponent {
  state = false;
}

It uses the following directive:

import { Directive, Input, Output } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

export const createModel = () => {
  const state$ = new BehaviorSubject(true);
  const processing$ = new BehaviorSubject(false);
  const toggle = async (newState = !state$.getValue()): Promise<void> => {
    console.log('Beginning toggle, newState = ' + newState);
    // Note that uncommenting the following line is a workaround for this issue:
    // await Promise.resolve();
    processing$.next(true);
    state$.next(newState);
    await new Promise((resolve) => setTimeout(resolve, 1000));
    processing$.next(false);
    console.log('Ending toggle, newState = ' + newState);
  };

  return {
    state$,
    processing$,
    toggle,
  };
};

@Directive({
  selector: '[myDirective]',
  exportAs: 'myDirective',
})
export class MyDirective {
  private model = createModel();

  @Input('myDirective')
  set state(value: boolean) {
    this.model.toggle(value);
  }

  @Output('myDirectiveChange')
  stateChange = this.model.state$;

  processing = this.model.processing$;

  toggle() {
    return this.model.toggle();
  }
}

When clicking on the "Toggle" button in the component, everything works as expected:

  • The first two lines state and d.state have the same boolean value (the opposite of the previous value), and the third line has processing = true
  • After 1s, the third line switches back to processing = false

When clicking on the "Toggle by binding" button, the same toggle method from the model is triggered, so the same behavior as before would be expected. However, Angular only refreshes the first line (state) and does not update the d.state and processing lines, even though they changed. After 1s, the second line (d.state) is updated with the correct value and the third line did not change at all (now that the value of processing is back to false).

Please provide a link to a minimal reproduction of the bug

https://stackblitz.com/edit/angular-ivy-liytcm?file=src/app/app.component.ts

Please provide the exception or error you saw

There is no error in the console.

Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 14.0.7
Node: 16.17.1
Package Manager: npm 8.15.0 
OS: linux x64

Angular: 14.2.4
... animations, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1402.4
@angular-devkit/build-angular   14.2.4
@angular-devkit/core            14.2.4
@angular-devkit/schematics      14.0.7
@angular/cli                    14.0.7
@schematics/angular             14.0.7
rxjs                            7.5.7
typescript                      4.7.4

Anything else?

No response

@JoostK
Copy link
Member

JoostK commented Sep 30, 2022

This is working as expected, except for #45612 where no error is thrown because this is an OnPush component.

The state changes that occur from within the directive after its input binding changes happen after the change detector has already updated the DOM with the values, given that those values occur in the view before assigning the directive input.

This is a typical ExpressionHasChangedAfterItHasBeenChecked error, which would surface if the component is not OnPush. It's generally bad practice to emit into an output when assigning an input, as doing so breaks unidirectional dataflow. This could be masked by making the EventEmitter asynchronous (by passing true as first constructor argument) but that will trigger another round of change detection.

Closing as this is working as expected and #45612 is tracking the issue for OnPush components.

@JoostK JoostK closed this as not planned Won't fix, can't repro, duplicate, stale Sep 30, 2022
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Oct 31, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants