Skip to content

Commit a35d59f

Browse files
Merge pull request #454 from Workiva/improve-forwardref-documentation
CPLAT-9327 Improve forwardRef documentation, add displayName argument
2 parents 5ca5761 + 04b7b2c commit a35d59f

File tree

7 files changed

+440
-30
lines changed

7 files changed

+440
-30
lines changed

lib/src/component/ref_util.dart

Lines changed: 133 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
import 'dart:js_util';
16+
1517
import 'package:over_react/src/component_declaration/component_type_checking.dart';
1618
import 'package:react/react_client/react_interop.dart' as react_interop;
1719
import 'package:react/react_client.dart';
@@ -52,52 +54,153 @@ Ref<T> createRef<T>() {
5254
///
5355
/// > __NOTE:__ This should only be used to wrap components that extend from `Component2`.
5456
///
55-
/// __Example__:
57+
/// __Example 1:__ Forwarding refs to DOM components
5658
///
57-
/// UiFactory<DomProps> DivForwarded = forwardRef<DomProps>((props, ref) {
58-
/// return (Dom.div()
59-
/// ..ref = ref
60-
/// ..className = 'special-class'
61-
/// )(
62-
/// props.children
63-
/// );
64-
/// })(Dom.div);
59+
/// ```dart
60+
/// import 'dart:html';
61+
/// import 'package:over_react/over_react.dart';
62+
/// import 'package:over_react/react_dom.dart' as react_dom;
6563
///
66-
/// ___ OR ___
64+
/// // ---------- Component Definition ----------
6765
///
68-
/// UiFactory<FooProps> FooForwarded = forwardRef<FooProps>((props, ref) {
69-
/// return (Foo()
70-
/// ..forwardedRef = ref
71-
/// )();
72-
/// })(Foo);
66+
/// final FancyButton = forwardRef<DomProps>((props, ref) {
67+
/// final classes = ClassNameBuilder.fromProps(props)
68+
/// ..add('FancyButton');
7369
///
74-
/// UiFactory<FooProps> Foo = _$Foo;
70+
/// return (Dom.button()
71+
/// ..addProps(getPropsToForward(props, onlyCopyDomProps: true))
72+
/// ..className = classes.toClassName()
73+
/// ..ref = ref
74+
/// )('Click me!');
75+
/// })(Dom.button);
7576
///
76-
/// mixin FooProps on UiProps {
77-
/// Ref forwardedRef;
78-
/// }
77+
/// // ---------- Component Consumption ----------
7978
///
80-
/// @Component2()
81-
/// class FooComponent extends UiComponent2<FooProps> {
82-
/// @override
83-
/// render() {
84-
/// return (Dom.button()
85-
/// ..ref = props.forwardedRef
86-
/// )('Click this button');
87-
/// }
88-
/// }
79+
/// void main() {
80+
/// setClientConfiguration();
81+
/// final ref = createRef<Element>();
82+
///
83+
/// react_dom.render(
84+
/// (FancyButton()
85+
/// ..ref = ref
86+
/// ..onClick = (_) {
87+
/// print(ref.current.outerHtml);
88+
/// }
89+
/// )(),
90+
/// querySelector('#idOfSomeNodeInTheDom')
91+
/// );
92+
///
93+
/// // You can still get a ref directly to the DOM button:
94+
/// final buttonNode = ref.current;
95+
/// }
96+
/// ```
97+
///
98+
/// __Example 2:__ Forwarding refs in higher-order components
99+
///
100+
/// ```dart
101+
/// import 'dart:html';
102+
/// import 'package:over_react/over_react.dart';
103+
/// import 'package:over_react/react_dom.dart' as react_dom;
104+
///
105+
/// // ---------- Component Definitions ----------
106+
///
107+
/// final FancyButton = forwardRef<DomProps>((props, ref) {
108+
/// final classes = ClassNameBuilder.fromProps(props)
109+
/// ..add('FancyButton');
110+
///
111+
/// return (Dom.button()
112+
/// ..addProps(getPropsToForward(props, onlyCopyDomProps: true))
113+
/// ..className = classes.toClassName()
114+
/// ..ref = ref
115+
/// )('Click me!');
116+
/// })(Dom.button);
117+
///
118+
/// final LogProps = forwardRef<LogPropsProps>((props, ref) {
119+
/// return (_LogProps()
120+
/// ..addProps(props)
121+
/// .._forwardedRef = ref
122+
/// )('Click me!');
123+
/// })(_LogProps);
124+
///
125+
/// UiFactory<LogPropsProps> _LogProps = _$_LogProps;
126+
///
127+
/// mixin LogPropsProps on UiProps {
128+
/// BuilderOnlyUiFactory<DomProps> builder;
129+
///
130+
/// // Private since we only use this to pass along the value of `ref` to
131+
/// // the return value of forwardRef.
132+
/// //
133+
/// // Consumers can set this private field value using the public `ref` setter.
134+
/// Ref _forwardedRef;
135+
/// }
136+
///
137+
/// @Component2()
138+
/// class LogPropsComponent extends UiComponent2<LogPropsProps> {
139+
/// @override
140+
/// void componentDidUpdate(Map prevProps, _, [__]) {
141+
/// print('old props: $prevProps');
142+
/// print('new props: $props');
143+
/// }
144+
///
145+
/// @override
146+
/// render() {
147+
/// return (props.builder()
148+
/// ..modifyProps(addUnconsumedDomProps)
149+
/// ..ref = props._forwardedRef
150+
/// )(props.children);
151+
/// }
152+
/// }
153+
///
154+
/// // ---------- Component Consumption ----------
155+
///
156+
/// void main() {
157+
/// setClientConfiguration();
158+
/// final ref = createRef<Element>();
159+
///
160+
/// react_dom.render(
161+
/// (LogProps()
162+
/// ..builder = FancyButton
163+
/// ..className = 'btn btn-primary'
164+
/// ..ref = ref
165+
/// ..onClick = (_) {
166+
/// print(ref.current.outerHtml);
167+
/// }
168+
/// )(),
169+
/// querySelector('#idOfSomeNodeInTheDom')
170+
/// );
171+
///
172+
/// // You can still get a ref directly to the DOM button:
173+
/// final buttonNode = ref.current;
174+
/// }
175+
/// ```
89176
///
90177
/// Learn more: <https://reactjs.org/docs/forwarding-refs.html>.
91178
UiFactory<TProps> Function(UiFactory<TProps>) forwardRef<TProps extends UiProps>(
92-
Function(TProps props, Ref ref) wrapperFunction) {
179+
Function(TProps props, Ref ref) wrapperFunction, {String displayName}) {
93180

94181
UiFactory<TProps> wrapWithForwardRef(UiFactory<TProps> factory) {
95182
enforceMinimumComponentVersionFor(factory().componentFactory);
183+
184+
if (displayName == null) {
185+
final componentFactoryType = factory().componentFactory.type;
186+
if (componentFactoryType is String) {
187+
// DomComponent
188+
displayName = componentFactoryType;
189+
} else if (componentFactoryType is Function) {
190+
// JS component factories, Dart function components, Dart composite components
191+
displayName = getProperty(componentFactoryType, 'displayName');
96192

193+
if (displayName == null) {
194+
final ctor = getProperty(componentFactoryType, 'constructor');
195+
displayName = (ctor == null ? null : getProperty(ctor, 'name')) ?? 'Anonymous';
196+
}
197+
}
198+
}
199+
97200
Object wrapProps(Map props, Ref ref) {
98201
return wrapperFunction(factory(props), ref);
99202
}
100-
ReactComponentFactoryProxy hoc = react_interop.forwardRef(wrapProps);
203+
ReactComponentFactoryProxy hoc = react_interop.forwardRef(wrapProps, displayName: displayName);
101204
setComponentTypeMeta(hoc, isHoc: true, parentType: factory().componentFactory);
102205

103206
TProps forwardedFactory([Map props]) {

test/over_react/component/forward_ref_test.dart

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
library forward_ref_test;
1616

1717
import 'dart:html';
18+
import 'dart:js_util';
1819

1920
import 'package:test/test.dart';
2021
import 'package:over_react/over_react.dart';
@@ -56,12 +57,64 @@ main() {
5657

5758
expect(refObject.current, TypeMatcher<DivElement>());
5859
});
60+
61+
group('- sets displayName on the rendered component as expected', () {
62+
test('when displayName argument is not passed to forwardRef', () {
63+
UiFactory<DomProps> DivForwarded = forwardRef<DomProps>((props, ref) {
64+
return (Dom.div()
65+
..ref = ref
66+
)();
67+
})(Dom.div);
68+
69+
final refObject = createRef<DivElement>();
70+
final vDomElement = (DivForwarded()..ref = refObject)();
71+
72+
expect(getProperty(getProperty(vDomElement.type, 'render'), 'displayName'), 'div');
73+
});
74+
75+
test('when displayName argument is passed to forwardRef', () {
76+
UiFactory<DomProps> DivForwarded = forwardRef<DomProps>((props, ref) {
77+
return (Dom.div()
78+
..ref = ref
79+
)();
80+
}, displayName: 'SomethingCustom')(Dom.div);
81+
82+
final refObject = createRef<DivElement>();
83+
final vDomElement = (DivForwarded()..ref = refObject)();
84+
85+
expect(getProperty(getProperty(vDomElement.type, 'render'), 'displayName'), 'SomethingCustom');
86+
});
87+
});
5988
});
6089

6190
group('on a component with a dart component child', () {
6291
forwardRefTest(BasicChild, verifyRefValue: (ref) {
6392
expect(ref, TypeMatcher<BasicChildComponent>());
6493
});
94+
95+
group('- sets displayName on the rendered component as expected', () {
96+
test('when displayName argument is not passed to forwardRef', () {
97+
UiFactory<BasicProps> BasicForwarded = forwardRef<BasicProps>((props, ref) {
98+
return (BasicChild()..ref = ref)();
99+
})(Basic);
100+
101+
final Ref refObject = createRef();
102+
final vDomElement = (BasicForwarded()..ref = refObject)();
103+
104+
expect(getProperty(getProperty(vDomElement.type, 'render'), 'displayName'), 'Basic');
105+
});
106+
107+
test('when displayName argument is passed to forwardRef', () {
108+
UiFactory<BasicProps> BasicForwarded = forwardRef<BasicProps>((props, ref) {
109+
return (BasicChild()..ref = ref)();
110+
}, displayName: 'BasicForwarded')(Basic);
111+
112+
final Ref refObject = createRef();
113+
final vDomElement = (BasicForwarded()..ref = refObject)();
114+
115+
expect(getProperty(getProperty(vDomElement.type, 'render'), 'displayName'), 'BasicForwarded');
116+
});
117+
});
65118
});
66119
});
67120
}

