diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 78c1a6da82..9a73023438 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -426,6 +426,10 @@ function cleanUpView(viewOrContainer: LView | LContainer): void { if ((viewOrContainer as LView).length >= HEADER_OFFSET) { 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 // 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. diff --git a/packages/core/test/acceptance/integration_spec.ts b/packages/core/test/acceptance/integration_spec.ts index da36e55537..0360ecb21d 100644 --- a/packages/core/test/acceptance/integration_spec.ts +++ b/packages/core/test/acceptance/integration_spec.ts @@ -5,7 +5,7 @@ * 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 */ -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 {By} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -111,4 +111,40 @@ describe('acceptance integration tests', () => { 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: '
'}) + 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(); + }); + }); diff --git a/tools/material-ci/angular_material_test_blocklist.js b/tools/material-ci/angular_material_test_blocklist.js index 7df0187add..2d8f7c9aa0 100644 --- a/tools/material-ci/angular_material_test_blocklist.js +++ b/tools/material-ci/angular_material_test_blocklist.js @@ -361,10 +361,6 @@ window.testBlocklist = { "error": "Error: Expected 'none' to be falsy.", "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": { "error": "TypeError: Cannot read property 'querySelectorAll' of null", "notes": "Unknown"