Skip to content

CPLAT-9327 Improve forwardRef documentation, add displayName argument #454

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
163 changes: 133 additions & 30 deletions lib/src/component/ref_util.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import 'dart:js_util';

import 'package:over_react/src/component_declaration/component_type_checking.dart';
import 'package:react/react_client/react_interop.dart' as react_interop;
import 'package:react/react_client.dart';
Expand Down Expand Up @@ -52,52 +54,153 @@ Ref<T> createRef<T>() {
///
/// > __NOTE:__ This should only be used to wrap components that extend from `Component2`.
///
/// __Example__:
/// __Example 1:__ Forwarding refs to DOM components
///
/// UiFactory<DomProps> DivForwarded = forwardRef<DomProps>((props, ref) {
/// return (Dom.div()
/// ..ref = ref
/// ..className = 'special-class'
/// )(
/// props.children
/// );
/// })(Dom.div);
/// ```dart
/// import 'dart:html';
/// import 'package:over_react/over_react.dart';
/// import 'package:over_react/react_dom.dart' as react_dom;
///
/// ___ OR ___
/// // ---------- Component Definition ----------
///
/// UiFactory<FooProps> FooForwarded = forwardRef<FooProps>((props, ref) {
/// return (Foo()
/// ..forwardedRef = ref
/// )();
/// })(Foo);
/// final FancyButton = forwardRef<DomProps>((props, ref) {
/// final classes = ClassNameBuilder.fromProps(props)
/// ..add('FancyButton');
///
/// UiFactory<FooProps> Foo = _$Foo;
/// return (Dom.button()
/// ..addProps(getPropsToForward(props, onlyCopyDomProps: true))
/// ..className = classes.toClassName()
/// ..ref = ref
/// )('Click me!');
/// })(Dom.button);
///
/// mixin FooProps on UiProps {
/// Ref forwardedRef;
/// }
/// // ---------- Component Consumption ----------
///
/// @Component2()
/// class FooComponent extends UiComponent2<FooProps> {
/// @override
/// render() {
/// return (Dom.button()
/// ..ref = props.forwardedRef
/// )('Click this button');
/// }
/// }
/// void main() {
/// setClientConfiguration();
/// final ref = createRef<Element>();
///
/// react_dom.render(
/// (FancyButton()
/// ..ref = ref
/// ..onClick = (_) {
/// print(ref.current.outerHtml);
/// }
/// )(),
/// querySelector('#idOfSomeNodeInTheDom')
/// );
///
/// // You can still get a ref directly to the DOM button:
/// final buttonNode = ref.current;
/// }
/// ```
///
/// __Example 2:__ Forwarding refs in higher-order components
///
/// ```dart
/// import 'dart:html';
/// import 'package:over_react/over_react.dart';
/// import 'package:over_react/react_dom.dart' as react_dom;
///
/// // ---------- Component Definitions ----------
///
/// final FancyButton = forwardRef<DomProps>((props, ref) {
/// final classes = ClassNameBuilder.fromProps(props)
/// ..add('FancyButton');
///
/// return (Dom.button()
/// ..addProps(getPropsToForward(props, onlyCopyDomProps: true))
/// ..className = classes.toClassName()
/// ..ref = ref
/// )('Click me!');
/// })(Dom.button);
///
/// final LogProps = forwardRef<LogPropsProps>((props, ref) {
/// return (_LogProps()
/// ..addProps(props)
/// .._forwardedRef = ref
/// )('Click me!');
/// })(_LogProps);
///
/// UiFactory<LogPropsProps> _LogProps = _$_LogProps;
///
/// mixin LogPropsProps on UiProps {
Copy link
Contributor

Choose a reason for hiding this comment

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

Please double check the boilerplate here - I updated this example from what it was previously.

/// BuilderOnlyUiFactory<DomProps> builder;
///
/// // Private since we only use this to pass along the value of `ref` to
/// // the return value of forwardRef.
/// //
/// // Consumers can set this private field value using the public `ref` setter.
/// Ref _forwardedRef;
/// }
///
/// @Component2()
/// class LogPropsComponent extends UiComponent2<LogPropsProps> {
/// @override
/// void componentDidUpdate(Map prevProps, _, [__]) {
/// print('old props: $prevProps');
/// print('new props: $props');
/// }
///
/// @override
/// render() {
/// return (props.builder()
/// ..modifyProps(addUnconsumedDomProps)
/// ..ref = props._forwardedRef
/// )(props.children);
/// }
/// }
///
/// // ---------- Component Consumption ----------
///
/// void main() {
/// setClientConfiguration();
/// final ref = createRef<Element>();
///
/// react_dom.render(
/// (LogProps()
/// ..builder = FancyButton
/// ..className = 'btn btn-primary'
/// ..ref = ref
/// ..onClick = (_) {
/// print(ref.current.outerHtml);
/// }
/// )(),
/// querySelector('#idOfSomeNodeInTheDom')
/// );
///
/// // You can still get a ref directly to the DOM button:
/// final buttonNode = ref.current;
/// }
/// ```
///
/// Learn more: <https://reactjs.org/docs/forwarding-refs.html>.
UiFactory<TProps> Function(UiFactory<TProps>) forwardRef<TProps extends UiProps>(
Function(TProps props, Ref ref) wrapperFunction) {
Function(TProps props, Ref ref) wrapperFunction, {String displayName}) {

UiFactory<TProps> wrapWithForwardRef(UiFactory<TProps> factory) {
enforceMinimumComponentVersionFor(factory().componentFactory);

if (displayName == null) {
final componentFactoryType = factory().componentFactory.type;
if (componentFactoryType is String) {
// DomComponent
displayName = componentFactoryType;
} else if (componentFactoryType is Function) {
// JS component factories, Dart function components, Dart composite components
displayName = getProperty(componentFactoryType, 'displayName');

if (displayName == null) {
final ctor = getProperty(componentFactoryType, 'constructor');
displayName = (ctor == null ? null : getProperty(ctor, 'name')) ?? 'Anonymous';
}
}
}

Object wrapProps(Map props, Ref ref) {
return wrapperFunction(factory(props), ref);
}
ReactComponentFactoryProxy hoc = react_interop.forwardRef(wrapProps);
ReactComponentFactoryProxy hoc = react_interop.forwardRef(wrapProps, displayName: displayName);
setComponentTypeMeta(hoc, isHoc: true, parentType: factory().componentFactory);

TProps forwardedFactory([Map props]) {
Expand Down
53 changes: 53 additions & 0 deletions test/over_react/component/forward_ref_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
library forward_ref_test;

import 'dart:html';
import 'dart:js_util';

import 'package:test/test.dart';
import 'package:over_react/over_react.dart';
Expand Down Expand Up @@ -56,12 +57,64 @@ main() {

expect(refObject.current, TypeMatcher<DivElement>());
});

group('- sets displayName on the rendered component as expected', () {
test('when displayName argument is not passed to forwardRef', () {
UiFactory<DomProps> DivForwarded = forwardRef<DomProps>((props, ref) {
return (Dom.div()
..ref = ref
)();
})(Dom.div);

final refObject = createRef<DivElement>();
final vDomElement = (DivForwarded()..ref = refObject)();

expect(getProperty(getProperty(vDomElement.type, 'render'), 'displayName'), 'div');
});

test('when displayName argument is passed to forwardRef', () {
UiFactory<DomProps> DivForwarded = forwardRef<DomProps>((props, ref) {
return (Dom.div()
..ref = ref
)();
}, displayName: 'SomethingCustom')(Dom.div);

final refObject = createRef<DivElement>();
final vDomElement = (DivForwarded()..ref = refObject)();

expect(getProperty(getProperty(vDomElement.type, 'render'), 'displayName'), 'SomethingCustom');
});
});
});

group('on a component with a dart component child', () {
forwardRefTest(BasicChild, verifyRefValue: (ref) {
expect(ref, TypeMatcher<BasicChildComponent>());
});

group('- sets displayName on the rendered component as expected', () {
test('when displayName argument is not passed to forwardRef', () {
UiFactory<BasicProps> BasicForwarded = forwardRef<BasicProps>((props, ref) {
return (BasicChild()..ref = ref)();
})(Basic);

final Ref refObject = createRef();
final vDomElement = (BasicForwarded()..ref = refObject)();

expect(getProperty(getProperty(vDomElement.type, 'render'), 'displayName'), 'Basic');
});

test('when displayName argument is passed to forwardRef', () {
UiFactory<BasicProps> BasicForwarded = forwardRef<BasicProps>((props, ref) {
return (BasicChild()..ref = ref)();
}, displayName: 'BasicForwarded')(Basic);

final Ref refObject = createRef();
final vDomElement = (BasicForwarded()..ref = refObject)();

expect(getProperty(getProperty(vDomElement.type, 'render'), 'displayName'), 'BasicForwarded');
});
});
});
});
}
Expand Down
13 changes: 13 additions & 0 deletions web/component2/index.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import 'src/demos.dart';

