Skip to content

Commit 1feaf0b

Browse files
angular: Unsubscribe from state subscriptions on renderer destroy (#2377)
Previously, subscriptions to the JSON Forms state in Angular were never removed when renderer components are destroyed. With this commit, all subscriptions will be unsubscribed when the component is destroyed. The base renderer offers an addSubscription method to register one or multiple subscriptions to automatically be unsubscribed on destroy. With this, extending renderers do not need to implement the unsubscribe again. Closes #2354 Co-authored-by: Lucas Koehler <lkoehler@eclipsesource.com>
1 parent d7f34b9 commit 1feaf0b

File tree

8 files changed

+121
-130
lines changed

8 files changed

+121
-130
lines changed

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

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,7 @@
2222
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2323
THE SOFTWARE.
2424
*/
25-
import {
26-
ChangeDetectionStrategy,
27-
Component,
28-
OnDestroy,
29-
OnInit,
30-
} from '@angular/core';
25+
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
3126
import {
3227
JsonFormsAngularService,
3328
JsonFormsAbstractControl,
@@ -169,7 +164,7 @@ import {
169164
})
170165
export class ArrayLayoutRenderer
171166
extends JsonFormsAbstractControl<StatePropsOfArrayLayout>
172-
implements OnInit, OnDestroy
167+
implements OnInit
173168
{
174169
noData: boolean;
175170
translations: ArrayTranslations = {};

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

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,11 @@ import {
3838
rankWith,
3939
uiTypeIs,
4040
} from '@jsonforms/core';
41-
import { Component, OnDestroy, OnInit } from '@angular/core';
41+
import { Component, OnInit } from '@angular/core';
4242
import {
4343
JsonFormsAngularService,
4444
JsonFormsBaseRenderer,
4545
} from '@jsonforms/angular';
46-
import { Subscription } from 'rxjs';
4746

4847
@Component({
4948
selector: 'jsonforms-categorization-layout',
@@ -69,41 +68,36 @@ import { Subscription } from 'rxjs';
6968
})
7069
export class CategorizationTabLayoutRenderer
7170
extends JsonFormsBaseRenderer<Categorization>
72-
implements OnInit, OnDestroy
71+
implements OnInit
7372
{
7473
hidden: boolean;
7574
visibleCategories: (Category | Categorization)[];
76-
private subscription: Subscription;
7775
categoryLabels: string[];
7876

7977
constructor(private jsonFormsService: JsonFormsAngularService) {
8078
super();
8179
}
8280

8381
ngOnInit() {
84-
this.subscription = this.jsonFormsService.$state.subscribe({
85-
next: (state: JsonFormsState) => {
86-
const props = mapStateToLayoutProps(state, this.getOwnProps());
87-
this.hidden = !props.visible;
88-
this.visibleCategories = this.uischema.elements.filter(
89-
(category: Category | Categorization) =>
90-
isVisible(category, props.data, undefined, getAjv(state))
91-
);
92-
this.categoryLabels = this.visibleCategories.map((element) =>
93-
deriveLabelForUISchemaElement(
94-
element as Labelable<boolean>,
95-
state.jsonforms.i18n?.translate ??
96-
defaultJsonFormsI18nState.translate
97-
)
98-
);
99-
},
100-
});
101-
}
102-
103-
ngOnDestroy() {
104-
if (this.subscription) {
105-
this.subscription.unsubscribe();
106-
}
82+
this.addSubscription(
83+
this.jsonFormsService.$state.subscribe({
84+
next: (state: JsonFormsState) => {
85+
const props = mapStateToLayoutProps(state, this.getOwnProps());
86+
this.hidden = !props.visible;
87+
this.visibleCategories = this.uischema.elements.filter(
88+
(category: Category | Categorization) =>
89+
isVisible(category, props.data, undefined, getAjv(state))
90+
);
91+
this.categoryLabels = this.visibleCategories.map((element) =>
92+
deriveLabelForUISchemaElement(
93+
element as Labelable<boolean>,
94+
state.jsonforms.i18n?.translate ??
95+
defaultJsonFormsI18nState.translate
96+
)
97+
);
98+
},
99+
})
100+
);
107101
}
108102
}
109103

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

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
THE SOFTWARE.
2424
*/
2525
import {
26-
OnDestroy,
2726
OnInit,
2827
ChangeDetectorRef,
2928
Component,
@@ -42,18 +41,16 @@ import {
4241
UISchemaElement,
4342
JsonSchema,
4443
} from '@jsonforms/core';
45-
import type { Subscription } from 'rxjs';
4644

4745
@Component({
4846
template: '',
4947
})
5048
export class LayoutRenderer<T extends Layout>
5149
extends JsonFormsBaseRenderer<T>
52-
implements OnInit, OnDestroy
50+
implements OnInit
5351
{
5452
hidden: boolean;
5553
label: string | undefined;
56-
private subscription: Subscription;
5754

5855
constructor(
5956
private jsonFormsService: JsonFormsAngularService,
@@ -63,20 +60,16 @@ export class LayoutRenderer<T extends Layout>
6360
}
6461

6562
ngOnInit() {
66-
this.subscription = this.jsonFormsService.$state.subscribe({
67-
next: (state: JsonFormsState) => {
68-
const props = mapStateToLayoutProps(state, this.getOwnProps());
69-
this.label = props.label;
70-
this.hidden = !props.visible;
71-
this.changeDetectionRef.markForCheck();
72-
},
73-
});
74-
}
75-
76-
ngOnDestroy() {
77-
if (this.subscription) {
78-
this.subscription.unsubscribe();
79-
}
63+
this.addSubscription(
64+
this.jsonFormsService.$state.subscribe({
65+
next: (state: JsonFormsState) => {
66+
const props = mapStateToLayoutProps(state, this.getOwnProps());
67+
this.label = props.label;
68+
this.hidden = !props.visible;
69+
this.changeDetectionRef.markForCheck();
70+
},
71+
})
72+
);
8073
}
8174

8275
trackElement(_index: number, renderProp: OwnPropsOfRenderer): string {

packages/angular-material/src/library/other/label.renderer.ts

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2323
THE SOFTWARE.
2424
*/
25-
import { Component, OnDestroy, OnInit } from '@angular/core';
25+
import { Component, OnInit } from '@angular/core';
2626
import {
2727
JsonFormsAngularService,
2828
JsonFormsBaseRenderer,
@@ -36,7 +36,6 @@ import {
3636
rankWith,
3737
uiTypeIs,
3838
} from '@jsonforms/core';
39-
import { Subscription } from 'rxjs';
4039

4140
@Component({
4241
selector: 'LabelRenderer',
@@ -51,33 +50,27 @@ import { Subscription } from 'rxjs';
5150
})
5251
export class LabelRenderer
5352
extends JsonFormsBaseRenderer<LabelElement>
54-
implements OnDestroy, OnInit
53+
implements OnInit
5554
{
5655
label: string;
5756
visible: boolean;
5857

59-
private subscription: Subscription;
60-
6158
constructor(private jsonFormsService: JsonFormsAngularService) {
6259
super();
6360
}
6461
ngOnInit() {
65-
this.subscription = this.jsonFormsService.$state.subscribe({
66-
next: (state: JsonFormsState) => {
67-
const props = mapStateToLabelProps(
68-
state,
69-
this.getOwnProps() as OwnPropsOfLabel
70-
);
71-
this.visible = props.visible;
72-
this.label = props.text;
73-
},
74-
});
75-
}
76-
77-
ngOnDestroy() {
78-
if (this.subscription) {
79-
this.subscription.unsubscribe();
80-
}
62+
this.addSubscription(
63+
this.jsonFormsService.$state.subscribe({
64+
next: (state: JsonFormsState) => {
65+
const props = mapStateToLabelProps(
66+
state,
67+
this.getOwnProps() as OwnPropsOfLabel
68+
);
69+
this.visible = props.visible;
70+
this.label = props.text;
71+
},
72+
})
73+
);
8174
}
8275
}
8376

packages/angular/src/library/abstract-control.ts

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ import {
3939
ValidationErrors,
4040
ValidatorFn,
4141
} from '@angular/forms';
42-
import type { Subscription } from 'rxjs';
4342

4443
import { JsonFormsBaseRenderer } from './base.renderer';
4544
import { JsonFormsAngularService } from './jsonforms.service';
@@ -58,7 +57,6 @@ export abstract class JsonFormsAbstractControl<
5857
@Input() visible: boolean;
5958

6059
form: FormControl;
61-
subscription: Subscription;
6260
data: any;
6361
label: string;
6462
description: string;
@@ -99,41 +97,45 @@ export abstract class JsonFormsAbstractControl<
9997
}
10098

10199
ngOnInit() {
102-
this.jsonFormsService.$state.subscribe({
103-
next: (state: JsonFormsState) => {
104-
const props = this.mapToProps(state);
105-
const {
106-
data,
107-
enabled,
108-
errors,
109-
label,
110-
required,
111-
schema,
112-
rootSchema,
113-
visible,
114-
path,
115-
config,
116-
} = props;
117-
this.label = computeLabel(
118-
label,
119-
required,
120-
config ? config.hideRequiredAsterisk : false
121-
);
122-
this.data = data;
123-
this.error = errors;
124-
this.enabled = enabled;
125-
this.isEnabled() ? this.form.enable() : this.form.disable();
126-
this.hidden = !visible;
127-
this.scopedSchema = schema;
128-
this.rootSchema = rootSchema;
129-
this.description =
130-
this.scopedSchema !== undefined ? this.scopedSchema.description : '';
131-
this.id = props.id;
132-
this.form.setValue(data);
133-
this.propsPath = path;
134-
this.mapAdditionalProps(props);
135-
},
136-
});
100+
this.addSubscription(
101+
this.jsonFormsService.$state.subscribe({
102+
next: (state: JsonFormsState) => {
103+
const props = this.mapToProps(state);
104+
const {
105+
data,
106+
enabled,
107+
errors,
108+
label,
109+
required,
110+
schema,
111+
rootSchema,
112+
visible,
113+
path,
114+
config,
115+
} = props;
116+
this.label = computeLabel(
117+
label,
118+
required,
119+
config ? config.hideRequiredAsterisk : false
120+
);
121+
this.data = data;
122+
this.error = errors;
123+
this.enabled = enabled;
124+
this.isEnabled() ? this.form.enable() : this.form.disable();
125+
this.hidden = !visible;
126+
this.scopedSchema = schema;
127+
this.rootSchema = rootSchema;
128+
this.description =
129+
this.scopedSchema !== undefined
130+
? this.scopedSchema.description
131+
: '';
132+
this.id = props.id;
133+
this.form.setValue(data);
134+
this.propsPath = path;
135+
this.mapAdditionalProps(props);
136+
},
137+
})
138+
);
137139
this.triggerValidation();
138140
}
139141

@@ -146,9 +148,7 @@ export abstract class JsonFormsAbstractControl<
146148
}
147149

148150
ngOnDestroy() {
149-
if (this.subscription) {
150-
this.subscription.unsubscribe();
151-
}
151+
super.ngOnDestroy();
152152
removeId(this.id);
153153
}
154154

packages/angular/src/library/base.renderer.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,30 @@
2222
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
2323
THE SOFTWARE.
2424
*/
25-
import { Directive, Input } from '@angular/core';
25+
import { Directive, Input, OnDestroy } from '@angular/core';
2626
import {
2727
JsonSchema,
2828
OwnPropsOfRenderer,
2929
UISchemaElement,
3030
} from '@jsonforms/core';
31+
import { Subscription } from 'rxjs';
3132

3233
@Directive()
33-
export class JsonFormsBaseRenderer<T extends UISchemaElement> {
34+
export class JsonFormsBaseRenderer<T extends UISchemaElement>
35+
implements OnDestroy
36+
{
3437
@Input() uischema: T;
3538
@Input() schema: JsonSchema;
3639
@Input() path: string;
40+
protected subscriptions: Subscription = new Subscription();
41+
42+
protected addSubscription(subscription: Subscription | Subscription[]) {
43+
if (Array.isArray(subscription)) {
44+
subscription.forEach((sub) => this.subscriptions.add(sub));
45+
} else {
46+
this.subscriptions.add(subscription);
47+
}
48+
}
3749

3850
protected getOwnProps(): OwnPropsOfRenderer {
3951
return {
@@ -42,4 +54,8 @@ export class JsonFormsBaseRenderer<T extends UISchemaElement> {
4254
path: this.path,
4355
};
4456
}
57+
58+
ngOnDestroy(): void {
59+
this.subscriptions.unsubscribe();
60+
}
4561
}

0 commit comments

Comments
 (0)