Skip to content

Improve forwardRef documentation, add displayName argument #251

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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
21 changes: 15 additions & 6 deletions lib/react_client/component_factory.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:js/js.dart';
import 'package:react/react.dart';
import 'package:react/react_client.dart';
import 'package:react/react_client/js_backed_map.dart';
import 'package:react/react_client/private_utils.dart';
import 'package:react/react_client/react_interop.dart';

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

ReactJsComponentFactoryProxy(ReactClass jsClass,
{this.shouldConvertDomProps: true, this.alwaysReturnChildrenAsList: false})
: this.type = jsClass,
this.factory = React.createFactory(jsClass) {
final List<String> _additionalRefPropKeys;

ReactJsComponentFactoryProxy(
ReactClass jsClass, {
this.shouldConvertDomProps: true,
this.alwaysReturnChildrenAsList: false,
List<String> additionalRefPropKeys = const [],
}) : this.type = jsClass,
this.factory = React.createFactory(jsClass),
this._additionalRefPropKeys = additionalRefPropKeys {
if (jsClass == null) {
throw new ArgumentError('`jsClass` must not be null. '
'Ensure that the JS component class you\'re referencing is available and being accessed correctly.');
Expand All @@ -295,8 +302,10 @@ class ReactJsComponentFactoryProxy extends ReactComponentFactoryProxy {
@override
ReactElement build(Map props, [List childrenArgs]) {
dynamic children = generateChildren(childrenArgs, shouldAlwaysBeList: alwaysReturnChildrenAsList);
JsMap convertedProps =
generateJsProps(props, shouldConvertEventHandlers: shouldConvertDomProps, convertCallbackRefValue: false);
JsMap convertedProps = generateJsProps(props,
shouldConvertEventHandlers: shouldConvertDomProps,
convertCallbackRefValue: false,
additionalRefPropKeys: _additionalRefPropKeys);
return React.createElement(type, convertedProps, children);
}
}
Expand Down
5 changes: 5 additions & 0 deletions lib/react_client/js_interop_helpers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,8 @@ _convertDataTree(data) {

return _convert(data);
}

/// Keeps track of functions found when converting JS props to Dart props.
///
/// See: [forwardRef] for usage / context.
final isRawJsFunctionFromProps = Expando<bool>();
12 changes: 12 additions & 0 deletions lib/react_client/private_utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
@JS()
library react_client.private_utils;

import 'dart:js_util';

import 'package:js/js.dart';
import 'js_backed_map.dart';

@JS('Object.defineProperty')
external void defineProperty(dynamic object, String propertyName, JsMap descriptor);

String getJsFunctionName(Function object) => getProperty(object, 'name') ?? getProperty(object, '\$static_name');
103 changes: 99 additions & 4 deletions lib/react_client/react_interop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import 'package:react/react_client.dart' show ComponentFactory;
import 'package:react/react_client/bridge.dart';
import 'package:react/react_client/js_backed_map.dart';
import 'package:react/react_client/component_factory.dart' show ReactJsComponentFactoryProxy;
import 'package:react/react_client/js_interop_helpers.dart';
import 'package:react/react_client/private_utils.dart';
import 'package:react/src/react_client/dart2_interop_workaround_bindings.dart';

typedef ReactElement ReactJsComponentFactory(props, children);
Expand All @@ -32,6 +34,7 @@ typedef dynamic JsPropValidator(
@JS()
abstract class React {
external static String get version;
external static ReactElement cloneElement(ReactElement element, [JsMap props, dynamic children]);
external static ReactContext createContext([
dynamic defaultValue,
int Function(dynamic currentValue, dynamic nextValue) calculateChangedBits,
Expand Down Expand Up @@ -132,15 +135,107 @@ class JsRef {

/// Automatically passes a [Ref] through a component to one of its children.
///
/// __Example 1:__ Forwarding refs to DOM components
///
/// _[Analogous JS Demo](https://reactjs.org/docs/forwarding-refs.html#forwarding-refs-to-dom-components)_
///
/// ```dart
/// import 'dart:html';
/// import 'package:react/react.dart' as react;
///
/// // ---------- Component Definition ----------
/// final FancyButton = react.forwardRef((props, ref) {
/// return react.button({'ref': ref, 'className': 'FancyButton'}, 'Click me!');
/// }, displayName: 'FancyButton');
///
/// // ---------- Component Consumption ----------
/// void main() {
/// final ref = createRef<Element>();
///
/// react_dom.render(FancyButton({'ref': ref}));
///
/// // You can now get a ref directly to the DOM button:
/// final buttonNode = ref.current;
/// }
/// ```
///
/// __Example 2:__ Forwarding refs in higher-order components
///
/// _[Analogous JS Demo](https://reactjs.org/docs/forwarding-refs.html#forwarding-refs-in-higher-order-components)_
///
/// ```dart
/// import 'dart:html';
/// import 'package:react/react.dart' as react;
/// import 'package:react/react_client.dart' show setClientConfiguration, ReactJsComponentFactoryProxy;
/// import 'package:react/react_dom.dart' as react_dom;
///
/// // ---------- Component Definitions ----------
///
/// final FancyButton = react.forwardRef((props, ref) {
/// return react.button({'ref': ref, 'className': 'FancyButton'}, 'Click me!');
/// }, displayName: 'FancyButton');
///
/// class _LogProps extends react.Component2 {
/// @override
/// void componentDidUpdate(Map prevProps, _, [__]) {
/// print('old props: $prevProps');
/// print('new props: ${this.props}');
/// }
///
/// @override
/// render() {
/// final propsToForward = {...props}..remove('forwardedRef');
///
/// // Assign the custom prop `forwardedRef` as a ref on the component passed in via `props.component`
/// return props['component']({...propsToForward, 'ref': props['forwardedRef']}, props['children']);
/// }
/// }
/// final _logPropsHoc = react.registerComponent2(() => _LogProps());
///
/// final LogProps = react.forwardRef((props, ref) {
/// // Note the second param "ref" provided by react.forwardRef.
/// // We can pass it along to LogProps as a regular prop, e.g. "forwardedRef"
/// // And it can then be attached to the Component.
/// return _logPropsHoc({...props, 'forwardedRef': ref});
/// // Optional: Make the displayName more useful for the React dev tools.
/// // See: https://reactjs.org/docs/forwarding-refs.html#displaying-a-custom-name-in-devtools
/// }, displayName: 'LogProps(${_logPropsHoc.type.displayName})');
///
/// // ---------- Component Consumption ----------
/// void main() {
/// setClientConfiguration();
/// final ref = react.createRef<Element>();
///
/// react_dom.render(LogProps({'component': FancyButton, 'ref': ref}),
/// querySelector('#idOfSomeNodeInTheDom'));
///
/// // You can still get a ref directly to the DOM button:
/// final buttonNode = ref.current;
/// }
/// ```
/// See: <https://reactjs.org/docs/forwarding-refs.html>.
ReactJsComponentFactoryProxy forwardRef(Function(Map props, Ref ref) wrapperFunction) {
var hoc = React.forwardRef(allowInterop((JsMap props, JsRef ref) {
ReactJsComponentFactoryProxy forwardRef(
Function(Map props, Ref ref) wrapperFunction, {
String displayName = 'Anonymous',
}) {
final wrappedComponent = allowInterop((JsMap props, JsRef ref) {
final dartProps = JsBackedMap.backedBy(props);
for (var value in dartProps.values) {
if (value is Function) {
// Tag functions that came straight from the JS
// so that we know to pass them through as-is during prop conversion.
isRawJsFunctionFromProps[value] = true;
}
}

final dartRef = Ref.fromJs(ref);
return wrapperFunction(dartProps, dartRef);
}));
});
defineProperty(wrappedComponent, 'displayName', jsify({'value': displayName}));

var hoc = React.forwardRef(wrappedComponent);

return new ReactJsComponentFactoryProxy(hoc, shouldConvertDomProps: false);
return ReactJsComponentFactoryProxy(hoc);
}

abstract class ReactDom {
Expand Down
83 changes: 53 additions & 30 deletions lib/src/react_client/factory_util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ import 'package:react/src/typedefs.dart';

import 'event_prop_key_to_event_factory.dart';

@JS('Object.defineProperty')
external void defineProperty(dynamic object, String propertyName, JsMap descriptor);

/// Converts a list of variadic children arguments to children that should be passed to ReactJS.
///
/// Returns:
Expand All @@ -44,14 +41,26 @@ convertEventHandlers(Map args) {
args.forEach((propKey, value) {
var eventFactory = eventPropKeyToEventFactory[propKey];
if (eventFactory != null && value != null) {
// Apply allowInterop here so that the function we store in `originalEventHandlers`
// is the same one we'll retrieve from the JS props.
var reactDartConvertedEventHandler = allowInterop((events.SyntheticEvent e, [_, __]) {
value(eventFactory(e));
});

args[propKey] = reactDartConvertedEventHandler;
originalEventHandlers[reactDartConvertedEventHandler] = value;
// Don't attempt to convert functions that have already been converted, or functions
// that were passed in as JS props.
final handlerHasAlreadyBeenConverted = unconvertJsEventHandler(value) != null;
if (!handlerHasAlreadyBeenConverted && !(isRawJsFunctionFromProps[value] ?? false)) {
// Apply allowInterop here so that the function we store in [_originalEventHandlers]
// is the same one we'll retrieve from the JS props.
var reactDartConvertedEventHandler = allowInterop((e, [_, __]) {
// To support Dart code calling converted handlers,
// check for Dart events and pass them through directly.
// Otherwise, convert the JS events like normal.
if (e is SyntheticEvent) {
value(e);
} else {
value(eventFactory(e as events.SyntheticEvent));
}
});

args[propKey] = reactDartConvertedEventHandler;
originalEventHandlers[reactDartConvertedEventHandler] = value;
}
}
});
}
Expand All @@ -63,24 +72,34 @@ void convertRefValue(Map args) {
}
}

void convertRefValue2(Map args, {bool convertCallbackRefValue = true}) {
var ref = args['ref'];

if (ref is Ref) {
args['ref'] = ref.jsRef;
// If the ref is a callback, pass ReactJS a function that will call it
// with the Dart Component instance, not the ReactComponent instance.
//
// Use CallbackRef<Null> to check arity, since parameters could be non-dynamic, and thus
// would fail the `is CallbackRef<dynamic>` check.
// See https://github.com/dart-lang/sdk/issues/34593 for more information on arity checks.
} else if (ref is CallbackRef<Null> && convertCallbackRefValue) {
args['ref'] = allowInterop((dynamic instance) {
// Call as dynamic to perform dynamic dispatch, since we can't cast to CallbackRef<dynamic>,
// and since calling with non-null values will fail at runtime due to the CallbackRef<Null> typing.
if (instance is ReactComponent && instance.dartComponent != null) return (ref as dynamic)(instance.dartComponent);
return (ref as dynamic)(instance);
});
void convertRefValue2(
Map args, {
bool convertCallbackRefValue = true,
List<String> additionalRefPropKeys = const [],
}) {
final refKeys = ['ref', ...additionalRefPropKeys];

for (final refKey in refKeys) {
var ref = args[refKey];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#nit ref could be final

Suggested change
var ref = args[refKey];
final ref = args[refKey];

if (ref is Ref) {
args[refKey] = ref.jsRef;
// If the ref is a callback, pass ReactJS a function that will call it
// with the Dart Component instance, not the ReactComponent instance.
//
// Use _CallbackRef<Null> to check arity, since parameters could be non-dynamic, and thus
// would fail the `is _CallbackRef<dynamic>` check.
// See https://github.com/dart-lang/sdk/issues/34593 for more information on arity checks.
} else if (ref is CallbackRef<Null> && convertCallbackRefValue) {
args[refKey] = allowInterop((dynamic instance) {
// Call as dynamic to perform dynamic dispatch, since we can't cast to _CallbackRef<dynamic>,
// and since calling with non-null values will fail at runtime due to the _CallbackRef<Null> typing.
if (instance is ReactComponent && instance.dartComponent != null) {
return (ref as dynamic)(instance.dartComponent);
}

return (ref as dynamic)(instance);
});
}
}
}

Expand Down Expand Up @@ -126,11 +145,15 @@ JsMap generateJsProps(Map props,
{bool shouldConvertEventHandlers = true,
bool convertRefValue = true,
bool convertCallbackRefValue = true,
List<String> additionalRefPropKeys = const [],
bool wrapWithJsify = true}) {
final propsForJs = JsBackedMap.from(props);

if (shouldConvertEventHandlers) convertEventHandlers(propsForJs);
if (convertRefValue) convertRefValue2(propsForJs, convertCallbackRefValue: convertCallbackRefValue);
if (convertRefValue) {
convertRefValue2(propsForJs,
convertCallbackRefValue: convertCallbackRefValue, additionalRefPropKeys: additionalRefPropKeys);
}

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