Skip to content
This repository was archived by the owner on Nov 26, 2018. It is now read-only.

Commit d3a9bd8

Browse files
committed
Added unsubscribing of listeners on component unmount
1 parent 667a986 commit d3a9bd8

5 files changed

Lines changed: 183 additions & 21 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
export interface FieldListenerRepository {
2+
add(fieldName: string, callback: (value: any) => void): number;
3+
remove(fieldName: string, id: number);
4+
trigger(fieldName: string, value: any);
5+
}
6+
7+
interface Listener {
8+
id: number;
9+
callback: (value: any) => void;
10+
}
11+
12+
export class FieldListenerRepositoryImpl implements FieldListenerRepository {
13+
14+
private listeners: { [fieldName: string]: Listener[] } = {};
15+
16+
private index = 1;
17+
18+
public add(fieldName: string, callback: (value: any) => void): number {
19+
if (typeof this.listeners[fieldName] === 'undefined') {
20+
this.listeners[fieldName] = [];
21+
}
22+
const listener = {
23+
id: this.index++,
24+
callback: callback
25+
};
26+
this.listeners[fieldName].push(listener);
27+
28+
return listener.id;
29+
}
30+
31+
public remove(fieldName: string, id: number) {
32+
if (typeof this.listeners[fieldName] === 'undefined') {
33+
throw new Error('Trying to unregister a listener for a field that is not registered');
34+
}
35+
this.listeners[fieldName] = this.listeners[fieldName].filter(listener => listener.id !== id);
36+
}
37+
38+
public trigger(fieldName: string, value: any) {
39+
if (typeof this.listeners[fieldName] !== 'undefined') {
40+
this.listeners[fieldName].forEach(listener => listener.callback(value));
41+
}
42+
}
43+
44+
public getFieldListenersForFieldName(fieldName: string): Array<(value: any) => void> | undefined {
45+
const listeners = this.listeners[fieldName];
46+
if (typeof listeners !== 'undefined') {
47+
return listeners.map(fieldListener => fieldListener.callback);
48+
}
49+
}
50+
}

src/business/form.ts

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
import {FieldListenerRepository, FieldListenerRepositoryImpl} from './callback_repository';
12
export class Form {
2-
33
private values: { [key: string]: any } = {};
44

55
private validationErrors: { [fieldName: string]: any } = {};
66

7-
private fieldListeners: { [fieldName: string]: Array<(value: any) => void> } = {};
7+
private _fieldListenerRepository: FieldListenerRepository;
8+
9+
constructor(fieldListenerRepository: FieldListenerRepository | null = null) {
10+
this._fieldListenerRepository = fieldListenerRepository !== null
11+
? fieldListenerRepository
12+
: new FieldListenerRepositoryImpl();
13+
}
814

915
public getFieldValue(fieldName: string): any {
1016
return this.values[fieldName] || '';
@@ -17,44 +23,37 @@ export class Form {
1723
delete this.validationErrors[fieldName];
1824
});
1925

20-
this.triggerMultipleFieldListeners(fieldNames);
26+
this.triggerMany(fieldNames);
2127
}
2228

2329
public setFieldValue(fieldName: string, value: any) {
2430
this.values[fieldName] = value;
2531
delete this.validationErrors[fieldName];
2632

27-
this.triggerFieldListeners(fieldName);
33+
this._fieldListenerRepository.trigger(fieldName, value);
2834
}
2935

3036
public setValidationErrors(errors: { [fieldName: string]: string }) {
3137
this.validationErrors = errors;
3238

3339
const fieldNames = Object.keys(this.validationErrors);
34-
this.triggerMultipleFieldListeners(fieldNames);
40+
this.triggerMany(fieldNames);
3541
}
3642

3743
public getValidationError(fieldName: string): string | undefined {
3844
return this.validationErrors[fieldName];
3945
}
4046

