Skip to content

Commit 546472f

Browse files
Merge pull request #251 from cleandart/5.4.0/improve-forwardref-examples-and-typing
Improve forwardRef documentation, add displayName argument
2 parents c2171bf + cb28775 commit 546472f

10 files changed

+368
-50
lines changed

lib/react_client/component_factory.dart

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:js/js.dart';
88
import 'package:react/react.dart';
99
import 'package:react/react_client.dart';
1010
import 'package:react/react_client/js_backed_map.dart';
11+
import 'package:react/react_client/private_utils.dart';
1112
import 'package:react/react_client/react_interop.dart';
1213

1314
import 'package:react/src/context.dart';
@@ -282,10 +283,16 @@ class ReactJsComponentFactoryProxy extends ReactComponentFactoryProxy {
282283
/// Default: `false`
283284
final bool alwaysReturnChildrenAsList;
284285

285-
ReactJsComponentFactoryProxy(ReactClass jsClass,
286-
{this.shouldConvertDomProps: true, this.alwaysReturnChildrenAsList: false})
287-
: this.type = jsClass,
288-
this.factory = React.createFactory(jsClass) {
286+
final List<String> _additionalRefPropKeys;
287+
288+
ReactJsComponentFactoryProxy(
289+
ReactClass jsClass, {
290+
this.shouldConvertDomProps: true,
291+
this.alwaysReturnChildrenAsList: false,
292+
List<String> additionalRefPropKeys = const [],
293+
}) : this.type = jsClass,
294+
this.factory = React.createFactory(jsClass),
295+
this._additionalRefPropKeys = additionalRefPropKeys {
289296
if (jsClass == null) {
290297
throw new ArgumentError('`jsClass` must not be null. '
291298
'Ensure that the JS component class you\'re referencing is available and being accessed correctly.');
@@ -295,8 +302,10 @@ class ReactJsComponentFactoryProxy extends ReactComponentFactoryProxy {
295302
@override
296303
ReactElement build(Map props, [List childrenArgs]) {
297304
dynamic children = generateChildren(childrenArgs, shouldAlwaysBeList: alwaysReturnChildrenAsList);
298-
JsMap convertedProps =
299-
generateJsProps(props, shouldConvertEventHandlers: shouldConvertDomProps, convertCallbackRefValue: false);
305+
JsMap convertedProps = generateJsProps(props,
306+
shouldConvertEventHandlers: shouldConvertDomProps,
307+
convertCallbackRefValue: false,
308+
additionalRefPropKeys: _additionalRefPropKeys);
300309
return React.createElement(type, convertedProps, children);
301310
}
302311
}

lib/react_client/js_interop_helpers.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,8 @@ _convertDataTree(data) {
8888

8989
return _convert(data);
9090
}
91+
92+
/// Keeps track of functions found when converting JS props to Dart props.
93+
///
94+
/// See: [forwardRef] for usage / context.
95+
final isRawJsFunctionFromProps = Expando<bool>();

lib/react_client/private_utils.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
@JS()
2+
library react_client.private_utils;
3+
4+
import 'dart:js_util';
5+
6+
import 'package:js/js.dart';
7+
import 'js_backed_map.dart';
8+
9+
@JS('Object.defineProperty')
10+
external void defineProperty(dynamic object, String propertyName, JsMap descriptor);
11+
12+
String getJsFunctionName(Function object) => getProperty(object, 'name') ?? getProperty(object, '\$static_name');

lib/react_client/react_interop.dart

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import 'package:react/react_client.dart' show ComponentFactory;
1919
import 'package:react/react_client/bridge.dart';
2020
import 'package:react/react_client/js_backed_map.dart';
2121
import 'package:react/react_client/component_factory.dart' show ReactJsComponentFactoryProxy;
22+
import 'package:react/react_client/js_interop_helpers.dart';
23+
import 'package:react/react_client/private_utils.dart';
2224
import 'package:react/src/react_client/dart2_interop_workaround_bindings.dart';
2325

2426
typedef ReactElement ReactJsComponentFactory(props, children);
@@ -32,6 +34,7 @@ typedef dynamic JsPropValidator(
3234
@JS()
3335
abstract class React {
3436
external static String get version;
37+
external static ReactElement cloneElement(ReactElement element, [JsMap props, dynamic children]);
3538
external static ReactContext createContext([
3639
dynamic defaultValue,
3740
int Function(dynamic currentValue, dynamic nextValue) calculateChangedBits,
@@ -132,15 +135,107 @@ class JsRef {
132135

133136
/// Automatically passes a [Ref] through a component to one of its children.
134137
///
138+
/// __Example 1:__ Forwarding refs to DOM components
139+
///
140+
/// _[Analogous JS Demo](https://reactjs.org/docs/forwarding-refs.html#forwarding-refs-to-dom-components)_
141+
///
142+
/// ```dart
143+
/// import 'dart:html';
144+
/// import 'package:react/react.dart' as react;
145+
///
146+
/// // ---------- Component Definition ----------
147+
/// final FancyButton = react.forwardRef((props, ref) {
148+
/// return react.button({'ref': ref, 'className': 'FancyButton'}, 'Click me!');
149+
/// }, displayName: 'FancyButton');
150+
///
151+
/// // ---------- Component Consumption ----------
152+
/// void main() {
153+
/// final ref = createRef<Element>();
154+
///
155+
/// react_dom.render(FancyButton({'ref': ref}));
156+
///
157+
/// // You can now get a ref directly to the DOM button:
158+
/// final buttonNode = ref.current;
159+
/// }
160+
/// ```
161+
///
162+
/// __Example 2:__ Forwarding refs in higher-order components
163+
///
164+
/// _[Analogous JS Demo](https://reactjs.org/docs/forwarding-refs.html#forwarding-refs-in-higher-order-components)_
165+
///
166+
/// ```dart
167+
/// import 'dart:html';
168+
/// import 'package:react/react.dart' as react;
169+
/// import 'package:react/react_client.dart' show setClientConfiguration, ReactJsComponentFactoryProxy;
170+
/// import 'package:react/react_dom.dart' as react_dom;
171+
///
172+
/// // ---------- Component Definitions ----------
173+
///
174+
/// final FancyButton = react.forwardRef((props, ref) {
175+
/// return react.button({'ref': ref, 'className': 'FancyButton'}, 'Click me!');
176+
/// }, displayName: 'FancyButton');
177+
///
178+
/// class _LogProps extends react.Component2 {
179+
/// @override
180+
/// void componentDidUpdate(Map prevProps, _, [__]) {
181+
/// print('old props: $prevProps');
182+
/// print('new props: ${this.props}');
183+
/// }
184+
///
185+
/// @override
186+
/// render() {
187+
/// final propsToForward = {...props}..remove('forwardedRef');
188+
///
189+
/// // Assign the custom prop `forwardedRef` as a ref on the component passed in via `props.component`
190+
/// return props['component']({...propsToForward, 'ref': props['forwardedRef']}, props['children']);
191+
/// }
192+
/// }
193+
/// final _logPropsHoc = react.registerComponent2(() => _LogProps());
194+
///
195+
/// final LogProps = react.forwardRef((props, ref) {
196+
/// // Note the second param "ref" provided by react.forwardRef.
197+
/// // We can pass it along to LogProps as a regular prop, e.g. "forwardedRef"
198+
/// // And it can then be attached to the Component.
199+
/// return _logPropsHoc({...props, 'forwardedRef': ref});
200+
/// // Optional: Make the displayName more useful for the React dev tools.
201+
/// // See: https://reactjs.org/docs/forwarding-refs.html#displaying-a-custom-name-in-devtools
202+
/// }, displayName: 'LogProps(${_logPropsHoc.type.displayName})');
203+
///
204+
/// // ---------- Component Consumption ----------
205+
/// void main() {
206+
/// setClientConfiguration();
207+
/// final ref = react.createRef<Element>();
208+
///
209+
/// react_dom.render(LogProps({'component': FancyButton, 'ref': ref}),
210+
/// querySelector('#idOfSomeNodeInTheDom'));
211+
///
212+
/// // You can still get a ref directly to the DOM button:
213+
/// final buttonNode = ref.current;
214+
/// }
215+
/// ```
135216
/// See: <https://reactjs.org/docs/forwarding-refs.html>.
136-
ReactJsComponentFactoryProxy forwardRef(Function(Map props, Ref ref) wrapperFunction) {
137-
var hoc = React.forwardRef(allowInterop((JsMap props, JsRef ref) {
217+
ReactJsComponentFactoryProxy forwardRef(
218+
Function(Map props, Ref ref) wrapperFunction, {
219+
String displayName = 'Anonymous',
220+
}) {
221+
final wrappedComponent = allowInterop((JsMap props, JsRef ref) {
138222
final dartProps = JsBackedMap.backedBy(props);
223+
for (var value in dartProps.values) {
224+
if (value is Function) {
225+
// Tag functions that came straight from the JS
226+
// so that we know to pass them through as-is during prop conversion.
227+
isRawJsFunctionFromProps[value] = true;
228+
}
229+
}
230+
139231
final dartRef = Ref.fromJs(ref);
140232
return wrapperFunction(dartProps, dartRef);
141-
}));
233+
});
234+
defineProperty(wrappedComponent, 'displayName', jsify({'value': displayName}));
235+
236+
var hoc = React.forwardRef(wrappedComponent);
142237

143-
return new ReactJsComponentFactoryProxy(hoc, shouldConvertDomProps: false);
238+
return ReactJsComponentFactoryProxy(hoc);
144239
}
145240

146241
abstract class ReactDom {

lib/src/react_client/factory_util.dart

Lines changed: 53 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@ import 'package:react/src/typedefs.dart';
1717

1818
import 'event_prop_key_to_event_factory.dart';
1919

20-
@JS('Object.defineProperty')
21-
external void defineProperty(dynamic object, String propertyName, JsMap descriptor);
22-
2320
/// Converts a list of variadic children arguments to children that should be passed to ReactJS.
2421
///
2522
/// Returns:
@@ -44,14 +41,26 @@ convertEventHandlers(Map args) {
4441
args.forEach((propKey, value) {
4542
var eventFactory = eventPropKeyToEventFactory[propKey];
4643
if (eventFactory != null && value != null) {
47-
// Apply allowInterop here so that the function we store in `originalEventHandlers`
48-
// is the same one we'll retrieve from the JS props.
49-
var reactDartConvertedEventHandler = allowInterop((events.SyntheticEvent e, [_, __]) {
50-
value(eventFactory(e));
51-
});
52-
53-
args[propKey] = reactDartConvertedEventHandler;
54-
originalEventHandlers[reactDartConvertedEventHandler] = value;
44+
// Don't attempt to convert functions that have already been converted, or functions
45+
// that were passed in as JS props.
46+
final handlerHasAlreadyBeenConverted = unconvertJsEventHandler(value) != null;
47+
if (!handlerHasAlreadyBeenConverted && !(isRawJsFunctionFromProps[value] ?? false)) {
48+
// Apply allowInterop here so that the function we store in [_originalEventHandlers]
49+
// is the same one we'll retrieve from the JS props.
50+
var reactDartConvertedEventHandler = allowInterop((e, [_, __]) {
51+
// To support Dart code calling converted handlers,
52+
// check for Dart events and pass them through directly.
53+
// Otherwise, convert the JS events like normal.
54+
if (e is SyntheticEvent) {
55+
value(e);
56+
} else {
57+
value(eventFactory(e as events.SyntheticEvent));
58+
}
59+
});
60+
61+
args[propKey] = reactDartConvertedEventHandler;
62+
originalEventHandlers[reactDartConvertedEventHandler] = value;
63+
}
5564
}
5665
});
5766
}
@@ -63,24 +72,34 @@ void convertRefValue(Map args) {
6372
}
6473
}
6574

66-
void convertRefValue2(Map args, {bool convertCallbackRefValue = true}) {
67-
var ref = args['ref'];
68-
69-
if (ref is Ref) {
70-
args['ref'] = ref.jsRef;
71-
// If the ref is a callback, pass ReactJS a function that will call it
72-
// with the Dart Component instance, not the ReactComponent instance.
73-
//
74-
// Use CallbackRef<Null> to check arity, since parameters could be non-dynamic, and thus
75-
// would fail the `is CallbackRef<dynamic>` check.
76-
// See https://github.com/dart-lang/sdk/issues/34593 for more information on arity checks.
77-
} else if (ref is CallbackRef<Null> && convertCallbackRefValue) {
78-
args['ref'] = allowInterop((dynamic instance) {
79-
// Call as dynamic to perform dynamic dispatch, since we can't cast to CallbackRef<dynamic>,
80-
// and since calling with non-null values will fail at runtime due to the CallbackRef<Null> typing.
81-
if (instance is ReactComponent && instance.dartComponent != null) return (ref as dynamic)(instance.dartComponent);
82-
return (ref as dynamic)(instance);
83-
});
75+
void convertRefValue2(
76+
Map args, {
77+
bool convertCallbackRefValue = true,
78+
List<String> additionalRefPropKeys = const [],
79+
}) {
80+
final refKeys = ['ref', ...additionalRefPropKeys];
81+
82+
for (final refKey in refKeys) {
83+
var ref = args[refKey];
84+
if (ref is Ref) {
85+
args[refKey] = ref.jsRef;
86+
// If the ref is a callback, pass ReactJS a function that will call it
87+
// with the Dart Component instance, not the ReactComponent instance.
88+
//
89+
// Use _CallbackRef<Null> to check arity, since parameters could be non-dynamic, and thus
90+
// would fail the `is _CallbackRef<dynamic>` check.
91+
// See https://github.com/dart-lang/sdk/issues/34593 for more information on arity checks.
92+
} else if (ref is CallbackRef<Null> && convertCallbackRefValue) {
93+
args[refKey] = allowInterop((dynamic instance) {
94+
// Call as dynamic to perform dynamic dispatch, since we can't cast to _CallbackRef<dynamic>,
95+
// and since calling with non-null values will fail at runtime due to the _CallbackRef<Null> typing.
96+
if (instance is ReactComponent && instance.dartComponent != null) {
97+
return (ref as dynamic)(instance.dartComponent);
98+
}
99+
100+
return (ref as dynamic)(instance);
101+
});
102+
}
84103
}
85104
}
86105

@@ -126,11 +145,15 @@ JsMap generateJsProps(Map props,
126145
{bool shouldConvertEventHandlers = true,
127146
bool convertRefValue = true,
128147
bool convertCallbackRefValue = true,
148+
List<String> additionalRefPropKeys = const [],
129149
bool wrapWithJsify = true}) {
130150
final propsForJs = JsBackedMap.from(props);
131151

132152
if (shouldConvertEventHandlers) convertEventHandlers(propsForJs);
133-
if (convertRefValue) convertRefValue2(propsForJs, convertCallbackRefValue: convertCallbackRefValue);
153+
if (convertRefValue) {
154+
convertRefValue2(propsForJs,
155+
convertCallbackRefValue: convertCallbackRefValue, additionalRefPropKeys: additionalRefPropKeys);
156+
}
134157

135158
return wrapWithJsify ? jsifyAndAllowInterop(propsForJs) : propsForJs.jsObject;
136159
}

0 commit comments

Comments
 (0)