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

Removing ViewRef causes error in Ivy #38201

Closed
waterplea opened this issue Jul 23, 2020 · 7 comments
Closed

Removing ViewRef causes error in Ivy #38201

waterplea opened this issue Jul 23, 2020 · 7 comments

Comments

@waterplea
Copy link
Contributor

@waterplea waterplea commented Jul 23, 2020

🐞 bug report

Affected Package

The issue is caused by package @angular/core

Is this a regression?

Yes, the previous version in which this bug was not present was: 8

Description

I have a dropdown directive that creates a new view in a ViewContainerRef of dropdowns host component. If that new view also has this directive it can create another view. In ngOnDestroy of that directive I remove this view from ViewContainerRef.

When parent dropdown is removed, I get an error in Ivy on destruction of nested directive created view.

Seems like destruction of first view causes destruction of the second view in sync and when I attempt to remove second view I get error, because length is already updated as if parent view is removed (length is 1):

get length(): number {
return this._lContainer.length - CONTAINER_HEADER_OFFSET;
}

However both views are still there in indexOf:

indexOf(viewRef: viewEngine_ViewRef): number {
const viewRefsArr = this._lContainer[VIEW_REFS];
return viewRefsArr !== null ? viewRefsArr.indexOf(viewRef) : -1;
}

So nested view reports being at index of 1 and this assertion fails thinking there is only index of 0 because it thinks length is 1:

private _adjustIndex(index?: number, shift: number = 0) {
if (index == null) {
return this.length + shift;
}
if (ngDevMode) {
assertGreaterThan(index, -1, `ViewRef index must be positive, got ${index}`);
// +1 because it's legal to insert at the end.
assertLessThan(index, this.length + 1 + shift, 'index');
}
return index;
}

🔬 Minimal Reproduction

Toggle parent, then toggle child, then toggle parent again to have them both removed
https://stackblitz.com/edit/angular-ivy-viewref-bug

Here's working example on Angular 8 with ViewEngine:
https://stackblitz.com/edit/angular-ve-viewref

🔥 Exception or Error

ERROR
Error: ASSERTION ERROR: index [Expected=> 1 < 1 <=Actual]

🌍 Your Environment

Angular Version:

10.0.5
@ngbot ngbot bot added this to the needsTriage milestone Jul 23, 2020
@ngbot ngbot bot added this to the needsTriage milestone Jul 23, 2020
@Achilles1515
Copy link

@Achilles1515 Achilles1515 commented Jul 24, 2020

Nice one.

Looks like the array item removal being done on...

// view_engine_compatibility.ts
// ViewContainerRef
remove(index?: number): void {
        this.allocateContainerIfNeeded();
        const adjustedIdx = this._adjustIndex(index, -1);
        removeView(this._lContainer, adjustedIdx);
        removeFromArray(this._lContainer[VIEW_REFS]!, adjustedIdx); // <--- this line
      }

...needs to be occurring...

// node_manipulation.ts
export function removeView(lContainer: LContainer, removeIndex: number) {
  const detachedView = detachView(lContainer, removeIndex);
  // <--- here
  detachedView && destroyLView(detachedView[TVIEW], detachedView);
}

As a workaround, you can just call destroy() on the EmbeddedViewRef itself.

export class DropdownDirective implements OnInit, OnDestroy {
  // ...
  ngOnDestroy() {
    // this.host.remove(this.ref);
    this.ref.destroy();
  }
}

@waterplea
Copy link
Contributor Author

@waterplea waterplea commented Jul 27, 2020

Thanks, @Achilles1515 don't I need to remove it though? What happens to the destroyed but not removed view? Will that also work for ComponentRefs hostView? I checked it logging length of ViewContainerRef and it seems to be a valid workaround.

@pkozlowski-opensource
Copy link
Member

@pkozlowski-opensource pkozlowski-opensource commented Jul 27, 2020

@waterplea this looks like a valid bug in ivy indeed! I wonder if you would like to send a PR correcting this issue, given that you did quite a bit of digging into it? I can help with the process getting PR to the "land-able" state. If not I can fix this issue, please let me know what works best for you.

@ngbot ngbot bot removed this from the needsTriage milestone Jul 27, 2020
@ngbot ngbot bot added this to the Backlog milestone Jul 27, 2020
@ngbot ngbot bot removed this from the needsTriage milestone Jul 27, 2020
@ngbot ngbot bot added this to the Backlog milestone Jul 27, 2020
@Achilles1515
Copy link

@Achilles1515 Achilles1515 commented Jul 27, 2020

Thanks, @Achilles1515 don't I need to remove it though? What happens to the destroyed but not removed view? Will that also work for ComponentRefs hostView? I checked it logging length of ViewContainerRef and it seems to be a valid workaround.

Calling ViewRef.destroy() does remove it from a ViewContainerRef, if it is attached to one. It first detaches it, then destroys it.

See here

  destroy(): void {
    if (this._appRef) {
      this._appRef.detachView(this);
    } else if (this._viewContainerRef) {
      const index = this._viewContainerRef.indexOf(this);

      if (index > -1) {
        this._viewContainerRef.detach(index);
      }

      this._viewContainerRef = null;
    }
    destroyLView(this._lView[TVIEW], this._lView);
  }

It should work for EmbeddedViewRefs and ComponentRef's host view, as they are both a ViewRef.

@Achilles1515
Copy link

@Achilles1515 Achilles1515 commented Jul 27, 2020

@pkozlowski-opensource

One route would be to just remove the removeView function entirely (I think it is only being called from ViewContainerRef.remove()), and just inline the detaching and destruction (similar to what is done in the snippet above from ViewRef.destroy()).

export function removeView(lContainer: LContainer, removeIndex: number) {
const detachedView = detachView(lContainer, removeIndex);
detachedView && destroyLView(detachedView[TVIEW], detachedView);
}

Doing so would make the ViewContainerRef.remove() method this to fix the bug (imports would also need to be updated):

      remove(index?: number): void {
        this.allocateContainerIfNeeded();
        const adjustedIdx = this._adjustIndex(index, -1);
        const detachedView = detachView(this._lContainer, adjustedIdx);
        if (detachedView) {
          // Before destroying the view, remove it from the container's array of `ViewRef`s.
          // This ensures the view container length is updated before calling
          // `destroyLView`, which could recursively call view container methods that
          // rely on an accurate container length.
          // (e.g. a method on this view container being called by a child directive's OnDestroy lifecycle hook)
          removeFromArray(this._lContainer[VIEW_REFS]!, adjustedIdx);
          destroyLView(detachedView[TVIEW], detachedView);
        }
      }

As referenced in my first comment, another option is to move the

removeFromArray(this._lContainer[VIEW_REFS]!, adjustedIdx);

call into the removeView function, but I don't know if you guys would rather keep those removeFromArray calls directly in the ViewContainerRef class for visibility.

@waterplea
Copy link
Contributor Author

@waterplea waterplea commented Jul 27, 2020

I think removing function should be fine. I'll look into it more later, @pkozlowski-opensource I will try to make a PR.

atscott added a commit that referenced this issue Aug 13, 2020
 1�738317)

When removal of one view causes removal of another one from the same
ViewContainerRef it triggers an error with views length calculation. This commit
fixes this bug by removing a view from the list of available views before invoking
actual view removal (which might be recursive and relies on the length of the list
of available views).

Fixes #38201.

PR Close #38317
@atscott atscott closed this in b071495 Aug 13, 2020
profanis added a commit to profanis/angular that referenced this issue Sep 5, 2020
…ngular#38317)

When removal of one view causes removal of another one from the same
ViewContainerRef it triggers an error with views length calculation. This commit
fixes this bug by removing a view from the list of available views before invoking
actual view removal (which might be recursive and relies on the length of the list
of available views).

Fixes angular#38201.

PR Close angular#38317
@angular-automatic-lock-bot
Copy link

@angular-automatic-lock-bot angular-automatic-lock-bot bot commented Sep 13, 2020

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 Sep 13, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
4 participants