41-
public listenForFieldChange(fieldName: string, callback: (value: any) => void) {
42-
if (typeof this.fieldListeners[fieldName] === 'undefined') {
43-
this.fieldListeners[fieldName] = [];
44-
}
45-
this.fieldListeners[fieldName].push(callback);
47+
public listenForFieldChange(fieldName: string, callback: (value: any) => void): number {
48+
return this._fieldListenerRepository.add(fieldName, callback);
4649
}
4750

48-
private triggerMultipleFieldListeners(fieldNames: string[]) {
49-
fieldNames.forEach((fieldName) => {
50-
this.triggerFieldListeners(fieldName);
51-
});
51+
public removeListenerForFieldName(fieldName: string, id: number) {
52+
this._fieldListenerRepository.remove(fieldName, id);
5253
}
5354

54-
private triggerFieldListeners(fieldName: string) {
55-
if (typeof this.fieldListeners[fieldName] !== 'undefined') {
56-
this.fieldListeners[fieldName].forEach((callback) => callback(this.getFieldValue(fieldName)));
57-
}
55+
private triggerMany(fieldNames: string[]) {
56+
fieldNames.forEach(fieldName => this._fieldListenerRepository.trigger(fieldName, this.getFieldValue(fieldName)));
5857
}
5958

6059
}

src/composers/field.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export function Field<Props>(WrappedComponent: IncomingField<Props>,
2323
value: this.props.form.getFieldValue(this.getFieldName())
2424
};
2525

26+
private listener: number;
27+
2628
public getFieldName(): string {
2729
if (fieldName !== null) {
2830
return fieldName;
@@ -40,14 +42,18 @@ export function Field<Props>(WrappedComponent: IncomingField<Props>,
4042
}
4143
}
4244

45+
public componentWillUnmount() {
46+
this.props.form.removeListenerForFieldName(this.getFieldName(), this.listener);
47+
}
48+
4349
public componentWillUpdate(nextProps: Props & OwnProps & FormProps) {
4450
if (typeof nextProps.value !== 'undefined' && this.props.value !== nextProps.value) {
4551
this.props.form.setFieldValue(this.getFieldName(), nextProps.value);
4652
}
4753
}
4854

4955
public componentDidMount() {
50-
this.props.form.listenForFieldChange(this.getFieldName(), (value: any) => {
56+
this.listener = this.props.form.listenForFieldChange(this.getFieldName(), (value: any) => {
5157
this.setState({
5258
value: value
5359
});

src/composers/listener.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ export function Listener<Props>(WrappedComponent: IncomingListener<Props>, field
1616
values: this.getValues()
1717
};
1818

19+
private listeners: Array<{ fieldName: string, listener: number }> = [];
20+
1921
public getValues(): any {
2022
const result: any = {};
2123
fieldNames.forEach(fieldName => {
@@ -27,16 +29,24 @@ export function Listener<Props>(WrappedComponent: IncomingListener<Props>, field
2729

2830
public componentDidMount() {
2931
fieldNames.forEach(fieldName => {
30-
this.props.form.listenForFieldChange(fieldName, (value: any) => {
32+
const listener = this.props.form.listenForFieldChange(fieldName, (value: any) => {
3133
const values = {...this.state.values};
3234
values[fieldName] = value;
3335
this.setState({
3436
values: values
3537
});
3638
});
39+
this.listeners.push({
40+
fieldName: fieldName,
41+
listener: listener
42+
});
3743
});
3844
}
3945

46+
public componentWillUnmount() {
47+
this.listeners.forEach(listener => this.props.form.removeListenerForFieldName(listener.fieldName, listener.listener));
48+
}
49+
4050
public render() {
4151
return <WrappedComponent {...this.props} {...this.state.values}/>;
4252
}

tests/bonn.test.tsx

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as React from 'react';
22
import {Bonn, Field, FieldProps, FormProps, Listener} from '../src/bonn';
33
import {Form} from '../src/business/form';
44
import {mount} from 'enzyme';
5+
import {FieldListenerRepositoryImpl} from '../src/business/callback_repository';
56

67
describe('Bonn', function () {
78

@@ -288,8 +289,56 @@ describe('Bonn', function () {
288289

289290
/* Then */
290291
expect(result.text()).not.toContain('Foutje');
291-
})
292+
});
293+
294+
it('should unsubscribe its listeners when component is unmounted', function () {
295+
/* Given */
296+
const fieldListenerRepositoryImpl = new FieldListenerRepositoryImpl();
297+
const form = new Form(fieldListenerRepositoryImpl);
298+
299+
class MyField extends React.Component<FieldProps, {}> {
300+
render() {
301+
return <div>
302+
<input name="field"/>
303+
{this.props.validationError}
304+
</div>
305+
}
306+
}
307+
const Component = Field<{}>(MyField);
308+
309+
/* When */
310+
const result = mount(<Component name="field" form={form}/>);
311+
result.unmount();
292312

313+
/* Then */
314+
expect(fieldListenerRepositoryImpl.getFieldListenersForFieldName('field')).not.toBeUndefined();
315+
expect(fieldListenerRepositoryImpl.getFieldListenersForFieldName('field').length).toBe(0);
316+
});
317+
318+
it('should leave listeners that are listening to the same field', function () {
319+
/* Given */
320+
const fieldListenerRepositoryImpl = new FieldListenerRepositoryImpl();
321+
const form = new Form(fieldListenerRepositoryImpl);
322+
323+
class MyField extends React.Component<FieldProps, {}> {
324+
render() {
325+
return <div>
326+
<input name="field"/>
327+
{this.props.validationError}
328+
</div>
329+
}
330+
}
331+
const Component = Field<{}>(MyField);
332+
333+
/* When */
334+
const first = mount(<Component name="field" form={form}/>);
335+
first.unmount();
336+
const second = mount(<Component name="field" form={form}/>);
337+
338+
/* Then */
339+
expect(fieldListenerRepositoryImpl.getFieldListenersForFieldName('field')).not.toBeUndefined();
340+
expect(fieldListenerRepositoryImpl.getFieldListenersForFieldName('field').length).toBe(1);
341+
});
293342
});
294343

295344
describe('Listener', function () {
@@ -349,6 +398,54 @@ describe('Bonn', function () {
349398
expect(numRendersForComponent).toBe(2);
350399
});
351400

401+
it('should unsubscribe its listeners when component is unmounted', function () {
402+
/* Given */
403+
const fieldListenerRepositoryImpl = new FieldListenerRepositoryImpl();
404+
const form = new Form(fieldListenerRepositoryImpl);
405+
406+
class MyComponent extends React.Component<FormProps, {}> {
407+
render() {
408+
return <div></div>
409+
}
410+
}
411+
412+
/* When */
413+
const Component = Listener<{}>(MyComponent, ['field']);
414+
415+
/* When */
416+
const result = mount(<Component form={form}/>);
417+
result.unmount();
418+
419+
/* Then */
420+
expect(fieldListenerRepositoryImpl.getFieldListenersForFieldName('field')).not.toBeUndefined();
421+
expect(fieldListenerRepositoryImpl.getFieldListenersForFieldName('field').length).toBe(0);
422+
});
423+
424+
it('should unsubscribe multiple listeners when component is unmounted', function () {
425+
/* Given */
426+
const fieldListenerRepositoryImpl = new FieldListenerRepositoryImpl();
427+
const form = new Form(fieldListenerRepositoryImpl);
428+
429+
class MyComponent extends React.Component<FormProps, {}> {
430+
render() {
431+
return <div></div>
432+
}
433+
}
434+
435+
/* When */
436+
const Component = Listener<{}>(MyComponent, ['field', 'other']);
437+
438+
/* When */
439+
const result = mount(<Component form={form}/>);
440+
result.unmount();
441+
442+
/* Then */
443+
expect(fieldListenerRepositoryImpl.getFieldListenersForFieldName('field')).not.toBeUndefined();
444+
expect(fieldListenerRepositoryImpl.getFieldListenersForFieldName('other')).not.toBeUndefined();
445+
expect(fieldListenerRepositoryImpl.getFieldListenersForFieldName('field').length).toBe(0);
446+
expect(fieldListenerRepositoryImpl.getFieldListenersForFieldName('other').length).toBe(0);
447+
});
448+
352449
});
353450

354451
});

0 commit comments

Comments
 (0)