Skip to content

Commit bbc5c35

Browse files
elicwhitefacebook-github-bot
authored andcommitted
Convert to using forwardRef
Summary: TextInput now acts as a host component and can be passed directly to our new APIs that require a host component. Callsites no longer need to call ``` inputRef.getNativeRef() ``` We mutate the ref to the host component adding the imperative methods of the TextInput so you can still call `inputRef.clear` and `inputRef.isFocused`. Changelog: [General][Changed] TextInput now uses `forwardRef` allowing it to be used directly by new APIs requiring a host component. Reviewed By: yungsters Differential Revision: D18458408 fbshipit-source-id: 1f149fd575210d702fa0fdf3d05bb2162436a773
1 parent 99dc4e2 commit bbc5c35

File tree

4 files changed

+135
-81
lines changed

4 files changed

+135
-81
lines changed

Libraries/Components/TextInput/TextInput.js

+105-67
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,18 @@ const TextInputState = require('./TextInputState');
2121
const TouchableWithoutFeedback = require('../Touchable/TouchableWithoutFeedback');
2222

2323
const invariant = require('invariant');
24+
const nullthrows = require('nullthrows');
2425
const requireNativeComponent = require('../../ReactNative/requireNativeComponent');
26+
const setAndForwardRef = require('../../Utilities/setAndForwardRef');
2527

2628
import type {TextStyleProp, ViewStyleProp} from '../../StyleSheet/StyleSheet';
2729
import type {ColorValue} from '../../StyleSheet/StyleSheetTypes';
2830
import type {ViewProps} from '../View/ViewPropTypes';
2931
import type {SyntheticEvent, ScrollEvent} from '../../Types/CoreEventTypes';
3032
import type {PressEvent} from '../../Types/CoreEventTypes';
31-
import type {
32-
HostComponent,
33-
MeasureOnSuccessCallback,
34-
MeasureInWindowOnSuccessCallback,
35-
MeasureLayoutOnSuccessCallback,
36-
} from '../../Renderer/shims/ReactNativeTypes';
33+
import type {HostComponent} from '../../Renderer/shims/ReactNativeTypes';
34+
35+
type ReactRefSetter<T> = {current: null | T} | ((ref: null | T) => mixed);
3736

3837
let AndroidTextInput;
3938
let RCTMultilineTextInputView;
@@ -676,6 +675,10 @@ export type Props = $ReadOnly<{|
676675
* If `true`, contextMenuHidden is hidden. The default value is `false`.
677676
*/
678677
contextMenuHidden?: ?boolean,
678+
679+
forwardedRef?: ?ReactRefSetter<
680+
React.ElementRef<HostComponent<mixed>> & ImperativeMethods,
681+
>,
679682
|}>;
680683

