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:
parent
a06824aef6
commit
c0757d1d44
|
@ -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.
|
||||||
|
|
|
@ -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();
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue