Skip to content

Commit 14ec315

Browse files
committed
fix(angular-material): repaint array layouts on external state changes
ArrayLayoutRenderer is OnPush but never scheduled a check of its own, so it was only rendered when an event inside its own view happened to mark it dirty. A state change originating anywhere else - the form being set readonly, data being set programmatically, an item being added by another renderer - updated the component's fields but never reached the template. It now calls markForCheck when it maps new props, like the layout renderers and the list with detail already do.
1 parent 22883df commit 14ec315

2 files changed

Lines changed: 40 additions & 1 deletion

File tree

‎packages/angular-material/src/library/layouts/array-layout.renderer.ts‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,13 @@
2222
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2323
THE SOFTWARE.
2424
*/
25-
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
25+
import {
26+
ChangeDetectionStrategy,
27+
ChangeDetectorRef,
28+
Component,
29+
inject,
30+
OnInit,
31+
} from '@angular/core';
2632
import { CommonModule } from '@angular/common';
2733
import { MatCardModule } from '@angular/material/card';
2834
import { MatButtonModule } from '@angular/material/button';
@@ -210,6 +216,7 @@ export class ArrayLayoutRenderer
210216
itemProps: OwnPropsOfRenderer[] = [];
211217
private detailUiSchemaDeps: unknown[] | undefined;
212218
private itemPropsDeps: unknown[] | undefined;
219+
private changeDetectorRef = inject(ChangeDetectorRef);
213220
mapToProps(
214221
state: JsonFormsState
215222
): StatePropsOfArrayLayout & { translations: ArrayTranslations } {
@@ -258,6 +265,12 @@ export class ArrayLayoutRenderer
258265
this.translations = props.translations;
259266
this.detailUiSchema = this.resolveDetailUiSchema(props);
260267
this.updateItemProps(props, this.detailUiSchema);
268+
// The component is OnPush, so it is only checked when an event inside its
269+
// own view marks it dirty. A state change with no such event - the form
270+
// being set readonly, data being set programmatically, an item being added
271+
// from elsewhere - has to schedule the check itself, otherwise none of the
272+
// above reaches the template.
273+
this.changeDetectorRef.markForCheck();
261274
}
262275
private resolveDetailUiSchema(props: ArrayLayoutProps): UISchemaElement {
263276
const deps = [

‎packages/angular-material/test/array-layout.spec.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2323
THE SOFTWARE.
2424
*/
25+
import { ChangeDetectorRef } from '@angular/core';
2526
import { ComponentFixture, waitForAsync } from '@angular/core/testing';
2627
import { MatIcon } from '@angular/material/icon';
2728
import { MatBadge } from '@angular/material/badge';
@@ -247,6 +248,31 @@ describe('Array layout ui schema handling', () => {
247248
expect(fixture.componentInstance.getProps(1).uischema).toBe(detail);
248249
});
249250

251+
it('schedules a check so that state changes reach the template', () => {
252+
const uischema = cloneDeep(TEST_UISCHEMA);
253+
254+
setupMockStore(fixture, {
255+
data: { test: [{}] },
256+
schema: TEST_SCHEMA,
257+
uischema,
258+
});
259+
fixture.componentInstance.ngOnInit();
260+
fixture.detectChanges();
261+
262+
// the renderer is OnPush, so without this nothing repaints for a change
263+
// that did not originate from an event in its own view. As the fixture root
264+
// it is checked unconditionally, hence the explicit expectation.
265+
const changeDetectorRef = (fixture.componentInstance as any)
266+
.changeDetectorRef as ChangeDetectorRef;
267+
spyOn(changeDetectorRef, 'markForCheck').and.callThrough();
268+
269+
getJsonFormsService(fixture.componentInstance).updateCore(
270+
Actions.update('test', () => [{}, {}])
271+
);
272+
273+
expect(changeDetectorRef.markForCheck).toHaveBeenCalled();
274+
});
275+
250276
it('renders no items for data that is not an array', () => {
251277
const uischema = cloneDeep(TEST_UISCHEMA);
252278

0 commit comments

Comments
 (0)