Skip to content
This repository was archived by the owner on Jun 2, 2024. It is now read-only.
Merged
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
59 changes: 59 additions & 0 deletions src/components/Resolve/Resolve.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { mount } from 'enzyme';
import * as React from 'react';
import Resolve from './Resolve';

describe('resolve', () => {
const timeout = 10;

const resolvable = resolved =>
new Promise(resolve => {
setTimeout(() => {
resolve(resolved);
}, timeout);
});

const rejectable = rejected =>
new Promise((_, reject) => {
setTimeout(() => {
reject(rejected);
}, timeout);
});

test('no pending, resolve, reject', () => {
const wrapper = mount(<Resolve promise={resolvable('resolved')} />);
expect(wrapper.html()).toEqual(null);
});

test('pending', () => {
const pending = <p>horse</p>;
const result = '<p>horse</p>';
const wrapper = mount(
<Resolve promise={resolvable('resolved')} pending={pending} />,
);
expect(wrapper.html()).toEqual(result);
});

test('resolve', done => {
const resolved = value => <p>{value}</p>;
const result = '<p>resolved</p>';
const wrapper = mount(
<Resolve promise={resolvable('resolved')} resolved={resolved} />,
);
setTimeout(() => {
expect(wrapper.html()).toEqual(result);
done();
}, timeout + 5);
});

test('resolve', done => {
const rejected = value => <p>{value}</p>;
const result = '<p>rejected</p>';
const wrapper = mount(
<Resolve promise={rejectable('rejected')} rejected={rejected} />,
);
setTimeout(() => {
expect(wrapper.html()).toEqual(result);
done();
}, timeout + 5);
});
});
134 changes: 134 additions & 0 deletions src/components/Resolve/Resolve.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import * as PropTypes from 'prop-types';
import * as React from 'react';
import { isPromise } from '../../utils';
import { statusTypes } from './config';

export interface IResolveProps {
/** The promise to be resolved */
promise?: Promise<any>;

/** Will be displayed after promise is resolved */
resolved?: (value) => React.ReactNode;

/** Will be displayed while promise is handled */
pending?: React.ReactNode;

/** Will be displayed if promise is rejected */
rejected?: (error) => React.ReactNode;
}

const initialState = {
status: statusTypes.none,
value: '',
};

type IResolveState = Readonly<typeof initialState>;

/**
* A component to render based on a promise
*
* @example
* <Resolve
* promise = {aPromise}
* resolved = {
* value => <p>{`Resolved value is ${value}`}</p>
* } />
*/
class Resolve extends React.Component<IResolveProps, IResolveState> {
// PropTypes
public static propTypes = {
pending: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
promise: isPromise,
rejected: PropTypes.func,
resolved: PropTypes.func,
};

// Initialize state
public readonly state: IResolveState = initialState;

// Create escape hatch to stop handling of promise if unmounted
public unmounted = false;

public componentDidMount() {
// Start handling the promise, must happen after mount as setState is called when promise is handled
this._handlePromise(this.props.promise);
}

public componentDidUpdate(prevProps) {
if (this.props.promise !== prevProps.promise) {
this.setState({
status: statusTypes.none,
});
this._handlePromise(this.props.promise);
}
}

public componentWillUnmount() {
this.unmounted = true;
}

// Promise resolver function
public _handlePromise(promise) {
// Store the current promise to fast exit if promise is change during handling
const currentPromise = promise;
this.setState({
status: statusTypes.pending,
});
promise
.then(success => {
// Escape early as promise is changed
if (currentPromise !== promise) {
return;
}
if (!this.unmounted) {
this.setState({
status: statusTypes.resolved,
value: success,
});
}
})
.catch(reason => {
// Escape early as promise is changed
if (currentPromise !== promise) {
return;
}
if (!this.unmounted) {
this.setState({
status: statusTypes.rejected,
value: reason,
});
}
});
}

public render() {
const { pending, resolved, rejected } = this.props;
const { status, value } = this.state;

switch (status) {
case statusTypes.none:
break;
case statusTypes.pending:
if (pending) {
return pending;
}
break;
case statusTypes.resolved:
if (resolved) {
return resolved(value);
}
break;
case statusTypes.rejected:
if (rejected) {
return rejected(value);
}
break;
default:
break;
}

return null;
}
}

export default Resolve;
6 changes: 6 additions & 0 deletions src/components/Resolve/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const statusTypes = {
none: 'none',
pending: 'pending',
rejected: 'rejected',
resolved: 'resolved',
};
1 change: 1 addition & 0 deletions src/components/Resolve/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './Resolve';
1 change: 1 addition & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './isEmptyChildren';
export * from './isPromise/isPromise';
31 changes: 31 additions & 0 deletions src/utils/isPromise/isPromise.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { isPromise } from './isPromise';

describe('isPromise type check', () => {
test('Should return positive if supplied a promise', () => {
const prom = new Promise(resolve => {
setTimeout(() => {
resolve(1);
}, 10);
});
const result = isPromise({ prom }, 'prom', 'component');
expect(result).toEqual(null);
});
test('Should return positive if supplied a nothing', () => {
const result = isPromise({}, 'prom', 'component');
expect(result).toEqual(null);
});
[
'not a promise',
a => a,
true,
1,
['contains', 'not', 'a', 'promise'],
null,
undefined,
].forEach(notPromise =>
test(`Should return negative if supplied a ${typeof notPromise}`, () => {
const result = isPromise({ prom: 'not a promise' }, 'prom', 'component');
expect(result).toMatchObject(new Error('prom in component is not a promise'));
}),
);
});
8 changes: 8 additions & 0 deletions src/utils/isPromise/isPromise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const isPromise = (props: object, propName: string, componentName: string) => {
if (props[propName]) {
return props[propName] instanceof Promise
? null
: new Error(`${propName} in ${componentName} is not a promise`);
}
return null;
};