681684
type DefaultProps = $ReadOnly<{|
@@ -684,11 +687,11 @@ type DefaultProps = $ReadOnly<{|
684687
underlineColorAndroid: 'transparent',
685688
|}>;
686689

687-
type State = {|
688-
currentlyFocusedField: typeof TextInputState.currentlyFocusedField,
689-
focusTextInput: typeof TextInputState.focusTextInput,
690-
blurTextInput: typeof TextInputState.blurTextInput,
691-
|};
690+
type ImperativeMethods = $ReadOnly<{|
691+
clear: () => void,
692+
isFocused: () => boolean,
693+
getNativeRef: () => ?React.ElementRef<HostComponent<mixed>>,
694+
|}>;
692695

693696
const emptyFunctionThatReturnsTrue = () => true;
694697

@@ -803,22 +806,14 @@ const emptyFunctionThatReturnsTrue = () => true;
803806
* or control this param programmatically with native code.
804807
*
805808
*/
806-
class TextInput extends React.Component<Props, State> {
809+
class InternalTextInput extends React.Component<Props> {
807810
static defaultProps: DefaultProps = {
808811
allowFontScaling: true,
809812
rejectResponderTermination: true,
810813
underlineColorAndroid: 'transparent',
811814
};
812815

813-
static propTypes = DeprecatedTextInputPropTypes;
814-
815-
static State: State = {
816-
currentlyFocusedField: TextInputState.currentlyFocusedField,
817-
focusTextInput: TextInputState.focusTextInput,
818-
blurTextInput: TextInputState.blurTextInput,
819-
};
820-
821-
_inputRef: ?React.ElementRef<HostComponent<mixed>> = null;
816+
_inputRef: null | React.ElementRef<HostComponent<mixed>> = null;
822817
_focusSubscription: ?Function = undefined;
823818
_lastNativeText: ?Stringish = null;
824819
_lastNativeSelection: ?Selection = null;
@@ -833,7 +828,11 @@ class TextInput extends React.Component<Props, State> {
833828
}
834829

835830
if (this.props.autoFocus) {
836-
this._rafId = requestAnimationFrame(this.focus);
831+
this._rafId = requestAnimationFrame(() => {
832+
if (this._inputRef) {
833+
this._inputRef.focus();
834+
}
835+
});
837836
}
838837
}
839838

@@ -874,7 +873,7 @@ class TextInput extends React.Component<Props, State> {
874873
componentWillUnmount() {
875874
this._focusSubscription && this._focusSubscription.remove();
876875
if (this.isFocused()) {
877-
this.blur();
876+
nullthrows(this._inputRef).blur();
878877
}
879878
const tag = ReactNative.findNodeHandle(this._inputRef);
880879
if (tag != null) {
@@ -889,7 +888,9 @@ class TextInput extends React.Component<Props, State> {
889888
* Removes all text from the `TextInput`.
890889
*/
891890
clear: () => void = () => {
892-
this.setNativeProps({text: ''});
891+
if (this._inputRef != null) {
892+
this._inputRef.setNativeProps({text: ''});
893+
}
893894
};
894895

895896
/**
@@ -906,35 +907,6 @@ class TextInput extends React.Component<Props, State> {
906907
return this._inputRef;
907908
};
908909

909-
// From NativeMethodsMixin
910-
// We need these instead of using forwardRef because we also have the other
911-
// methods we expose
912-
blur: () => void = () => {
913-
this._inputRef && this._inputRef.blur();
914-
};
915-
focus: () => void = () => {
916-
this._inputRef && this._inputRef.focus();
917-
};
918-
measure: (callback: MeasureOnSuccessCallback) => void = callback => {
919-
this._inputRef && this._inputRef.measure(callback);
920-
};
921-
measureInWindow: (
922-
callback: MeasureInWindowOnSuccessCallback,
923-
) => void = callback => {
924-
this._inputRef && this._inputRef.measureInWindow(callback);
925-
};
926-
measureLayout: (
927-
relativeToNativeNode: number | React.ElementRef<HostComponent<mixed>>,
928-
onSuccess: MeasureLayoutOnSuccessCallback,
929-
onFail?: () => void,
930-
) => void = (relativeToNativeNode, onSuccess, onFail) => {
931-
this._inputRef &&
932-
this._inputRef.measureLayout(relativeToNativeNode, onSuccess, onFail);
933-
};
934-
setNativeProps: (nativeProps: Object) => void = nativeProps => {
935-
this._inputRef && this._inputRef.setNativeProps(nativeProps);
936-
};
937-
938910
render(): React.Node {
939911
let textInput = null;
940912
let additionalTouchableProps: {|
@@ -1045,13 +1017,44 @@ class TextInput extends React.Component<Props, State> {
10451017
: '';
10461018
}
10471019

1048-
_setNativeRef = (ref: any) => {
1049-
this._inputRef = ref;
1050-
};
1020+
_setNativeRef = setAndForwardRef({
1021+
getForwardedRef: () => this.props.forwardedRef,
1022+
setLocalRef: ref => {
1023+
this._inputRef = ref;
1024+
1025+
/*
1026+
Hi reader from the future. I'm sorry for this.
1027+
1028+
This is a hack. Ideally we would forwardRef to the underlying
1029+
host component. However, since TextInput has it's own methods that can be
1030+
called as well, if we used the standard forwardRef then these
1031+
methods wouldn't be accessible and thus be a breaking change.
1032+
1033+
We have a couple of options of how to handle this:
1034+
- Return a new ref with everything we methods from both. This is problematic
1035+
because we need React to also know it is a host component which requires
1036+
internals of the class implementation of the ref.
1037+
- Break the API and have some other way to call one set of the methods or
1038+
the other. This is our long term approach as we want to eventually
1039+
get the methods on host components off the ref. So instead of calling
1040+
ref.measure() you might call ReactNative.measure(ref). This would hopefully
1041+
let the ref for TextInput then have the methods like `.clear`. Or we do it
1042+
the other way and make it TextInput.clear(textInputRef) which would be fine
1043+
too. Either way though is a breaking change that is longer term.
1044+
- Mutate this ref. :( Gross, but accomplishes what we need in the meantime
1045+
before we can get to the long term breaking change.
1046+
*/
1047+
if (ref) {
1048+
ref.clear = this.clear;
1049+
ref.isFocused = this.isFocused;
1050+
ref.getNativeRef = this.getNativeRef;
1051+
}
1052+
},
1053+
});
10511054

10521055
_onPress = (event: PressEvent) => {
10531056
if (this.props.editable || this.props.editable === undefined) {
1054-
this.focus();
1057+
nullthrows(this._inputRef).focus();
10551058
}
10561059
};
10571060

@@ -1117,16 +1120,44 @@ class TextInput extends React.Component<Props, State> {
11171120
};
11181121
}
11191122

1120-
class InternalTextInputType extends ReactNative.NativeComponent<Props> {
1121-
clear() {}
1122-
1123-
getNativeRef(): ?React.ElementRef<HostComponent<mixed>> {}
1124-
1125-
// $FlowFixMe
1126-
isFocused(): boolean {}
1127-
}
1123+
const ExportedForwardRef: React.AbstractComponent<
1124+
React.ElementConfig<typeof InternalTextInput>,
1125+
React.ElementRef<HostComponent<mixed>> & ImperativeMethods,
1126+
> = React.forwardRef(function TextInput(
1127+
props,
1128+
forwardedRef: ReactRefSetter<
1129+
React.ElementRef<HostComponent<mixed>> & ImperativeMethods,
1130+
>,
1131+
) {
1132+
return <InternalTextInput {...props} forwardedRef={forwardedRef} />;
1133+
});
11281134

1129-
const TypedTextInput = ((TextInput: any): Class<InternalTextInputType>);
1135+
// $FlowFixMe
1136+
ExportedForwardRef.defaultProps = {
1137+
allowFontScaling: true,
1138+
rejectResponderTermination: true,
1139+
underlineColorAndroid: 'transparent',
1140+
};
1141+
1142+
// TODO: Deprecate this
1143+
// $FlowFixMe
1144+
ExportedForwardRef.propTypes = DeprecatedTextInputPropTypes;
1145+
1146+
// $FlowFixMe
1147+
ExportedForwardRef.State = {
1148+
currentlyFocusedField: TextInputState.currentlyFocusedField,
1149+
focusTextInput: TextInputState.focusTextInput,
1150+
blurTextInput: TextInputState.blurTextInput,
1151+
};
1152+
1153+
type TextInputComponentStatics = $ReadOnly<{|
1154+
State: $ReadOnly<{|
1155+
currentlyFocusedField: typeof TextInputState.currentlyFocusedField,
1156+
focusTextInput: typeof TextInputState.focusTextInput,
1157+
blurTextInput: typeof TextInputState.blurTextInput,
1158+
|}>,
1159+
propTypes: typeof DeprecatedTextInputPropTypes,
1160+
|}>;
11301161

11311162
const styles = StyleSheet.create({
11321163
multilineInput: {
@@ -1137,4 +1168,11 @@ const styles = StyleSheet.create({
11371168
},
11381169
});
11391170

1140-
module.exports = TypedTextInput;
1171+
module.exports = ((ExportedForwardRef: any): React.AbstractComponent<
1172+
React.ElementConfig<typeof InternalTextInput>,
1173+
$ReadOnly<{|
1174+
...React.ElementRef<HostComponent<mixed>>,
1175+
...ImperativeMethods,
1176+
|}>,
1177+
> &
1178+
TextInputComponentStatics);

Libraries/Components/TextInput/__tests__/TextInput-test.js

+17-9
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ const ReactTestRenderer = require('react-test-renderer');
1616
const TextInput = require('../TextInput');
1717
const ReactNative = require('../../../Renderer/shims/ReactNative');
1818

19-
import type {FocusEvent} from '../TextInput';
2019
import Component from '@reactions/component';
2120

2221
const {enter} = require('../../../Utilities/ReactNativeTestTools');
@@ -25,16 +24,19 @@ jest.unmock('../TextInput');
2524

2625
describe('TextInput tests', () => {
2726
let input;
27+
let inputRef;
2828
let onChangeListener;
2929
let onChangeTextListener;
3030
const initialValue = 'initialValue';
3131
beforeEach(() => {
32+
inputRef = React.createRef(null);
3233
onChangeListener = jest.fn();
3334
onChangeTextListener = jest.fn();
3435
const renderTree = ReactTestRenderer.create(
3536
<Component initialState={{text: initialValue}}>
3637
{({setState, state}) => (
3738
<TextInput
39+
ref={inputRef}
3840
value={state.text}
3941
onChangeText={text => {
4042
onChangeTextListener(text);
@@ -50,14 +52,20 @@ describe('TextInput tests', () => {
5052
input = renderTree.root.findByType(TextInput);
5153
});
5254
it('has expected instance functions', () => {
53-
expect(input.instance.isFocused).toBeInstanceOf(Function); // Would have prevented S168585
54-
expect(input.instance.clear).toBeInstanceOf(Function);
55-
expect(input.instance.focus).toBeInstanceOf(Function);
56-
expect(input.instance.blur).toBeInstanceOf(Function);
57-
expect(input.instance.setNativeProps).toBeInstanceOf(Function);
58-
expect(input.instance.measure).toBeInstanceOf(Function);
59-
expect(input.instance.measureInWindow).toBeInstanceOf(Function);
60-
expect(input.instance.measureLayout).toBeInstanceOf(Function);
55+
expect(inputRef.current.isFocused).toBeInstanceOf(Function); // Would have prevented S168585
56+
expect(inputRef.current.clear).toBeInstanceOf(Function);
57+
expect(inputRef.current.focus).toBeInstanceOf(jest.fn().constructor);
58+
expect(inputRef.current.blur).toBeInstanceOf(jest.fn().constructor);
59+
expect(inputRef.current.setNativeProps).toBeInstanceOf(
60+
jest.fn().constructor,
61+
);
62+
expect(inputRef.current.measure).toBeInstanceOf(jest.fn().constructor);
63+
expect(inputRef.current.measureInWindow).toBeInstanceOf(
64+
jest.fn().constructor,
65+
);
66+
expect(inputRef.current.measureLayout).toBeInstanceOf(
67+
jest.fn().constructor,
68+
);
6169
});
6270
it('calls onChange callbacks', () => {
6371
expect(input.props.value).toBe(initialValue);

Libraries/Renderer/shims/ReactNativeTypes.js

+1-4
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,7 @@ export type NativeMethods = {
119119
};
120120

121121
export type NativeMethodsMixinType = NativeMethods;
122-
export type HostComponent<T> = AbstractComponent<
123-
T,
124-
$ReadOnly<$Exact<NativeMethods>>,
125-
>;
122+
export type HostComponent<T> = AbstractComponent<T, $ReadOnly<NativeMethods>>;
126123

127124
type SecretInternalsType = {
128125
NativeMethodsMixin: NativeMethodsMixinType,

jest/setup.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,10 @@ jest
8989
mockComponent('../Libraries/Text/Text', MockNativeMethods),
9090
)
9191
.mock('../Libraries/Components/TextInput/TextInput', () =>
92-
mockComponent('../Libraries/Components/TextInput/TextInput'),
92+
mockComponent(
93+
'../Libraries/Components/TextInput/TextInput',
94+
MockNativeMethods,
95+
),
9396
)
9497
.mock('../Libraries/Modal/Modal', () =>
9598
mockComponent('../Libraries/Modal/Modal'),
@@ -312,6 +315,14 @@ jest
312315
render() {
313316
return React.createElement(viewName, this.props, this.props.children);
314317
}
318+
319+
// The methods that exist on host components
320+
blur = jest.fn();
321+
focus = jest.fn();
322+
measure = jest.fn();
323+
measureInWindow = jest.fn();
324+
measureLayout = jest.fn();
325+
setNativeProps = jest.fn();
315326
};
316327

317328
if (viewName === 'RCTView') {

0 commit comments

Comments
 (0)