/
Async.tsx
153 lines (128 loc) · 4.59 KB
/
Async.tsx
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
153
import * as React from 'react';
import { instanceOf } from 'prop-types';
import { assert } from './assert';
import { FieldFeedbacks, FieldFeedbacksChildContext } from './FieldFeedbacks';
import { FieldFeedbackValidation } from './FieldFeedbackValidation';
import { FormWithConstraints, FormWithConstraintsChildContext } from './FormWithConstraints';
import { InputElement } from './InputElement';
import { withValidateFieldEventEmitter } from './withValidateFieldEventEmitter';
export enum Status {
None,
Pending,
Rejected,
Resolved
}
export interface AsyncProps<T> {
promise: (value: string) => Promise<T>;
pending?: React.ReactNode;
then?: (value: T) => React.ReactNode;
catch?: (reason: any) => React.ReactNode;
}
interface AsyncState<T> {
status: Status;
value?: T | unknown /* Error */;
}
export interface AsyncChildContext {
async: Async<any>;
}
export type AsyncContext = FormWithConstraintsChildContext & FieldFeedbacksChildContext;
// [Asynchronous form errors and messages in AngularJS](https://jaysoo.ca/2014/10/14/async-form-errors-and-messages-in-angularjs/)
// [Support for asynchronous values (like Promises and Observables)](https://github.com/facebook/react/issues/6481)
// https://github.com/capaj/react-promise
// [How to render promises in React](https://gist.github.com/hex13/6d46f8b54631871ea8bf87576b635c49)
// Cannot be inside a separated npm package since FieldFeedback needs to attach itself to Async
class AsyncComponent<T = any> extends React.PureComponent<AsyncProps<T>, AsyncState<T>> {}
export class Async<T>
extends withValidateFieldEventEmitter<
// FieldFeedback returns FieldFeedbackValidation
FieldFeedbackValidation,
typeof AsyncComponent
>(AsyncComponent)
implements React.ChildContextProvider<AsyncChildContext>
{
static contextTypes: React.ValidationMap<AsyncContext> = {
form: instanceOf(FormWithConstraints).isRequired,
fieldFeedbacks: instanceOf(FieldFeedbacks).isRequired
};
context!: AsyncContext;
static childContextTypes: React.ValidationMap<AsyncChildContext> = {
async: instanceOf(Async).isRequired
};
getChildContext(): AsyncChildContext {
return {
async: this
};
}
state: AsyncState<T> = {
status: Status.None
};
componentDidMount() {
this.context.fieldFeedbacks.addValidateFieldEventListener(this.validate);
}
componentWillUnmount() {
this.context.fieldFeedbacks.removeValidateFieldEventListener(this.validate);
}
validate = (input: InputElement) => {
const { form, fieldFeedbacks } = this.context;
let validations;
const field = form.fieldsStore.getField(input.name)!;
if (
(fieldFeedbacks.props.stop === 'first' && field.hasFeedbacks(fieldFeedbacks.key)) ||
(fieldFeedbacks.props.stop === 'first-error' && field.hasErrors(fieldFeedbacks.key)) ||
(fieldFeedbacks.props.stop === 'first-warning' && field.hasWarnings(fieldFeedbacks.key)) ||
(fieldFeedbacks.props.stop === 'first-info' && field.hasInfos(fieldFeedbacks.key))
) {
// Reset UI
this.setState({ status: Status.None });
} else {
validations = this._validate(input);
}
return validations;
};
// FIXME
// With React 18, we have to wait for setState() to finish
// This way FieldFeedback is mounted and thus FieldFeedback.validate() is called thx to the event
async setStateSync(state: AsyncState<T>) {
return new Promise<void>(resolve => {
this.setState(state, resolve);
});
}
async _validate(input: InputElement) {
let state: AsyncState<T> = {
status: Status.Pending
};
this.setState(state);
try {
const value = await this.props.promise(input.value);
state = { status: Status.Resolved, value };
} catch (e) {
state = { status: Status.Rejected, value: e };
}
await this.setStateSync(state);
// FIXME
// With React 18, we have to wait for setState() to finish
// This way FieldFeedback is mounted and thus FieldFeedback.validate() is called thx to the event
// We could also "await wait(1)"
return this.emitValidateFieldEvent(input);
}
render() {
const { props, state } = this;
let element = null;
switch (state.status) {
case Status.None:
break;
case Status.Pending:
if (props.pending) element = props.pending;
break;
case Status.Resolved:
if (props.then) element = props.then(state.value);
break;
case Status.Rejected:
if (props.catch) element = props.catch(state.value);
break;
default:
assert(false, `Unknown status: '${state.status}'`);
}
return element;
}
}