fix(ivy): attached flag not being reset when view is destroyed (#29064)

Currently we only reset the `Attached` flag of a view if it is detached through its parent, however this means that if a root view is destroyed, its flag will never be reset. This manifested itself in one of the Material tests where we were destroying the root view.

This PR resolves FW-1130.

PR Close #29064
This commit is contained in:
Kristiyan Kostadinov 2019-03-01 22:05:49 +01:00 committed by Andrew Kushnir
parent a06824aef6
commit c0757d1d44
3 changed files with 41 additions and 5 deletions

View File

@ -426,6 +426,10 @@ function cleanUpView(viewOrContainer: LView | LContainer): void {
if ((viewOrContainer as LView).length >= HEADER_OFFSET) { if ((viewOrContainer as LView).length >= HEADER_OFFSET) {
const view = viewOrContainer as LView; const view = viewOrContainer as LView;
// Usually the Attached flag is removed when the view is detached from its parent, however
// if it's a root view, the flag won't be unset hence why we're also removing on destroy.
view[FLAGS] &= ~LViewFlags.Attached;
// Mark the LView as destroyed *before* executing the onDestroy hooks. An onDestroy hook // Mark the LView as destroyed *before* executing the onDestroy hooks. An onDestroy hook
// runs arbitrary user code, which could include its own `viewRef.destroy()` (or similar). If // runs arbitrary user code, which could include its own `viewRef.destroy()` (or similar). If
// We don't flag the view as destroyed before the hooks, this could lead to an infinite loop. // We don't flag the view as destroyed before the hooks, this could lead to an infinite loop.

View File

@ -5,7 +5,7 @@
* Use of this source code is governed by an MIT-style license that can be * Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Component, Directive, HostBinding, HostListener, QueryList, ViewChildren} from '@angular/core'; import {Component, Directive, HostBinding, HostListener, Input, QueryList, ViewChildren} from '@angular/core';
import {TestBed} from '@angular/core/testing'; import {TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser'; import {By} from '@angular/platform-browser';
import {expect} from '@angular/platform-browser/testing/src/matchers'; import {expect} from '@angular/platform-browser/testing/src/matchers';
@ -111,4 +111,40 @@ describe('acceptance integration tests', () => {
expect(element.classList.contains('foo')).toBeTruthy(); expect(element.classList.contains('foo')).toBeTruthy();
}); });
it('should not set inputs after destroy', () => {
@Directive({
selector: '[no-assign-after-destroy]',
})
class NoAssignAfterDestroy {
private _isDestroyed = false;
@Input()
get value() { return this._value; }
set value(newValue: any) {
if (this._isDestroyed) {
throw Error('Cannot assign to value after destroy.');
}
this._value = newValue;
}
private _value: any;
ngOnDestroy() { this._isDestroyed = true; }
}
@Component({template: '<div no-assign-after-destroy [value]="directiveValue"></div>'})
class App {
directiveValue = 'initial-value';
}
TestBed.configureTestingModule({declarations: [NoAssignAfterDestroy, App]});
let fixture = TestBed.createComponent(App);
fixture.destroy();
expect(() => {
fixture = TestBed.createComponent(App);
fixture.detectChanges();
}).not.toThrow();
});
}); });

View File

@ -361,10 +361,6 @@ window.testBlocklist = {
"error": "Error: Expected 'none' to be falsy.", "error": "Error: Expected 'none' to be falsy.",
"notes": "Unknown" "notes": "Unknown"
}, },
"MatCalendar calendar with min and max date should update the minDate in the child view if it changed after an interaction": {
"error": "Error: This PortalOutlet has already been disposed",
"notes": "Unknown"
},
"MatTable with basic data source should be able to create a table with the right content and without when row": { "MatTable with basic data source should be able to create a table with the right content and without when row": {
"error": "TypeError: Cannot read property 'querySelectorAll' of null", "error": "TypeError: Cannot read property 'querySelectorAll' of null",
"notes": "Unknown" "notes": "Unknown"