web/component2/index.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import 'src/demos.dart';
2222

2323
void main() {
2424
setClientConfiguration();
25+
final fancyButtonNodeRef = createRef<Element>();
2526

2627
react_dom.render(
2728
buttonExamplesDemo(), querySelector('$demoMountNodeSelectorPrefix--button'));
@@ -41,6 +42,18 @@ void main() {
4142
react_dom.render(
4243
radioToggleButtonDemo(), querySelector('$demoMountNodeSelectorPrefix--radio-toggle'));
4344

45+
react_dom.render(
46+
(LogProps()
47+
..builder = FancyButton
48+
..className = 'btn btn-primary'
49+
..ref = fancyButtonNodeRef
50+
..onClick = (_) {
51+
print(fancyButtonNodeRef.current.outerHtml);
52+
}
53+
)(),
54+
querySelector('$demoMountNodeSelectorPrefix--forwardRef'),
55+
);
56+
4457
react_dom.render(
4558
(v2.ErrorBoundary()
4659
..onComponentDidCatch = (error, info) {

web/component2/index.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ <h2>ToggleButton (Radio)</h2>
6767
around with the component rendered below.</p>
6868
<div class="component-demo__mount--radio-toggle"></div>
6969
</div>
70+
<div class="col-xs-12 col-lg-6 p-3">
71+
<h2>ForwardRef</h2>
72+
<p>Modify the source of <code>/web/component2/src/demos/forward_ref.dart</code> to play
73+
around with the component rendered below.</p>
74+
<div class="component-demo__mount--forwardRef"></div>
75+
</div>
7076
<div class="col-xs-12 col-lg-6 p-3">
7177
<h2>Component That Throws A Caught Error</h2>
7278
<p><strong>(Deprecated ErrorBoundary Component)</strong></p>

web/component2/src/demos.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export 'demos/button/button-active.dart';
2323
export 'demos/button/button-disabled.dart';
2424

2525
export 'demos/custom_error_boundary.dart';
26+
export 'demos/forward_ref.dart';
2627

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

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import 'package:over_react/over_react.dart';
2+
3+
// ignore: uri_has_not_been_generated
4+
part 'forward_ref.over_react.g.dart';
5+
6+
final FancyButton = forwardRef<DomProps>((props, ref) {
7+
final classes = ClassNameBuilder.fromProps(props)
8+
..add('FancyButton');
9+
10+
return (Dom.button()
11+
..addProps(getPropsToForward(props, onlyCopyDomProps: true))
12+
..className = classes.toClassName()
13+
..ref = ref
14+
)('Click me!');
15+
}, displayName: 'FancyButton')(Dom.button);
16+
17+
final LogProps = forwardRef<LogPropsProps>((props, ref) {
18+
return (_LogProps()
19+
..addProps(props)
20+
.._forwardedRef = ref
21+
)('Click me!');
22+
}, displayName: 'LogProps')(_LogProps);
23+
24+
@Factory()
25+
UiFactory<LogPropsProps> _LogProps = _$_LogProps;
26+
27+
@Props()
28+
class _$LogPropsProps extends UiProps {
29+
BuilderOnlyUiFactory<DomProps> builder;
30+
31+
// Private since we only use this to pass along the value of `ref` to
32+
// the return value of forwardRef.
33+
//
34+
// Consumers can set this private field value using the public `ref` setter.
35+
Ref _forwardedRef;
36+
}
37+
38+
@Component2()
39+
class LogPropsComponent extends UiComponent2<LogPropsProps> {
40+
@override
41+
void componentDidUpdate(Map prevProps, _, [__]) {
42+
print('old props: $prevProps');
43+
print('new props: $props');
44+
}
45+
46+
@override
47+
render() {
48+
return (props.builder()
49+
..modifyProps(addUnconsumedDomProps)
50+
..ref = props._forwardedRef
51+
)(props.children);
52+
}
53+
}

0 commit comments

Comments
 (0)