Skip to content
This repository was archived by the owner on Nov 26, 2018. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions src/business/field_listener_repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
export interface FieldListenerRepository {
subscribe(field: string, callback: (value: any) => void): Listener;
unsubscribe(toBeUnsubscribedListener: Listener);
trigger(field: string, value: any);
}

export interface Listener {
id: number;
field: string;
callback: (value: any) => void;
}

export class FieldListenerRepositoryImpl implements FieldListenerRepository {

private listeners: { [field: string]: Listener[] } = {};

private index = 1;

public subscribe(field: string, callback: (value: any) => void): Listener {
if (typeof this.listeners[field] === 'undefined') {
this.listeners[field] = [];
}
const listener = {
id: this.index++,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dont wanna use a Symbol?

field: field,
callback: callback
};
this.listeners[field].push(listener);

return listener;
}

public unsubscribe(toBeUnsubscribedListener: Listener) {
if (typeof this.listeners[toBeUnsubscribedListener.field] === 'undefined') {
throw new Error('Trying to unregister a listener for a field that is not registered');
}
this.listeners[toBeUnsubscribedListener.field] = this
.listeners[toBeUnsubscribedListener.field]
.filter(listener => listener.id !== toBeUnsubscribedListener.id);
}

public trigger(field: string, value: any) {
if (typeof this.listeners[field] !== 'undefined') {
this.listeners[field].forEach(listener => listener.callback(value));
}
}

public getFieldListenersForFieldName(field: string): Array<(value: any) => void> | undefined {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a FieldListener in this method name? This repository already holds so called Listener objects but here we only return a array of callback. Isn't a method name like getFieldListenerCallbacksForFieldName or something similiar more fitting?

const listeners = this.listeners[field];
if (typeof listeners !== 'undefined') {
return listeners.map(fieldListener => fieldListener.callback);
}
}
}
35 changes: 17 additions & 18 deletions src/business/form.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import {FieldListenerRepository, FieldListenerRepositoryImpl, Listener} from './field_listener_repository';
export class Form {

private values: { [key: string]: any } = {};

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

private fieldListeners: { [fieldName: string]: Array<(value: any) => void> } = {};
private _fieldListenerRepository: FieldListenerRepository;

constructor(fieldListenerRepository: FieldListenerRepository | null = null) {
Copy link
Copy Markdown

@DJWassink DJWassink Apr 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why a | null type instead of making the fieldListenerRepository nullable?

this._fieldListenerRepository = fieldListenerRepository !== null
? fieldListenerRepository
: new FieldListenerRepositoryImpl();
}

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

this.triggerMultipleFieldListeners(fieldNames);
this.triggerMany(fieldNames);
}

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

this.triggerFieldListeners(fieldName);
this._fieldListenerRepository.trigger(fieldName, value);
}

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

const fieldNames = Object.keys(this.validationErrors);
this.triggerMultipleFieldListeners(fieldNames);
this.triggerMany(fieldNames);
}

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

