Skip to content

Commit 670d438

Browse files
authoredOct 3, 2022
fix(angular): call ngOnChanges after mount (#23596)
* fix(angular): call ngOnChanges after mounting
1 parent 0976034 commit 670d438

File tree

3 files changed

+97
-4
lines changed

3 files changed

+97
-4
lines changed
 

‎npm/angular/src/mount.ts

+20-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ window.Mocha['__zone_patch__'] = false
88
import 'zone.js/testing'
99

1010
import { CommonModule } from '@angular/common'
11-
import { Component, EventEmitter, Type } from '@angular/core'
11+
import { Component, EventEmitter, SimpleChange, SimpleChanges, Type } from '@angular/core'
1212
import {
1313
ComponentFixture,
1414
getTestBed,
@@ -215,10 +215,9 @@ function setupFixture<T> (
215215
* @param {ComponentFixture<T>} fixture Fixture for debugging and testing a component.
216216
* @returns {T} Component being mounted
217217
*/
218-
function setupComponent<T> (
218+
function setupComponent<T extends { ngOnChanges? (changes: SimpleChanges): void }> (
219219
config: MountConfig<T>,
220-
fixture: ComponentFixture<T>,
221-
): T {
220+
fixture: ComponentFixture<T>): T {
222221
let component: T = fixture.componentInstance
223222

224223
if (config?.componentProperties) {
@@ -235,6 +234,23 @@ function setupComponent<T> (
235234
})
236235
}
237236

237+
// Manually call ngOnChanges when mounting components using the class syntax.
238+
// This is necessary because we are assigning input values to the class directly
239+
// on mount and therefore the ngOnChanges() lifecycle is not triggered.
240+
if (component.ngOnChanges && config.componentProperties) {
241+
const { componentProperties } = config
242+
243+
const simpleChanges: SimpleChanges = Object.entries(componentProperties).reduce((acc, [key, value]) => {
244+
acc[key] = new SimpleChange(null, value, true)
245+
246+
return acc
247+
}, {})
248+
249+
if (Object.keys(componentProperties).length > 0) {
250+
component.ngOnChanges(simpleChanges)
251+
}
252+
}
253+
238254
return component
239255
}
240256

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core'
2+
3+
@Component({
4+
selector: 'app-lifecycle',
5+
template: `<p>Hi {{ name }}. ngOnInit fired: {{ ngOnInitFired }} and ngOnChanges fired: {{ ngOnChangesFired }} and conditionalName: {{ conditionalName }}</p>`
6+
})
7+
export class LifecycleComponent implements OnInit, OnChanges {
8+
@Input() name = ''
9+
ngOnInitFired = false
10+
ngOnChangesFired = false
11+
conditionalName = false
12+
13+
ngOnInit(): void {
14+
this.ngOnInitFired = true
15+
}
16+
17+
ngOnChanges(changes: SimpleChanges): void {
18+
this.ngOnChangesFired = true;
19+
if (changes['name'].currentValue === 'CONDITIONAL NAME') {
20+
this.conditionalName = true
21+
}
22+
}
23+
}

‎system-tests/project-fixtures/angular/src/app/mount.cy.ts

+54
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { ButtonOutputComponent } from "./components/button-output.component";
88
import { createOutputSpy } from 'cypress/angular';
99
import { EventEmitter, Component } from '@angular/core';
1010
import { ProjectionComponent } from "./components/projection.component";
11+
import { LifecycleComponent } from "./components/lifecycle.component";
1112

1213
@Component({
1314
template: `<app-projection>Hello World</app-projection>`
@@ -164,7 +165,60 @@ describe("angular mount", () => {
164165
})
165166
cy.get('h3').contains('Hello World')
166167
})
168+
169+
it('handles ngOnChanges on mount', () => {
170+
cy.mount(LifecycleComponent, {
171+
componentProperties: {
172+
name: 'Angular'
173+
}
174+
})
175+
176+
cy.get('p').should('have.text', 'Hi Angular. ngOnInit fired: true and ngOnChanges fired: true and conditionalName: false')
177+
})
178+
179+
it('handles ngOnChanges on mount with templates', () => {
180+
cy.mount('<app-lifecycle [name]="name"></app-lifecycle>', {
181+
declarations: [LifecycleComponent],
182+
componentProperties: {
183+
name: 'Angular'
184+
}
185+
})
186+
187+
cy.get('p').should('have.text', 'Hi Angular. ngOnInit fired: true and ngOnChanges fired: true and conditionalName: false')
188+
})
167189

190+
it('creates simpleChanges from componentProperties and calls ngOnChanges on Mount', () => {
191+
cy.mount(LifecycleComponent, {
192+
componentProperties: {
193+
name: 'CONDITIONAL NAME'
194+
}
195+
})
196+
cy.get('p').should('have.text', 'Hi CONDITIONAL NAME. ngOnInit fired: true and ngOnChanges fired: true and conditionalName: true')
197+
})
198+
199+
it('creates simpleChanges from componentProperties and calls ngOnChanges on Mount with template', () => {
200+
cy.mount('<app-lifecycle [name]="name"></app-lifecycle>', {
201+
declarations: [LifecycleComponent],
202+
componentProperties: {
203+
name: 'CONDITIONAL NAME'
204+
}
205+
})
206+
cy.get('p').should('have.text', 'Hi CONDITIONAL NAME. ngOnInit fired: true and ngOnChanges fired: true and conditionalName: true')
207+
})
208+
209+
it('ngOnChanges is not fired when no componentProperties given', () => {
210+
cy.mount(LifecycleComponent)
211+
cy.get('p').should('have.text', 'Hi . ngOnInit fired: true and ngOnChanges fired: false and conditionalName: false')
212+
})
213+
214+
it('ngOnChanges is not fired when no componentProperties given with template', () => {
215+
cy.mount('<app-lifecycle></app-lifecycle>', {
216+
declarations: [LifecycleComponent]
217+
})
218+
cy.get('p').should('have.text', 'Hi . ngOnInit fired: true and ngOnChanges fired: false and conditionalName: false')
219+
})
220+
221+
168222
describe("teardown", () => {
169223
beforeEach(() => {
170224
cy.get("[id^=root]").should("not.exist");

0 commit comments

Comments
 (0)