| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152 |
63x
7x
7x
7x
6x
6x
1x
17x
17x
8x
9x
2x
7x
7x
7x
15x
15x
10x
10x
5x
10x
21x
21x
| import React, {PropTypes} from 'react';
import cx from 'classnames';
import omit from 'boundless-utils-omit-keys';
const get = (base, path, fallback) => path.split('.').reduce((current, fragment) => current[fragment] || fallback, base);
/**
* __A higher-order component for rendering data that isn't ready yet.__
*
* There are plenty of situations where you need to fetch content to be displayed, but want
* to show some sort of loading graphic in the interim. This component helps to simplify
* that pattern by handling common types of promises and providing a simple mechanism
* for materializing the fulfilled payload into JSX.
*/
export default class Async extends React.PureComponent {
static propTypes = {
/**
* any [React-supported attribute](https://facebook.github.io/react/docs/tags-and-attributes.html#html-attributes)
*/
'*': PropTypes.any,
/**
* a promise, function that returns a promise, or other type of renderable content; if a function is passed, it will
* be called with the current props
*
* Promise example:
*
* ```jsx
* const listDataPromise = fetch('/some/list/data/endpoint').then(
* (response) => response.ok ? response.json() : 'Failed to receive list data',
* (error) => error.message,
* ).then((payload) => {
* if (typeof payload === 'string') {
* return (<div className='error'>{payload}</div>);
* }
*
* return (
* <ul>
* {payload.map((item) => (<li key={item.id}>{item.content}</li>))}
* </ul>
* );
* });
*
* <Async>{listDataPromise}</Async>
*
* Function example:
*
* ```jsx
* const fetchListData = (props) => fetch(props['data-endpoint']).then(
* (response) => response.ok ? response.json() : 'Failed to receive list data',
* (error) => error.message,
* ).then((payload) => {
* if (typeof payload === 'string') {
* return (<div className='error'>{payload}</div>);
* }
*
* return (
* <ul>
* {payload.map((item) => (<li key={item.id}>{item.content}</li>))}
* </ul>
* );
* });
*
* <Async data-endpoint='/some/list/data/endpoint'>{fetchListData}</Async>
* ```
*/
children: PropTypes.oneOfType([
PropTypes.func,
PropTypes.node,
PropTypes.instanceOf(Promise),
]).isRequired,
/** a callback for when real content has been rendered; this will be called immediately if normal JSX is passed to Async, or, in the case of a promise, upon resolution or rejection */
childrenDidRender: PropTypes.func,
/** content to be shown while the promise is in "pending" state (like a loading graphic, perhaps) */
pendingContent: PropTypes.node,
}
static defaultProps = {
children: <div />,
childrenDidRender: () => {},
pendingContent: <div />,
}
static internalKeys = Object.keys(Async.defaultProps)
mounted = false
promise = null
state = {}
handlePromiseFulfillment(context, payload) {
Iif (!this.mounted) { return; }
// only set the component if the promise that is fulfilled matches
// the one we're tracking in state, otherwise ignore it and retain the previous data
this.setState(function renderPayloadIfPromiseMatches(state) {
if (this.promise === context) {
this.promise = null;
return {component: payload};
}
return state;
}, this.fireRenderCallback);
}
handleChildren(children) {
let content = children;
if (React.isValidElement(content)) {
return this.setState({component: content}, this.fireRenderCallback);
} else if (typeof content === 'function') {
return this.handleChildren(content(this.props));
}
const boundHandler = this.handlePromiseFulfillment.bind(this, content);
// this is kept outside state so it can be set immediately if the props change
this.promise = content;
this.setState({component: null}, () => content.then(boundHandler, boundHandler));
}
fireRenderCallback() {
Eif (this.state.component) {
this.props.childrenDidRender();
}
}
componentWillMount() { this.handleChildren(this.props.children); }
componentDidMount() { this.mounted = true; }
componentWillReceiveProps(nextProps) { this.handleChildren(nextProps.children); }
componentWillUnmount() { this.mounted = false; }
render() {
const {props, state} = this;
return React.cloneElement(state.component || props.pendingContent, {
...omit(props, Async.internalKeys),
className: cx(
'b-async',
props.className,
state.component === null && get(props, 'pendingContent.props.className'),
state.component && get(state, 'component.props.className', ''),
{'b-async-pending': state.component === null}
),
});
}
}
|