public listenForFieldChange(fieldName: string, callback: (value: any) => void) {
if (typeof this.fieldListeners[fieldName] === 'undefined') {
this.fieldListeners[fieldName] = [];
}
this.fieldListeners[fieldName].push(callback);
public subscribe(fieldName: string, callback: (value: any) => void): Listener {
return this._fieldListenerRepository.subscribe(fieldName, callback);
}

private triggerMultipleFieldListeners(fieldNames: string[]) {
fieldNames.forEach((fieldName) => {
this.triggerFieldListeners(fieldName);
});
public unsubscribe(listener: Listener) {
this._fieldListenerRepository.unsubscribe(listener);
}

private triggerFieldListeners(fieldName: string) {
if (typeof this.fieldListeners[fieldName] !== 'undefined') {
this.fieldListeners[fieldName].forEach((callback) => callback(this.getFieldValue(fieldName)));
}
private triggerMany(fieldNames: string[]) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe wanna move this to the repo? Seems like a method the repo would want ownership of, if in the future it's behavior gotta change. (like we had before)

fieldNames.forEach(fieldName => this._fieldListenerRepository.trigger(fieldName, this.getFieldValue(fieldName)));
}

}
9 changes: 8 additions & 1 deletion src/composers/field.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import {FormProps} from './bonn';
import {Listener} from '../business/field_listener_repository';
export interface FieldProps {
value: any;
validationError: string | undefined;
Expand All @@ -23,6 +24,8 @@ export function Field<Props>(WrappedComponent: IncomingField<Props>,
value: this.props.form.getFieldValue(this.getFieldName())
};

private listener: Listener;

public getFieldName(): string {
if (fieldName !== null) {
return fieldName;
Expand All @@ -40,14 +43,18 @@ export function Field<Props>(WrappedComponent: IncomingField<Props>,
}
}

public componentWillUnmount() {
this.props.form.unsubscribe(this.listener);
}

public componentWillUpdate(nextProps: Props & OwnProps & FormProps) {
if (typeof nextProps.value !== 'undefined' && this.props.value !== nextProps.value) {
this.props.form.setFieldValue(this.getFieldName(), nextProps.value);
}
}

public componentDidMount() {
this.props.form.listenForFieldChange(this.getFieldName(), (value: any) => {
this.listener = this.props.form.subscribe(this.getFieldName(), (value: any) => {
this.setState({
value: value
});
Expand Down
10 changes: 9 additions & 1 deletion src/composers/listener.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as React from 'react';
import {FormProps} from './bonn';
import {Listener} from '../business/field_listener_repository';

export interface ListenerState {
values: any;
Expand All @@ -16,6 +17,8 @@ export function Listener<Props>(WrappedComponent: IncomingListener<Props>, field
values: this.getValues()
};

private listeners: Listener[] = [];

public getValues(): any {
const result: any = {};
fieldNames.forEach(fieldName => {
Expand All @@ -27,16 +30,21 @@ export function Listener<Props>(WrappedComponent: IncomingListener<Props>, field

public componentDidMount() {
fieldNames.forEach(fieldName => {
this.props.form.listenForFieldChange(fieldName, (value: any) => {
const listener = this.props.form.subscribe(fieldName, (value: any) => {
const values = {...this.state.values};
values[fieldName] = value;
this.setState({
values: values
});
});
this.listeners.push(listener);
});
}

public componentWillUnmount() {
this.listeners.forEach(listener => this.props.form.unsubscribe(listener));
}

public render() {
return <WrappedComponent {...this.props} {...this.state.values}/>;
}
Expand Down
103 changes: 100 additions & 3 deletions tests/bonn.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as React from 'react';
import {Bonn, Field, FieldProps, FormProps, Listener} from '../src/bonn';
import {Form} from '../src/business/form';
import {mount} from 'enzyme';
import {FieldListenerRepositoryImpl} from '../src/business/field_listener_repository';

describe('Bonn', function () {

Expand Down Expand Up @@ -113,10 +114,10 @@ describe('Bonn', function () {
let numberOfTimesBlubListenerTriggered = 0;
class MyWrappedComponent extends React.Component<FormProps, {}> {
componentWillMount() {
this.props.form.listenForFieldChange('foo', () => {
this.props.form.subscribe('foo', () => {
numberOfTimesFooListenerTriggered++;
});
this.props.form.listenForFieldChange('blub', () => {
this.props.form.subscribe('blub', () => {
numberOfTimesBlubListenerTriggered++;
});
}
Expand Down Expand Up @@ -288,8 +289,56 @@ describe('Bonn', function () {

/* Then */
expect(result.text()).not.toContain('Foutje');
})
});

it('should unsubscribe its listeners when component is unmounted', function () {
/* Given */
const fieldListenerRepositoryImpl = new FieldListenerRepositoryImpl();
const form = new Form(fieldListenerRepositoryImpl);

class MyField extends React.Component<FieldProps, {}> {
render() {
return <div>
<input name="field"/>
{this.props.validationError}
</div>
}
}
const Component = Field<{}>(MyField);

/* When */
const result = mount(<Component name="field" form={form}/>);
result.unmount();

/* Then */
expect(fieldListenerRepositoryImpl.getFieldListenersForFieldName('field')).not.toBeUndefined();
expect(fieldListenerRepositoryImpl.getFieldListenersForFieldName('field').length).toBe(0);
});

it('should leave listeners that are listening to the same field', function () {
/* Given */
const fieldListenerRepositoryImpl = new FieldListenerRepositoryImpl();
const form = new Form(fieldListenerRepositoryImpl);

class MyField extends React.Component<FieldProps, {}> {
render() {
return <div>
<input name="field"/>
{this.props.validationError}
</div>
}
}
const Component = Field<{}>(MyField);

/* When */
const first = mount(<Component name="field" form={form}/>);
first.unmount();
const second = mount(<Component name="field" form={form}/>);

/* Then */
expect(fieldListenerRepositoryImpl.getFieldListenersForFieldName('field')).not.toBeUndefined();
expect(fieldListenerRepositoryImpl.getFieldListenersForFieldName('field').length).toBe(1);
});
});

describe('Listener', function () {
Expand Down Expand Up @@ -349,6 +398,54 @@ describe('Bonn', function () {
expect(numRendersForComponent).toBe(2);
});

it('should unsubscribe its listeners when component is unmounted', function () {
/* Given */
const fieldListenerRepositoryImpl = new FieldListenerRepositoryImpl();
const form = new Form(fieldListenerRepositoryImpl);

class MyComponent extends React.Component<FormProps, {}> {
render() {
return <div></div>
}
}

/* When */
const Component = Listener<{}>(MyComponent, ['field']);

/* When */
const result = mount(<Component form={form}/>);
result.unmount();

/* Then */
expect(fieldListenerRepositoryImpl.getFieldListenersForFieldName('field')).not.toBeUndefined();
expect(fieldListenerRepositoryImpl.getFieldListenersForFieldName('field').length).toBe(0);
});

it('should unsubscribe multiple listeners when component is unmounted', function () {
/* Given */
const fieldListenerRepositoryImpl = new FieldListenerRepositoryImpl();
const form = new Form(fieldListenerRepositoryImpl);

class MyComponent extends React.Component<FormProps, {}> {
render() {
return <div></div>
}
}

/* When */
const Component = Listener<{}>(MyComponent, ['field', 'other']);

/* When */
const result = mount(<Component form={form}/>);
result.unmount();

/* Then */
expect(fieldListenerRepositoryImpl.getFieldListenersForFieldName('field')).not.toBeUndefined();
expect(fieldListenerRepositoryImpl.getFieldListenersForFieldName('other')).not.toBeUndefined();
expect(fieldListenerRepositoryImpl.getFieldListenersForFieldName('field').length).toBe(0);
expect(fieldListenerRepositoryImpl.getFieldListenersForFieldName('other').length).toBe(0);
});

});

});