void main() {
setClientConfiguration();
final fancyButtonNodeRef = createRef<Element>();

react_dom.render(
buttonExamplesDemo(), querySelector('$demoMountNodeSelectorPrefix--button'));
Expand All @@ -41,6 +42,18 @@ void main() {
react_dom.render(
radioToggleButtonDemo(), querySelector('$demoMountNodeSelectorPrefix--radio-toggle'));

react_dom.render(
(LogProps()
..builder = FancyButton
..className = 'btn btn-primary'
..ref = fancyButtonNodeRef
..onClick = (_) {
print(fancyButtonNodeRef.current.outerHtml);
}
)(),
querySelector('$demoMountNodeSelectorPrefix--forwardRef'),
);

react_dom.render(
(v2.ErrorBoundary()
..onComponentDidCatch = (error, info) {
Expand Down
6 changes: 6 additions & 0 deletions web/component2/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ <h2>ToggleButton (Radio)</h2>
around with the component rendered below.</p>
<div class="component-demo__mount--radio-toggle"></div>
</div>
<div class="col-xs-12 col-lg-6 p-3">
<h2>ForwardRef</h2>
<p>Modify the source of <code>/web/component2/src/demos/forward_ref.dart</code> to play
around with the component rendered below.</p>
<div class="component-demo__mount--forwardRef"></div>
</div>
<div class="col-xs-12 col-lg-6 p-3">
<h2>Component That Throws A Caught Error</h2>
<p><strong>(Deprecated ErrorBoundary Component)</strong></p>
Expand Down
1 change: 1 addition & 0 deletions web/component2/src/demos.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export 'demos/button/button-active.dart';
export 'demos/button/button-disabled.dart';

export 'demos/custom_error_boundary.dart';
export 'demos/forward_ref.dart';

export '../src/demo_components/prop_validation_wrap.dart';

Expand Down
53 changes: 53 additions & 0 deletions web/component2/src/demos/forward_ref.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import 'package:over_react/over_react.dart';

// ignore: uri_has_not_been_generated
part 'forward_ref.over_react.g.dart';

final FancyButton = forwardRef<DomProps>((props, ref) {
final classes = ClassNameBuilder.fromProps(props)
..add('FancyButton');

return (Dom.button()
..addProps(getPropsToForward(props, onlyCopyDomProps: true))
..className = classes.toClassName()
..ref = ref
)('Click me!');
}, displayName: 'FancyButton')(Dom.button);

final LogProps = forwardRef<LogPropsProps>((props, ref) {
return (_LogProps()
..addProps(props)
.._forwardedRef = ref
)('Click me!');
}, displayName: 'LogProps')(_LogProps);

@Factory()
UiFactory<LogPropsProps> _LogProps = _$_LogProps;

@Props()
class _$LogPropsProps extends UiProps {
BuilderOnlyUiFactory<DomProps> builder;

// Private since we only use this to pass along the value of `ref` to
// the return value of forwardRef.
//
// Consumers can set this private field value using the public `ref` setter.
Ref _forwardedRef;
}

@Component2()
class LogPropsComponent extends UiComponent2<LogPropsProps> {
@override
void componentDidUpdate(Map prevProps, _, [__]) {
print('old props: $prevProps');
print('new props: $props');
}

@override
render() {
return (props.builder()
..modifyProps(addUnconsumedDomProps)
..ref = props._forwardedRef
)(props.children);
}
}
Loading